浏览代码

MacOS auth for Spotify

philippe44 1 年之前
父节点
当前提交
bdceb2d832

+ 1 - 1
components/spotify/cspot/bell/external/nanopb/extra/FindNanopb.cmake

@@ -298,7 +298,7 @@ function(NANOPB_GENERATE_CPP SRCS HDRS)
   
   if(MSVC)
       unset(CUSTOM_COMMAND_PREFIX)
-  endif()    
+  endif()
 
 endfunction()
 

+ 12 - 2
components/spotify/cspot/bell/main/io/HTTPClient.cpp

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

+ 12 - 1
components/spotify/cspot/bell/main/io/include/HTTPClient.h

@@ -55,8 +55,10 @@ class HTTPClient {
     void connect(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 post(const std::string& url, Headers headers = {},
+        const std::vector<uint8_t>& body = {});
 
     std::string_view body();
     std::vector<uint8_t> bytes();
@@ -102,5 +104,14 @@ class HTTPClient {
     response->get(url, headers);
     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

+ 1 - 5
components/spotify/cspot/include/AccessKeyFetcher.h

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

+ 3 - 0
components/spotify/cspot/include/CSpotContext.h

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

+ 76 - 41
components/spotify/cspot/src/AccessKeyFetcher.cpp

@@ -6,14 +6,17 @@
 #include <type_traits>       // for remove_extent_t
 #include <vector>            // for vector
 
-#include "BellLogger.h"      // for AbstractLogger
-#include "CSpotContext.h"    // for Context
-#include "Logger.h"          // for CSPOT_LOG
-#include "MercurySession.h"  // for MercurySession, MercurySession::Res...
-#include "Packet.h"          // for cspot
-#include "TimeProvider.h"    // for TimeProvider
-#include "Utils.h"           // for string_format
-#include "WrappedSemaphore.h"
+#include "BellLogger.h"    // for AbstractLogger
+#include "CSpotContext.h"  // for Context
+#include "HTTPClient.h"
+#include "Logger.h"            // for CSPOT_LOG
+#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 "TimeProvider.h"      // for TimeProvider
+#include "Utils.h"             // for string_format
+
 #ifdef BELL_ONLY_CJSON
 #include "cJSON.h"
 #else
@@ -21,6 +24,8 @@
 #include "nlohmann/json_fwd.hpp"  // for json
 #endif
 
+#include "protobuf/login5.pb.h"  // for LoginRequest
+
 using namespace cspot;
 
 static std::string CLIENT_ID =
@@ -31,9 +36,7 @@ static std::string SCOPES =
     "recently-played";  // Required access scopes
 
 AccessKeyFetcher::AccessKeyFetcher(std::shared_ptr<cspot::Context> ctx)
-    : ctx(ctx) {
-  this->updateSemaphore = std::make_shared<bell::WrappedSemaphore>();
-}
+    : ctx(ctx) {}
 
 bool AccessKeyFetcher::isExpired() {
   if (accessKey.empty()) {
@@ -65,40 +68,72 @@ void AccessKeyFetcher::updateAccessKey() {
 
   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 =
-      string_format("hm://keymaster/token/authenticated?client_id=%s&scope=%s",
-                    CLIENT_ID.c_str(), SCOPES.c_str());
-  auto timeProvider = this->ctx->timeProvider;
+  // Assign necessary request fields
+  loginRequest.client_info.client_id.funcs.encode = &bell::nanopb::encodeString;
+  loginRequest.client_info.client_id.arg = &CLIENT_ID;
 
-  ctx->session->execute(
-      MercurySession::RequestType::GET, url,
-      [this, timeProvider](MercurySession::Response& res) {
-        if (res.fail)
-          return;
-        auto accessJSON =
-            std::string((char*)res.parts[0].data(), res.parts[0].size());
-#ifdef BELL_ONLY_CJSON
-        cJSON* jsonBody = cJSON_Parse(accessJSON.c_str());
-        this->accessKey =
-            cJSON_GetObjectItem(jsonBody, "accessToken")->valuestring;
-        int expiresIn = cJSON_GetObjectItem(jsonBody, "expiresIn")->valueint;
-        cJSON_Delete(jsonBody);
-#else
-        auto jsonBody = nlohmann::json::parse(accessJSON);
-        this->accessKey = jsonBody["accessToken"];
-        int expiresIn = jsonBody["expiresIn"];
-#endif
-        expiresIn = expiresIn / 2;  // Refresh token before it expires
+  loginRequest.client_info.device_id.funcs.encode = &bell::nanopb::encodeString;
+  loginRequest.client_info.device_id.arg = &ctx->config.deviceId;
+
+  loginRequest.login_method.stored_credential.username.funcs.encode =
+      &bell::nanopb::encodeString;
+  loginRequest.login_method.stored_credential.username.arg =
+      &ctx->config.username;
+
+  // Set login method to stored credential
+  loginRequest.which_login_method = LoginRequest_stored_credential_tag;
+  loginRequest.login_method.stored_credential.data.funcs.encode =
+      &bell::nanopb::encodeVector;
+  loginRequest.login_method.stored_credential.data.arg = &ctx->config.authData;
+
+  // Max retry of 3, can receive different hash cat types
+  int retryCount = 3;
+  bool success = false;
+
+  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 =
+          ctx->timeProvider->getSyncedTimestamp() + (expiresIn * 1000);
+    } else {
+      CSPOT_LOG(error, "Failed to fetch access token");
+    }
 
-        this->expiresAt =
-            timeProvider->getSyncedTimestamp() + (expiresIn * 1000);
-        updateSemaphore->give();
-      });
+    // Free up allocated memory for response
+    pb_release(LoginResponse_fields, &loginResponse);
 
-  updateSemaphore->twait(5000);
+    retryCount--;
+  } while (retryCount >= 0 && !success);
 
-  // Mark as not pending for refresh
   keyPending = false;
 }

+ 1 - 1
components/spotify/cspot/src/TrackPlayer.cpp

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