MacOS auth for Spotify

This commit is contained in:
philippe44
2023-07-25 19:08:30 -07:00
parent 8b764c0c2d
commit bdceb2d832
7 changed files with 106 additions and 51 deletions

View File

@@ -27,7 +27,7 @@ HTTPClient::Response::~Response() {
void HTTPClient::Response::rawRequest(const std::string& url, void HTTPClient::Response::rawRequest(const std::string& url,
const std::string& method, const std::string& method,
const std::string& content, const std::vector<uint8_t>& content,
Headers& headers) { Headers& headers) {
urlParser = bell::URLParser::parse(url); urlParser = bell::URLParser::parse(url);
@@ -50,6 +50,10 @@ void HTTPClient::Response::rawRequest(const std::string& url,
} }
socketStream << reqEnd; socketStream << reqEnd;
// Write request body
if (content.size() > 0) {
socketStream.write((const char*)content.data(), content.size());
}
socketStream.flush(); socketStream.flush();
// Parse response // Parse response
@@ -115,7 +119,13 @@ void HTTPClient::Response::readResponseHeaders() {
void HTTPClient::Response::get(const std::string& url, Headers headers) { void HTTPClient::Response::get(const std::string& url, Headers headers) {
std::string method = "GET"; std::string method = "GET";
return this->rawRequest(url, method, "", headers); return this->rawRequest(url, method, {}, headers);
}
void HTTPClient::Response::post(const std::string& url, Headers headers,
const std::vector<uint8_t>& body) {
std::string method = "POST";
return this->rawRequest(url, method, body, headers);
} }
size_t HTTPClient::Response::contentLength() { size_t HTTPClient::Response::contentLength() {

View File

@@ -55,8 +55,10 @@ class HTTPClient {
void connect(const std::string& url); void connect(const std::string& url);
void rawRequest(const std::string& method, const std::string& url, void rawRequest(const std::string& method, const std::string& url,
const std::string& content, Headers& headers); const std::vector<uint8_t>& content, Headers& headers);
void get(const std::string& url, Headers headers = {}); void get(const std::string& url, Headers headers = {});
void post(const std::string& url, Headers headers = {},
const std::vector<uint8_t>& body = {});
std::string_view body(); std::string_view body();
std::vector<uint8_t> bytes(); std::vector<uint8_t> bytes();
@@ -102,5 +104,14 @@ class HTTPClient {
response->get(url, headers); response->get(url, headers);
return response; return response;
} }
static std::unique_ptr<Response> post(const std::string& url,
Headers headers = {},
const std::vector<uint8_t>& body = {}) {
auto response = std::make_unique<Response>();
response->connect(url);
response->post(url, headers, body);
return response;
}
}; };
} // namespace bell } // namespace bell

View File

@@ -1,13 +1,10 @@
#pragma once #pragma once
#include <atomic> // or std::atomic
#include <functional> // for function #include <functional> // for function
#include <memory> // for shared_ptr #include <memory> // for shared_ptr
#include <string> // for string #include <string> // for string
#include <atomic>
namespace bell {
class WrappedSemaphore;
};
namespace cspot { namespace cspot {
struct Context; struct Context;
@@ -35,7 +32,6 @@ class AccessKeyFetcher {
private: private:
std::shared_ptr<cspot::Context> ctx; std::shared_ptr<cspot::Context> ctx;
std::shared_ptr<bell::WrappedSemaphore> updateSemaphore;
std::atomic<bool> keyPending = false; std::atomic<bool> keyPending = false;
std::string accessKey; std::string accessKey;

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <stdint.h>
#include <memory> #include <memory>
#include "LoginBlob.h" #include "LoginBlob.h"
@@ -14,6 +15,7 @@ struct Context {
AudioFormat audioFormat = AudioFormat::AudioFormat_OGG_VORBIS_160; AudioFormat audioFormat = AudioFormat::AudioFormat_OGG_VORBIS_160;
std::string deviceId; std::string deviceId;
std::string deviceName; std::string deviceName;
std::vector<uint8_t> authData;
int volume; int volume;
std::string username; std::string username;
@@ -33,6 +35,7 @@ struct Context {
ctx->session = std::make_shared<MercurySession>(ctx->timeProvider); ctx->session = std::make_shared<MercurySession>(ctx->timeProvider);
ctx->config.deviceId = blob->getDeviceId(); ctx->config.deviceId = blob->getDeviceId();
ctx->config.deviceName = blob->getDeviceName(); ctx->config.deviceName = blob->getDeviceName();
ctx->config.authData = blob->authData;
ctx->config.volume = 0; ctx->config.volume = 0;
ctx->config.username = blob->getUserName(); ctx->config.username = blob->getUserName();

View File

@@ -8,12 +8,15 @@
#include "BellLogger.h" // for AbstractLogger #include "BellLogger.h" // for AbstractLogger
#include "CSpotContext.h" // for Context #include "CSpotContext.h" // for Context
#include "HTTPClient.h"
#include "Logger.h" // for CSPOT_LOG #include "Logger.h" // for CSPOT_LOG
#include "MercurySession.h" // for MercurySession, MercurySession::Res... #include "MercurySession.h" // for MercurySession, MercurySession::Res...
#include "NanoPBExtensions.h" // for bell::nanopb::encode...
#include "NanoPBHelper.h" // for pbEncode and pbDecode
#include "Packet.h" // for cspot #include "Packet.h" // for cspot
#include "TimeProvider.h" // for TimeProvider #include "TimeProvider.h" // for TimeProvider
#include "Utils.h" // for string_format #include "Utils.h" // for string_format
#include "WrappedSemaphore.h"
#ifdef BELL_ONLY_CJSON #ifdef BELL_ONLY_CJSON
#include "cJSON.h" #include "cJSON.h"
#else #else
@@ -21,6 +24,8 @@
#include "nlohmann/json_fwd.hpp" // for json #include "nlohmann/json_fwd.hpp" // for json
#endif #endif
#include "protobuf/login5.pb.h" // for LoginRequest
using namespace cspot; using namespace cspot;
static std::string CLIENT_ID = static std::string CLIENT_ID =
@@ -31,9 +36,7 @@ static std::string SCOPES =
"recently-played"; // Required access scopes "recently-played"; // Required access scopes
AccessKeyFetcher::AccessKeyFetcher(std::shared_ptr<cspot::Context> ctx) AccessKeyFetcher::AccessKeyFetcher(std::shared_ptr<cspot::Context> ctx)
: ctx(ctx) { : ctx(ctx) {}
this->updateSemaphore = std::make_shared<bell::WrappedSemaphore>();
}
bool AccessKeyFetcher::isExpired() { bool AccessKeyFetcher::isExpired() {
if (accessKey.empty()) { if (accessKey.empty()) {
@@ -65,40 +68,72 @@ void AccessKeyFetcher::updateAccessKey() {
keyPending = true; keyPending = true;
CSPOT_LOG(info, "Access token expired, fetching new one..."); // Prepare a protobuf login request
static LoginRequest loginRequest = LoginRequest_init_zero;
static LoginResponse loginResponse = LoginResponse_init_zero;
std::string url = // Assign necessary request fields
string_format("hm://keymaster/token/authenticated?client_id=%s&scope=%s", loginRequest.client_info.client_id.funcs.encode = &bell::nanopb::encodeString;
CLIENT_ID.c_str(), SCOPES.c_str()); loginRequest.client_info.client_id.arg = &CLIENT_ID;
auto timeProvider = this->ctx->timeProvider;
ctx->session->execute( loginRequest.client_info.device_id.funcs.encode = &bell::nanopb::encodeString;
MercurySession::RequestType::GET, url, loginRequest.client_info.device_id.arg = &ctx->config.deviceId;
[this, timeProvider](MercurySession::Response& res) {
if (res.fail) loginRequest.login_method.stored_credential.username.funcs.encode =
return; &bell::nanopb::encodeString;
auto accessJSON = loginRequest.login_method.stored_credential.username.arg =
std::string((char*)res.parts[0].data(), res.parts[0].size()); &ctx->config.username;
#ifdef BELL_ONLY_CJSON
cJSON* jsonBody = cJSON_Parse(accessJSON.c_str()); // Set login method to stored credential
this->accessKey = loginRequest.which_login_method = LoginRequest_stored_credential_tag;
cJSON_GetObjectItem(jsonBody, "accessToken")->valuestring; loginRequest.login_method.stored_credential.data.funcs.encode =
int expiresIn = cJSON_GetObjectItem(jsonBody, "expiresIn")->valueint; &bell::nanopb::encodeVector;
cJSON_Delete(jsonBody); loginRequest.login_method.stored_credential.data.arg = &ctx->config.authData;
#else
auto jsonBody = nlohmann::json::parse(accessJSON); // Max retry of 3, can receive different hash cat types
this->accessKey = jsonBody["accessToken"]; int retryCount = 3;
int expiresIn = jsonBody["expiresIn"]; bool success = false;
#endif
expiresIn = expiresIn / 2; // Refresh token before it expires do {
auto encodedRequest = pbEncode(LoginRequest_fields, &loginRequest);
CSPOT_LOG(info, "Access token expired, fetching new one... %d",
encodedRequest.size());
// Perform a login5 request, containing the encoded protobuf data
auto response = bell::HTTPClient::post(
"https://login5.spotify.com/v3/login",
{{"Content-Type", "application/x-protobuf"}}, encodedRequest);
auto responseBytes = response->bytes();
// Deserialize the response
pbDecode(loginResponse, LoginResponse_fields, responseBytes);
if (loginResponse.which_response == LoginResponse_ok_tag) {
// Successfully received an auth token
CSPOT_LOG(info, "Access token sucessfully fetched");
success = true;
accessKey = std::string(loginResponse.response.ok.access_token);
// Expire in ~30 minutes
int expiresIn = 3600 / 2;
if (loginResponse.response.ok.has_access_token_expires_in) {
int expiresIn = loginResponse.response.ok.access_token_expires_in / 2;
}
this->expiresAt = this->expiresAt =
timeProvider->getSyncedTimestamp() + (expiresIn * 1000); ctx->timeProvider->getSyncedTimestamp() + (expiresIn * 1000);
updateSemaphore->give(); } else {
}); CSPOT_LOG(error, "Failed to fetch access token");
}
updateSemaphore->twait(5000); // Free up allocated memory for response
pb_release(LoginResponse_fields, &loginResponse);
retryCount--;
} while (retryCount >= 0 && !success);
// Mark as not pending for refresh
keyPending = false; keyPending = false;
} }

View File

@@ -49,7 +49,7 @@ static long vorbisTellCb(TrackPlayer* self) {
TrackPlayer::TrackPlayer(std::shared_ptr<cspot::Context> ctx, TrackPlayer::TrackPlayer(std::shared_ptr<cspot::Context> ctx,
std::shared_ptr<cspot::TrackQueue> trackQueue, std::shared_ptr<cspot::TrackQueue> trackQueue,
EOFCallback eof, TrackLoadedCallback trackLoaded) EOFCallback eof, TrackLoadedCallback trackLoaded)
: bell::Task("cspot_player", 32 * 1024, 5, 1) { : bell::Task("cspot_player", 48 * 1024, 5, 1) {
this->ctx = ctx; this->ctx = ctx;
this->eofCallback = eof; this->eofCallback = eof;
this->trackLoaded = trackLoaded; this->trackLoaded = trackLoaded;