Skip to content

Commit

Permalink
core: Obtain spclient access token using login5 instead of keymaster (F…
Browse files Browse the repository at this point in the history
  • Loading branch information
kingosticks committed Aug 14, 2024
1 parent de2e7b3 commit 31dd847
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 5 deletions.
93 changes: 88 additions & 5 deletions core/src/spclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use crate::{
},
connect::PutStateRequest,
extended_metadata::BatchedEntityRequest,
login5::{LoginRequest, LoginResponse},
},
token::Token,
version::spotify_semantic_version,
Expand All @@ -44,6 +45,7 @@ component! {
accesspoint: Option<SocketAddress> = None,
strategy: RequestStrategy = RequestStrategy::default(),
client_token: Option<Token> = None,
auth_token: Option<Token> = None,
}
}

Expand Down Expand Up @@ -149,6 +151,91 @@ impl SpClient {
Ok(())
}

async fn auth_token_request<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
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<Token, Error> {
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<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
let body = message.write_to_bytes()?;

Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions protocol/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 31dd847

Please sign in to comment.