Credentials helper if config not present or unparseable

Fixes #1
This commit is contained in:
Michael Edwards
2019-03-27 22:35:31 +01:00
committed by Henrik Friedrichsen
parent 58f34b9288
commit d78e71871a
5 changed files with 248 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ maintenance = { status = "experimental" }
[dependencies]
clap = "2.32.0"
chrono = "0.4"
reqwest = "0.9.11"
crossbeam-channel = "0.3.8"
directories = "1.0"
failure = "0.1.3"
@@ -34,13 +35,16 @@ rand = "0.6.5"
[dependencies.librespot]
git = "https://github.com/librespot-org/librespot.git"
rev = "2fb901a743906a5b49b3148dbfa85074964dd745"
rev = "14721f4"
default-features = false
[dependencies.cursive]
version = "0.11.1"
default-features = false
[dependencies.winapi]
git = "https://github.com/overdrivenpotato/winapi-rs"
[features]
pulseaudio_backend = ["librespot/pulseaudio-backend"]
rodio_backend = ["librespot/rodio-backend"]

169
src/authentication.rs Normal file
View File

@@ -0,0 +1,169 @@
use std::path::Path;
use cursive::view::Identifiable;
use cursive::views::*;
use cursive::{CbSink, Cursive};
use librespot::core::authentication::Credentials as RespotCredentials;
use librespot::protocol::authentication::AuthenticationType;
pub fn create_credentials(path: &Path) -> Result<RespotCredentials, String> {
let mut login_cursive = Cursive::default();
let mut info_buf = TextContent::new("Failed to authenticate\n");
info_buf.append(format!(
"Cannot read config file from {}\n",
path.to_str().unwrap()
));
let info_view = Dialog::around(TextView::new_with_content(info_buf.clone()))
.button("Login", move |s| {
let login_view = Dialog::new()
.title("Login with Spotify username and password")
.content(
ListView::new()
.child("username", EditView::new().with_id("spotify_user"))
.child(
"password",
EditView::new().secret().with_id("spotify_password"),
),
)
.button("Login", |s| {
let username = s
.call_on_id("spotify_user", |view: &mut EditView| view.get_content())
.unwrap()
.to_string();
let auth_data = s
.call_on_id("spotify_password", |view: &mut EditView| view.get_content())
.unwrap()
.to_string()
.as_bytes()
.to_vec();
s.set_user_data::<Result<RespotCredentials, String>>(Ok(RespotCredentials {
username,
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
auth_data,
}));
s.quit();
})
.button("Quit", |s| s.quit());
s.pop_layer();
s.add_layer(login_view);
})
.button("Login with Facebook", |s| {
let urls: std::collections::HashMap<String, String> =
reqwest::get("https://login2.spotify.com/v1/config")
.expect("didn't connect")
.json()
.expect("didn't parse");
// not a dialog to let people copy & paste the URL
let url_notice = TextView::new(format!("Browse to {}", urls.get("login_url").unwrap()));
let controls = Button::new("Quit", |s| s.quit());
let login_view = LinearLayout::new(cursive::direction::Orientation::Vertical)
.child(url_notice)
.child(controls);
url_open(urls.get("login_url").unwrap().to_string());
crappy_poller(urls.get("credentials_url").unwrap(), &s.cb_sink());
s.pop_layer();
s.add_layer(login_view)
})
.button("Quit", |s| s.quit());
login_cursive.add_layer(info_view);
login_cursive.run();
login_cursive
.user_data()
.cloned()
.unwrap_or(Err("Didn't obtain any credentials".to_string()))
}
// TODO: better with futures?
fn crappy_poller(url: &str, app_sink: &CbSink) {
let app_sink = app_sink.clone();
let url = url.to_string();
std::thread::spawn(move || {
let timeout = std::time::Duration::from_secs(5 * 60);
let start_time = std::time::SystemTime::now();
while std::time::SystemTime::now()
.duration_since(start_time)
.unwrap_or(timeout)
< timeout
{
if let Ok(mut response) = reqwest::get(&url) {
if response.status() != reqwest::StatusCode::ACCEPTED {
let result = match response.status() {
reqwest::StatusCode::OK => {
let creds = response
.json::<AuthResponse>()
.expect("Unable to parse")
.credentials;
Ok(creds)
}
_ => Err(format!(
"Facebook auth failed with code {}: {}",
response.status(),
response.text().unwrap()
)),
};
app_sink
.send(Box::new(|s: &mut Cursive| {
s.set_user_data(result);
s.quit();
}))
.unwrap();
return;
}
}
std::thread::sleep(std::time::Duration::from_millis(1000));
}
app_sink
.send(Box::new(|s: &mut Cursive| {
s.set_user_data::<Result<RespotCredentials, String>>(Err(
"Timed out authenticating".to_string(),
));
s.quit();
}))
.unwrap();
});
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AuthResponse {
pub credentials: RespotCredentials,
pub error: Option<String>,
}
// Thanks to Marko Mijalkovic for this, but I don't want the url crate
#[cfg(target_os = "windows")]
pub fn url_open(url: String) {
extern crate shell32;
extern crate winapi;
use std::ffi::CString;
use std::ptr;
unsafe {
shell32::ShellExecuteA(
ptr::null_mut(),
CString::new("open").unwrap().as_ptr(),
CString::new(url.replace("\n", "%0A")).unwrap().as_ptr(),
ptr::null(),
ptr::null(),
winapi::SW_SHOWNORMAL,
);
}
}
#[cfg(target_os = "macos")]
pub fn url_open(url: String) {
let _ = std::process::Command::new("open").arg(url).output();
}
#[cfg(target_os = "linux")]
pub fn url_open(url: String) {
let _ = std::process::Command::new("xdg-open").arg(url).output();
}

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use directories::ProjectDirs;
@@ -8,8 +8,6 @@ pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Config {
pub username: String,
pub password: String,
pub keybindings: Option<HashMap<String, String>>,
pub theme: Option<ConfigTheme>,
pub use_nerdfont: Option<bool>,
@@ -42,7 +40,7 @@ pub fn config_path(file: &str) -> PathBuf {
let proj_dirs = proj_dirs();
let cfg_dir = proj_dirs.config_dir();
trace!("{:?}", cfg_dir);
if !cfg_dir.is_dir() {
if cfg_dir.exists() && !cfg_dir.is_dir() {
fs::remove_file(cfg_dir).expect("unable to remove old config file");
}
if !cfg_dir.exists() {
@@ -63,3 +61,54 @@ pub fn cache_path(file: &str) -> PathBuf {
pb.push(file);
pb
}
/// Configuration and credential file helper
/// Creates a default configuration if none exist, otherwise will optionally overwrite
/// the file if it fails to parse
pub fn load_or_generate_default<
P: AsRef<Path>,
T: serde::Serialize + serde::de::DeserializeOwned,
F: Fn(&Path) -> Result<T, String>,
>(
path: P,
default: F,
default_on_parse_failure: bool,
) -> Result<T, String> {
let path = path.as_ref();
// Nothing exists so just write the default and return it
if !path.exists() {
let value = default(&path)?;
return write_content_helper(&path, value);
}
// load the serialized content. Always report this failure
let contents = std::fs::read_to_string(&path)
.map_err(|e| format!("Unable to read {}: {}", path.to_string_lossy(), e))?;
// Deserialize the content, optionally fall back to default if it fails
let result = toml::from_str(&contents);
if default_on_parse_failure {
if let Err(_) = result {
let value = default(&path)?;
return write_content_helper(&path, value);
}
}
result.map_err(|e| format!("Unable to parse {}: {}", path.to_string_lossy(), e))
}
fn write_content_helper<P: AsRef<Path>, T: serde::Serialize>(
path: P,
value: T,
) -> Result<T, String> {
let content =
toml::to_string_pretty(&value).map_err(|e| format!("Failed serializing value: {}", e))?;
fs::write(path.as_ref(), content)
.map(|_| value)
.map_err(|e| {
format!(
"Failed writing content to {}: {}",
path.as_ref().display(),
e
)
})
}

View File

@@ -35,6 +35,9 @@ use clap::{App, Arg};
use cursive::traits::Identifiable;
use cursive::Cursive;
use librespot::core::authentication::Credentials;
mod authentication;
mod commands;
mod config;
mod events;
@@ -98,21 +101,22 @@ fn main() {
// Things here may cause the process to abort; we must do them before creating curses windows
// otherwise the error message will not be seen by a user
let path = config::config_path("config.toml");
let cfg: ::config::Config = {
let path = config::config_path("config.toml");
::config::load_or_generate_default(path, |_| Ok(::config::Config::default()), false)
.unwrap_or_else(|e| {
eprintln!("{}", e);
process::exit(1);
})
};
let cfg: config::Config = {
let contents = std::fs::read_to_string(&path).unwrap_or_else(|_| {
eprintln!("Cannot read config file from {}", path.to_str().unwrap());
eprintln!(
"Expected a config file with this format:\n{}",
toml::to_string_pretty(&config::Config::default()).unwrap()
);
process::exit(1)
});
toml::from_str(&contents).unwrap_or_else(|e| {
eprintln!("{}", e);
process::exit(1)
})
let credentials: Credentials = {
let path = config::config_path("credentials.toml");
::config::load_or_generate_default(path, authentication::create_credentials, true)
.unwrap_or_else(|e| {
eprintln!("{}", e);
process::exit(1);
})
};
let theme = theme::load(&cfg);
@@ -122,11 +126,7 @@ fn main() {
let event_manager = EventManager::new(cursive.cb_sink().clone());
let spotify = Arc::new(spotify::Spotify::new(
event_manager.clone(),
cfg.username.clone(),
cfg.password.clone(),
));
let spotify = Arc::new(spotify::Spotify::new(event_manager.clone(), credentials));
let queue = Arc::new(queue::Queue::new(spotify.clone()));

View File

@@ -184,13 +184,13 @@ impl futures::Future for Worker {
}
impl Spotify {
pub fn new(events: EventManager, user: String, password: String) -> Spotify {
pub fn new(events: EventManager, credentials: Credentials) -> Spotify {
let player_config = PlayerConfig {
bitrate: Bitrate::Bitrate320,
normalisation: false,
normalisation_pregain: 0.0,
};
let credentials = Credentials::with_password(user.clone(), password.clone());
let user = credentials.username.clone();
let (tx, rx) = mpsc::unbounded();
{