Skip to content

Commit

Permalink
Move main::authenticate to the top level of the library
Browse files Browse the repository at this point in the history
Also clean up a few dependencies that the binary had that were
transitive dependencies only
  • Loading branch information
dsheets committed Dec 12, 2023
1 parent 96c38a3 commit 185fe2c
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 113 deletions.
4 changes: 1 addition & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
105 changes: 105 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<dyn error::Error>),
CouldNotAddUser(Box<dyn error::Error>),
EncryptionFailed(Box<dyn error::Error>),
MissingClientId,
AccessTokenRetrievalFailure(Box<dyn error::Error>),
}

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<net::DeviceInfo, Error> {
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)
}
146 changes: 39 additions & 107 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<AuthType> 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<client::AuthType> 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)]
Expand All @@ -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<Credentials, std::io::Error> {
// Username
Expand All @@ -54,97 +77,6 @@ fn ask_user_credentials() -> Result<Credentials, std::io::Error> {
})
}

#[derive(Debug)]
pub enum Error {
CouldNotGetDeviceInfo(String, Box<dyn error::Error>),
CouldNotAddUser(Box<dyn error::Error>),
EncryptionFailed(Box<dyn error::Error>),
MissingClientId,
AccessTokenRetrievalFailure(Box<dyn error::Error>),
}

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<net::DeviceInfo, Error> {
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();

Expand All @@ -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}")
})
})
Expand All @@ -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}"));

Expand Down

0 comments on commit 185fe2c

Please sign in to comment.