Skip to content

Commit

Permalink
Support client-token header in API requests
Browse files Browse the repository at this point in the history
  • Loading branch information
devgianlu committed Jan 6, 2022
1 parent 5bbad63 commit 08b7890
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 5 deletions.
14 changes: 13 additions & 1 deletion lib/src/main/java/xyz/gianlu/librespot/core/Session.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 devgianlu
* Copyright 2022 devgianlu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -826,6 +826,7 @@ private Inner(@NotNull Connect.DeviceType deviceType, @NotNull String deviceName
public static abstract class AbsBuilder<T extends AbsBuilder> {
protected final Configuration conf;
protected String deviceId = null;
protected String clientToken = null;
protected String deviceName = "librespot-java";
protected Connect.DeviceType deviceType = Connect.DeviceType.COMPUTER;
protected String preferredLocale = "en";
Expand Down Expand Up @@ -874,6 +875,16 @@ public T setDeviceId(@Nullable String deviceId) {
return (T) this;
}

/**
* Sets the client token. If empty, it will be retrieved.
*
* @param token A 168 bytes Base64 encoded string
*/
public T setClientToken(@Nullable String token) {
this.clientToken = token;
return (T) this;
}

/**
* Sets the device type.
*
Expand Down Expand Up @@ -1034,6 +1045,7 @@ public Session create() throws IOException, GeneralSecurityException, SpotifyAut
Session session = new Session(new Inner(deviceType, deviceName, deviceId, preferredLocale, conf));
session.connect();
session.authenticate(loginCredentials);
session.api().setClientToken(clientToken);
return session;
}
}
Expand Down
60 changes: 57 additions & 3 deletions lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 devgianlu
* Copyright 2022 devgianlu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,8 @@
package xyz.gianlu.librespot.dealer;

import com.google.protobuf.Message;
import com.spotify.clienttoken.data.v0.Connectivity;
import com.spotify.clienttoken.http.v0.ClientToken;
import com.spotify.connectstate.Connect;
import com.spotify.extendedmetadata.ExtendedMetadata;
import com.spotify.metadata.Metadata;
Expand All @@ -26,9 +28,10 @@
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.gianlu.librespot.core.ApResolver;
import xyz.gianlu.librespot.Version;
import xyz.gianlu.librespot.core.Session;
import xyz.gianlu.librespot.mercury.MercuryClient;
import xyz.gianlu.librespot.mercury.MercuryRequests;
import xyz.gianlu.librespot.metadata.*;

import java.io.IOException;
Expand All @@ -43,6 +46,7 @@ public final class ApiClient {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiClient.class);
private final Session session;
private final String baseUrl;
private String clientToken = null;

public ApiClient(@NotNull Session session) {
this.session = session;
Expand All @@ -54,7 +58,7 @@ public static RequestBody protoBody(@NotNull Message msg) {
return new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.get("application/protobuf");
return MediaType.get("application/x-protobuf");
}

@Override
Expand All @@ -66,10 +70,17 @@ public void writeTo(@NotNull BufferedSink sink) throws IOException {

@NotNull
private Request buildRequest(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body) throws IOException, MercuryClient.MercuryException {
if (clientToken == null) {
ClientToken.ClientTokenResponse resp = clientToken();
clientToken = resp.getGrantedToken().getToken();
LOGGER.debug("Updated client token: {}", clientToken);
}

Request.Builder request = new Request.Builder();
request.method(method, body);
if (headers != null) request.headers(headers);
request.addHeader("Authorization", "Bearer " + session.tokens().get("playlist-read"));
request.addHeader("client-token", clientToken);
request.url(baseUrl + suffix);
return request.build();
}
Expand Down Expand Up @@ -201,6 +212,49 @@ public ExtendedMetadata.BatchedExtensionResponse getExtendedMetadata(@NotNull Ex
}
}

@NotNull
private ClientToken.ClientTokenResponse clientToken() throws IOException {
ClientToken.ClientTokenRequest protoReq = ClientToken.ClientTokenRequest.newBuilder()
.setRequestType(ClientToken.ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST)
.setClientData(ClientToken.ClientDataRequest.newBuilder()
.setClientId(MercuryRequests.KEYMASTER_CLIENT_ID)
.setClientVersion(Version.versionNumber())
.setConnectivitySdkData(Connectivity.ConnectivitySdkData.newBuilder()
.setDeviceId(session.deviceId())
.setPlatformSpecificData(Connectivity.PlatformSpecificData.newBuilder()
.setWindows(Connectivity.NativeWindowsData.newBuilder()
.setSomething1(10)

This comment has been minimized.

Copy link
@roderickvd

roderickvd Jan 22, 2022

Member

This seems to be the OS version and field 3 is the build number.

This comment has been minimized.

Copy link
@devgianlu

devgianlu Jan 22, 2022

Author Member

Yeah I think there are updated proto definitions somewhere.

.setSomething3(21370)
.setSomething4(2)
.setSomething6(9)
.setSomething7(332)
.setSomething8(34404)
.setSomething10(true)
.build())
.build())
.build())
.build())
.build();

Request.Builder req = new Request.Builder()
.url("https://clienttoken.spotify.com/v1/clienttoken")
.header("Accept", "application/x-protobuf")
.header("Content-Encoding", "")
.post(protoBody(protoReq));

try (Response resp = session.client().newCall(req.build()).execute()) {
StatusCodeException.checkStatus(resp);

ResponseBody body = resp.body();
if (body == null) throw new IOException();
return ClientToken.ClientTokenResponse.parseFrom(body.byteStream());
}
}

public void setClientToken(@Nullable String clientToken) {
this.clientToken = clientToken;
}

public static class StatusCodeException extends IOException {
public final int code;

Expand Down
125 changes: 125 additions & 0 deletions lib/src/main/proto/client_token.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
syntax = "proto3";

package spotify.clienttoken.http.v0;

import "connectivity.proto";

option optimize_for = CODE_SIZE;
option java_package = "com.spotify.clienttoken.http.v0";

message ClientTokenRequest {
ClientTokenRequestType request_type = 1;

oneof request {
ClientDataRequest client_data = 2;
ChallengeAnswersRequest challenge_answers = 3;
}
}

message ClientDataRequest {
string client_version = 1;
string client_id = 2;

oneof data {
data.v0.ConnectivitySdkData connectivity_sdk_data = 3;
}
}

message ChallengeAnswersRequest {
string state = 1;
repeated ChallengeAnswer answers = 2;
}

message ClientTokenResponse {
ClientTokenResponseType response_type = 1;

oneof response {
GrantedTokenResponse granted_token = 2;
ChallengesResponse challenges = 3;
}
}

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 {
string state = 1;
repeated Challenge challenges = 2;
}

message ClientSecretParameters {
string salt = 1;
}

message EvaluateJSParameters {
string code = 1;
repeated string libraries = 2;
}

message HashCashParameters {
int32 length = 1;
string prefix = 2;
}

message Challenge {
ChallengeType type = 1;

oneof parameters {
ClientSecretParameters client_secret_parameters = 2;
EvaluateJSParameters evaluate_js_parameters = 3;
HashCashParameters evaluate_hashcash_parameters = 4;
}
}

message ClientSecretHMACAnswer {
string hmac = 1;
}

message EvaluateJSAnswer {
string result = 1;
}

message HashCashAnswer {
string suffix = 1;
}

message ChallengeAnswer {
ChallengeType ChallengeType = 1;

oneof answer {
ClientSecretHMACAnswer client_secret = 2;
EvaluateJSAnswer evaluate_js = 3;
HashCashAnswer hash_cash = 4;
}
}

message ClientTokenBadRequest {
string message = 1;
}

enum ClientTokenRequestType {
REQUEST_UNKNOWN = 0;
REQUEST_CLIENT_DATA_REQUEST = 1;
REQUEST_CHALLENGE_ANSWERS_REQUEST = 2;
}

enum ClientTokenResponseType {
RESPONSE_UNKNOWN = 0;
RESPONSE_GRANTED_TOKEN_RESPONSE = 1;
RESPONSE_CHALLENGES_RESPONSE = 2;
}

enum ChallengeType {
CHALLENGE_UNKNOWN = 0;
CHALLENGE_CLIENT_SECRET_HMAC = 1;
CHALLENGE_EVALUATE_JS = 2;
CHALLENGE_HASH_CASH = 3;
}
51 changes: 51 additions & 0 deletions lib/src/main/proto/connectivity.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
syntax = "proto3";

package spotify.clienttoken.data.v0;

option optimize_for = CODE_SIZE;
option java_package = "com.spotify.clienttoken.data.v0";

message ConnectivitySdkData {
PlatformSpecificData platform_specific_data = 1;
string device_id = 2;
}

message PlatformSpecificData {
oneof data {
NativeAndroidData android = 1;
NativeIOSData ios = 2;
NativeWindowsData windows = 4;
}
}

message NativeAndroidData {
int32 major_sdk_version = 1;
int32 minor_sdk_version = 2;
int32 patch_sdk_version = 3;
uint32 api_version = 4;
Screen screen_dimensions = 5;
}

message NativeIOSData {
int32 user_interface_idiom = 1;
bool target_iphone_simulator = 2;
string hw_machine = 3;
string system_version = 4;
string simulator_model_identifier = 5;
}

message NativeWindowsData {
int32 something1 = 1;
int32 something3 = 3;
int32 something4 = 4;
int32 something6 = 6;
int32 something7 = 7;
int32 something8 = 8;
bool something10 = 10;
}

message Screen {
int32 width = 1;
int32 height = 2;
int32 density = 3;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 devgianlu
* Copyright 2022 devgianlu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -279,6 +279,12 @@ private String deviceId() {
return val == null || val.isEmpty() ? null : val;
}

@Nullable
private String clientToken() {
String val = config.get("clientToken");
return val == null || val.isEmpty() ? null : val;
}

@NotNull
private String deviceName() {
return config.get("deviceName");
Expand Down Expand Up @@ -363,6 +369,7 @@ public ZeroconfServer.Builder initZeroconfBuilder() {
.setPreferredLocale(preferredLocale())
.setDeviceType(deviceType())
.setDeviceName(deviceName())
.setClientToken(clientToken())
.setDeviceId(deviceId())
.setListenPort(config.get("zeroconf.listenPort"));

Expand All @@ -378,6 +385,7 @@ public Session.Builder initSessionBuilder() throws IOException, GeneralSecurityE
.setPreferredLocale(preferredLocale())
.setDeviceType(deviceType())
.setDeviceName(deviceName())
.setClientToken(clientToken())
.setDeviceId(deviceId());

switch (authStrategy()) {
Expand Down
1 change: 1 addition & 0 deletions player/src/main/resources/default.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
deviceId = "" ### Device ID (40 chars, leave empty for random) ###
clientToken = "" ### Client Token (168 bytes Base64 encoded) ###
deviceName = "librespot-java" ### Device name ###
deviceType = "COMPUTER" ### Device type (COMPUTER, TABLET, SMARTPHONE, SPEAKER, TV, AVR, STB, AUDIO_DONGLE, GAME_CONSOLE, CAST_VIDEO, CAST_AUDIO, AUTOMOBILE, WEARABLE, UNKNOWN_SPOTIFY, CAR_THING, UNKNOWN) ###
preferredLocale = "en" ### Preferred locale ###
Expand Down

0 comments on commit 08b7890

Please sign in to comment.