mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Get access token via login5 (#1344)
* core: Obtain spclient access token using login5 instead of keymaster (Fixes #1179) * core: move solving hashcash into util * login5: add login for mobile --------- Co-authored-by: Nick Steel <nick@nsteel.co.uk>
This commit is contained in:
parent
d8e84238ab
commit
4580dab73f
9 changed files with 362 additions and 55 deletions
|
@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- [core] The `access_token` for http requests is now acquired by `login5`
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
### Changed
|
- [core] Add `login` (mobile) and `auth_token` retrieval via login5
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
|
@ -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
|
// Connect *after* all message listeners are registered
|
||||||
session.connect(credentials, true).await?;
|
session.connect(credentials, true).await?;
|
||||||
|
|
||||||
|
@ -490,7 +493,22 @@ impl SpircTask {
|
||||||
},
|
},
|
||||||
connection_id_update = self.connection_id_update.next() => match connection_id_update {
|
connection_id_update = self.connection_id_update.next() => match connection_id_update {
|
||||||
Some(result) => match result {
|
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),
|
Err(e) => error!("could not parse connection ID update: {}", e),
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
|
@ -109,7 +109,9 @@ impl HttpClient {
|
||||||
let os_version = System::os_version().unwrap_or_else(|| zero_str.clone());
|
let os_version = System::os_version().unwrap_or_else(|| zero_str.clone());
|
||||||
|
|
||||||
let (spotify_platform, os_version) = match OS {
|
let (spotify_platform, os_version) = match OS {
|
||||||
|
// example os_version: 30
|
||||||
"android" => ("Android", os_version),
|
"android" => ("Android", os_version),
|
||||||
|
// example os_version: 17
|
||||||
"ios" => ("iOS", os_version),
|
"ios" => ("iOS", os_version),
|
||||||
"macos" => ("OSX", zero_str),
|
"macos" => ("OSX", zero_str),
|
||||||
"windows" => ("Win32", zero_str),
|
"windows" => ("Win32", zero_str),
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub mod diffie_hellman;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod file_id;
|
pub mod file_id;
|
||||||
pub mod http_client;
|
pub mod http_client;
|
||||||
|
pub mod login5;
|
||||||
pub mod mercury;
|
pub mod mercury;
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
mod proxytunnel;
|
mod proxytunnel;
|
||||||
|
|
265
core/src/login5.rs
Normal file
265
core/src/login5.rs
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
use crate::spclient::CLIENT_TOKEN;
|
||||||
|
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, LoginError, LoginOk, 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 {
|
||||||
|
auth_token: Option<Token> = None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum Login5Error {
|
||||||
|
#[error("Login request was denied: {0:?}")]
|
||||||
|
FaultyRequest(LoginError),
|
||||||
|
#[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<Login5Error> for Error {
|
||||||
|
fn from(err: Login5Error) -> Self {
|
||||||
|
match err {
|
||||||
|
Login5Error::NoStoredCredentials | Login5Error::OnlyForMobile => {
|
||||||
|
Error::unavailable(err)
|
||||||
|
}
|
||||||
|
Login5Error::RetriesFailed(_) | Login5Error::FaultyRequest(_) => {
|
||||||
|
Error::failed_precondition(err)
|
||||||
|
}
|
||||||
|
Login5Error::CodeChallenge => Error::unimplemented(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Login5Manager {
|
||||||
|
async fn request(&self, message: &LoginRequest) -> Result<Bytes, Error> {
|
||||||
|
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(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?)
|
||||||
|
.body(body.into())?;
|
||||||
|
|
||||||
|
self.session().http_client().request_body(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login5_request(&self, login: Login_method) -> Result<LoginOk, Error> {
|
||||||
|
let client_id = match OS {
|
||||||
|
"macos" | "windows" => self.session().client_id(),
|
||||||
|
_ => SessionConfig::default().client_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut login_request = 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 response = self.request(&login_request).await?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
let message = LoginResponse::parse_from_bytes(&response)?;
|
||||||
|
if let Some(Response::Ok(ok)) = message.response {
|
||||||
|
break Ok(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
// 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(Login5Error::RetriesFailed(MAX_LOGIN_TRIES).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
password: impl Into<String>,
|
||||||
|
) -> Result<(Token, Vec<u8>), Error> {
|
||||||
|
if !matches!(OS, "android" | "ios") {
|
||||||
|
// by manipulating the user-agent and client-id it can be also used/tested on desktop
|
||||||
|
return Err(Login5Error::OnlyForMobile.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
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.access_token,
|
||||||
|
token_response.access_token_expires_in,
|
||||||
|
);
|
||||||
|
|
||||||
|
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<Token, Error> {
|
||||||
|
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() {
|
||||||
|
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: auth_data,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let token_response = self.login5_request(method).await?;
|
||||||
|
let auth_token = Self::token_from_login(
|
||||||
|
token_response.access_token,
|
||||||
|
token_response.access_token_expires_in,
|
||||||
|
);
|
||||||
|
|
||||||
|
let token = self.lock(|inner| {
|
||||||
|
inner.auth_token = Some(auth_token.clone());
|
||||||
|
inner.auth_token.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
trace!("Got auth token: {:?}", auth_token);
|
||||||
|
|
||||||
|
token.ok_or(Login5Error::NoStoredCredentials.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_challenges(
|
||||||
|
login_request: &mut LoginRequest,
|
||||||
|
message: LoginResponse,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let challenges = message.challenges();
|
||||||
|
debug!(
|
||||||
|
"Received {} challenges, solving...",
|
||||||
|
challenges.challenges.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
for challenge in &challenges.challenges {
|
||||||
|
if challenge.has_code() {
|
||||||
|
return Err(Login5Error::CodeChallenge.into());
|
||||||
|
} else if !challenge.has_hashcash() {
|
||||||
|
debug!("Challenge was empty, skipping...");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash_cash_challenge = challenge.hashcash();
|
||||||
|
|
||||||
|
let mut suffix = [0u8; 0x10];
|
||||||
|
let duration = util::solve_hash_cash(
|
||||||
|
&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);
|
||||||
|
debug!("Solving hashcash took {seconds}s {nanos}ns");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
login_request.login_context = message.login_context;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_from_login(token: String, expires_in: i32) -> Token {
|
||||||
|
Token {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ use crate::{
|
||||||
config::SessionConfig,
|
config::SessionConfig,
|
||||||
connection::{self, AuthenticationError, Transport},
|
connection::{self, AuthenticationError, Transport},
|
||||||
http_client::HttpClient,
|
http_client::HttpClient,
|
||||||
|
login5::Login5Manager,
|
||||||
mercury::MercuryManager,
|
mercury::MercuryManager,
|
||||||
packet::PacketType,
|
packet::PacketType,
|
||||||
protocol::keyexchange::ErrorCode,
|
protocol::keyexchange::ErrorCode,
|
||||||
|
@ -101,6 +102,7 @@ struct SessionInternal {
|
||||||
mercury: OnceCell<MercuryManager>,
|
mercury: OnceCell<MercuryManager>,
|
||||||
spclient: OnceCell<SpClient>,
|
spclient: OnceCell<SpClient>,
|
||||||
token_provider: OnceCell<TokenProvider>,
|
token_provider: OnceCell<TokenProvider>,
|
||||||
|
login5: OnceCell<Login5Manager>,
|
||||||
cache: Option<Arc<Cache>>,
|
cache: Option<Arc<Cache>>,
|
||||||
|
|
||||||
handle: tokio::runtime::Handle,
|
handle: tokio::runtime::Handle,
|
||||||
|
@ -141,6 +143,7 @@ impl Session {
|
||||||
mercury: OnceCell::new(),
|
mercury: OnceCell::new(),
|
||||||
spclient: OnceCell::new(),
|
spclient: OnceCell::new(),
|
||||||
token_provider: OnceCell::new(),
|
token_provider: OnceCell::new(),
|
||||||
|
login5: OnceCell::new(),
|
||||||
handle: tokio::runtime::Handle::current(),
|
handle: tokio::runtime::Handle::current(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -310,6 +313,12 @@ impl Session {
|
||||||
.get_or_init(|| TokenProvider::new(self.weak()))
|
.get_or_init(|| TokenProvider::new(self.weak()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn login5(&self) -> &Login5Manager {
|
||||||
|
self.0
|
||||||
|
.login5
|
||||||
|
.get_or_init(|| Login5Manager::new(self.weak()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn time_delta(&self) -> i64 {
|
pub fn time_delta(&self) -> i64 {
|
||||||
self.0.data.read().time_delta
|
self.0.data.read().time_delta
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use data_encoding::HEXUPPER_PERMISSIVE;
|
use data_encoding::HEXUPPER_PERMISSIVE;
|
||||||
use futures_util::future::IntoStream;
|
use futures_util::future::IntoStream;
|
||||||
|
@ -16,7 +15,6 @@ use hyper::{
|
||||||
use hyper_util::client::legacy::ResponseFuture;
|
use hyper_util::client::legacy::ResponseFuture;
|
||||||
use protobuf::{Enum, Message, MessageFull};
|
use protobuf::{Enum, Message, MessageFull};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use sha1::{Digest, Sha1};
|
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -35,6 +33,7 @@ use crate::{
|
||||||
extended_metadata::BatchedEntityRequest,
|
extended_metadata::BatchedEntityRequest,
|
||||||
},
|
},
|
||||||
token::Token,
|
token::Token,
|
||||||
|
util,
|
||||||
version::spotify_semantic_version,
|
version::spotify_semantic_version,
|
||||||
Error, FileId, SpotifyId,
|
Error, FileId, SpotifyId,
|
||||||
};
|
};
|
||||||
|
@ -50,7 +49,7 @@ component! {
|
||||||
pub type SpClientResult = Result<Bytes, Error>;
|
pub type SpClientResult = Result<Bytes, Error>;
|
||||||
|
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
#[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)]
|
#[derive(Debug, Error)]
|
||||||
pub enum SpClientError {
|
pub enum SpClientError {
|
||||||
|
@ -108,47 +107,6 @@ impl SpClient {
|
||||||
Ok(format!("https://{}:{}", ap.0, ap.1))
|
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<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
|
async fn client_token_request<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
|
||||||
let body = message.write_to_bytes()?;
|
let body = message.write_to_bytes()?;
|
||||||
|
|
||||||
|
@ -233,10 +191,12 @@ impl SpClient {
|
||||||
ios_data.user_interface_idiom = 0;
|
ios_data.user_interface_idiom = 0;
|
||||||
ios_data.target_iphone_simulator = false;
|
ios_data.target_iphone_simulator = false;
|
||||||
ios_data.hw_machine = "iPhone14,5".to_string();
|
ios_data.hw_machine = "iPhone14,5".to_string();
|
||||||
|
// example system_version: 17
|
||||||
ios_data.system_version = os_version;
|
ios_data.system_version = os_version;
|
||||||
}
|
}
|
||||||
"android" => {
|
"android" => {
|
||||||
let android_data = platform_data.mut_android();
|
let android_data = platform_data.mut_android();
|
||||||
|
// example android_version: 30
|
||||||
android_data.android_version = os_version;
|
android_data.android_version = os_version;
|
||||||
android_data.api_version = 31;
|
android_data.api_version = 31;
|
||||||
"Pixel".clone_into(&mut android_data.device_name);
|
"Pixel".clone_into(&mut android_data.device_name);
|
||||||
|
@ -293,7 +253,7 @@ impl SpClient {
|
||||||
let length = hash_cash_challenge.length;
|
let length = hash_cash_challenge.length;
|
||||||
|
|
||||||
let mut suffix = [0u8; 0x10];
|
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 {
|
match answer {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
@ -468,11 +428,7 @@ impl SpClient {
|
||||||
.body(body.to_owned().into())?;
|
.body(body.to_owned().into())?;
|
||||||
|
|
||||||
// Reconnection logic: keep getting (cached) tokens because they might have expired.
|
// Reconnection logic: keep getting (cached) tokens because they might have expired.
|
||||||
let token = self
|
let token = self.session().login5().auth_token().await?;
|
||||||
.session()
|
|
||||||
.token_provider()
|
|
||||||
.get_token("playlist-read")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let headers_mut = request.headers_mut();
|
let headers_mut = request.headers_mut();
|
||||||
if let Some(ref hdrs) = headers {
|
if let Some(ref hdrs) = headers {
|
||||||
|
|
|
@ -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::{
|
use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
mem,
|
mem,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures_core::ready;
|
|
||||||
use futures_util::{future, FutureExt, Sink, SinkExt};
|
|
||||||
use tokio::{task::JoinHandle, time::timeout};
|
use tokio::{task::JoinHandle, time::timeout};
|
||||||
|
|
||||||
/// Returns a future that will flush the sink, even if flushing is temporarily completed.
|
/// Returns a future that will flush the sink, even if flushing is temporarily completed.
|
||||||
|
@ -120,3 +124,44 @@ impl<T: Seq> SeqGenerator<T> {
|
||||||
mem::replace(&mut self.0, value)
|
mem::replace(&mut self.0, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn solve_hash_cash(
|
||||||
|
ctx: &[u8],
|
||||||
|
prefix: &[u8],
|
||||||
|
length: i32,
|
||||||
|
dst: &mut [u8],
|
||||||
|
) -> Result<Duration, 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(now.elapsed())
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,13 @@ fn compile() {
|
||||||
proto_dir.join("playlist_permission.proto"),
|
proto_dir.join("playlist_permission.proto"),
|
||||||
proto_dir.join("playlist4_external.proto"),
|
proto_dir.join("playlist4_external.proto"),
|
||||||
proto_dir.join("spotify/clienttoken/v0/clienttoken_http.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("storage-resolve.proto"),
|
||||||
proto_dir.join("user_attributes.proto"),
|
proto_dir.join("user_attributes.proto"),
|
||||||
// TODO: remove these legacy protobufs when we are on the new API completely
|
// TODO: remove these legacy protobufs when we are on the new API completely
|
||||||
|
|
Loading…
Reference in a new issue