From 99f21a1618f76f43f029fafe38f998bf75570ac9 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Fri, 30 Jun 2023 22:55:45 +0100 Subject: [PATCH 01/12] core: Obtain spclient access token using login5 instead of keymaster (Fixes #1179) --- core/src/spclient.rs | 93 +++++++++++++++++++++++++++++++++++++++++--- protocol/build.rs | 7 ++++ 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 156cf9c82..d0113535e 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -33,6 +33,7 @@ use crate::{ }, connect::PutStateRequest, extended_metadata::BatchedEntityRequest, + login5::{LoginRequest, LoginResponse}, }, token::Token, version::spotify_semantic_version, @@ -44,6 +45,7 @@ component! { accesspoint: Option = None, strategy: RequestStrategy = RequestStrategy::default(), client_token: Option = None, + auth_token: Option = None, } } @@ -149,6 +151,91 @@ impl SpClient { Ok(()) } + async fn auth_token_request(&self, message: &M) -> Result { + let client_token = self.client_token().await?; + let body = message.write_to_bytes()?; + + let request = Request::builder() + .method(&Method::POST) + .uri("https://login5.spotify.com/v3/login") + .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) + .header(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?) + .body(body.into())?; + + self.session().http_client().request_body(request).await + } + + pub async fn auth_token(&self) -> Result { + let auth_token = self.lock(|inner| { + if let Some(token) = &inner.auth_token { + if token.is_expired() { + inner.auth_token = None; + } + } + inner.auth_token.clone() + }); + + if let Some(auth_token) = auth_token { + return Ok(auth_token); + } + + let client_id = match OS { + "macos" | "windows" => self.session().client_id(), + _ => SessionConfig::default().client_id, + }; + + let mut login_request = LoginRequest::new(); + login_request.client_info.mut_or_insert_default().client_id = client_id; + login_request.client_info.mut_or_insert_default().device_id = + self.session().device_id().to_string(); + + let stored_credential = login_request.mut_stored_credential(); + stored_credential.username = self.session().username().to_string(); + stored_credential.data = self.session().auth_data().clone(); + + let mut response = self.auth_token_request(&login_request).await?; + let mut count = 0; + const MAX_TRIES: u8 = 3; + + let token_response = loop { + count += 1; + + let message = LoginResponse::parse_from_bytes(&response)?; + // TODO: Handle hash cash stuff + if message.has_ok() { + break message.ok().to_owned(); + } + + if count < MAX_TRIES { + response = self.auth_token_request(&login_request).await?; + } else { + return Err(Error::failed_precondition(format!( + "Unable to solve any of {MAX_TRIES} hash cash challenges" + ))); + } + }; + + let auth_token = Token { + access_token: token_response.access_token.clone(), + expires_in: Duration::from_secs( + token_response + .access_token_expires_in + .try_into() + .unwrap_or(3600), + ), + token_type: "Bearer".to_string(), + scopes: vec![], + timestamp: Instant::now(), + }; + self.lock(|inner| { + inner.auth_token = Some(auth_token.clone()); + }); + + trace!("Got auth token: {:?}", auth_token); + + Ok(auth_token) + } + async fn client_token_request(&self, message: &M) -> Result { let body = message.write_to_bytes()?; @@ -468,11 +555,7 @@ impl SpClient { .body(body.to_owned().into())?; // Reconnection logic: keep getting (cached) tokens because they might have expired. - let token = self - .session() - .token_provider() - .get_token("playlist-read") - .await?; + let token = self.auth_token().await?; let headers_mut = request.headers_mut(); if let Some(ref hdrs) = headers { diff --git a/protocol/build.rs b/protocol/build.rs index e1378d378..8a0a8138b 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -28,6 +28,13 @@ fn compile() { proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), proto_dir.join("spotify/clienttoken/v0/clienttoken_http.proto"), + proto_dir.join("spotify/login5/v3/challenges/code.proto"), + proto_dir.join("spotify/login5/v3/challenges/hashcash.proto"), + proto_dir.join("spotify/login5/v3/client_info.proto"), + proto_dir.join("spotify/login5/v3/credentials/credentials.proto"), + proto_dir.join("spotify/login5/v3/identifiers/identifiers.proto"), + proto_dir.join("spotify/login5/v3/login5.proto"), + proto_dir.join("spotify/login5/v3/user_info.proto"), proto_dir.join("storage-resolve.proto"), proto_dir.join("user_attributes.proto"), // TODO: remove these legacy protobufs when we are on the new API completely From 0ea50e79722e0527491f471b146706cb2d1809b4 Mon Sep 17 00:00:00 2001 From: photovoltex Date: Mon, 16 Sep 2024 18:44:06 +0200 Subject: [PATCH 02/12] core: move login5 into own manager --- core/src/lib.rs | 1 + core/src/login5.rs | 102 +++++++++++++++++++++++++++++++++++++++++++ core/src/session.rs | 9 ++++ core/src/spclient.rs | 91 +------------------------------------- 4 files changed, 114 insertions(+), 89 deletions(-) create mode 100644 core/src/login5.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index f0ee345cf..9894bb708 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -22,6 +22,7 @@ pub mod diffie_hellman; pub mod error; pub mod file_id; pub mod http_client; +pub mod login5; pub mod mercury; pub mod packet; mod proxytunnel; diff --git a/core/src/login5.rs b/core/src/login5.rs new file mode 100644 index 000000000..327677d2c --- /dev/null +++ b/core/src/login5.rs @@ -0,0 +1,102 @@ +use std::env::consts::OS; +use std::time::{Duration, Instant}; +use bytes::Bytes; +use http::{HeaderValue, Method, Request}; +use http::header::ACCEPT; +use protobuf::Message; +use librespot_protocol::login5::{LoginRequest, LoginResponse}; +use crate::{Error, SessionConfig}; +use crate::token::Token; + +component! { + Login5Manager : Login5ManagerInner { + auth_token: Option = None, + } +} + +impl Login5Manager { + async fn auth_token_request(&self, message: &M) -> Result { + let client_token = self.session().spclient().client_token().await?; + let body = message.write_to_bytes()?; + + let request = Request::builder() + .method(&Method::POST) + .uri("https://login5.spotify.com/v3/login") + .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) + .header(crate::spclient::CLIENT_TOKEN, HeaderValue::from_str(&client_token)?) + .body(body.into())?; + + self.session().http_client().request_body(request).await + } + + pub async fn auth_token(&self) -> Result { + let auth_token = self.lock(|inner| { + if let Some(token) = &inner.auth_token { + if token.is_expired() { + inner.auth_token = None; + } + } + inner.auth_token.clone() + }); + + if let Some(auth_token) = auth_token { + return Ok(auth_token); + } + + let client_id = match OS { + "macos" | "windows" => self.session().client_id(), + _ => SessionConfig::default().client_id, + }; + + let mut login_request = LoginRequest::new(); + login_request.client_info.mut_or_insert_default().client_id = client_id; + login_request.client_info.mut_or_insert_default().device_id = + self.session().device_id().to_string(); + + let stored_credential = login_request.mut_stored_credential(); + stored_credential.username = self.session().username().to_string(); + stored_credential.data = self.session().auth_data().clone(); + + let mut response = self.auth_token_request(&login_request).await?; + let mut count = 0; + const MAX_TRIES: u8 = 3; + + let token_response = loop { + count += 1; + + let message = LoginResponse::parse_from_bytes(&response)?; + // TODO: Handle hash cash stuff + if message.has_ok() { + break message.ok().to_owned(); + } + + if count < MAX_TRIES { + response = self.auth_token_request(&login_request).await?; + } else { + return Err(Error::failed_precondition(format!( + "Unable to solve any of {MAX_TRIES} hash cash challenges" + ))); + } + }; + + let auth_token = Token { + access_token: token_response.access_token.clone(), + expires_in: Duration::from_secs( + token_response + .access_token_expires_in + .try_into() + .unwrap_or(3600), + ), + token_type: "Bearer".to_string(), + scopes: vec![], + timestamp: Instant::now(), + }; + self.lock(|inner| { + inner.auth_token = Some(auth_token.clone()); + }); + + trace!("Got auth token: {:?}", auth_token); + + Ok(auth_token) + } +} \ No newline at end of file diff --git a/core/src/session.rs b/core/src/session.rs index 3944ce83e..1692b6eae 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -31,6 +31,7 @@ use crate::{ config::SessionConfig, connection::{self, AuthenticationError, Transport}, http_client::HttpClient, + login5::Login5Manager, mercury::MercuryManager, packet::PacketType, protocol::keyexchange::ErrorCode, @@ -98,6 +99,7 @@ struct SessionInternal { mercury: OnceCell, spclient: OnceCell, token_provider: OnceCell, + login5: OnceCell, cache: Option>, handle: tokio::runtime::Handle, @@ -138,6 +140,7 @@ impl Session { mercury: OnceCell::new(), spclient: OnceCell::new(), token_provider: OnceCell::new(), + login5: OnceCell::new(), handle: tokio::runtime::Handle::current(), })) } @@ -286,6 +289,12 @@ impl Session { .get_or_init(|| TokenProvider::new(self.weak())) } + pub fn login5(&self) -> &Login5Manager { + self.0 + .login5 + .get_or_init(|| Login5Manager::new(self.weak())) + } + /// Returns an error, when we haven't received a ping for too long (2 minutes), /// which means that we silently lost connection to Spotify servers. async fn session_timeout(session: SessionWeak) -> io::Result<()> { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index d0113535e..9b49fe3fc 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -33,7 +33,6 @@ use crate::{ }, connect::PutStateRequest, extended_metadata::BatchedEntityRequest, - login5::{LoginRequest, LoginResponse}, }, token::Token, version::spotify_semantic_version, @@ -45,14 +44,13 @@ component! { accesspoint: Option = None, strategy: RequestStrategy = RequestStrategy::default(), client_token: Option = None, - auth_token: Option = None, } } pub type SpClientResult = Result; #[allow(clippy::declare_interior_mutable_const)] -const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token"); +pub const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token"); #[derive(Debug, Error)] pub enum SpClientError { @@ -151,91 +149,6 @@ impl SpClient { Ok(()) } - async fn auth_token_request(&self, message: &M) -> Result { - let client_token = self.client_token().await?; - let body = message.write_to_bytes()?; - - let request = Request::builder() - .method(&Method::POST) - .uri("https://login5.spotify.com/v3/login") - .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) - .header(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?) - .body(body.into())?; - - self.session().http_client().request_body(request).await - } - - pub async fn auth_token(&self) -> Result { - let auth_token = self.lock(|inner| { - if let Some(token) = &inner.auth_token { - if token.is_expired() { - inner.auth_token = None; - } - } - inner.auth_token.clone() - }); - - if let Some(auth_token) = auth_token { - return Ok(auth_token); - } - - let client_id = match OS { - "macos" | "windows" => self.session().client_id(), - _ => SessionConfig::default().client_id, - }; - - let mut login_request = LoginRequest::new(); - login_request.client_info.mut_or_insert_default().client_id = client_id; - login_request.client_info.mut_or_insert_default().device_id = - self.session().device_id().to_string(); - - let stored_credential = login_request.mut_stored_credential(); - stored_credential.username = self.session().username().to_string(); - stored_credential.data = self.session().auth_data().clone(); - - let mut response = self.auth_token_request(&login_request).await?; - let mut count = 0; - const MAX_TRIES: u8 = 3; - - let token_response = loop { - count += 1; - - let message = LoginResponse::parse_from_bytes(&response)?; - // TODO: Handle hash cash stuff - if message.has_ok() { - break message.ok().to_owned(); - } - - if count < MAX_TRIES { - response = self.auth_token_request(&login_request).await?; - } else { - return Err(Error::failed_precondition(format!( - "Unable to solve any of {MAX_TRIES} hash cash challenges" - ))); - } - }; - - let auth_token = Token { - access_token: token_response.access_token.clone(), - expires_in: Duration::from_secs( - token_response - .access_token_expires_in - .try_into() - .unwrap_or(3600), - ), - token_type: "Bearer".to_string(), - scopes: vec![], - timestamp: Instant::now(), - }; - self.lock(|inner| { - inner.auth_token = Some(auth_token.clone()); - }); - - trace!("Got auth token: {:?}", auth_token); - - Ok(auth_token) - } - async fn client_token_request(&self, message: &M) -> Result { let body = message.write_to_bytes()?; @@ -555,7 +468,7 @@ impl SpClient { .body(body.to_owned().into())?; // Reconnection logic: keep getting (cached) tokens because they might have expired. - let token = self.auth_token().await?; + let token = self.session().login5().auth_token().await?; let headers_mut = request.headers_mut(); if let Some(ref hdrs) = headers { From f6d032e819e48dbd24af25d186124263210d0502 Mon Sep 17 00:00:00 2001 From: photovoltex Date: Mon, 16 Sep 2024 19:29:50 +0200 Subject: [PATCH 03/12] core: move solving hashcash into util --- core/src/spclient.rs | 46 ++------------------------------------- core/src/util.rs | 51 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 9b49fe3fc..418789003 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -4,7 +4,6 @@ use std::{ time::{Duration, Instant}, }; -use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use data_encoding::HEXUPPER_PERMISSIVE; use futures_util::future::IntoStream; @@ -16,7 +15,6 @@ use hyper::{ use hyper_util::client::legacy::ResponseFuture; use protobuf::{Enum, Message, MessageFull}; use rand::RngCore; -use sha1::{Digest, Sha1}; use sysinfo::System; use thiserror::Error; @@ -35,6 +33,7 @@ use crate::{ extended_metadata::BatchedEntityRequest, }, token::Token, + util, version::spotify_semantic_version, Error, FileId, SpotifyId, }; @@ -108,47 +107,6 @@ impl SpClient { Ok(format!("https://{}:{}", ap.0, ap.1)) } - fn solve_hash_cash( - ctx: &[u8], - prefix: &[u8], - length: i32, - dst: &mut [u8], - ) -> Result<(), Error> { - // after a certain number of seconds, the challenge expires - const TIMEOUT: u64 = 5; // seconds - let now = Instant::now(); - - let md = Sha1::digest(ctx); - - let mut counter: i64 = 0; - let target: i64 = BigEndian::read_i64(&md[12..20]); - - let suffix = loop { - if now.elapsed().as_secs() >= TIMEOUT { - return Err(Error::deadline_exceeded(format!( - "{TIMEOUT} seconds expired" - ))); - } - - let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat(); - - let mut hasher = Sha1::new(); - hasher.update(prefix); - hasher.update(&suffix); - let md = hasher.finalize(); - - if BigEndian::read_i64(&md[12..20]).trailing_zeros() >= (length as u32) { - break suffix; - } - - counter += 1; - }; - - dst.copy_from_slice(&suffix); - - Ok(()) - } - async fn client_token_request(&self, message: &M) -> Result { let body = message.write_to_bytes()?; @@ -293,7 +251,7 @@ impl SpClient { let length = hash_cash_challenge.length; let mut suffix = [0u8; 0x10]; - let answer = Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix); + let answer = util::solve_hash_cash(&ctx, &prefix, length, &mut suffix); match answer { Ok(_) => { diff --git a/core/src/util.rs b/core/src/util.rs index a01f8b565..31cdd962b 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,12 +1,16 @@ +use crate::Error; +use byteorder::{BigEndian, ByteOrder}; +use futures_core::ready; +use futures_util::{future, FutureExt, Sink, SinkExt}; +use hmac::digest::Digest; +use sha1::Sha1; +use std::time::{Duration, Instant}; use std::{ future::Future, mem, pin::Pin, task::{Context, Poll}, }; - -use futures_core::ready; -use futures_util::{future, FutureExt, Sink, SinkExt}; use tokio::{task::JoinHandle, time::timeout}; /// Returns a future that will flush the sink, even if flushing is temporarily completed. @@ -120,3 +124,44 @@ impl SeqGenerator { mem::replace(&mut self.0, value) } } + +pub fn solve_hash_cash( + ctx: &[u8], + prefix: &[u8], + length: i32, + dst: &mut [u8], +) -> Result { + // after a certain number of seconds, the challenge expires + const TIMEOUT: u64 = 5; // seconds + let now = Instant::now(); + + let md = Sha1::digest(ctx); + + let mut counter: i64 = 0; + let target: i64 = BigEndian::read_i64(&md[12..20]); + + let suffix = loop { + if now.elapsed().as_secs() >= TIMEOUT { + return Err(Error::deadline_exceeded(format!( + "{TIMEOUT} seconds expired" + ))); + } + + let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat(); + + let mut hasher = Sha1::new(); + hasher.update(prefix); + hasher.update(&suffix); + let md = hasher.finalize(); + + if BigEndian::read_i64(&md[12..20]).trailing_zeros() >= (length as u32) { + break suffix; + } + + counter += 1; + }; + + dst.copy_from_slice(&suffix); + + Ok(now.elapsed()) +} From 367b8e351ebb2f026d1d76d6a2b73b31ae88d162 Mon Sep 17 00:00:00 2001 From: photovoltex Date: Mon, 16 Sep 2024 19:36:43 +0200 Subject: [PATCH 04/12] core: login5 handle challenges and errors --- core/src/login5.rs | 118 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 15 deletions(-) diff --git a/core/src/login5.rs b/core/src/login5.rs index 327677d2c..a10d87ab6 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -1,12 +1,22 @@ -use std::env::consts::OS; -use std::time::{Duration, Instant}; +use crate::spclient::CLIENT_TOKEN; +use crate::token::Token; +use crate::{util, Error, SessionConfig}; use bytes::Bytes; -use http::{HeaderValue, Method, Request}; use http::header::ACCEPT; -use protobuf::Message; -use librespot_protocol::login5::{LoginRequest, LoginResponse}; -use crate::{Error, SessionConfig}; -use crate::token::Token; +use http::{HeaderValue, Method, Request}; +use librespot_protocol::hashcash::HashcashSolution; +use librespot_protocol::login5::{ + ChallengeSolution, Challenges, LoginError, LoginRequest, LoginResponse, +}; +use protobuf::well_known_types::duration::Duration as ProtoDuration; +use protobuf::{Message, MessageField}; +use std::env::consts::OS; +use std::time::{Duration, Instant}; +use thiserror::Error; +use tokio::time::sleep; + +const MAX_LOGIN_TRIES: u8 = 3; +const LOGIN_TIMEOUT: Duration = Duration::from_secs(3); component! { Login5Manager : Login5ManagerInner { @@ -14,8 +24,22 @@ component! { } } +#[derive(Debug, Error)] +enum Login5Error { + #[error("Requesting login failed: {0:?}")] + FaultyRequest(LoginError), + #[error("doesn't support code challenge")] + CodeChallenge, +} + +impl From for Error { + fn from(err: Login5Error) -> Self { + Error::failed_precondition(err) + } +} + impl Login5Manager { - async fn auth_token_request(&self, message: &M) -> Result { + async fn auth_token_request(&self, message: &LoginRequest) -> Result { let client_token = self.session().spclient().client_token().await?; let body = message.write_to_bytes()?; @@ -23,12 +47,12 @@ impl Login5Manager { .method(&Method::POST) .uri("https://login5.spotify.com/v3/login") .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) - .header(crate::spclient::CLIENT_TOKEN, HeaderValue::from_str(&client_token)?) + .header(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?) .body(body.into())?; self.session().http_client().request_body(request).await } - + pub async fn auth_token(&self) -> Result { let auth_token = self.lock(|inner| { if let Some(token) = &inner.auth_token { @@ -59,22 +83,33 @@ impl Login5Manager { let mut response = self.auth_token_request(&login_request).await?; let mut count = 0; - const MAX_TRIES: u8 = 3; let token_response = loop { count += 1; let message = LoginResponse::parse_from_bytes(&response)?; - // TODO: Handle hash cash stuff if message.has_ok() { break message.ok().to_owned(); } - if count < MAX_TRIES { + if message.has_error() { + match message.error() { + LoginError::TIMEOUT | LoginError::TOO_MANY_ATTEMPTS => { + sleep(LOGIN_TIMEOUT).await + } + others => return Err(Login5Error::FaultyRequest(others).into()), + } + } + + if message.has_challenges() { + Self::handle_challenges(&mut login_request, message.challenges())? + } + + if count < MAX_LOGIN_TRIES { response = self.auth_token_request(&login_request).await?; } else { return Err(Error::failed_precondition(format!( - "Unable to solve any of {MAX_TRIES} hash cash challenges" + "Unable to solve any of {MAX_LOGIN_TRIES} hash cash challenges" ))); } }; @@ -91,6 +126,7 @@ impl Login5Manager { scopes: vec![], timestamp: Instant::now(), }; + self.lock(|inner| { inner.auth_token = Some(auth_token.clone()); }); @@ -99,4 +135,56 @@ impl Login5Manager { Ok(auth_token) } -} \ No newline at end of file + + fn handle_challenges( + login_request: &mut LoginRequest, + challenges: &Challenges, + ) -> Result<(), Error> { + info!( + "login5 response has {} challenges...", + challenges.challenges.len() + ); + + for challenge in &challenges.challenges { + if challenge.has_code() { + debug!("empty challenge, skipping"); + return Err(Login5Error::CodeChallenge.into()); + } else if !challenge.has_hashcash() { + debug!("empty challenge, skipping"); + continue; + } + + let hash_cash_challenge = challenge.hashcash(); + + let mut suffix = [0u8; 0x10]; + let duration = util::solve_hash_cash( + &login_request.login_context, + &hash_cash_challenge.prefix, + hash_cash_challenge.length, + &mut suffix, + )?; + + let (seconds, nanos) = (duration.as_secs() as i64, duration.subsec_nanos() as i32); + info!("solving login5 hashcash took {seconds}.{nanos}s"); + + let mut solution = ChallengeSolution::new(); + solution.set_hashcash(HashcashSolution { + suffix: Vec::from(suffix), + duration: MessageField::some(ProtoDuration { + seconds, + nanos, + ..Default::default() + }), + ..Default::default() + }); + + login_request + .challenge_solutions + .mut_or_insert_default() + .solutions + .push(solution); + } + + Ok(()) + } +} From 875d0c0bebe1d5b9dffce634e72b21cfdaed6bf2 Mon Sep 17 00:00:00 2001 From: photovoltex Date: Sat, 12 Oct 2024 14:30:42 +0200 Subject: [PATCH 05/12] core: add asynchronously lock to component --- core/src/component.rs | 13 +++++++++++-- core/src/login5.rs | 2 ++ core/src/spclient.rs | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/src/component.rs b/core/src/component.rs index ebe42e8d3..5591e30d0 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -1,7 +1,7 @@ macro_rules! component { ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { #[derive(Clone)] - pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>)>); + pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>, ::tokio::sync::Semaphore)>); impl $name { #[allow(dead_code)] pub(crate) fn new(session: $crate::session::SessionWeak) -> $name { @@ -9,7 +9,7 @@ macro_rules! component { $name(::std::sync::Arc::new((session, ::parking_lot::Mutex::new($inner { $($key : $value,)* - })))) + }), ::tokio::sync::Semaphore::new(1)))) } #[allow(dead_code)] @@ -18,6 +18,15 @@ macro_rules! component { f(&mut inner) } + /// See [::tokio::sync::Semaphore] for further infos. + /// + /// The returned permit has to be hold in scope. `let _ = ...?;` will drop the permit on + /// the spot. Instead use `let _lock = ...?;` to hold the permit in scope without using it. + #[allow(dead_code)] + async fn unique_lock(&self) -> Result<::tokio::sync::SemaphorePermit<'_>, $crate::error::Error> { + (self.0).2.acquire().await.map_err(Into::into) + } + #[allow(dead_code)] fn session(&self) -> $crate::session::Session { (self.0).0.upgrade() diff --git a/core/src/login5.rs b/core/src/login5.rs index a10d87ab6..19f3a2c5e 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -54,6 +54,8 @@ impl Login5Manager { } pub async fn auth_token(&self) -> Result { + let _lock = self.unique_lock().await?; + let auth_token = self.lock(|inner| { if let Some(token) = &inner.auth_token { if token.is_expired() { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 418789003..e286d6bd7 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -120,6 +120,8 @@ impl SpClient { } pub async fn client_token(&self) -> Result { + let _lock = self.unique_lock().await?; + let client_token = self.lock(|inner| { if let Some(token) = &inner.client_token { if token.is_expired() { From b923f06a45f31db25c99d55e6b087c98579b631c Mon Sep 17 00:00:00 2001 From: photovoltex Date: Sat, 12 Oct 2024 17:25:57 +0200 Subject: [PATCH 06/12] login5: add login_context to new followup request --- core/src/login5.rs | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/core/src/login5.rs b/core/src/login5.rs index 19f3a2c5e..3c3a438a5 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -53,6 +53,24 @@ impl Login5Manager { self.session().http_client().request_body(request).await } + fn new_login_request(&self) -> LoginRequest { + let client_id = match OS { + "macos" | "windows" => self.session().client_id(), + _ => SessionConfig::default().client_id, + }; + + let mut login_request = LoginRequest::new(); + login_request.client_info.mut_or_insert_default().client_id = client_id; + login_request.client_info.mut_or_insert_default().device_id = + self.session().device_id().to_string(); + + let stored_credential = login_request.mut_stored_credential(); + stored_credential.username = self.session().username().to_string(); + stored_credential.data = self.session().auth_data().clone(); + + login_request + } + pub async fn auth_token(&self) -> Result { let _lock = self.unique_lock().await?; @@ -69,19 +87,7 @@ impl Login5Manager { return Ok(auth_token); } - let client_id = match OS { - "macos" | "windows" => self.session().client_id(), - _ => SessionConfig::default().client_id, - }; - - let mut login_request = LoginRequest::new(); - login_request.client_info.mut_or_insert_default().client_id = client_id; - login_request.client_info.mut_or_insert_default().device_id = - self.session().device_id().to_string(); - - let stored_credential = login_request.mut_stored_credential(); - stored_credential.username = self.session().username().to_string(); - stored_credential.data = self.session().auth_data().clone(); + let mut login_request = self.new_login_request(); let mut response = self.auth_token_request(&login_request).await?; let mut count = 0; @@ -104,7 +110,10 @@ impl Login5Manager { } if message.has_challenges() { - Self::handle_challenges(&mut login_request, message.challenges())? + login_request = self.new_login_request(); + login_request.login_context = message.login_context.clone(); + + Self::handle_challenges(&mut login_request, message.challenges())?; } if count < MAX_LOGIN_TRIES { From 12809459dde8c02c3afd24ecefb75c484b37ffea Mon Sep 17 00:00:00 2001 From: photovoltex Date: Sat, 12 Oct 2024 19:52:43 +0200 Subject: [PATCH 07/12] core: update versions, add example versions --- core/src/http_client.rs | 2 ++ core/src/spclient.rs | 2 ++ core/src/version.rs | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 4b500cd6a..e7e25e928 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -109,7 +109,9 @@ impl HttpClient { let os_version = System::os_version().unwrap_or_else(|| zero_str.clone()); let (spotify_platform, os_version) = match OS { + // example os_version: 30 "android" => ("Android", os_version), + // example os_version: 17 "ios" => ("iOS", os_version), "macos" => ("OSX", zero_str), "windows" => ("Win32", zero_str), diff --git a/core/src/spclient.rs b/core/src/spclient.rs index e286d6bd7..2e0419de3 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -193,10 +193,12 @@ impl SpClient { ios_data.user_interface_idiom = 0; ios_data.target_iphone_simulator = false; ios_data.hw_machine = "iPhone14,5".to_string(); + // example system_version: 17 ios_data.system_version = os_version; } "android" => { let android_data = platform_data.mut_android(); + // example android_version: 30 android_data.android_version = os_version; android_data.api_version = 31; "Pixel".clone_into(&mut android_data.device_name); diff --git a/core/src/version.rs b/core/src/version.rs index d3870473d..dd14b7b9c 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -17,16 +17,16 @@ pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); /// The protocol version of the Spotify desktop client. -pub const SPOTIFY_VERSION: u64 = 117300517; +pub const SPOTIFY_VERSION: u64 = 124200290; /// The semantic version of the Spotify desktop client. pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.31.1205.g4d59ad7c"; /// The protocol version of the Spotify mobile app. -pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; +pub const SPOTIFY_MOBILE_VERSION: &str = "8.9.82.620"; /// The user agent to fall back to, if one could not be determined dynamically. -pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)"; +pub const FALLBACK_USER_AGENT: &str = "Spotify/124200290 Linux/0 (librespot)"; pub fn spotify_version() -> String { match std::env::consts::OS { From 8fdd6ffe65e49e2a6a02a185107851f67251f8a4 Mon Sep 17 00:00:00 2001 From: photovoltex Date: Sat, 12 Oct 2024 19:54:39 +0200 Subject: [PATCH 08/12] login5: add login for mobile --- core/src/login5.rs | 153 +++++++++++++++++++++++++++++---------------- 1 file changed, 100 insertions(+), 53 deletions(-) diff --git a/core/src/login5.rs b/core/src/login5.rs index 3c3a438a5..643b2816e 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -2,11 +2,15 @@ use crate::spclient::CLIENT_TOKEN; use crate::token::Token; use crate::{util, Error, SessionConfig}; use bytes::Bytes; -use http::header::ACCEPT; -use http::{HeaderValue, Method, Request}; -use librespot_protocol::hashcash::HashcashSolution; -use librespot_protocol::login5::{ - ChallengeSolution, Challenges, LoginError, LoginRequest, LoginResponse, +use http::{header::ACCEPT, HeaderValue, Method, Request}; +use librespot_protocol::{ + client_info::ClientInfo, + credentials::{Password, StoredCredential}, + hashcash::HashcashSolution, + login5::{ + login_request::Login_method, ChallengeSolution, Challenges, LoginError, LoginOk, + LoginRequest, LoginResponse, + }, }; use protobuf::well_known_types::duration::Duration as ProtoDuration; use protobuf::{Message, MessageField}; @@ -39,7 +43,7 @@ impl From for Error { } impl Login5Manager { - async fn auth_token_request(&self, message: &LoginRequest) -> Result { + async fn request(&self, message: &LoginRequest) -> Result { let client_token = self.session().spclient().client_token().await?; let body = message.write_to_bytes()?; @@ -53,51 +57,35 @@ impl Login5Manager { self.session().http_client().request_body(request).await } - fn new_login_request(&self) -> LoginRequest { + fn new_login_request(&self, login: Login_method) -> LoginRequest { let client_id = match OS { "macos" | "windows" => self.session().client_id(), _ => SessionConfig::default().client_id, }; - let mut login_request = LoginRequest::new(); - login_request.client_info.mut_or_insert_default().client_id = client_id; - login_request.client_info.mut_or_insert_default().device_id = - self.session().device_id().to_string(); - - let stored_credential = login_request.mut_stored_credential(); - stored_credential.username = self.session().username().to_string(); - stored_credential.data = self.session().auth_data().clone(); - - login_request - } - - pub async fn auth_token(&self) -> Result { - let _lock = self.unique_lock().await?; - - let auth_token = self.lock(|inner| { - if let Some(token) = &inner.auth_token { - if token.is_expired() { - inner.auth_token = None; - } - } - inner.auth_token.clone() - }); - - if let Some(auth_token) = auth_token { - return Ok(auth_token); + LoginRequest { + client_info: MessageField::some(ClientInfo { + client_id, + device_id: self.session().device_id().to_string(), + special_fields: Default::default(), + }), + login_method: Some(login), + ..Default::default() } + } - let mut login_request = self.new_login_request(); + async fn login5_request(&self, login: Login_method) -> Result { + let mut login_request = self.new_login_request(login.clone()); - let mut response = self.auth_token_request(&login_request).await?; + let mut response = self.request(&login_request).await?; let mut count = 0; - let token_response = loop { + loop { count += 1; let message = LoginResponse::parse_from_bytes(&response)?; if message.has_ok() { - break message.ok().to_owned(); + break Ok(message.ok().to_owned()); } if message.has_error() { @@ -110,33 +98,80 @@ impl Login5Manager { } if message.has_challenges() { - login_request = self.new_login_request(); login_request.login_context = message.login_context.clone(); - Self::handle_challenges(&mut login_request, message.challenges())?; } if count < MAX_LOGIN_TRIES { - response = self.auth_token_request(&login_request).await?; + response = self.request(&login_request).await?; } else { return Err(Error::failed_precondition(format!( "Unable to solve any of {MAX_LOGIN_TRIES} hash cash challenges" ))); } - }; + } + } - let auth_token = Token { - access_token: token_response.access_token.clone(), - expires_in: Duration::from_secs( - token_response - .access_token_expires_in - .try_into() - .unwrap_or(3600), - ), - token_type: "Bearer".to_string(), - scopes: vec![], - timestamp: Instant::now(), - }; + /// Login for android and ios + /// + /// This request doesn't require a connected session as it is the entrypoint for android or ios + /// + /// This request will only work when: + /// - client_id => android or ios | can be easily adjusted in [SessionConfig::default_for_os] + /// - user-agent => android or ios | has to be adjusted in [HttpClient::new](crate::http_client::HttpClient::new) + pub async fn login( + &self, + id: impl Into, + password: impl Into, + ) -> Result<(Token, Vec), Error> { + if !matches!(OS, "android" | "ios") { + // by manipulating the user-agent and client-id it can be also used/tested on desktop + return Err(Error::unavailable( + "login5 login only works for android and ios", + )); + } + + let method = Login_method::Password(Password { + id: id.into(), + password: password.into(), + ..Default::default() + }); + + let token_response = self.login5_request(method).await?; + let auth_token = Self::token_from_login(&token_response); + + Ok((auth_token, token_response.stored_credential)) + } + + /// Retrieve the access_token via login5 + /// + /// This request will only work when the store credentials match the client-id. Meaning that + /// stored credentials generated with the keymaster client-id will not work, for example, with + /// the android client-id. + pub async fn auth_token(&self) -> Result { + let _lock = self.unique_lock().await?; + + let auth_token = self.lock(|inner| { + if let Some(token) = &inner.auth_token { + if token.is_expired() { + inner.auth_token = None; + } + } + inner.auth_token.clone() + }); + + if let Some(auth_token) = auth_token { + return Ok(auth_token); + } + + let method = Login_method::StoredCredential(StoredCredential { + username: self.session().username().to_string(), + data: self.session().auth_data().clone(), + ..Default::default() + }); + + let token_response = self.login5_request(method).await?; + let auth_token = Self::token_from_login(&token_response); self.lock(|inner| { inner.auth_token = Some(auth_token.clone()); @@ -198,4 +233,16 @@ impl Login5Manager { Ok(()) } + + fn token_from_login(login: &LoginOk) -> Token { + Token { + access_token: login.access_token.clone(), + expires_in: Duration::from_secs( + login.access_token_expires_in.try_into().unwrap_or(3600), + ), + token_type: "Bearer".to_string(), + scopes: vec![], + timestamp: Instant::now(), + } + } } From 5e5944a8c8fb1058fe9d7576c0b9407d22c45adc Mon Sep 17 00:00:00 2001 From: photovoltex Date: Tue, 15 Oct 2024 22:44:05 +0200 Subject: [PATCH 09/12] core: revert component changes, pre-acquire tokens --- connect/src/spirc.rs | 20 +++++++++++++++++++- core/src/component.rs | 13 ++----------- core/src/login5.rs | 2 -- core/src/spclient.rs | 2 -- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 6eb3ab82d..68b0c0dec 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -337,6 +337,9 @@ impl Spirc { }), ); + // pre-acquire client_token, preventing multiple request while running + let _ = session.spclient().client_token().await?; + // Connect *after* all message listeners are registered session.connect(credentials, true).await?; @@ -490,7 +493,22 @@ impl SpircTask { }, connection_id_update = self.connection_id_update.next() => match connection_id_update { Some(result) => match result { - Ok(connection_id) => self.handle_connection_id_update(connection_id), + Ok(connection_id) => { + self.handle_connection_id_update(connection_id); + + // pre-acquire access_token, preventing multiple request while running + // pre-acquiring for the access_token will only last for one hour + // + // we need to fire the request after connecting, but can't do it right + // after, because by that we would miss certain packages, like this one + match self.session.login5().auth_token().await { + Ok(_) => debug!("successfully pre-acquire access_token and client_token"), + Err(why) => { + error!("{why}"); + break + } + } + }, Err(e) => error!("could not parse connection ID update: {}", e), } None => { diff --git a/core/src/component.rs b/core/src/component.rs index 5591e30d0..ebe42e8d3 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -1,7 +1,7 @@ macro_rules! component { ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { #[derive(Clone)] - pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>, ::tokio::sync::Semaphore)>); + pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>)>); impl $name { #[allow(dead_code)] pub(crate) fn new(session: $crate::session::SessionWeak) -> $name { @@ -9,7 +9,7 @@ macro_rules! component { $name(::std::sync::Arc::new((session, ::parking_lot::Mutex::new($inner { $($key : $value,)* - }), ::tokio::sync::Semaphore::new(1)))) + })))) } #[allow(dead_code)] @@ -18,15 +18,6 @@ macro_rules! component { f(&mut inner) } - /// See [::tokio::sync::Semaphore] for further infos. - /// - /// The returned permit has to be hold in scope. `let _ = ...?;` will drop the permit on - /// the spot. Instead use `let _lock = ...?;` to hold the permit in scope without using it. - #[allow(dead_code)] - async fn unique_lock(&self) -> Result<::tokio::sync::SemaphorePermit<'_>, $crate::error::Error> { - (self.0).2.acquire().await.map_err(Into::into) - } - #[allow(dead_code)] fn session(&self) -> $crate::session::Session { (self.0).0.upgrade() diff --git a/core/src/login5.rs b/core/src/login5.rs index 643b2816e..2b4f7d1e7 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -149,8 +149,6 @@ impl Login5Manager { /// stored credentials generated with the keymaster client-id will not work, for example, with /// the android client-id. pub async fn auth_token(&self) -> Result { - let _lock = self.unique_lock().await?; - let auth_token = self.lock(|inner| { if let Some(token) = &inner.auth_token { if token.is_expired() { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 2e0419de3..8725c45ee 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -120,8 +120,6 @@ impl SpClient { } pub async fn client_token(&self) -> Result { - let _lock = self.unique_lock().await?; - let client_token = self.lock(|inner| { if let Some(token) = &inner.client_token { if token.is_expired() { From c32feb09cf472296d8e6187cd3ac08af5b9d997f Mon Sep 17 00:00:00 2001 From: photovoltex Date: Tue, 15 Oct 2024 23:28:25 +0200 Subject: [PATCH 10/12] login5: improve logging, errors and remove clones --- core/src/login5.rs | 97 +++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/core/src/login5.rs b/core/src/login5.rs index 2b4f7d1e7..b13ed9853 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -3,13 +3,14 @@ use crate::token::Token; use crate::{util, Error, SessionConfig}; use bytes::Bytes; use http::{header::ACCEPT, HeaderValue, Method, Request}; +use librespot_protocol::login5::login_response::Response; use librespot_protocol::{ client_info::ClientInfo, credentials::{Password, StoredCredential}, hashcash::HashcashSolution, login5::{ - login_request::Login_method, ChallengeSolution, Challenges, LoginError, LoginOk, - LoginRequest, LoginResponse, + login_request::Login_method, ChallengeSolution, LoginError, LoginOk, LoginRequest, + LoginResponse, }, }; use protobuf::well_known_types::duration::Duration as ProtoDuration; @@ -30,15 +31,29 @@ component! { #[derive(Debug, Error)] enum Login5Error { - #[error("Requesting login failed: {0:?}")] + #[error("Login request was denied: {0:?}")] FaultyRequest(LoginError), - #[error("doesn't support code challenge")] + #[error("Code challenge is not supported")] CodeChallenge, + #[error("Tried to acquire token without stored credentials")] + NoStoredCredentials, + #[error("Couldn't successfully authenticate after {0} times")] + RetriesFailed(u8), + #[error("Login via login5 is only allowed for android or ios")] + OnlyForMobile, } impl From for Error { fn from(err: Login5Error) -> Self { - Error::failed_precondition(err) + match err { + Login5Error::NoStoredCredentials | Login5Error::OnlyForMobile => { + Error::unavailable(err) + } + Login5Error::RetriesFailed(_) | Login5Error::FaultyRequest(_) => { + Error::failed_precondition(err) + } + Login5Error::CodeChallenge => Error::unimplemented(err), + } } } @@ -57,13 +72,13 @@ impl Login5Manager { self.session().http_client().request_body(request).await } - fn new_login_request(&self, login: Login_method) -> LoginRequest { + async fn login5_request(&self, login: Login_method) -> Result { let client_id = match OS { "macos" | "windows" => self.session().client_id(), _ => SessionConfig::default().client_id, }; - LoginRequest { + let mut login_request = LoginRequest { client_info: MessageField::some(ClientInfo { client_id, device_id: self.session().device_id().to_string(), @@ -71,11 +86,7 @@ impl Login5Manager { }), login_method: Some(login), ..Default::default() - } - } - - async fn login5_request(&self, login: Login_method) -> Result { - let mut login_request = self.new_login_request(login.clone()); + }; let mut response = self.request(&login_request).await?; let mut count = 0; @@ -84,8 +95,8 @@ impl Login5Manager { count += 1; let message = LoginResponse::parse_from_bytes(&response)?; - if message.has_ok() { - break Ok(message.ok().to_owned()); + if let Some(Response::Ok(ok)) = message.response { + break Ok(ok); } if message.has_error() { @@ -98,16 +109,14 @@ impl Login5Manager { } if message.has_challenges() { - login_request.login_context = message.login_context.clone(); - Self::handle_challenges(&mut login_request, message.challenges())?; + // handles the challenges, and updates the login context with the response + Self::handle_challenges(&mut login_request, message)?; } if count < MAX_LOGIN_TRIES { response = self.request(&login_request).await?; } else { - return Err(Error::failed_precondition(format!( - "Unable to solve any of {MAX_LOGIN_TRIES} hash cash challenges" - ))); + return Err(Login5Error::RetriesFailed(MAX_LOGIN_TRIES).into()); } } } @@ -126,9 +135,7 @@ impl Login5Manager { ) -> Result<(Token, Vec), Error> { if !matches!(OS, "android" | "ios") { // by manipulating the user-agent and client-id it can be also used/tested on desktop - return Err(Error::unavailable( - "login5 login only works for android and ios", - )); + return Err(Login5Error::OnlyForMobile.into()); } let method = Login_method::Password(Password { @@ -138,7 +145,10 @@ impl Login5Manager { }); let token_response = self.login5_request(method).await?; - let auth_token = Self::token_from_login(&token_response); + let auth_token = Self::token_from_login( + token_response.access_token, + token_response.access_token_expires_in, + ); Ok((auth_token, token_response.stored_credential)) } @@ -149,6 +159,11 @@ impl Login5Manager { /// stored credentials generated with the keymaster client-id will not work, for example, with /// the android client-id. pub async fn auth_token(&self) -> Result { + let auth_data = self.session().auth_data(); + if auth_data.is_empty() { + return Err(Login5Error::NoStoredCredentials.into()); + } + let auth_token = self.lock(|inner| { if let Some(token) = &inner.auth_token { if token.is_expired() { @@ -164,37 +179,41 @@ impl Login5Manager { let method = Login_method::StoredCredential(StoredCredential { username: self.session().username().to_string(), - data: self.session().auth_data().clone(), + data: auth_data, ..Default::default() }); let token_response = self.login5_request(method).await?; - let auth_token = Self::token_from_login(&token_response); + let auth_token = Self::token_from_login( + token_response.access_token, + token_response.access_token_expires_in, + ); - self.lock(|inner| { + let token = self.lock(|inner| { inner.auth_token = Some(auth_token.clone()); + inner.auth_token.clone() }); trace!("Got auth token: {:?}", auth_token); - Ok(auth_token) + token.ok_or(Login5Error::NoStoredCredentials.into()) } fn handle_challenges( login_request: &mut LoginRequest, - challenges: &Challenges, + message: LoginResponse, ) -> Result<(), Error> { - info!( - "login5 response has {} challenges...", + let challenges = message.challenges(); + debug!( + "Received {} challenges, solving...", challenges.challenges.len() ); for challenge in &challenges.challenges { if challenge.has_code() { - debug!("empty challenge, skipping"); return Err(Login5Error::CodeChallenge.into()); } else if !challenge.has_hashcash() { - debug!("empty challenge, skipping"); + debug!("Challenge was empty, skipping..."); continue; } @@ -202,14 +221,14 @@ impl Login5Manager { let mut suffix = [0u8; 0x10]; let duration = util::solve_hash_cash( - &login_request.login_context, + &message.login_context, &hash_cash_challenge.prefix, hash_cash_challenge.length, &mut suffix, )?; let (seconds, nanos) = (duration.as_secs() as i64, duration.subsec_nanos() as i32); - info!("solving login5 hashcash took {seconds}.{nanos}s"); + debug!("Solving hashcash took {seconds}s {nanos}ns"); let mut solution = ChallengeSolution::new(); solution.set_hashcash(HashcashSolution { @@ -229,15 +248,15 @@ impl Login5Manager { .push(solution); } + login_request.login_context = message.login_context; + Ok(()) } - fn token_from_login(login: &LoginOk) -> Token { + fn token_from_login(token: String, expires_in: i32) -> Token { Token { - access_token: login.access_token.clone(), - expires_in: Duration::from_secs( - login.access_token_expires_in.try_into().unwrap_or(3600), - ), + access_token: token, + expires_in: Duration::from_secs(expires_in.try_into().unwrap_or(3600)), token_type: "Bearer".to_string(), scopes: vec![], timestamp: Instant::now(), From c15742012edd14a849948d90d897639e55e4a57c Mon Sep 17 00:00:00 2001 From: photovoltex Date: Tue, 15 Oct 2024 23:42:55 +0200 Subject: [PATCH 11/12] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbdf1ef2b..921726fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- [core] The `access_token` for http requests is now acquired by `login5` + ### Added -### Changed +- [core] Add `login` (mobile) and `auth_token` retrieval via login5 ### Removed From 3b394a89f4bcc2835db1e954f2ea678152aa54c8 Mon Sep 17 00:00:00 2001 From: photovoltex Date: Sat, 19 Oct 2024 19:15:29 +0200 Subject: [PATCH 12/12] core: revert version numbers --- core/src/version.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/version.rs b/core/src/version.rs index dd14b7b9c..d3870473d 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -17,16 +17,16 @@ pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); /// The protocol version of the Spotify desktop client. -pub const SPOTIFY_VERSION: u64 = 124200290; +pub const SPOTIFY_VERSION: u64 = 117300517; /// The semantic version of the Spotify desktop client. pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.31.1205.g4d59ad7c"; /// The protocol version of the Spotify mobile app. -pub const SPOTIFY_MOBILE_VERSION: &str = "8.9.82.620"; +pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; /// The user agent to fall back to, if one could not be determined dynamically. -pub const FALLBACK_USER_AGENT: &str = "Spotify/124200290 Linux/0 (librespot)"; +pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)"; pub fn spotify_version() -> String { match std::env::consts::OS {