Răsfoiți Sursa

manage Spotify credentials

philippe44 1 an în urmă
părinte
comite
5068309d25

+ 4 - 4
components/platform_config/nvs_utilities.h

@@ -13,14 +13,14 @@ esp_err_t store_nvs_value_len(nvs_type_t type, const char *key, void * data, siz
 esp_err_t store_nvs_value(nvs_type_t type, const char *key, void * data);
 esp_err_t get_nvs_value(nvs_type_t type, const char *key, void*value, const uint8_t buf_size);
 void * get_nvs_value_alloc(nvs_type_t type, const char *key);
-void * get_nvs_value_alloc_for_partition(const char * partition,const char * namespace,nvs_type_t type, const char *key, size_t * size);
-esp_err_t erase_nvs_for_partition(const char * partition, const char * namespace,const char *key);
-esp_err_t store_nvs_value_len_for_partition(const char * partition,const char * namespace,nvs_type_t type, const char *key, const void * data,size_t data_len);
+void * get_nvs_value_alloc_for_partition(const char * partition,const char * ns,nvs_type_t type, const char *key, size_t * size);
+esp_err_t erase_nvs_for_partition(const char * partition, const char * ns,const char *key);
+esp_err_t store_nvs_value_len_for_partition(const char * partition,const char * ns,nvs_type_t type, const char *key, const void * data,size_t data_len);
 esp_err_t erase_nvs(const char *key);
 void print_blob(const char *blob, size_t len);
 const char *type_to_str(nvs_type_t type);
 nvs_type_t str_to_type(const char *type);
-esp_err_t erase_nvs_partition(const char * partition, const char * namespace);
+esp_err_t erase_nvs_partition(const char * partition, const char * ns);
 void erase_settings_partition();
 #ifdef __cplusplus
 }

+ 12 - 0
components/platform_console/cmd_config.c

