From a7cc803ccb227ca53c2203dc203638113c10b662 Mon Sep 17 00:00:00 2001 From: HentaiHeavenVR Date: Wed, 17 Apr 2024 15:49:20 +0200 Subject: [PATCH] Add option to override Live Control Gateway (#225) * Add option to override Live Control Gateway * Add LCG clear command, and rename * Implement LCG domain verification logic --- .../configuration/backend-config.ts | 19 +- include/config/BackendConfig.h | 3 +- include/config/Config.h | 4 + include/serialization/JsonAPI.h | 8 + .../serialization/_fbs/ConfigFile_generated.h | 23 ++- schemas/ConfigFile.fbs | 3 + src/GatewayConnectionManager.cpp | 9 + src/config/BackendConfig.cpp | 13 +- src/config/Config.cpp | 44 +++++ src/serial/SerialInputHandler.cpp | 162 +++++++++++++++--- src/serialization/JsonAPI.cpp | 48 ++++++ 11 files changed, 303 insertions(+), 33 deletions(-) diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts index 54d691c1..edb4a223 100644 --- a/frontend/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts @@ -42,8 +42,18 @@ authToken(optionalEncoding?:any):string|Uint8Array|null { return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } +/** + * Override the Live-Control-Gateway (LCG) URL + */ +lcgOverride():string|null +lcgOverride(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +lcgOverride(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + static startBackendConfig(builder:flatbuffers.Builder) { - builder.startObject(2); + builder.startObject(3); } static addDomain(builder:flatbuffers.Builder, domainOffset:flatbuffers.Offset) { @@ -54,15 +64,20 @@ static addAuthToken(builder:flatbuffers.Builder, authTokenOffset:flatbuffers.Off builder.addFieldOffset(1, authTokenOffset, 0); } +static addLcgOverride(builder:flatbuffers.Builder, lcgOverrideOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, lcgOverrideOffset, 0); +} + static endBackendConfig(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); return offset; } -static createBackendConfig(builder:flatbuffers.Builder, domainOffset:flatbuffers.Offset, authTokenOffset:flatbuffers.Offset):flatbuffers.Offset { +static createBackendConfig(builder:flatbuffers.Builder, domainOffset:flatbuffers.Offset, authTokenOffset:flatbuffers.Offset, lcgOverrideOffset:flatbuffers.Offset):flatbuffers.Offset { BackendConfig.startBackendConfig(builder); BackendConfig.addDomain(builder, domainOffset); BackendConfig.addAuthToken(builder, authTokenOffset); + BackendConfig.addLcgOverride(builder, lcgOverrideOffset); return BackendConfig.endBackendConfig(builder); } } diff --git a/include/config/BackendConfig.h b/include/config/BackendConfig.h index f3dc2827..00522ce1 100644 --- a/include/config/BackendConfig.h +++ b/include/config/BackendConfig.h @@ -7,10 +7,11 @@ namespace OpenShock::Config { struct BackendConfig : public ConfigBase { BackendConfig(); - BackendConfig(const std::string& domain, const std::string& authToken); + BackendConfig(const std::string& domain, const std::string& authToken, const std::string& lcgOverride); std::string domain; std::string authToken; + std::string lcgOverride; void ToDefault() override; diff --git a/include/config/Config.h b/include/config/Config.h index 021d1f67..1509e45d 100644 --- a/include/config/Config.h +++ b/include/config/Config.h @@ -75,4 +75,8 @@ namespace OpenShock::Config { bool GetBackendAuthToken(std::string& out); bool SetBackendAuthToken(const std::string& token); bool ClearBackendAuthToken(); + bool HasBackendLCGOverride(); + bool GetBackendLCGOverride(std::string& out); + bool SetBackendLCGOverride(const std::string& lcgOverride); + bool ClearBackendLCGOverride(); } // namespace OpenShock::Config diff --git a/include/serialization/JsonAPI.h b/include/serialization/JsonAPI.h index 332d1819..a59bf81f 100644 --- a/include/serialization/JsonAPI.h +++ b/include/serialization/JsonAPI.h @@ -9,6 +9,13 @@ #include namespace OpenShock::Serialization::JsonAPI { + struct LcgInstanceDetailsResponse { + std::string name; + std::string version; + std::string currentTime; + std::string countryCode; + std::string fqdn; + }; struct BackendVersionResponse { std::string version; std::string commit; @@ -32,6 +39,7 @@ namespace OpenShock::Serialization::JsonAPI { std::string country; }; + bool ParseLcgInstanceDetailsJsonResponse(int code, const cJSON* root, LcgInstanceDetailsResponse& out); bool ParseBackendVersionJsonResponse(int code, const cJSON* root, BackendVersionResponse& out); bool ParseAccountLinkJsonResponse(int code, const cJSON* root, AccountLinkResponse& out); bool ParseDeviceInfoJsonResponse(int code, const cJSON* root, DeviceInfoResponse& out); diff --git a/include/serialization/_fbs/ConfigFile_generated.h b/include/serialization/_fbs/ConfigFile_generated.h index bf963348..93a6307b 100644 --- a/include/serialization/_fbs/ConfigFile_generated.h +++ b/include/serialization/_fbs/ConfigFile_generated.h @@ -419,7 +419,8 @@ struct BackendConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { } enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { VT_DOMAIN = 4, - VT_AUTH_TOKEN = 6 + VT_AUTH_TOKEN = 6, + VT_LCG_OVERRIDE = 8 }; /// Domain name of the backend server, e.g. "api.shocklink.net" const ::flatbuffers::String *domain() const { @@ -429,12 +430,18 @@ struct BackendConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { const ::flatbuffers::String *auth_token() const { return GetPointer(VT_AUTH_TOKEN); } + /// Override the Live-Control-Gateway (LCG) URL + const ::flatbuffers::String *lcg_override() const { + return GetPointer(VT_LCG_OVERRIDE); + } bool Verify(::flatbuffers::Verifier &verifier) const { return VerifyTableStart(verifier) && VerifyOffset(verifier, VT_DOMAIN) && verifier.VerifyString(domain()) && VerifyOffset(verifier, VT_AUTH_TOKEN) && verifier.VerifyString(auth_token()) && + VerifyOffset(verifier, VT_LCG_OVERRIDE) && + verifier.VerifyString(lcg_override()) && verifier.EndTable(); } }; @@ -449,6 +456,9 @@ struct BackendConfigBuilder { void add_auth_token(::flatbuffers::Offset<::flatbuffers::String> auth_token) { fbb_.AddOffset(BackendConfig::VT_AUTH_TOKEN, auth_token); } + void add_lcg_override(::flatbuffers::Offset<::flatbuffers::String> lcg_override) { + fbb_.AddOffset(BackendConfig::VT_LCG_OVERRIDE, lcg_override); + } explicit BackendConfigBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -463,8 +473,10 @@ struct BackendConfigBuilder { inline ::flatbuffers::Offset CreateBackendConfig( ::flatbuffers::FlatBufferBuilder &_fbb, ::flatbuffers::Offset<::flatbuffers::String> domain = 0, - ::flatbuffers::Offset<::flatbuffers::String> auth_token = 0) { + ::flatbuffers::Offset<::flatbuffers::String> auth_token = 0, + ::flatbuffers::Offset<::flatbuffers::String> lcg_override = 0) { BackendConfigBuilder builder_(_fbb); + builder_.add_lcg_override(lcg_override); builder_.add_auth_token(auth_token); builder_.add_domain(domain); return builder_.Finish(); @@ -478,13 +490,16 @@ struct BackendConfig::Traits { inline ::flatbuffers::Offset CreateBackendConfigDirect( ::flatbuffers::FlatBufferBuilder &_fbb, const char *domain = nullptr, - const char *auth_token = nullptr) { + const char *auth_token = nullptr, + const char *lcg_override = nullptr) { auto domain__ = domain ? _fbb.CreateString(domain) : 0; auto auth_token__ = auth_token ? _fbb.CreateString(auth_token) : 0; + auto lcg_override__ = lcg_override ? _fbb.CreateString(lcg_override) : 0; return OpenShock::Serialization::Configuration::CreateBackendConfig( _fbb, domain__, - auth_token__); + auth_token__, + lcg_override__); } struct SerialInputConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { diff --git a/schemas/ConfigFile.fbs b/schemas/ConfigFile.fbs index d7e88d8f..5810b956 100644 --- a/schemas/ConfigFile.fbs +++ b/schemas/ConfigFile.fbs @@ -42,6 +42,9 @@ table BackendConfig { /// Authentication token for the backend server auth_token:string; + + /// Override the Live-Control-Gateway (LCG) URL + lcg_override:string; } table SerialInputConfig { diff --git a/src/GatewayConnectionManager.cpp b/src/GatewayConnectionManager.cpp index 49c6cdd6..51339869 100644 --- a/src/GatewayConnectionManager.cpp +++ b/src/GatewayConnectionManager.cpp @@ -207,6 +207,15 @@ bool StartConnectingToLCG() { _lastConnectionAttempt = msNow; + if (Config::HasBackendLCGOverride()) { + std::string lcgOverride; + Config::GetBackendLCGOverride(lcgOverride); + + ESP_LOGD(TAG, "Connecting to overridden LCG endpoint %.*s", lcgOverride.size(), lcgOverride.data()); + s_wsClient->connect(lcgOverride.c_str()); + return true; + } + if (!Config::HasBackendAuthToken()) { ESP_LOGD(TAG, "No auth token, can't connect to LCG"); return false; diff --git a/src/config/BackendConfig.cpp b/src/config/BackendConfig.cpp index af560633..5e1177ed 100644 --- a/src/config/BackendConfig.cpp +++ b/src/config/BackendConfig.cpp @@ -7,7 +7,9 @@ const char* const TAG = "Config::BackendConfig"; using namespace OpenShock::Config; -BackendConfig::BackendConfig() : domain(OPENSHOCK_API_DOMAIN), authToken() { } +BackendConfig::BackendConfig() : domain(OPENSHOCK_API_DOMAIN), authToken(), lcgOverride() { } + +BackendConfig::BackendConfig(const std::string& domain, const std::string& authToken, const std::string& lcgOverride) : domain(domain), authToken(authToken), lcgOverride(lcgOverride) { } void BackendConfig::ToDefault() { domain = OPENSHOCK_API_DOMAIN; @@ -22,6 +24,7 @@ bool BackendConfig::FromFlatbuffers(const Serialization::Configuration::BackendC Internal::Utils::FromFbsStr(domain, config->domain(), OPENSHOCK_API_DOMAIN); Internal::Utils::FromFbsStr(authToken, config->auth_token(), ""); + Internal::Utils::FromFbsStr(lcgOverride, config->lcg_override(), ""); return true; } @@ -36,7 +39,9 @@ flatbuffers::Offset Back authTokenOffset = 0; } - return Serialization::Configuration::CreateBackendConfig(builder, domainOffset, authTokenOffset); + auto lcgOverrideOffset = builder.CreateString(lcgOverride); + + return Serialization::Configuration::CreateBackendConfig(builder, domainOffset, authTokenOffset, lcgOverrideOffset); } bool BackendConfig::FromJSON(const cJSON* json) { @@ -52,6 +57,7 @@ bool BackendConfig::FromJSON(const cJSON* json) { Internal::Utils::FromJsonStr(domain, json, "domain", OPENSHOCK_API_DOMAIN); Internal::Utils::FromJsonStr(authToken, json, "authToken", ""); + Internal::Utils::FromJsonStr(lcgOverride, json, "lcgOverride", ""); return true; } @@ -60,9 +66,12 @@ cJSON* BackendConfig::ToJSON(bool withSensitiveData) const { cJSON* root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "domain", domain.c_str()); + if (withSensitiveData) { cJSON_AddStringToObject(root, "authToken", authToken.c_str()); } + cJSON_AddStringToObject(root, "lcgOverride", lcgOverride.c_str()); + return root; } diff --git a/src/config/Config.cpp b/src/config/Config.cpp index 7864ff0e..52d726dc 100644 --- a/src/config/Config.cpp +++ b/src/config/Config.cpp @@ -695,3 +695,47 @@ bool Config::ClearBackendAuthToken() { _configData.backend.authToken.clear(); return _trySaveConfig(); } + +bool Config::HasBackendLCGOverride() { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + return !_configData.backend.lcgOverride.empty(); +} + +bool Config::GetBackendLCGOverride(std::string& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.backend.lcgOverride; + + return true; +} + +bool Config::SetBackendLCGOverride(const std::string& lcgOverride) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.backend.lcgOverride = lcgOverride; + return _trySaveConfig(); +} + +bool Config::ClearBackendLCGOverride() { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.backend.lcgOverride.clear(); + return _trySaveConfig(); +} diff --git a/src/serial/SerialInputHandler.cpp b/src/serial/SerialInputHandler.cpp index 4838a519..55c06081 100644 --- a/src/serial/SerialInputHandler.cpp +++ b/src/serial/SerialInputHandler.cpp @@ -220,6 +220,95 @@ void _handleAuthtokenCommand(char* arg, std::size_t argLength) { } } +void _handleLcgOverrideCommand(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength == 0) { + std::string lcgOverride; + if (!Config::GetBackendLCGOverride(lcgOverride)) { + SERPR_ERROR("Failed to get LCG override from config"); + return; + } + + // Get LCG override + SERPR_RESPONSE("LcgOverride|%s", lcgOverride.c_str()); + return; + } + + if (strncasecmp(arg, "clear", argLength) == 0) { + if (argLength != 5) { + SERPR_ERROR("Invalid command (clear command should not have any arguments)"); + return; + } + + bool result = OpenShock::Config::SetBackendLCGOverride(std::string()); + if (result) { + SERPR_SUCCESS("Cleared LCG override"); + } else { + SERPR_ERROR("Failed to clear LCG override"); + } + return; + } + + if (strncasecmp(arg, "set ", 4) == 0) { + if (argLength <= 4) { + SERPR_ERROR("Invalid command (set command should have an argument)"); + return; + } + + char* domain = arg + 4; + std::size_t domainLen = (arg + argLength) - domain; + + if (domainLen + 40 >= OPENSHOCK_URI_BUFFER_SIZE) { + SERPR_ERROR("Domain name too long, please try increasing the \"OPENSHOCK_URI_BUFFER_SIZE\" constant in source code"); + return; + } + + char uri[OPENSHOCK_URI_BUFFER_SIZE]; + sprintf(uri, "https://%.*s/1", static_cast(domainLen), domain); + + auto resp = HTTP::GetJSON( + uri, + { + {"Accept", "application/json"} + }, + Serialization::JsonAPI::ParseLcgInstanceDetailsJsonResponse, + {200} + ); + + if (resp.result != HTTP::RequestResult::Success) { + SERPR_ERROR("Tried to connect to \"%.*s\", but failed with status [%d], refusing to save domain to config", domainLen, domain, resp.code); + return; + } + + ESP_LOGI( + TAG, + "Successfully connected to \"%.*s\", name: %.*s, version: %.*s, current time: %.*s, country code: %.*s, FQDN: %.*s", + domainLen, + domain, + resp.data.name.size(), + resp.data.name.data(), + resp.data.version.size(), + resp.data.version.data(), + resp.data.currentTime.size(), + resp.data.currentTime.data(), + resp.data.countryCode.size(), + resp.data.countryCode.data(), + resp.data.fqdn.size(), + resp.data.fqdn.data() + ); + + bool result = OpenShock::Config::SetBackendLCGOverride(std::string(domain, domainLen)); + + if (result) { + SERPR_SUCCESS("Saved config"); + } else { + SERPR_ERROR("Failed to save config"); + } + return; + } + + SERPR_ERROR("Invalid subcommand"); +} + void _handleNetworksCommand(char* arg, std::size_t argLength) { cJSON* root; @@ -501,30 +590,35 @@ void _handleHelpCommand(char* arg, std::size_t argLength) { // Raw string literal (1+ to remove the first newline) Serial.print(1 + R"( -help print this menu -help print help for a command -version print version information -restart restart the board -sysinfo print debug information for various subsystems -echo get serial echo enabled -echo set serial echo enabled -validgpios list all valid GPIO pins -rftxpin get radio transmit pin -rftxpin set radio transmit pin -domain get backend domain -domain set backend domain -authtoken get auth token -authtoken set auth token -networks get all saved networks -networks set all saved networks -keepalive get shocker keep-alive enabled -keepalive set shocker keep-alive enabled -jsonconfig get configuration as JSON -jsonconfig set configuration from JSON -rawconfig get raw configuration as base64 -rawconfig set raw configuration from base64 -rftransmit transmit a RF command -factoryreset reset device to factory defaults and restart +help print this menu +help print help for a command +version print version information +restart restart the board +sysinfo print debug information for various subsystems +echo get serial echo enabled +echo set serial echo enabled +validgpios list all valid GPIO pins +rftxpin get radio transmit pin +rftxpin set radio transmit pin +domain get backend domain +domain set backend domain +authtoken get auth token +authtoken set auth token +authtoken clear auth token +lcgoverride get LCG override +lcgoverride set set LCG override +lcgoverride clear clear LCG override +networks get all saved networks +networks set all saved networks +networks clear all saved networks +keepalive get shocker keep-alive enabled +keepalive set shocker keep-alive enabled +jsonconfig get configuration as JSON +jsonconfig set configuration from JSON +rawconfig get raw configuration as base64 +rawconfig set raw configuration from base64 +rftransmit transmit a RF command +factoryreset reset device to factory defaults and restart )"); } @@ -621,6 +715,25 @@ authtoken [] )", _handleAuthtokenCommand, }; +static const SerialCmdHandler kLcgOverrideCmdHandler = { + "lcgoverride", + R"(lcgoverride + Get the domain overridden for LCG endpoint (if any). + +lcgoverride set + Set a domain to override the LCG endpoint. + Arguments: + must be a string. + Example: + lcgoverride set eu1-gateway.shocklink.net + +lcgoverride clear + Clear the overridden LCG endpoint. + Example: + lcgoverride clear +)", + _handleLcgOverrideCommand, +}; static const SerialCmdHandler kNetworksCmdHandler = { "networks", R"(networks @@ -813,6 +926,7 @@ bool SerialInputHandler::Init() { RegisterCommandHandler(kRfTxPinCmdHandler); RegisterCommandHandler(kDomainCmdHandler); RegisterCommandHandler(kAuthTokenCmdHandler); + RegisterCommandHandler(kLcgOverrideCmdHandler); RegisterCommandHandler(kNetworksCmdHandler); RegisterCommandHandler(kKeepAliveCmdHandler); RegisterCommandHandler(kJsonConfigCmdHandler); diff --git a/src/serialization/JsonAPI.cpp b/src/serialization/JsonAPI.cpp index dd98dbb0..eb15885f 100644 --- a/src/serialization/JsonAPI.cpp +++ b/src/serialization/JsonAPI.cpp @@ -8,6 +8,54 @@ const char* const TAG = "JsonAPI"; using namespace OpenShock::Serialization; +bool JsonAPI::ParseLcgInstanceDetailsJsonResponse(int code, const cJSON* root, JsonAPI::LcgInstanceDetailsResponse& out) { + (void)code; + + if (cJSON_IsObject(root) == 0) { + ESP_LOGJSONE("not an object", root); + return false; + } + + out = {}; + + const cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name"); + if (cJSON_IsString(name) == 0) { + ESP_LOGJSONE("value at 'data.name' is not a string", root); + return false; + } + + const cJSON* version = cJSON_GetObjectItemCaseSensitive(root, "version"); + if (cJSON_IsString(version) == 0) { + ESP_LOGJSONE("value at 'data.version' is not a string", root); + return false; + } + + const cJSON* currentTime = cJSON_GetObjectItemCaseSensitive(root, "currentTime"); + if (cJSON_IsString(currentTime) == 0) { + ESP_LOGJSONE("value at 'data.currentTime' is not a string", root); + return false; + } + + const cJSON* countryCode = cJSON_GetObjectItemCaseSensitive(root, "countryCode"); + if (cJSON_IsString(countryCode) == 0) { + ESP_LOGJSONE("value at 'data.countryCode' is not a string", root); + return false; + } + + const cJSON* fqdn = cJSON_GetObjectItemCaseSensitive(root, "fqdn"); + if (cJSON_IsString(fqdn) == 0) { + ESP_LOGJSONE("value at 'data.fqdn' is not a string", root); + return false; + } + + out.name = name->valuestring; + out.version = version->valuestring; + out.currentTime = currentTime->valuestring; + out.countryCode = countryCode->valuestring; + out.fqdn = fqdn->valuestring; + + return true; +} bool JsonAPI::ParseBackendVersionJsonResponse(int code, const cJSON* root, JsonAPI::BackendVersionResponse& out) { (void)code;