From 20928132588b5a01f3ccb5263be537b4d225774d Mon Sep 17 00:00:00 2001 From: Antoine Gersant Date: Sat, 6 Oct 2018 14:41:25 -0700 Subject: [PATCH] Changed LastFM auth flow from application flow to web-flow --- Cargo.lock | 28 ++++ Cargo.toml | 2 + src/api.rs | 22 ++++ src/config.rs | 36 +----- .../up.sql | 2 +- src/db/schema.sqlite | Bin 69632 -> 69632 bytes src/errors.rs | 2 + src/index.rs | 2 +- src/lastfm.rs | 122 ++++++++++++++++-- src/main.rs | 2 + src/user.rs | 21 ++- 11 files changed, 191 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85e7561..d72361f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -802,6 +802,11 @@ name = "matches" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "md5" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "memchr" version = "0.1.11" @@ -1193,6 +1198,7 @@ dependencies = [ "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "lewton 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "md5 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "metaflac 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "mount 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "mp3-duration 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1206,6 +1212,7 @@ dependencies = [ "rustfm-scrobble 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "secure-session 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde-xml-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "simplelog 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1500,6 +1507,16 @@ name = "serde" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "serde-xml-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "xml-rs 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "serde_derive" version = "1.0.24" @@ -1927,6 +1944,14 @@ name = "xdg" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "xml-rs" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum adler32 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6cbd0b9af8587c72beadc9f72d35b9fbb070982c9e6203e46e93f10df25f8f45" "checksum advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a" @@ -2025,6 +2050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d06ff7ff06f729ce5f4e227876cb88d10bc59cd4ae1e09fbb2bde15c850dc21" "checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" "checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" +"checksum md5 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "995999bcecec06dff8499bfafab45119c1d33a57d75a705b429d6d49f38c2f40" "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" "checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" "checksum metaflac 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1839a57e30c651fb9647d1c140dcda407282a2228cddb25a21c1708645621219" @@ -2096,6 +2122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum sequence_trie 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c915714ca833b1d4d6b8f6a9d72a3ff632fe45b40a8d184ef79c81bec6327eed" "checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" "checksum serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1c57ab4ec5fa85d08aaf8ed9245899d9bbdd66768945b21113b84d5f595cb6a1" +"checksum serde-xml-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0c06881f4313eec67d4ecfcd8e14339f6042cfc0de4b1bd3ceae74c29d597f68" "checksum serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "02c92ea07b6e49b959c1481804ebc9bfd92d3c459f1274c9a9546829e42a66ce" "checksum serde_derive_internals 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75c6aac7b99801a16db5b40b7bf0d7e4ba16e76fbf231e32a4677f271cac0603" "checksum serde_json 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)" = "67f7d2e9edc3523a9c8ec8cd6ec481b3a27810aafee3e625d311febd3e656b4c" @@ -2150,3 +2177,4 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum wrapped-vec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "06c29bb4abe93d1c8ef79b60f270d0efcaa6c5c97aaaaaaa0d477ea72f5f9e45" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a66b7c2281ebde13cf4391d70d4c7e5946c3c25e72a7b859ca8f677dcd0b0c61" +"checksum xml-rs 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7ec6c39eaa68382c8e31e35239402c0a9489d4141a8ceb0c716099a0b515b562" diff --git a/Cargo.toml b/Cargo.toml index 97e2e1b..7168402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ image = "0.15.0" iron = "0.5.1" rustfm-scrobble = "0.9.1" lewton = "0.6.2" +md5 = "0.4.0" metaflac = "0.1.8" mount = "0.3.0" mp3-duration = "0.1.0" @@ -33,6 +34,7 @@ secure-session = "0.2.0" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" +serde-xml-rs = "0.2.1" staticfile = "0.4.0" toml = "0.4.5" typemap = "0.3" diff --git a/src/api.rs b/src/api.rs index a1817b7..fa189dc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -204,6 +204,12 @@ fn get_endpoints(db: Arc, index_channel: Arc>>) auth_api_mount.mount("/playlist/", playlist_router); } + { + let db = db.clone(); + auth_api_mount.mount("/lastfm/auth/", move |request: &mut Request| { + self::lastfm_auth(request, db.deref()) + }); + } { let db = db.clone(); auth_api_mount.mount("/lastfm/now_playing/", move |request: &mut Request| { @@ -698,6 +704,22 @@ fn delete_playlist(request: &mut Request, db: &DB) -> IronResult { Ok(Response::with(status::Ok)) } +fn lastfm_auth(request: &mut Request, db: &DB) -> IronResult { + let input = request.get_ref::().unwrap(); + let username = match input.find(&["username"]) { + Some(¶ms::Value::String(ref username)) => username.clone(), + _ => return Err(Error::from(ErrorKind::MissingUsername).into()), + }; + let token = match input.find(&["token"]) { + Some(¶ms::Value::String(ref token)) => token.clone(), + _ => return Err(Error::from(ErrorKind::MissingPassword).into()), + }; + + lastfm::auth(db, &username, &token)?; + + Ok(Response::with(status::Ok)) +} + fn lastfm_now_playing(request: &mut Request, db: &DB) -> IronResult { let username = match request.extensions.get::() { Some(s) => s.username.clone(), diff --git a/src/config.rs b/src/config.rs index 53bf4ed..7cb62bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,10 +26,7 @@ pub struct MiscSettings { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Preferences { - pub lastfm_username: Option, - pub lastfm_password: Option, -} +pub struct Preferences {} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ConfigUser { @@ -256,31 +253,15 @@ pub fn amend(db: &T, new_config: &Config) -> Result<()> Ok(()) } -pub fn read_preferences(db: &T, username: &str) -> Result +pub fn read_preferences(_: &T, _: &str) -> Result where T: ConnectionSource { - use self::users::dsl::*; - let connection = db.get_connection(); - let (read_lastfm_username, read_lastfm_password) = users - .select((lastfm_username, lastfm_password)) - .filter(name.eq(username)) - .get_result(connection.deref())?; - Ok(Preferences { - lastfm_username: read_lastfm_username, - lastfm_password: read_lastfm_password, - }) + Ok(Preferences {}) } -pub fn write_preferences(db: &T, username: &str, preferences: &Preferences) -> Result<()> +pub fn write_preferences(_: &T, _: &str, _: &Preferences) -> Result<()> where T: ConnectionSource { - use self::users::dsl::*; - let connection = db.get_connection(); - diesel::update(users) - .set((lastfm_username.eq(&preferences.lastfm_username), - lastfm_password.eq(&preferences.lastfm_password))) - .filter(name.eq(username)) - .execute(connection.deref())?; Ok(()) } @@ -490,14 +471,7 @@ fn test_preferences_read_write() { }; amend(&db, &initial_config).unwrap(); - let old_preferences = read_preferences(&db, "Teddy🐻").unwrap(); - assert_eq!(old_preferences.lastfm_username, None); - assert_eq!(old_preferences.lastfm_password, None); - - let new_preferences = Preferences { - lastfm_username: Some("🐻FM".into()), - lastfm_password: Some("Secret🐻Secret".into()), - }; + let new_preferences = Preferences {}; write_preferences(&db, "Teddy🐻", &new_preferences).unwrap(); let read_preferences = read_preferences(&db, "Teddy🐻").unwrap(); diff --git a/src/db/migrations/20180303211100_add_last_fm_credentials/up.sql b/src/db/migrations/20180303211100_add_last_fm_credentials/up.sql index 3a01018..40cc2e1 100644 --- a/src/db/migrations/20180303211100_add_last_fm_credentials/up.sql +++ b/src/db/migrations/20180303211100_add_last_fm_credentials/up.sql @@ -1,2 +1,2 @@ ALTER TABLE users ADD COLUMN lastfm_username TEXT; -ALTER TABLE users ADD COLUMN lastfm_password TEXT; +ALTER TABLE users ADD COLUMN lastfm_session_key TEXT; diff --git a/src/db/schema.sqlite b/src/db/schema.sqlite index 74c0758b0802f1af0e6b9275488cb25f2caaee5a..9bea424ad9b1f6bb05ba211f4740eaf32a4e2896 100644 GIT binary patch delta 572 zcmZwEyGsKx9Ki9Ot(LY5N{9MZiVg~e%B8Y=HIqTryAb1L*#ajXipYJb$6iSgoDRS6lC6Bt5zf5&lro7$~vueft z*kycYGa5~$Qr7NxaV&nD$mjC;xuwO)`P^DHlh{lWj53Jz1I_};QfD9*9sdPDo8n4eTm2hY zI{dfAz=P&D{2Gb?+#qB9H3+c)6Ay(#@oHoL%_Q2}xckr@q5GMqX>_g*PIIfHua(w^ zm|1^&-@2G+KN)G7yJ~(Z*Y*~A^2fv1mBMV_w!^e*Vc~SvLS7`13A~oD~fk`EqUwO7i Qgz^aWY)e$1&pMYs0i2(db: &T, track: &Path) -> Result +#[derive(Debug, Deserialize)] +struct AuthResponseSessionName { + #[serde(rename = "$value")] + pub body: String, +} + +#[derive(Debug, Deserialize)] +struct AuthResponseSessionKey { + #[serde(rename = "$value")] + pub body: String, +} + +#[derive(Debug, Deserialize)] +struct AuthResponseSessionSubscriber { + #[serde(rename = "$value")] + pub body: i32, +} + +#[derive(Debug, Deserialize)] +struct AuthResponseSession { + pub name: AuthResponseSessionName, + pub key: AuthResponseSessionKey, + pub subscriber: AuthResponseSessionSubscriber, +} + +#[derive(Debug, Deserialize)] +struct AuthResponse { + pub status: String, + pub session: AuthResponseSession, +} + +fn scrobble_from_path(db: &T, track: &Path) -> Result where T: ConnectionSource + VFSSource { let song = index::get_song(db, track)?; @@ -19,26 +56,91 @@ fn scrobble_from_path(db: &T, track: &Path) -> Result song.album.unwrap_or("".into()))) } -pub fn scrobble(db: &T, username: &str, track: &Path) -> Result<()> +pub fn auth(db: &T, username: &str, token: &str) -> Result<(), errors::Error> + where T: ConnectionSource + VFSSource +{ + let mut params = HashMap::new(); + params.insert("token".to_string(), token.to_string()); + params.insert("api_key".to_string(), LASTFM_API_KEY.to_string()); + let mut response = match api_request("auth.getSession", ¶ms) { + Ok(r) => r, + Err(_) => bail!(errors::ErrorKind::LastFMAuthError), + }; + + let mut body = String::new(); + response.read_to_string(&mut body)?; + if !response.status().is_success() { + bail!(errors::ErrorKind::LastFMAuthError) + } + + let auth_response: AuthResponse = match deserialize(body.as_bytes()) { + Ok(d) => d, + Err(_) => bail!(errors::ErrorKind::LastFMDeserializationError) + }; + + user::set_lastfm_session_key(db, username, &auth_response.session.key.body) +} + +pub fn scrobble(db: &T, username: &str, track: &Path) -> Result<(), errors::Error> where T: ConnectionSource + VFSSource { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let scrobble = scrobble_from_path(db, track)?; - let (lastfm_username, lastfm_password) = user::get_lastfm_credentials(db, username)?; - scrobbler - .authenticate_with_password(lastfm_username, lastfm_password)?; + let auth_token = user::get_lastfm_session_key(db, username)?; + scrobbler.authenticate_with_session_key(auth_token); scrobbler.scrobble(scrobble)?; Ok(()) } -pub fn now_playing(db: &T, username: &str, track: &Path) -> Result<()> +pub fn now_playing(db: &T, username: &str, track: &Path) -> Result<(), errors::Error> where T: ConnectionSource + VFSSource { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let scrobble = scrobble_from_path(db, track)?; - let (lastfm_username, lastfm_password) = user::get_lastfm_credentials(db, username)?; - scrobbler - .authenticate_with_password(lastfm_username, lastfm_password)?; + let auth_token = user::get_lastfm_session_key(db, username)?; + scrobbler.authenticate_with_session_key(auth_token); scrobbler.now_playing(scrobble)?; Ok(()) } + +fn api_request(method: &str, params: &HashMap) -> Result +{ + let mut url = LASTFM_API_ROOT.to_string(); + url.push_str("?"); + + url.push_str(&format!("method={}&", method)); + for (k, v) in params.iter() + { + url.push_str(&format!("{}={}&", k, v)); + } + let api_signature = get_signature(method, params); + url.push_str(&format!("api_sig={}", api_signature)); + + let client = reqwest::Client::new()?; + let request = client.get(url.as_str()); + request.send() +} + +fn get_signature(method: &str, params: &HashMap) -> String +{ + let mut signature_data = params.clone(); + signature_data.insert("method".to_string(), method.to_string()); + + let mut param_names = Vec::new(); + for param_name in signature_data.keys() + { + param_names.push(param_name); + } + param_names.sort(); + + let mut signature = String::new(); + for param_name in param_names + { + signature.push_str((param_name.to_string() + signature_data[param_name].as_str()).as_str()) + } + + signature.push_str(LASTFM_API_SECRET); + + let digest = md5::compute(signature.as_bytes()); + format!("{:X}", digest) +} diff --git a/src/main.rs b/src/main.rs index 5a619b5..a232708 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ extern crate id3; extern crate image; extern crate iron; extern crate lewton; +extern crate md5; extern crate metaflac; extern crate mount; extern crate mp3_duration; @@ -32,6 +33,7 @@ extern crate serde; #[macro_use] extern crate serde_derive; extern crate serde_json; +extern crate serde_xml_rs; extern crate staticfile; extern crate toml; extern crate typemap; diff --git a/src/user.rs b/src/user.rs index 774125f..394d3f9 100644 --- a/src/user.rs +++ b/src/user.rs @@ -93,17 +93,28 @@ pub fn is_admin(db: &T, username: &str) -> Result Ok(is_admin != 0) } -pub fn get_lastfm_credentials(db: &T, username: &str) -> Result<(String, String)> +pub fn set_lastfm_session_key(db: &T, username: &str, token: &str) -> Result<()> where T: ConnectionSource { use db::users::dsl::*; let connection = db.get_connection(); - let credentials = users + diesel::update(users.filter(name.eq(username))) + .set(lastfm_session_key.eq(token)) + .execute(connection.deref())?; + Ok(()) +} + +pub fn get_lastfm_session_key(db: &T, username: &str) -> Result + where T: ConnectionSource +{ + use db::users::dsl::*; + let connection = db.get_connection(); + let token = users .filter(name.eq(username)) - .select((lastfm_username, lastfm_password)) + .select(lastfm_session_key) .get_result(connection.deref())?; - match credentials { - (Some(u), Some(p)) => Ok((u, p)), + match token { + Some(t) => Ok(t), _ => bail!(ErrorKind::MissingLastFMCredentials), } }