From 5068309d253fae97d4971340278b74b345bb14c2 Mon Sep 17 00:00:00 2001 From: philippe44 Date: Wed, 27 Sep 2023 19:36:38 -0700 Subject: [PATCH] manage Spotify credentials --- components/platform_config/nvs_utilities.h | 8 +- components/platform_console/cmd_config.c | 12 ++ components/spotify/Shim.cpp | 123 +++++++++++++----- .../external/opencore-aacdec/CMakeLists.txt | 6 +- .../bell/main/utilities/include/Crypto.h | 4 +- .../spotify/cspot/include/CSpotContext.h | 31 +++++ .../cspot/protobuf/authentication.options | 7 +- .../cspot/protobuf/authentication.proto | 29 +++++ components/spotify/cspot/src/LoginBlob.cpp | 4 +- components/spotify/cspot/src/Session.cpp | 10 +- 10 files changed, 193 insertions(+), 41 deletions(-) diff --git a/components/platform_config/nvs_utilities.h b/components/platform_config/nvs_utilities.h index e789fd46..5559ec3d 100644 --- a/components/platform_config/nvs_utilities.h +++ b/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 } diff --git a/components/platform_console/cmd_config.c b/components/platform_console/cmd_config.c index 3628ce51..b5d89d4e 100644 --- a/components/platform_console/cmd_config.c +++ b/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 = { diff --git a/components/spotify/Shim.cpp b/components/spotify/Shim.cpp index 4f490dfb..df0cbf0f 100644 --- a/components/spotify/Shim.cpp +++ b/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 isPaused, isConnected; + std::atomic isPaused; + enum states { ABORT, LINKED, DISCO }; + std::atomic 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 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 event } case cspot::SpircHandler::EventType::DISC: cmdHandler(CSPOT_DISC); - isConnected = false; + state = DISCO; break; case cspot::SpircHandler::EventType::SEEK: { cmdHandler(CSPOT_SEEK, std::get(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(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(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(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(); } } diff --git a/components/spotify/cspot/bell/external/opencore-aacdec/CMakeLists.txt b/components/spotify/cspot/bell/external/opencore-aacdec/CMakeLists.txt index 2a7b3299..1e50052e 100644 --- a/components/spotify/cspot/bell/external/opencore-aacdec/CMakeLists.txt +++ b/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/") \ No newline at end of file diff --git a/components/spotify/cspot/bell/main/utilities/include/Crypto.h b/components/spotify/cspot/bell/main/utilities/include/Crypto.h index 1ea17a71..667f13ff 100644 --- a/components/spotify/cspot/bell/main/utilities/include/Crypto.h +++ b/components/spotify/cspot/bell/main/utilities/include/Crypto.h @@ -31,8 +31,8 @@ class CryptoMbedTLS { CryptoMbedTLS(); ~CryptoMbedTLS(); // Base64 - std::vector base64Decode(const std::string& data); - std::string base64Encode(const std::vector& data); + static std::vector base64Decode(const std::string& data); + static std::string base64Encode(const std::vector& data); // Sha1 void sha1Init(); diff --git a/components/spotify/cspot/include/CSpotContext.h b/components/spotify/cspot/include/CSpotContext.h index ed0cfd8e..2f274ce6 100644 --- a/components/spotify/cspot/include/CSpotContext.h +++ b/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; std::shared_ptr 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 createFromBlob( std::shared_ptr blob) { diff --git a/components/spotify/cspot/protobuf/authentication.options b/components/spotify/cspot/protobuf/authentication.options index 33aad19f..2ec38a0c 100644 --- a/components/spotify/cspot/protobuf/authentication.options +++ b/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 \ No newline at end of file +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 diff --git a/components/spotify/cspot/protobuf/authentication.proto b/components/spotify/cspot/protobuf/authentication.proto index d3896147..079ab290 100644 --- a/components/spotify/cspot/protobuf/authentication.proto +++ b/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; } \ No newline at end of file diff --git a/components/spotify/cspot/src/LoginBlob.cpp b/components/spotify/cspot/src/LoginBlob.cpp index feb5e8d2..8790071d 100644 --- a/components/spotify/cspot/src/LoginBlob.cpp +++ b/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); diff --git a/components/spotify/cspot/src/Session.cpp b/components/spotify/cspot/src/Session.cpp index 74c3f18a..7d76b86a 100644 --- a/components/spotify/cspot/src/Session.cpp +++ b/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; @@ -79,9 +83,13 @@ std::vector Session::authenticate(std::shared_ptr 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( - {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: {