From b1c90e8789c281edcda1578da9fda0011dc8c30c Mon Sep 17 00:00:00 2001 From: David Sheets Date: Sun, 10 Dec 2023 21:37:50 +0000 Subject: [PATCH 01/13] Split into lib (spotify-connect-client) and executable crates --- Cargo.lock | 17 ++++++++++++++--- Cargo.toml | 16 ++++++---------- lib/Cargo.toml | 20 ++++++++++++++++++++ {src => lib/src}/auth.rs | 24 +----------------------- lib/src/lib.rs | 3 +++ {src => lib/src}/net.rs | 0 {src => lib/src}/proto.rs | 0 src/main.rs | 35 ++++++++++++++++++++++++++++------- 8 files changed, 72 insertions(+), 43 deletions(-) create mode 100644 lib/Cargo.toml rename {src => lib/src}/auth.rs (75%) create mode 100644 lib/src/lib.rs rename {src => lib/src}/net.rs (100%) rename {src => lib/src}/proto.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index ced500a..176111e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1045,11 +1045,23 @@ dependencies = [ name = "spotify-connect" version = "0.1.0" dependencies = [ - "aes", - "aes-ctr", "base64", "clap", "dirs", + "librespot-core", + "librespot-protocol", + "rand", + "rpassword", + "spotify-connect-client", +] + +[[package]] +name = "spotify-connect-client" +version = "0.1.0" +dependencies = [ + "aes", + "aes-ctr", + "base64", "hex", "hmac 0.12.1", "librespot-core", @@ -1057,7 +1069,6 @@ dependencies = [ "minreq", "pbkdf2 0.11.0", "rand", - "rpassword", "serde", "serde_json", "sha-1 0.10.0", diff --git a/Cargo.toml b/Cargo.toml index 9b537f9..c44dab8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,21 +3,17 @@ name = "spotify-connect" version = "0.1.0" edition = "2021" +[workspace] + +[dependencies.spotify-connect-client] +path = "lib" +version = "0.1.0" + [dependencies] -aes = "0.6.0" -aes-ctr = "0.6.0" base64 = "0.13.0" clap = { version = "3.1.6", features = ["derive"] } dirs = "4.0" -hex = "0.4.3" -hmac = "0.12.1" librespot-core = "0.3.1" librespot-protocol = "0.3.1" -minreq = { version = "2.6", default-features = false, features = ['urlencoding'] } -pbkdf2 = "0.11" rand = "0.8.5" -serde = "1.0.136" -serde_json = "1.0.79" -sha-1 = "0.10.0" -tokio = "1.17.0" rpassword = "6.0.1" diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..c0bc764 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "spotify-connect-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +aes = "0.6.0" +aes-ctr = "0.6.0" +base64 = "0.13.0" +hex = "0.4.3" +hmac = "0.12.1" +librespot-core = "0.3.1" +librespot-protocol = "0.3.1" +minreq = { version = "2.6", default-features = false, features = ['urlencoding'] } +pbkdf2 = "0.11" +rand = "0.8.5" +serde = "1.0.136" +serde_json = "1.0.79" +sha-1 = "0.10.0" +tokio = "1.17.0" diff --git a/src/auth.rs b/lib/src/auth.rs similarity index 75% rename from src/auth.rs rename to lib/src/auth.rs index 44e5013..1afa35e 100644 --- a/src/auth.rs +++ b/lib/src/auth.rs @@ -2,26 +2,6 @@ use librespot_core::{ authentication::Credentials, cache::Cache, config::SessionConfig, keymaster, session::Session, }; use librespot_protocol::authentication::AuthenticationType; -use std::io::Write; - -/// Prompt the user for its Spotify username and password -pub fn ask_user_credentials() -> Result { - // Username - print!("Spotify username: "); - std::io::stdout().flush()?; - let mut username = String::new(); - std::io::stdin().read_line(&mut username)?; - username = username.trim_end().to_string(); - - // Password - let password = rpassword::prompt_password(&format!("Password for {username}: "))?; - - Ok(Credentials { - username, - auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, - auth_data: password.as_bytes().into(), - }) -} /// Create reusable credentials /// @@ -30,10 +10,8 @@ pub fn ask_user_credentials() -> Result { /// the user authenticate with the username/password couple. pub fn create_reusable_credentials( cache: Cache, + credentials: Credentials, ) -> Result> { - // Authenticate with username/password - let credentials = ask_user_credentials()?; - let connection = tokio::runtime::Builder::new_current_thread() .enable_all() .build() diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..34b8138 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod net; +pub mod proto; diff --git a/src/net.rs b/lib/src/net.rs similarity index 100% rename from src/net.rs rename to lib/src/net.rs diff --git a/src/proto.rs b/lib/src/proto.rs similarity index 100% rename from src/proto.rs rename to lib/src/proto.rs diff --git a/src/main.rs b/src/main.rs index 0735e34..3b87b4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ +use std::io::Write; + use clap::{ArgEnum, Parser}; -use librespot_core::{cache::Cache, diffie_hellman::DhLocalKeys}; +use librespot_core::{authentication::Credentials, cache::Cache, diffie_hellman::DhLocalKeys}; +use librespot_protocol::authentication::AuthenticationType; -mod auth; -mod net; -mod proto; +use spotify_connect_client::{auth, net, proto}; /// Use the Spotify Connect feature to authenticate yourself on remote devices #[derive(Parser, Debug)] @@ -38,6 +39,25 @@ impl Default for AuthType { } } +/// Prompt the user for its Spotify username and password +fn ask_user_credentials() -> Result { + // Username + print!("Spotify username: "); + std::io::stdout().flush()?; + let mut username = String::new(); + std::io::stdin().read_line(&mut username)?; + username = username.trim_end().to_string(); + + // Password + let password = rpassword::prompt_password(&format!("Password for {username}: "))?; + + Ok(Credentials { + username, + auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, + auth_data: password.as_bytes().into(), + }) +} + fn main() { // Parse arguments let args = Args::parse(); @@ -61,18 +81,19 @@ fn main() { AuthType::Reusable | AuthType::AccessToken => { cache.credentials().unwrap_or_else(|| { // Cache is empty, authenticate to create the credentials - auth::create_reusable_credentials(cache) + let credentials = ask_user_credentials().expect("Getting username and password failed"); + auth::create_reusable_credentials(cache, credentials) .expect("Getting reusable credentials from spotify failed") }) } AuthType::Password => { - auth::ask_user_credentials().expect("Getting username and password failed") + ask_user_credentials().expect("Getting username and password failed") } AuthType::DefaultToken => { token_type = Some("default"); let credentials = cache.credentials().unwrap_or_else(|| { - auth::ask_user_credentials().expect("Getting username and password failed") + ask_user_credentials().expect("Getting username and password failed") }); auth::change_to_token_credentials(credentials).expect("Token retrieval failed") From 8741f1b7628450735a87e9dcc7305ca4ba708cf1 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Sun, 10 Dec 2023 21:38:34 +0000 Subject: [PATCH 02/13] Add *~ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ea8c4bf..bd819bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +*~ \ No newline at end of file From 3cd509907e5ce70238b019ac7c401cfeabb335a1 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Sun, 10 Dec 2023 21:45:05 +0000 Subject: [PATCH 03/13] Silencio el clippy! --- lib/src/proto.rs | 2 +- src/main.rs | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/src/proto.rs b/lib/src/proto.rs index a8fa830..0a7c141 100644 --- a/lib/src/proto.rs +++ b/lib/src/proto.rs @@ -101,7 +101,7 @@ pub fn encrypt_blob( let remote_device_key = base64::decode(remote_device_key)?; let shared_key = local_keys.shared_secret(&remote_device_key); - let base_key = Sha1::digest(&shared_key); + let base_key = Sha1::digest(shared_key); let base_key = &base_key[..16]; let checksum_key = { diff --git a/src/main.rs b/src/main.rs index 3b87b4d..a785c02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,20 +25,15 @@ struct Args { auth_type: AuthType, } -#[derive(Clone, Debug, PartialEq, Eq, ArgEnum)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ArgEnum)] enum AuthType { + #[default] Reusable, Password, DefaultToken, AccessToken, } -impl Default for AuthType { - fn default() -> AuthType { - AuthType::Reusable - } -} - /// Prompt the user for its Spotify username and password fn ask_user_credentials() -> Result { // Username @@ -49,7 +44,7 @@ fn ask_user_credentials() -> Result { username = username.trim_end().to_string(); // Password - let password = rpassword::prompt_password(&format!("Password for {username}: "))?; + let password = rpassword::prompt_password(format!("Password for {username}: "))?; Ok(Credentials { username, From 813d40222b0b74dc36ad901b8ffb6ff5dfffe4b1 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 16:39:37 +0000 Subject: [PATCH 04/13] net: fix an addUser method/querystring/body bug Key-value pairs were being POSTed (correct by Spotify's spec) for action=addUser but they were in the query string rather than in the x-www-form-urlencoded body. This caused librespot-java 1.6.3 to refuse connection as it could not find the action type (addUser) because it wasn't in the body. The official Spotify client uses the body of the POST. The rust librespot application must be lenient in this regard. The added urlencoding dependency was already a transitive dependency of the minreq create with 'urlencoding' feature. --- lib/Cargo.toml | 1 + lib/src/net.rs | 34 ++++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index c0bc764..8ba7715 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,3 +18,4 @@ serde = "1.0.136" serde_json = "1.0.79" sha-1 = "0.10.0" tokio = "1.17.0" +urlencoding = "2.1" diff --git a/lib/src/net.rs b/lib/src/net.rs index 65ab554..e9b9b32 100644 --- a/lib/src/net.rs +++ b/lib/src/net.rs @@ -40,20 +40,34 @@ pub fn add_user( let device_id = hex::encode(Sha1::digest("spotify-connect".as_bytes())); let login_id = hex::encode(rand::thread_rng().gen::<[u8; 16]>()); - let mut request = minreq::post(base_url) - .with_header("Content-Type", "application/x-www-form-urlencoded") - .with_param("action", "addUser") - .with_param("userName", username) - .with_param("blob", blob) - .with_param("clientKey", my_public_key) - .with_param("deviceId", device_id) - .with_param("deviceName", "spotify-connect") - .with_param("loginId", login_id); + let params = [ + ("action", "addUser"), + ("userName", username), + ("blob", blob), + ("clientKey", my_public_key), + ("deviceId", &device_id), + ("deviceName", "spotify-connect"), + ("loginId", &login_id), + ]; + let mut body = String::with_capacity(1024); + for (i, (k, v)) in params.iter().enumerate() { + if i != 0 { + body.push('&'); + } + body.push_str(k); + body.push('='); + body.push_str(&urlencoding::encode(v)); + } if let Some(token_type) = token_type { - request = request.with_param("tokenType", token_type); + body.push_str("&tokenType="); + body.push_str(&urlencoding::encode(token_type)); } + let request = minreq::post(base_url) + .with_header("Content-Type", "application/x-www-form-urlencoded") + .with_body(body); + let response = request.send()?; let v: Value = serde_json::from_str(response.as_str()?)?; From 0a159eeb8ce38e7d9339743b600d594f25bf5bfe Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 16:56:24 +0000 Subject: [PATCH 05/13] main: split authenticate function out of main Also adds log and env_logger dependencies so the broken out function can report its status. --- Cargo.lock | 253 +++++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 2 + src/main.rs | 164 +++++++++++++++++++++++----------- 3 files changed, 357 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 176111e..f4d5911 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,13 +45,22 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -80,6 +89,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "block-buffer" version = "0.9.0" @@ -145,7 +160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" dependencies = [ "atty", - "bitflags", + "bitflags 1.3.2", "clap_derive", "indexmap", "lazy_static", @@ -246,6 +261,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -391,7 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64", - "bitflags", + "bitflags 1.3.2", "bytes", "headers-core", "http", @@ -424,6 +462,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hex" version = "0.4.3" @@ -483,6 +527,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.18" @@ -542,6 +592,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.1" @@ -556,9 +617,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.121" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "librespot-core" @@ -612,14 +673,17 @@ dependencies = [ "protobuf-codegen-pure", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + [[package]] name = "log" -version = "0.4.16" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "matches" @@ -903,7 +967,7 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -917,6 +981,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "rpassword" version = "6.0.1" @@ -938,6 +1019,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.9" @@ -1048,8 +1142,10 @@ dependencies = [ "base64", "clap", "dirs", + "env_logger", "librespot-core", "librespot-protocol", + "log", "rand", "rpassword", "spotify-connect-client", @@ -1073,6 +1169,7 @@ dependencies = [ "serde_json", "sha-1 0.10.0", "tokio", + "urlencoding", ] [[package]] @@ -1290,7 +1387,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7141e445af09c8919f1d5f8a20dae0b20c3b57a45dee0d5823c6ed5d237f15a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "chrono", "rustc_version", ] @@ -1353,3 +1450,135 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml index c44dab8..1d729b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ version = "0.1.0" base64 = "0.13.0" clap = { version = "3.1.6", features = ["derive"] } dirs = "4.0" +env_logger = "0.10.1" librespot-core = "0.3.1" librespot-protocol = "0.3.1" +log = "0.4.20" rand = "0.8.5" rpassword = "6.0.1" diff --git a/src/main.rs b/src/main.rs index a785c02..f03a7d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ -use std::io::Write; +use std::{error, fmt, io::Write}; use clap::{ArgEnum, Parser}; use librespot_core::{authentication::Credentials, cache::Cache, diffie_hellman::DhLocalKeys}; use librespot_protocol::authentication::AuthenticationType; +use log::info; use spotify_connect_client::{auth, net, proto}; @@ -53,75 +54,84 @@ fn ask_user_credentials() -> Result { }) } -fn main() { - // Parse arguments - let args = Args::parse(); - let base_url = format!("http://{}:{}{}", args.ip, args.port, args.path); - let mut token_type = None; - - // Get device information - let device_info = net::get_device_info(&base_url) - .unwrap_or_else(|_| panic!("Impossible to get device information from {base_url}")); - println!("Found `{}`. Trying to connect...", device_info.remote_name); - - // Prepare cache - let mut cache_path = dirs::cache_dir().expect("Impossible to find the user cache directory."); - cache_path.push("spotify-connect"); - - let cache = Cache::new(Some(cache_path), None, None) - .expect("Impossible to open cache path: {cache_path}"); +#[derive(Debug)] +pub enum Error { + CouldNotGetDeviceInfo(String, Box), + CouldNotAddUser(Box), + EncryptionFailed(Box), + MissingClientId, + AccessTokenRetrievalFailure(Box), +} - // Get credentials - let credentials = match args.auth_type { - AuthType::Reusable | AuthType::AccessToken => { - cache.credentials().unwrap_or_else(|| { - // Cache is empty, authenticate to create the credentials - let credentials = ask_user_credentials().expect("Getting username and password failed"); - auth::create_reusable_credentials(cache, credentials) - .expect("Getting reusable credentials from spotify failed") - }) - } - AuthType::Password => { - ask_user_credentials().expect("Getting username and password failed") +impl error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::CouldNotGetDeviceInfo(base_url, e) => f.write_fmt(format_args!( + "Could not get device information from {base_url}: {e}" + )), + Error::CouldNotAddUser(e) => f.write_fmt(format_args!( + "Authentication on the remote remote device failed: {e}" + )), + Error::EncryptionFailed(e) => { + f.write_fmt(format_args!("Encryption of credentials failed: {e}")) + } + Error::MissingClientId => f.write_str( + "To authenticate with an access token, the remote device should provide a clientID", + ), + Error::AccessTokenRetrievalFailure(e) => f.write_fmt(format_args!( + "The access token could not be retrieved from Spotify: {e}" + )), } - AuthType::DefaultToken => { - token_type = Some("default"); + } +} - let credentials = cache.credentials().unwrap_or_else(|| { - ask_user_credentials().expect("Getting username and password failed") - }); +fn authenticate( + sock_addr: &std::net::SocketAddr, + path: &str, + credentials: &Credentials, + auth_type: &AuthType, +) -> Result { + let base_url = format!("http://{}:{}{path}", sock_addr.ip(), sock_addr.port()); - auth::change_to_token_credentials(credentials).expect("Token retrieval failed") - } - }; + // Get device information + let device_info = net::get_device_info(&base_url) + .map_err(|e| Error::CouldNotGetDeviceInfo(base_url.clone(), e))?; + info!("Found `{}`. Trying to connect...", device_info.remote_name); - let (blob, my_public_key) = match args.auth_type { + let (blob, my_public_key) = match auth_type { AuthType::Reusable | AuthType::Password | AuthType::DefaultToken => { // Generate the blob - let blob = proto::build_blob(&credentials, &device_info.device_id); + let blob = proto::build_blob(credentials, &device_info.device_id); // Encrypt the blob let local_keys = DhLocalKeys::random(&mut rand::thread_rng()); let encrypted_blob = proto::encrypt_blob(&blob, &local_keys, &device_info.public_key) - .expect("Encryption of credentials failed"); + .map_err(Error::EncryptionFailed)?; (encrypted_blob, base64::encode(local_keys.public_key())) } AuthType::AccessToken => { - token_type = Some("accesstoken"); - - let client_id = device_info.client_id.expect( - "To authenticate with an access token, the remote device should provide a clientID", - ); - let scope = device_info.scope.unwrap_or_else(|| "streaming".into()); + let client_id = device_info + .client_id + .as_deref() + .ok_or(Error::MissingClientId)?; + let scope = device_info.scope.as_deref().unwrap_or("streaming"); - let token = auth::get_token(credentials.clone(), &client_id, &scope) - .expect("The access token could not be retrieved"); + let token = auth::get_token(credentials.clone(), client_id, scope) + .map_err(Error::AccessTokenRetrievalFailure)?; (token, "".to_string()) } }; + let token_type = match auth_type { + AuthType::Reusable | AuthType::Password => None, + AuthType::DefaultToken => Some("default"), + AuthType::AccessToken => Some("accesstoken"), + }; + // Send the authentication request net::add_user( &base_url, @@ -130,7 +140,61 @@ fn main() { &my_public_key, token_type, ) - .expect("Authentication on the remote device failed"); + .map_err(Error::CouldNotAddUser)?; + + Ok(device_info) +} + +fn main() { + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .parse_default_env() + .init(); + + // Parse arguments + let args = Args::parse(); + + // Prepare cache + let mut cache_path = dirs::cache_dir().expect("Impossible to find the user cache directory."); + cache_path.push("spotify-connect"); + + let cache = Cache::new(Some(cache_path.as_path()), None, None).unwrap_or_else(|e| { + panic!( + "Impossible to open cache path {}: {e}", + cache_path.display() + ) + }); + + // Get credentials + let credentials = match args.auth_type { + AuthType::Reusable | AuthType::AccessToken => { + cache.credentials().unwrap_or_else(|| { + // Cache is empty, authenticate to create the credentials + let credentials = + ask_user_credentials().expect("Getting username and password failed"); + auth::create_reusable_credentials(cache, credentials).unwrap_or_else(|e| { + panic!("Getting reusable credentials from spotify failed: {e}") + }) + }) + } + AuthType::Password => ask_user_credentials().expect("Getting username and password failed"), + AuthType::DefaultToken => { + let credentials = cache.credentials().unwrap_or_else(|| { + ask_user_credentials().expect("Getting username and password failed") + }); + + auth::change_to_token_credentials(credentials) + .unwrap_or_else(|e| panic!("Token retrieval failed: {e}")) + } + }; + + let device_info = authenticate( + &std::net::SocketAddr::new(args.ip, args.port), + &args.path, + &credentials, + &args.auth_type, + ) + .unwrap_or_else(|e| panic!("authentication failed: {e}")); println!( "🎉 Connected as `{}` on `{}` 🎉", From fda25a6b542fdef7baeb956268776c17b11a7939 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 20:47:49 +0000 Subject: [PATCH 06/13] Move main::authenticate to the top level of the library Also clean up a few dependencies that the binary had that were transitive dependencies only --- Cargo.lock | 3 +- Cargo.toml | 4 +- lib/Cargo.toml | 1 + lib/src/lib.rs | 105 +++++++++++++++++++++++++++++++++++ src/main.rs | 148 +++++++++++++------------------------------------ 5 files changed, 148 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4d5911..24b9c3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1139,14 +1139,12 @@ dependencies = [ name = "spotify-connect" version = "0.1.0" dependencies = [ - "base64", "clap", "dirs", "env_logger", "librespot-core", "librespot-protocol", "log", - "rand", "rpassword", "spotify-connect-client", ] @@ -1162,6 +1160,7 @@ dependencies = [ "hmac 0.12.1", "librespot-core", "librespot-protocol", + "log", "minreq", "pbkdf2 0.11.0", "rand", diff --git a/Cargo.toml b/Cargo.toml index 1d729b0..b2522ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,10 @@ path = "lib" version = "0.1.0" [dependencies] -base64 = "0.13.0" clap = { version = "3.1.6", features = ["derive"] } dirs = "4.0" env_logger = "0.10.1" librespot-core = "0.3.1" librespot-protocol = "0.3.1" -log = "0.4.20" -rand = "0.8.5" +log = "0.4" rpassword = "6.0.1" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8ba7715..4e84933 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -11,6 +11,7 @@ hex = "0.4.3" hmac = "0.12.1" librespot-core = "0.3.1" librespot-protocol = "0.3.1" +log = "0.4" minreq = { version = "2.6", default-features = false, features = ['urlencoding'] } pbkdf2 = "0.11" rand = "0.8.5" diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 34b8138..3a9bd7f 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,3 +1,108 @@ +use std::{error, fmt}; + +use librespot_core::{authentication::Credentials, diffie_hellman::DhLocalKeys}; +use log::info; + pub mod auth; pub mod net; pub mod proto; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum AuthType { + #[default] + Reusable, + Password, + DefaultToken, + AccessToken, +} + +#[derive(Debug)] +pub enum Error { + CouldNotGetDeviceInfo(String, Box), + CouldNotAddUser(Box), + EncryptionFailed(Box), + MissingClientId, + AccessTokenRetrievalFailure(Box), +} + +impl error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::CouldNotGetDeviceInfo(base_url, e) => f.write_fmt(format_args!( + "Could not get device information from {base_url}: {e}" + )), + Error::CouldNotAddUser(e) => f.write_fmt(format_args!( + "Authentication on the remote remote device failed: {e}" + )), + Error::EncryptionFailed(e) => { + f.write_fmt(format_args!("Encryption of credentials failed: {e}")) + } + Error::MissingClientId => f.write_str( + "To authenticate with an access token, the remote device should provide a clientID", + ), + Error::AccessTokenRetrievalFailure(e) => f.write_fmt(format_args!( + "The access token could not be retrieved from Spotify: {e}" + )), + } + } +} + +pub fn authenticate( + sock_addr: &std::net::SocketAddr, + path: &str, + credentials: &Credentials, + auth_type: &AuthType, +) -> Result { + let base_url = format!("http://{}:{}{path}", sock_addr.ip(), sock_addr.port()); + + // Get device information + let device_info = net::get_device_info(&base_url) + .map_err(|e| Error::CouldNotGetDeviceInfo(base_url.clone(), e))?; + info!("Found `{}`. Trying to connect...", device_info.remote_name); + + let (blob, my_public_key) = match auth_type { + AuthType::Reusable | AuthType::Password | AuthType::DefaultToken => { + // Generate the blob + let blob = proto::build_blob(credentials, &device_info.device_id); + + // Encrypt the blob + let local_keys = DhLocalKeys::random(&mut rand::thread_rng()); + let encrypted_blob = proto::encrypt_blob(&blob, &local_keys, &device_info.public_key) + .map_err(Error::EncryptionFailed)?; + + (encrypted_blob, base64::encode(local_keys.public_key())) + } + AuthType::AccessToken => { + let client_id = device_info + .client_id + .as_deref() + .ok_or(Error::MissingClientId)?; + let scope = device_info.scope.as_deref().unwrap_or("streaming"); + + let token = auth::get_token(credentials.clone(), client_id, scope) + .map_err(Error::AccessTokenRetrievalFailure)?; + + (token, "".to_string()) + } + }; + + let token_type = match auth_type { + AuthType::Reusable | AuthType::Password => None, + AuthType::DefaultToken => Some("default"), + AuthType::AccessToken => Some("accesstoken"), + }; + + // Send the authentication request + net::add_user( + &base_url, + &credentials.username, + &blob, + &my_public_key, + token_type, + ) + .map_err(Error::CouldNotAddUser)?; + + Ok(device_info) +} diff --git a/src/main.rs b/src/main.rs index f03a7d1..9f30075 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,43 @@ -use std::{error, fmt, io::Write}; +use std::io::Write; use clap::{ArgEnum, Parser}; -use librespot_core::{authentication::Credentials, cache::Cache, diffie_hellman::DhLocalKeys}; +use librespot_core::{authentication::Credentials, cache::Cache}; use librespot_protocol::authentication::AuthenticationType; -use log::info; -use spotify_connect_client::{auth, net, proto}; +use spotify_connect_client as client; + +// repeated here to be able to use ArgEnum without creating a library dependency on clap +#[derive(Clone, Debug, Default, ArgEnum)] +pub enum AuthType { + #[default] + Reusable, + Password, + DefaultToken, + AccessToken, +} + +impl From for client::AuthType { + fn from(value: AuthType) -> Self { + match value { + AuthType::Reusable => client::AuthType::Reusable, + AuthType::Password => client::AuthType::Password, + AuthType::DefaultToken => client::AuthType::DefaultToken, + AuthType::AccessToken => client::AuthType::AccessToken, + } + } +} + +// included here so that the two types stay in sync +impl From for AuthType { + fn from(value: client::AuthType) -> Self { + match value { + client::AuthType::Reusable => AuthType::Reusable, + client::AuthType::Password => AuthType::Password, + client::AuthType::DefaultToken => AuthType::DefaultToken, + client::AuthType::AccessToken => AuthType::AccessToken, + } + } +} /// Use the Spotify Connect feature to authenticate yourself on remote devices #[derive(Parser, Debug)] @@ -26,15 +58,6 @@ struct Args { auth_type: AuthType, } -#[derive(Clone, Debug, Default, PartialEq, Eq, ArgEnum)] -enum AuthType { - #[default] - Reusable, - Password, - DefaultToken, - AccessToken, -} - /// Prompt the user for its Spotify username and password fn ask_user_credentials() -> Result { // Username @@ -54,97 +77,6 @@ fn ask_user_credentials() -> Result { }) } -#[derive(Debug)] -pub enum Error { - CouldNotGetDeviceInfo(String, Box), - CouldNotAddUser(Box), - EncryptionFailed(Box), - MissingClientId, - AccessTokenRetrievalFailure(Box), -} - -impl error::Error for Error {} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::CouldNotGetDeviceInfo(base_url, e) => f.write_fmt(format_args!( - "Could not get device information from {base_url}: {e}" - )), - Error::CouldNotAddUser(e) => f.write_fmt(format_args!( - "Authentication on the remote remote device failed: {e}" - )), - Error::EncryptionFailed(e) => { - f.write_fmt(format_args!("Encryption of credentials failed: {e}")) - } - Error::MissingClientId => f.write_str( - "To authenticate with an access token, the remote device should provide a clientID", - ), - Error::AccessTokenRetrievalFailure(e) => f.write_fmt(format_args!( - "The access token could not be retrieved from Spotify: {e}" - )), - } - } -} - -fn authenticate( - sock_addr: &std::net::SocketAddr, - path: &str, - credentials: &Credentials, - auth_type: &AuthType, -) -> Result { - let base_url = format!("http://{}:{}{path}", sock_addr.ip(), sock_addr.port()); - - // Get device information - let device_info = net::get_device_info(&base_url) - .map_err(|e| Error::CouldNotGetDeviceInfo(base_url.clone(), e))?; - info!("Found `{}`. Trying to connect...", device_info.remote_name); - - let (blob, my_public_key) = match auth_type { - AuthType::Reusable | AuthType::Password | AuthType::DefaultToken => { - // Generate the blob - let blob = proto::build_blob(credentials, &device_info.device_id); - - // Encrypt the blob - let local_keys = DhLocalKeys::random(&mut rand::thread_rng()); - let encrypted_blob = proto::encrypt_blob(&blob, &local_keys, &device_info.public_key) - .map_err(Error::EncryptionFailed)?; - - (encrypted_blob, base64::encode(local_keys.public_key())) - } - AuthType::AccessToken => { - let client_id = device_info - .client_id - .as_deref() - .ok_or(Error::MissingClientId)?; - let scope = device_info.scope.as_deref().unwrap_or("streaming"); - - let token = auth::get_token(credentials.clone(), client_id, scope) - .map_err(Error::AccessTokenRetrievalFailure)?; - - (token, "".to_string()) - } - }; - - let token_type = match auth_type { - AuthType::Reusable | AuthType::Password => None, - AuthType::DefaultToken => Some("default"), - AuthType::AccessToken => Some("accesstoken"), - }; - - // Send the authentication request - net::add_user( - &base_url, - &credentials.username, - &blob, - &my_public_key, - token_type, - ) - .map_err(Error::CouldNotAddUser)?; - - Ok(device_info) -} - fn main() { env_logger::Builder::new() .filter_level(log::LevelFilter::Info) @@ -172,7 +104,7 @@ fn main() { // Cache is empty, authenticate to create the credentials let credentials = ask_user_credentials().expect("Getting username and password failed"); - auth::create_reusable_credentials(cache, credentials).unwrap_or_else(|e| { + client::auth::create_reusable_credentials(cache, credentials).unwrap_or_else(|e| { panic!("Getting reusable credentials from spotify failed: {e}") }) }) @@ -183,16 +115,16 @@ fn main() { ask_user_credentials().expect("Getting username and password failed") }); - auth::change_to_token_credentials(credentials) + client::auth::change_to_token_credentials(credentials) .unwrap_or_else(|e| panic!("Token retrieval failed: {e}")) } }; - let device_info = authenticate( + let device_info = client::authenticate( &std::net::SocketAddr::new(args.ip, args.port), &args.path, &credentials, - &args.auth_type, + &args.auth_type.into(), ) .unwrap_or_else(|e| panic!("authentication failed: {e}")); From d0f28054adedf0d67d7aaa8da429e7030d6480f1 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 21:29:42 +0000 Subject: [PATCH 07/13] main: loosen some dependency version constraints --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b2522ac..69571b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ version = "0.1.0" [dependencies] clap = { version = "3.1.6", features = ["derive"] } -dirs = "4.0" -env_logger = "0.10.1" +dirs = "4" +env_logger = "0.10" librespot-core = "0.3.1" librespot-protocol = "0.3.1" log = "0.4" -rpassword = "6.0.1" +rpassword = "6" From ec539700534ccbbf679e4c5d3aa1d1715b228f90 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 21:31:27 +0000 Subject: [PATCH 08/13] net::DeviceInfo: cleanup, comments, add activeUser field Also make the type serde Serializable because one may wish to store device info to examine later --- lib/src/net.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/src/net.rs b/lib/src/net.rs index e9b9b32..ba2e496 100644 --- a/lib/src/net.rs +++ b/lib/src/net.rs @@ -1,21 +1,20 @@ use rand::Rng; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::Value; use sha1::{Digest, Sha1}; -#[derive(Debug, Deserialize)] +// see +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct DeviceInfo { #[serde(rename = "deviceID")] pub device_id: String, - #[serde(rename = "remoteName")] pub remote_name: String, - #[serde(rename = "publicKey")] pub public_key: String, - #[serde(rename = "tokenType")] - pub token_type: Option, - #[serde(rename = "clientID")] - pub client_id: Option, - pub scope: Option, + pub active_user: Option, // undocumented but useful and returned by both librespot and librespot-java + pub token_type: Option, // required at least as of 2.9.0 + pub client_id: Option, // required at least as of 2.9.0 + pub scope: Option, // required at least as of 2.9.0 } /// Get the necessary information from the remote device From 18601fd1e2ddc72daac212cd36ada49692b048b3 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 21:32:24 +0000 Subject: [PATCH 09/13] main: tell the user who was booted from the device (if any) --- src/main.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9f30075..21f7f9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,8 +128,14 @@ fn main() { ) .unwrap_or_else(|e| panic!("authentication failed: {e}")); + let more_info = match device_info.active_user.as_deref() { + Some("") => " (no prior active user)".to_string(), + Some(username) => format!(" (was {username})"), + None => "".to_string(), + }; + println!( - "🎉 Connected as `{}` on `{}` 🎉", - credentials.username, device_info.remote_name + "🎉 Connected as `{}` on `{}`{} 🎉", + credentials.username, device_info.remote_name, more_info, ); } From 87816740b0320fe876a2396182812c2669f7daec Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 21:52:26 +0000 Subject: [PATCH 10/13] authenticate: accept hostnames, too --- lib/src/lib.rs | 5 +++-- src/main.rs | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3a9bd7f..07e96e7 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -50,12 +50,13 @@ impl fmt::Display for Error { } pub fn authenticate( - sock_addr: &std::net::SocketAddr, + host_or_ip: &str, + port: u16, path: &str, credentials: &Credentials, auth_type: &AuthType, ) -> Result { - let base_url = format!("http://{}:{}{path}", sock_addr.ip(), sock_addr.port()); + let base_url = format!("http://{}:{}{path}", host_or_ip, port); // Get device information let device_info = net::get_device_info(&base_url) diff --git a/src/main.rs b/src/main.rs index 21f7f9d..1134f5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,8 +43,8 @@ impl From for AuthType { #[derive(Parser, Debug)] #[clap(version, about)] struct Args { - /// IP address of the remote device - ip: std::net::IpAddr, + /// Hostname or IP address of the remote device + host_or_ip: String, /// Port on which the remote device is listening port: u16, @@ -121,7 +121,8 @@ fn main() { }; let device_info = client::authenticate( - &std::net::SocketAddr::new(args.ip, args.port), + &args.host_or_ip, + args.port, &args.path, &credentials, &args.auth_type.into(), From 4541bec80c69131e9093cb3400121eabb1fc09f0 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 22:23:04 +0000 Subject: [PATCH 11/13] Update librespot dependencies to 0.4.2 --- Cargo.lock | 127 +++++++++++++++++++++++++++++++++--------------- Cargo.toml | 4 +- lib/Cargo.toml | 4 +- lib/src/auth.rs | 29 +++++++---- src/main.rs | 2 +- 5 files changed, 113 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24b9c3d..7b6df71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aes" version = "0.6.0" @@ -71,6 +86,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -125,6 +155,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -410,6 +449,12 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "glob" version = "0.3.0" @@ -623,9 +668,9 @@ checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "librespot-core" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255e8d8d719c020895079d140baf0b0edec8447d39a7e4760708f33b7cafaafb" +checksum = "046349f25888e644bf02d9c5de0164b2a493d29aa4ce18e1ad0b756da9b55d6d" dependencies = [ "aes", "base64", @@ -664,9 +709,9 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b3699b05cb4c50caa5a5b7f5b3aadb928dfcc91cf1aa632c0dabce3ccc3ee4" +checksum = "5d6d3ac6196ac0ea67bbe039f56d6730a5d8b31502ef9bce0f504ed729dcb39f" dependencies = [ "glob", "protobuf", @@ -703,6 +748,15 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "minreq" version = "2.6.0" @@ -715,34 +769,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.2" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "log", - "miow", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -776,6 +809,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.10.0" @@ -1010,6 +1052,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1127,9 +1175,9 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -1256,17 +1304,18 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ + "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "pin-project-lite", "socket2", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1282,16 +1331,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -1373,9 +1422,9 @@ checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" [[package]] name = "uuid" -version = "0.8.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", ] diff --git a/Cargo.toml b/Cargo.toml index 69571b2..3e8815e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ version = "0.1.0" clap = { version = "3.1.6", features = ["derive"] } dirs = "4" env_logger = "0.10" -librespot-core = "0.3.1" -librespot-protocol = "0.3.1" +librespot-core = "0.4.2" +librespot-protocol = "0.4.2" log = "0.4" rpassword = "6" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 4e84933..0656fb4 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -9,8 +9,8 @@ aes-ctr = "0.6.0" base64 = "0.13.0" hex = "0.4.3" hmac = "0.12.1" -librespot-core = "0.3.1" -librespot-protocol = "0.3.1" +librespot-core = "0.4.2" +librespot-protocol = "0.4.2" log = "0.4" minreq = { version = "2.6", default-features = false, features = ['urlencoding'] } pbkdf2 = "0.11" diff --git a/lib/src/auth.rs b/lib/src/auth.rs index 1afa35e..bb5e0e9 100644 --- a/lib/src/auth.rs +++ b/lib/src/auth.rs @@ -12,21 +12,26 @@ pub fn create_reusable_credentials( cache: Cache, credentials: Credentials, ) -> Result> { - let connection = tokio::runtime::Builder::new_current_thread() + let (_session, _credentials) = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(async { - Session::connect(SessionConfig::default(), credentials, Some(cache.clone())).await - }); - - connection?; + let store_credentials = true; + Session::connect( + SessionConfig::default(), + credentials, + Some(cache.clone()), + store_credentials, + ) + .await + })?; // The reusable credentials are automatically saved in the cache. Reading // them back. cache .credentials() - .ok_or_else(|| "There is no reusable credentials saved in cache".into()) + .ok_or_else(|| "There are no reusable credentials saved in cache".into()) } /// Transform existing credentials into token credentials @@ -56,9 +61,15 @@ pub fn get_token( .build() .unwrap() .block_on(async { - let session = Session::connect(SessionConfig::default(), credentials, None) - .await - .expect("Impossible to create a Spotify session"); + let store_credentials = false; + let (session, _credentials) = Session::connect( + SessionConfig::default(), + credentials, + None, + store_credentials, + ) + .await + .expect("Impossible to create a Spotify session"); keymaster::get_token(&session, client_id, scope) .await diff --git a/src/main.rs b/src/main.rs index 1134f5d..ed251e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,7 +90,7 @@ fn main() { let mut cache_path = dirs::cache_dir().expect("Impossible to find the user cache directory."); cache_path.push("spotify-connect"); - let cache = Cache::new(Some(cache_path.as_path()), None, None).unwrap_or_else(|e| { + let cache = Cache::new(Some(cache_path.as_path()), None, None, None).unwrap_or_else(|e| { panic!( "Impossible to open cache path {}: {e}", cache_path.display() From 378addf56d60278e2ee231cc4c8cf7bf7b11886f Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 22:39:21 +0000 Subject: [PATCH 12/13] auth: return errors instead of panicking --- lib/src/auth.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/src/auth.rs b/lib/src/auth.rs index bb5e0e9..1b34664 100644 --- a/lib/src/auth.rs +++ b/lib/src/auth.rs @@ -14,8 +14,7 @@ pub fn create_reusable_credentials( ) -> Result> { let (_session, _credentials) = tokio::runtime::Builder::new_current_thread() .enable_all() - .build() - .unwrap() + .build()? .block_on(async { let store_credentials = true; Session::connect( @@ -58,23 +57,27 @@ pub fn get_token( ) -> Result> { let token = tokio::runtime::Builder::new_current_thread() .enable_all() - .build() - .unwrap() + .build()? .block_on(async { let store_credentials = false; - let (session, _credentials) = Session::connect( + let connect_result: Result<_, Box> = Session::connect( SessionConfig::default(), credentials, None, store_credentials, ) .await - .expect("Impossible to create a Spotify session"); + .map_err(|e| format!("Unable to create a Spotify session: {e}").into()); - keymaster::get_token(&session, client_id, scope) - .await - .expect("Impossible to get a token from the Spotify session") - }); + match connect_result { + Ok((session, _credentials)) => keymaster::get_token(&session, client_id, scope) + .await + .map_err(|e| { + format!("Unable to get a token from the Spotify session: {e:?}").into() + }), + Err(e) => Err(e), + } + })?; Ok(token.access_token) } From 9b0844ae43a8f58b9245190480a139d28e4ff770 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Fri, 15 Dec 2023 20:57:40 +0000 Subject: [PATCH 13/13] lib::net: fix clientID serialized key name --- lib/src/net.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/net.rs b/lib/src/net.rs index ba2e496..4b46abb 100644 --- a/lib/src/net.rs +++ b/lib/src/net.rs @@ -13,7 +13,8 @@ pub struct DeviceInfo { pub public_key: String, pub active_user: Option, // undocumented but useful and returned by both librespot and librespot-java pub token_type: Option, // required at least as of 2.9.0 - pub client_id: Option, // required at least as of 2.9.0 + #[serde(rename = "clientID")] + pub client_id: Option, // required at least as of 2.9.0 pub scope: Option, // required at least as of 2.9.0 }