diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 1aa0da00..5ca736d9 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -5,7 +5,7 @@ use futures_util::future::IntoStream; use http::header::HeaderValue; use hyper::{ client::ResponseFuture, - header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, + header::{ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_TYPE, RANGE}, Body, HeaderMap, Method, Request, }; use protobuf::Message; @@ -17,16 +17,19 @@ use crate::{ cdn_url::CdnUrl, error::ErrorKind, protocol::{ - canvaz::EntityCanvazRequest, connect::PutStateRequest, + canvaz::EntityCanvazRequest, + clienttoken_http::{ClientTokenRequest, ClientTokenRequestType, ClientTokenResponse}, + connect::PutStateRequest, extended_metadata::BatchedEntityRequest, }, - Error, FileId, SpotifyId, + version, Error, FileId, SpotifyId, }; component! { SpClient : SpClientInner { accesspoint: Option = None, strategy: RequestStrategy = RequestStrategy::default(), + client_token: String = String::new(), } } @@ -88,6 +91,51 @@ impl SpClient { Ok(format!("https://{}:{}", ap.0, ap.1)) } + pub async fn client_token(&self) -> Result { + // TODO: implement expiry + let client_token = self.lock(|inner| inner.client_token.clone()); + if !client_token.is_empty() { + return Ok(client_token); + } + + let mut message = ClientTokenRequest::new(); + message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); + + let client_data = message.mut_client_data(); + client_data.set_client_id(self.session().client_id()); + client_data.set_client_version(version::SEMVER.to_string()); + + let connectivity_data = client_data.mut_connectivity_sdk_data(); + connectivity_data.set_device_id(self.session().device_id().to_string()); + + let platform_data = connectivity_data.mut_platform_specific_data(); + let windows_data = platform_data.mut_windows(); + windows_data.set_os_version(10); + windows_data.set_os_build(21370); + windows_data.set_unknown_value_4(2); + windows_data.set_unknown_value_6(9); + windows_data.set_unknown_value_7(332); + windows_data.set_unknown_value_8(34404); + windows_data.set_unknown_value_10(true); + + let body = protobuf::text_format::print_to_string(&message); + + let request = Request::builder() + .method(&Method::POST) + .uri("https://clienttoken.spotify.com/v1/clienttoken") + .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) + .header(CONTENT_ENCODING, HeaderValue::from_static("")) + .body(Body::from(body))?; + + let response = self.session().http_client().request_body(request).await?; + let response = ClientTokenResponse::parse_from_bytes(&response)?; + + let client_token = response.get_granted_token().get_token().to_owned(); + self.lock(|inner| inner.client_token = client_token.clone()); + + Ok(client_token) + } + pub async fn request_with_protobuf( &self, method: &Method, @@ -100,7 +148,7 @@ impl SpClient { let mut headers = headers.unwrap_or_else(HeaderMap::new); headers.insert( CONTENT_TYPE, - HeaderValue::from_static("application/protobuf"), + HeaderValue::from_static("application/x-protobuf"), ); self.request(method, endpoint, Some(headers), Some(body)) @@ -132,6 +180,9 @@ impl SpClient { let body = body.unwrap_or_else(String::new); + let client_token = self.client_token().await; + trace!("CLIENT TOKEN: {:?}", client_token); + loop { tries += 1; diff --git a/protocol/build.rs b/protocol/build.rs index aa107607..b7c3f44d 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -17,6 +17,7 @@ fn compile() { let files = &[ proto_dir.join("connect.proto"), + proto_dir.join("connectivity.proto"), proto_dir.join("devices.proto"), proto_dir.join("entity_extension_data.proto"), proto_dir.join("extended_metadata.proto"), @@ -26,6 +27,7 @@ fn compile() { proto_dir.join("playlist_annotate3.proto"), proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), + proto_dir.join("spotify/clienttoken/v0/clienttoken_http.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 diff --git a/protocol/proto/connectivity.proto b/protocol/proto/connectivity.proto index f7e64a3c..757f48c4 100644 --- a/protocol/proto/connectivity.proto +++ b/protocol/proto/connectivity.proto @@ -1,5 +1,3 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - syntax = "proto3"; package spotify.clienttoken.data.v0; @@ -17,6 +15,7 @@ message PlatformSpecificData { oneof data { NativeAndroidData android = 1; NativeIOSData ios = 2; + NativeWindowsData windows = 4; } } @@ -36,6 +35,16 @@ message NativeIOSData { string simulator_model_identifier = 5; } +message NativeWindowsData { + int32 os_version = 1; + int32 os_build = 3; + int32 unknown_value_4 = 4; + int32 unknown_value_6 = 6; + int32 unknown_value_7 = 7; + int32 unknown_value_8 = 8; + bool unknown_value_10 = 10; +} + message Screen { int32 width = 1; int32 height = 2; diff --git a/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto b/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto index 92d50f42..c60cdcaf 100644 --- a/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto +++ b/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto @@ -1,5 +1,3 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - syntax = "proto3"; package spotify.clienttoken.http.v0; @@ -24,7 +22,7 @@ message ClientDataRequest { string client_id = 2; oneof data { - data.v0.ConnectivitySdkData connectivity_sdk_data = 3; + spotify.clienttoken.data.v0.ConnectivitySdkData connectivity_sdk_data = 3; } } @@ -42,10 +40,15 @@ message ClientTokenResponse { } } +message TokenDomain { + string domain = 1; +} + message GrantedTokenResponse { string token = 1; int32 expires_after_seconds = 2; int32 refresh_after_seconds = 3; + repeated TokenDomain domains = 4; } message ChallengesResponse {