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