From 185fe2c5343c11226d5e5eedc57f53a34cb0e829 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 12 Dec 2023 20:47:49 +0000 Subject: [PATCH] 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 | 4 +- Cargo.toml | 3 - lib/Cargo.toml | 1 + lib/src/lib.rs | 105 +++++++++++++++++++++++++++++++++++ src/main.rs | 146 +++++++++++++------------------------------------ 5 files changed, 146 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4d5911..a589bf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1139,14 +1139,11 @@ 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 +1159,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..f9dd9a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,9 @@ 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" 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 7b0e4c9..481cc0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,43 @@ use std::{error, fmt, 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).parse_default_env().init(); @@ -169,7 +101,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}") }) }) @@ -180,16 +112,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}"));