diff --git a/core/src/session.rs b/core/src/session.rs old mode 100755 new mode 100644 index 07b193bf3..de39dc196 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -77,6 +77,7 @@ struct SessionData { client_brand_name: String, client_model_name: String, connection_id: String, + auth_data: Vec, time_delta: i64, invalid: bool, user_data: UserData, @@ -175,6 +176,7 @@ impl Session { let username = reusable_credentials.username.as_ref().expect("Username"); info!("Authenticated as \"{}\" !", username); self.set_username(username); + self.set_auth_data(&reusable_credentials.auth_data); if let Some(cache) = self.cache() { if store_credentials { let cred_changed = cache @@ -472,6 +474,14 @@ impl Session { username.clone_into(&mut self.0.data.write().user_data.canonical_username); } + pub fn auth_data(&self) -> Vec { + self.0.data.read().auth_data.clone() + } + + pub fn set_auth_data(&self, auth_data: &Vec,) { + self.0.data.write().auth_data = auth_data.clone(); + } + pub fn country(&self) -> String { self.0.data.read().user_data.country.clone() } diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 156cf9c82..9f26e4191 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,90 @@ 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::from(body))?; + + 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 +554,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