-
Notifications
You must be signed in to change notification settings - Fork 633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Kingosticks change: Obtain spclient access token using login5 instead of keymaster (Fixes librespot-org#1179) #1220
Changes from all commits
96e02fd
ba08273
f065908
a54cc8f
f815318
40c2a4d
87dddf2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,7 @@ use crate::{ | |
}, | ||
connect::PutStateRequest, | ||
extended_metadata::BatchedEntityRequest, | ||
login5::{LoginRequest, LoginResponse}, | ||
}, | ||
token::Token, | ||
version::spotify_version, | ||
|
@@ -43,6 +44,7 @@ component! { | |
accesspoint: Option<SocketAddress> = None, | ||
strategy: RequestStrategy = RequestStrategy::default(), | ||
client_token: Option<Token> = None, | ||
auth_token: Option<Token> = None, | ||
} | ||
} | ||
|
||
|
@@ -148,6 +150,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::from(body))?; | ||
|
||
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_blob().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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As is, there is still no hash cash handling so this error message makes no sense. |
||
))); | ||
} | ||
}; | ||
|
||
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()?; | ||
|
||
|
@@ -462,27 +549,22 @@ impl SpClient { | |
.body(Body::from(body.to_owned()))?; | ||
|
||
// Reconnection logic: keep getting (cached) tokens because they might have expired. | ||
let token = self | ||
.session() | ||
.token_provider() | ||
.get_token("playlist-read") | ||
.await?; | ||
let auth_token = self.auth_token().await?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
EDIT: Cancel that, I forgot what |
||
|
||
let headers_mut = request.headers_mut(); | ||
if let Some(ref hdrs) = headers { | ||
*headers_mut = hdrs.clone(); | ||
} | ||
headers_mut.insert( | ||
AUTHORIZATION, | ||
HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?, | ||
HeaderValue::from_str(&format!( | ||
"{} {}", | ||
auth_token.token_type, auth_token.access_token, | ||
))?, | ||
); | ||
|
||
if let Ok(client_token) = self.client_token().await { | ||
headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?); | ||
} else { | ||
// currently these endpoints seem to work fine without it | ||
warn!("Unable to get client token. Trying to continue without..."); | ||
} | ||
let client_token = self.client_token().await?; | ||
headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changes how we handle the failing case. The old code used to continue anyway, maybe that's still what we want. I don't remember why I thought I had to change it... |
||
|
||
last_response = self.session().http_client().request_body(request).await; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally this would be improved to check for the error cases where a retry is not sensible:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the very least, a retry in the face of
BAD_REQUEST
or aTOO_MANY_ATTEMPTS
error response is a bit rude.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simple exponential backup would be nice too, ideally implemented in a reusable way. But could leave that to another day, I'm just trying to keep a record of what was left unfinished.
EDIT: Actually, there is a solid handling of the HTTP side of this in our
HttpClient
. Given this is protobuf over HTTP, it depends what Spotify do. i.e. do they reflect protobuf errors in the HTTP response? I don't think I ever looked into that. @roderickvd do you know?