@@ -131,6 +131,7 @@ static struct {
 	struct arg_str *deviceName;
 //	struct arg_int *volume;
 	struct arg_int *bitrate;
+    struct arg_int *zeroConf;
 	struct arg_end *end;
 } cspot_args;
 static struct {
@@ -656,6 +657,9 @@ static int do_cspot_config(int argc, char **argv){
 	if(cspot_args.bitrate->count>0){
 		cjson_update_number(&cspot_config,cspot_args.bitrate->hdr.longopts,cspot_args.bitrate->ival[0]);
 	}	
+    if(cspot_args.zeroConf->count>0){
+		cjson_update_number(&cspot_config,cspot_args.zeroConf->hdr.longopts,cspot_args.zeroConf->ival[0]);
+	}	
 	
 	if(!nerrors ){
 		fprintf(f,"Storing cspot parameters.\n");
@@ -668,6 +672,9 @@ static int do_cspot_config(int argc, char **argv){
 		if(cspot_args.bitrate->count>0){
 			fprintf(f,"Bitrate changed to %u\n",cspot_args.bitrate->ival[0]);
 		}
+        if(cspot_args.zeroConf->count>0){
+			fprintf(f,"ZeroConf changed to %u\n",cspot_args.zeroConf->ival[0]);
+		}
 	}
 	if(!nerrors ){
 		fprintf(f,"Done.\n");
@@ -853,6 +860,10 @@ cJSON * cspot_cb(){
 	if(cspot_values){
 		cJSON_AddNumberToObject(values,cspot_args.bitrate->hdr.longopts,cJSON_GetNumberValue(cspot_values));
 	}
+    cspot_values = cJSON_GetObjectItem(cspot_config,cspot_args.zeroConf->hdr.longopts);
+	if(cspot_values){
+		cJSON_AddNumberToObject(values,cspot_args.zeroConf->hdr.longopts,cJSON_GetNumberValue(cspot_values));
+	}
 
 	cJSON_Delete(cspot_config);
 	return values;
@@ -1286,6 +1297,7 @@ static void register_known_templates_config(){
 static void register_cspot_config(){
 	cspot_args.deviceName = arg_str1(NULL,"deviceName","","Device Name");
 	cspot_args.bitrate = arg_int1(NULL,"bitrate","96|160|320","Streaming Bitrate (kbps)");
+    cspot_args.zeroConf = arg_int1(NULL,"zeroConf","0|1","Force use of ZeroConf");
 //	cspot_args.volume = arg_int1(NULL,"volume","","Spotify Volume");
 	cspot_args.end = arg_end(1);
 	 const esp_console_cmd_t cmd = {

+ 94 - 29
components/spotify/Shim.cpp

@@ -30,10 +30,16 @@
 #include "cspot_private.h"
 #include "cspot_sink.h"
 #include "platform_config.h"
+#include "nvs_utilities.h"
 #include "tools.h"
 
 static class cspotPlayer *player;
 
+static const struct {
+    const char *ns;
+    const char *credentials;
+} spotify_ns = { .ns = "spotify", .credentials = "credentials" };
+
 /****************************************************************************************
  * Player's main class  & task
  */
@@ -42,7 +48,11 @@ class cspotPlayer : public bell::Task {
 private:
     std::string name;
     bell::WrappedSemaphore clientConnected;
-    std::atomic<bool> isPaused, isConnected;
+    std::atomic<bool> isPaused;
+    enum states { ABORT, LINKED, DISCO };
+    std::atomic<states> state;
+    std::string credentials;
+    bool zeroConf;
         
     int startOffset, volume = 0, bitrate = 160;
     httpd_handle_t serverHandle;
@@ -57,6 +67,7 @@ private:
     void eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event);
     void trackHandler(void);
     size_t pcmWrite(uint8_t *pcm, size_t bytes, std::string_view trackId);
+    void enableZeroConf(void);
 
     void runTask();
 
@@ -79,8 +90,25 @@ cspotPlayer::cspotPlayer(const char* name, httpd_handle_t server, int port, cspo
     if ((item = cJSON_GetObjectItem(config, "volume")) != NULL) volume = item->valueint;
     if ((item = cJSON_GetObjectItem(config, "bitrate")) != NULL) bitrate = item->valueint;   
     if ((item = cJSON_GetObjectItem(config, "deviceName") ) != NULL) this->name = item->valuestring;
-    else this->name = name;
-    cJSON_Delete(config);
+    else this->name = name; 
+    
+    if ((item = cJSON_GetObjectItem(config, "zeroConf")) != NULL) {
+        zeroConf = item->valueint;
+        cJSON_Delete(config);
+    } else {
+        zeroConf = true;
+        cJSON_AddNumberToObject(config, "zeroConf", 1);
+        config_set_cjson_str_and_free("cspot_config", config);
+    }
+    
+    // get optional credentials from own NVS
+    if (!zeroConf) {
+        char *credentials = (char*) get_nvs_value_alloc_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, NULL);
+        if (credentials) {
+            this->credentials = credentials;
+            free(credentials); 
+        }
+    }
 
     if (bitrate != 96 && bitrate != 160 && bitrate != 320) bitrate = 160;
 }
@@ -207,7 +235,7 @@ void cspotPlayer::eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event
     }
     case cspot::SpircHandler::EventType::DISC:
         cmdHandler(CSPOT_DISC);
-        isConnected = false;
+        state = DISCO;
         break;
     case cspot::SpircHandler::EventType::SEEK: {
         cmdHandler(CSPOT_SEEK, std::get<int>(event->data));
@@ -265,7 +293,7 @@ void cspotPlayer::command(cspot_event_t event) {
      * generate any cspot::event */
     case CSPOT_DISC:
         cmdHandler(CSPOT_DISC);
-        isConnected = false;
+        state = ABORT;
         break;
     // spirc->setRemoteVolume does not generate a cspot::event so call cmdHandler
     case CSPOT_VOLUME_UP:
@@ -285,34 +313,48 @@ void cspotPlayer::command(cspot_event_t event) {
 	}
 }
 
-void cspotPlayer::runTask() {
+void cspotPlayer::enableZeroConf(void) {
     httpd_uri_t request = {
 		.uri = "/spotify_info",
 		.method = HTTP_GET,
 		.handler = ::handleGET,
 		.user_ctx = NULL,
-	};
-
+    };
+    
     // register GET and POST handler for built-in server
     httpd_register_uri_handler(serverHandle, &request);
     request.method = HTTP_POST;
     request.handler = ::handlePOST;
     httpd_register_uri_handler(serverHandle, &request);
-
-    // construct blob for that player
-    blob = std::make_unique<cspot::LoginBlob>(name);
+        
+    CSPOT_LOG(info, "ZeroConf mode (port %d)", serverPort);
 
     // Register mdns service, for spotify to find us
     bell::MDNSService::registerService( blob->getDeviceName(), "_spotify-connect", "_tcp", "", serverPort,
-            { {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });
-            
+                                       { {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });    
+}
+ 
+void cspotPlayer::runTask() {
+    bool useZeroConf = zeroConf;
+    
+    // construct blob for that player
+    blob = std::make_unique<cspot::LoginBlob>(name);
+                  
     CSPOT_LOG(info, "CSpot instance service name %s (id %s)", blob->getDeviceName().c_str(), blob->getDeviceId().c_str());
+    
+    if (!zeroConf && !credentials.empty()) {
+        blob->loadJson(credentials);
+        CSPOT_LOG(info, "Reusable credentials mode");
+    } else {      
+        // whether we want it or not we must use ZeroConf
+        useZeroConf = true;
+        enableZeroConf();
+    }
 
     // gone with the wind...
     while (1) {
-        clientConnected.wait();
-
-        CSPOT_LOG(info, "Spotify client connected for %s", name.c_str());
+        if (useZeroConf) clientConnected.wait();
+        CSPOT_LOG(info, "Spotify client launched for %s", name.c_str());
 
         auto ctx = cspot::Context::createFromBlob(blob);
 
@@ -321,12 +363,26 @@ void cspotPlayer::runTask() {
         else ctx->config.audioFormat = AudioFormat_OGG_VORBIS_160;
 
         ctx->session->connectWithRandomAp();
-        auto token = ctx->session->authenticate(blob);
+        ctx->config.authData = ctx->session->authenticate(blob);
 
         // Auth successful
-        if (token.size() > 0) {
+        if (ctx->config.authData.size() > 0) {
+            // we might have been forced to use zeroConf, so store credentials and reset zeroConf usage
+            if (!zeroConf) {
+                useZeroConf = false;
+                // can't call store_nvs... from a task running on EXTRAM stack
+                TimerHandle_t timer = xTimerCreate( "credentials", 1, pdFALSE, strdup(ctx->getCredentialsJson().c_str()),
+                            [](TimerHandle_t xTimer) {
+                                auto credentials = (char*) pvTimerGetTimerID(xTimer);
+                                store_nvs_value_len_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, credentials, 0);
+                                free(credentials);
+                                xTimerDelete(xTimer, portMAX_DELAY);
+                            } );
+                xTimerStart(timer, portMAX_DELAY);
+            }
+
             spirc = std::make_unique<cspot::SpircHandler>(ctx);
-            isConnected = true;            
+            state = LINKED;
 			
             // set call back to calculate a hash on trackId
             spirc->getTrackPlayer()->setDataCallback(
@@ -347,7 +403,7 @@ void cspotPlayer::runTask() {
             cmdHandler(CSPOT_VOLUME, volume);
 
             // exit when player has stopped (received a DISC)
-            while (isConnected) {
+            while (state == LINKED) {
                 ctx->session->handlePacket();
 
                 // low-accuracy polling events
@@ -371,23 +427,32 @@ void cspotPlayer::runTask() {
                         spirc->setPause(true);
                     }
                 }
+                
+                // on disconnect, stay in the core loop unless we are in ZeroConf mode
+                if (state == DISCO) {
+                    // update volume then
+                    cJSON *config = config_alloc_get_cjson("cspot_config");
+                    cJSON_DeleteItemFromObject(config, "volume");
+                    cJSON_AddNumberToObject(config, "volume", volume);
+                    config_set_cjson_str_and_free("cspot_config", config);
+                    
+                    // in ZeroConf mod, stay connected (in this loop)
+                    if (!zeroConf) state = LINKED;
+                }
             }
 
             spirc->disconnect();
             spirc.reset();
 
             CSPOT_LOG(info, "disconnecting player %s", name.c_str());
+        } else {
+            CSPOT_LOG(error, "failed authentication, forcing ZeroConf");
+            if (!useZeroConf) enableZeroConf();
+            useZeroConf = true;
         }
-
+            
         // we want to release memory ASAP and for sure
-        ctx.reset();
-        token.clear();
-        
-        // update volume when we disconnect
-        cJSON *config = config_alloc_get_cjson("cspot_config");
-        cJSON_DeleteItemFromObject(config, "volume");
-        cJSON_AddNumberToObject(config, "volume", volume);
-        config_set_cjson_str_and_free("cspot_config", config);
+        ctx.reset();      
     }
 }
 

+ 4 - 2
components/spotify/cspot/bell/external/opencore-aacdec/CMakeLists.txt

@@ -1,7 +1,9 @@
 file(GLOB AACDEC_SOURCES "src/*.c")
 file(GLOB AACDEC_HEADERS "src/*.h" "oscl/*.h" "include/*.h")
 
-add_library(opencore-aacdec SHARED ${AACDEC_SOURCES})
+add_library(opencore-aacdec STATIC ${AACDEC_SOURCES})
+if(NOT MSVC)
+	target_compile_options(opencore-aacdec PRIVATE -Wno-array-parameter)
+endif()	
 add_definitions(-DAAC_PLUS -DHQ_SBR -DPARAMETRICSTEREO -DC_EQUIVALENT)
-target_compile_options(opencore-aacdec PRIVATE -Wno-array-parameter)
 target_include_directories(opencore-aacdec PUBLIC "src/" "oscl/" "include/")

+ 2 - 2
components/spotify/cspot/bell/main/utilities/include/Crypto.h

@@ -31,8 +31,8 @@ class CryptoMbedTLS {
   CryptoMbedTLS();
   ~CryptoMbedTLS();
   // Base64
-  std::vector<uint8_t> base64Decode(const std::string& data);
-  std::string base64Encode(const std::vector<uint8_t>& data);
+  static std::vector<uint8_t> base64Decode(const std::string& data);
+  static std::string base64Encode(const std::vector<uint8_t>& data);
 
   // Sha1
   void sha1Init();

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

@@ -6,7 +6,16 @@
 #include "LoginBlob.h"
 #include "MercurySession.h"
 #include "TimeProvider.h"
+#include "Crypto.h"  
 #include "protobuf/metadata.pb.h"
+#include "protobuf/authentication.pb.h"      // for AuthenticationType_AUTHE...
+#ifdef BELL_ONLY_CJSON
+#include "cJSON.h"
+#else
+#include "nlohmann/detail/json_pointer.hpp"  // for json_pointer<>::string_t
+#include "nlohmann/json.hpp"      // for basic_json<>::object_t, basic_json
+#include "nlohmann/json_fwd.hpp"  // for json
+#endif
 
 namespace cspot {
 struct Context {
@@ -26,6 +35,28 @@ struct Context {
 
   std::shared_ptr<TimeProvider> timeProvider;
   std::shared_ptr<cspot::MercurySession> session;
+  std::string getCredentialsJson() {
+#ifdef BELL_ONLY_CJSON
+      cJSON* json_obj = cJSON_CreateObject();
+      cJSON_AddStringToObject(json_obj, "authData", Crypto::base64Encode(config.authData).c_str());
+      cJSON_AddNumberToObject(json_obj, "authType", AuthenticationType_AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS);
+      cJSON_AddStringToObject(json_obj, "username", config.username.c_str());
+
+      char* str = cJSON_PrintUnformatted(json_obj);
+      cJSON_Delete(json_obj);
+      std::string json_objStr(str);
+      free(str);
+
+      return json_objStr;
+#else
+      nlohmann::json obj;
+      obj["authData"] = Crypto::base64Encode(config.authData);
+      obj["authType"] = AuthenticationType_AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS;
+      obj["username"] = config.username;
+
+      return obj.dump();
+#endif
+  }
 
   static std::shared_ptr<Context> createFromBlob(
       std::shared_ptr<LoginBlob> blob) {

+ 6 - 1
components/spotify/cspot/protobuf/authentication.options

@@ -2,4 +2,9 @@ LoginCredentials.username max_size:30, fixed_length:false
 LoginCredentials.auth_data max_size:512, fixed_length:false
 SystemInfo.system_information_string max_size:16, fixed_length:false
 SystemInfo.device_id max_size:50, fixed_length:false
-ClientResponseEncrypted.version_string max_size:32, fixed_length:false
+ClientResponseEncrypted.version_string max_size:32, fixed_length:false
+APWelcome.canonical_username max_size:30, fixed_length:false
+APWelcome.reusable_auth_credentials max_size:512, fixed_length:false
+APWelcome.lfs_secret max_size:128, fixed_length:false
+AccountInfoFacebook.access_token max_size:128, fixed_length:false
+AccountInfoFacebook.machine_id max_size:50, fixed_length:false

+ 29 - 0
components/spotify/cspot/protobuf/authentication.proto

@@ -37,6 +37,11 @@ enum Os {
     OS_BCO = 0x16;
 }
 
+enum AccountType {
+    Spotify = 0x0;
+    Facebook = 0x1;
+}
+
 enum AuthenticationType {
     AUTHENTICATION_USER_PASS = 0x0;
     AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS = 0x1;
@@ -62,4 +67,28 @@ message ClientResponseEncrypted {
     required LoginCredentials login_credentials = 0xa; 
     required SystemInfo system_info = 0x32; 
     optional string version_string = 0x46; 
+}
+
+message APWelcome {
+    required string canonical_username = 0xa;
+    required AccountType account_type_logged_in = 0x14;
+    required AccountType credentials_type_logged_in = 0x19;
+    required AuthenticationType reusable_auth_credentials_type = 0x1e;
+    required bytes reusable_auth_credentials = 0x28;
+    optional bytes lfs_secret = 0x32; 
+    optional AccountInfo account_info = 0x3c;
+    optional AccountInfoFacebook fb = 0x46;
+}
+
+message AccountInfo {
+    optional AccountInfoSpotify spotify = 0x1;
+    optional AccountInfoFacebook facebook = 0x2;
+}
+
+message AccountInfoSpotify {
+}
+
+message AccountInfoFacebook {
+    optional string access_token = 0x1;
+    optional string machine_id = 0x2;
 }

+ 2 - 2
components/spotify/cspot/src/LoginBlob.cpp

@@ -142,8 +142,8 @@ void LoginBlob::loadJson(const std::string& json) {
   cJSON* root = cJSON_Parse(json.c_str());
   this->authType = cJSON_GetObjectItem(root, "authType")->valueint;
   this->username = cJSON_GetObjectItem(root, "username")->valuestring;
-  std::string authDataObject =
-      cJSON_GetObjectItem(root, "authData")->valuestring;
+  std::string authDataObject = cJSON_GetObjectItem(root, "authData")->valuestring;
+  this->authData = crypto->base64Decode(authDataObject);
   cJSON_Delete(root);
 #else
   auto root = nlohmann::json::parse(json);

+ 9 - 1
components/spotify/cspot/src/Session.cpp

@@ -17,6 +17,10 @@
 #include "PlainConnection.h"    // for PlainConnection, timeoutCallback
 #include "ShannonConnection.h"  // for ShannonConnection
 
+#include "pb_decode.h"
+#include "NanoPBHelper.h"  // for pbPutString, pbEncode, pbDecode
+#include "protobuf/authentication.pb.h"
+
 using random_bytes_engine =
     std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
 
@@ -79,9 +83,13 @@ std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob) {
   auto packet = this->shanConn->recvPacket();
   switch (packet.command) {
     case AUTH_SUCCESSFUL_COMMAND: {
+      APWelcome welcome;
       CSPOT_LOG(debug, "Authorization successful");
+      pbDecode(welcome, APWelcome_fields, packet.data);
       return std::vector<uint8_t>(
-          {0x1});  // TODO: return actual reusable credentaials to be stored somewhere
+          welcome.reusable_auth_credentials.bytes, 
+          welcome.reusable_auth_credentials.bytes + welcome.reusable_auth_credentials.size
+      );
       break;
     }
     case AUTH_DECLINED_COMMAND: {