diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 74fcf0de..4c1e0700 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -757,6 +757,18 @@ void MyMesh::begin(FILESYSTEM *fs) { _fs = fs; // load persisted prefs _cli.loadPrefs(_fs); + + // Ensure analyzer servers are enabled by default (in case no prefs were loaded) + if (_prefs.mqtt_analyzer_us_enabled == 0 && _prefs.mqtt_analyzer_eu_enabled == 0) { + _prefs.mqtt_analyzer_us_enabled = 1; // enabled + _prefs.mqtt_analyzer_eu_enabled = 1; // enabled + MESH_DEBUG_PRINTLN("Setting analyzer servers to enabled by default"); + } + + // Set MQTT origin to actual device name (not build-time ADVERT_NAME) + StrHelper::strncpy(_prefs.mqtt_origin, _prefs.node_name, sizeof(_prefs.mqtt_origin)); + MESH_DEBUG_PRINTLN("MQTT origin set to device name: %s", _prefs.mqtt_origin); + acl.load(_fs); // TODO: key_store.begin(); region_map.load(_fs); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 766582d3..31231ab7 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -129,6 +129,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Let's Mesh Analyzer settings file.read((uint8_t *)&_prefs->mqtt_analyzer_us_enabled, sizeof(_prefs->mqtt_analyzer_us_enabled)); // 344 file.read((uint8_t *)&_prefs->mqtt_analyzer_eu_enabled, sizeof(_prefs->mqtt_analyzer_eu_enabled)); // 345 + file.read((uint8_t *)&_prefs->mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key)); // 346 // 209 >>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge) @@ -264,6 +265,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { // Let's Mesh Analyzer settings file.write((uint8_t *)&_prefs->mqtt_analyzer_us_enabled, sizeof(_prefs->mqtt_analyzer_us_enabled)); // 344 file.write((uint8_t *)&_prefs->mqtt_analyzer_eu_enabled, sizeof(_prefs->mqtt_analyzer_eu_enabled)); // 345 + file.write((uint8_t *)&_prefs->mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key)); // 346 // 209 >>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge) @@ -494,6 +496,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %s", _prefs->mqtt_analyzer_us_enabled ? "on" : "off"); } else if (memcmp(config, "mqtt.analyzer.eu", 17) == 0) { sprintf(reply, "> %s", _prefs->mqtt_analyzer_eu_enabled ? "on" : "off"); + } else if (memcmp(config, "mqtt.owner", 10) == 0) { + if (_prefs->mqtt_owner_public_key[0] != '\0') { + sprintf(reply, "> %s", _prefs->mqtt_owner_public_key); + } else { + strcpy(reply, "> (not set)"); + } } else if (memcmp(config, "mqtt.config.valid", 17) == 0) { // Check if MQTT configuration is valid using static method bool valid = MQTTBridge::isConfigValid(_prefs); @@ -777,6 +785,31 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _prefs->mqtt_analyzer_eu_enabled = memcmp(&config[17], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); + } else if (memcmp(config, "mqtt.owner ", 11) == 0) { + // Validate that it's a valid hex string of the correct length (64 hex chars = 32 bytes) + const char* owner_key = &config[11]; + int key_len = strlen(owner_key); + if (key_len == 64) { + // Validate hex characters + bool valid = true; + for (int i = 0; i < key_len; i++) { + if (!((owner_key[i] >= '0' && owner_key[i] <= '9') || + (owner_key[i] >= 'A' && owner_key[i] <= 'F') || + (owner_key[i] >= 'a' && owner_key[i] <= 'f'))) { + valid = false; + break; + } + } + if (valid) { + StrHelper::strncpy(_prefs->mqtt_owner_public_key, owner_key, sizeof(_prefs->mqtt_owner_public_key)); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: invalid hex characters in public key"); + } + } else { + strcpy(reply, "Error: public key must be 64 hex characters (32 bytes)"); + } #endif } else { sprintf(reply, "unknown config: %s", config); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 35d9abaa..f0a95899 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -106,17 +106,12 @@ struct MQTTPrefs { uint16_t mqtt_port; // MQTT server port char mqtt_username[32]; // MQTT username char mqtt_password[64]; // MQTT password -======= ->>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge) // Let's Mesh Analyzer settings uint8_t mqtt_analyzer_us_enabled; // Enable US analyzer server uint8_t mqtt_analyzer_eu_enabled; // Enable EU analyzer server -<<<<<<< HEAD char mqtt_owner_public_key[65]; // Owner public key (hex string, same length as repeater public key) char mqtt_email[64]; // Owner email address for matching nodes with owners -======= ->>>>>>> 6f42dc3 (Implement Let's Mesh Analyzer integration in MQTT Bridge) }; #endif diff --git a/src/helpers/JWTHelper.cpp b/src/helpers/JWTHelper.cpp index c3b4359b..8f906711 100644 --- a/src/helpers/JWTHelper.cpp +++ b/src/helpers/JWTHelper.cpp @@ -14,7 +14,9 @@ bool JWTHelper::createAuthToken( unsigned long issuedAt, unsigned long expiresIn, char* token, - size_t tokenSize + size_t tokenSize, + const char* owner, + const char* client ) { Serial.printf("JWTHelper: Starting JWT creation for audience: %s\n", audience); @@ -52,7 +54,7 @@ bool JWTHelper::createAuthToken( // Create payload with HEX public key (not base64!) char payload[512]; - size_t payloadLen = createPayload(publicKeyHex, audience, issuedAt, expiresIn, payload, sizeof(payload)); + size_t payloadLen = createPayload(publicKeyHex, audience, issuedAt, expiresIn, payload, sizeof(payload), owner, client); if (payloadLen == 0) { Serial.printf("JWTHelper: Failed to create payload\n"); return false; @@ -219,11 +221,19 @@ size_t JWTHelper::createPayload( unsigned long issuedAt, unsigned long expiresIn, char* output, - size_t outputSize + size_t outputSize, + const char* owner, + const char* client ) { Serial.printf("JWTHelper: createPayload called with outputSize: %d\n", (int)outputSize); Serial.printf("JWTHelper: publicKey: %s, audience: %s, issuedAt: %lu, expiresIn: %lu\n", publicKey, audience, issuedAt, expiresIn); + if (owner) { + Serial.printf("JWTHelper: owner: %s\n", owner); + } + if (client) { + Serial.printf("JWTHelper: client: %s\n", client); + } // Create JWT payload DynamicJsonDocument doc(512); @@ -235,6 +245,16 @@ size_t JWTHelper::createPayload( doc["exp"] = issuedAt + expiresIn; } + // Add optional owner field if provided + if (owner && strlen(owner) > 0) { + doc["owner"] = owner; + } + + // Add optional client field if provided + if (client && strlen(client) > 0) { + doc["client"] = client; + } + // Use temporary buffer for JSON char jsonBuffer[512]; size_t len = serializeJson(doc, jsonBuffer, sizeof(jsonBuffer)); diff --git a/src/helpers/JWTHelper.h b/src/helpers/JWTHelper.h index a0890c8a..40148acf 100644 --- a/src/helpers/JWTHelper.h +++ b/src/helpers/JWTHelper.h @@ -20,6 +20,8 @@ public: * @param expiresIn Expiration time in seconds (0 for no expiration) * @param token Buffer to store the resulting token * @param tokenSize Size of the token buffer + * @param owner Optional owner public key in hex format (nullptr if not set) + * @param client Optional client string (nullptr if not set) * @return true if token was created successfully */ static bool createAuthToken( @@ -28,7 +30,9 @@ public: unsigned long issuedAt = 0, unsigned long expiresIn = 0, char* token = nullptr, - size_t tokenSize = 0 + size_t tokenSize = 0, + const char* owner = nullptr, + const char* client = nullptr ); private: @@ -61,6 +65,8 @@ private: * @param expiresIn Expiration time in seconds (0 for no expiration) * @param output Output buffer * @param outputSize Size of output buffer + * @param owner Optional owner public key in hex format (nullptr if not set) + * @param client Optional client string (nullptr if not set) * @return Length of payload, or 0 on error */ static size_t createPayload( @@ -69,7 +75,9 @@ private: unsigned long issuedAt, unsigned long expiresIn, char* output, - size_t outputSize + size_t outputSize, + const char* owner = nullptr, + const char* client = nullptr ); }; diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 02f09e53..88039055 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -999,12 +999,35 @@ bool MQTTBridge::createAuthToken() { bool us_token_created = false; bool eu_token_created = false; + // Get current time for expiration tracking + unsigned long current_time = time(nullptr); + unsigned long expires_in = 86400; // 24 hours + + // Prepare owner public key (if set) - convert to uppercase hex + const char* owner_key = nullptr; + char owner_key_uppercase[65]; + if (_prefs->mqtt_owner_public_key[0] != '\0') { + // Copy and convert to uppercase + strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1); + owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0'; + for (int i = 0; owner_key_uppercase[i]; i++) { + owner_key_uppercase[i] = toupper(owner_key_uppercase[i]); + } + owner_key = owner_key_uppercase; + MQTT_DEBUG_PRINTLN("Using owner public key: %s", owner_key); + } + + // Build client version string (same format as used in status messages) + char client_version[64]; + snprintf(client_version, sizeof(client_version), "meshcoretomqtt/%s", _build_date); + // Create JWT token for US server if (_analyzer_us_enabled) { MQTT_DEBUG_PRINTLN("Creating JWT token for US server..."); if (JWTHelper::createAuthToken( *_identity, "mqtt-us-v1.letsmesh.net", - 0, 86400, _auth_token_us, sizeof(_auth_token_us))) { + 0, expires_in, _auth_token_us, sizeof(_auth_token_us), + owner_key, client_version)) { MQTT_DEBUG_PRINTLN("Created auth token for US server"); us_token_created = true; } else { @@ -1017,7 +1040,8 @@ bool MQTTBridge::createAuthToken() { MQTT_DEBUG_PRINTLN("Creating JWT token for EU server..."); if (JWTHelper::createAuthToken( *_identity, "mqtt-eu-v1.letsmesh.net", - 0, 86400, _auth_token_eu, sizeof(_auth_token_eu))) { + 0, expires_in, _auth_token_eu, sizeof(_auth_token_eu), + owner_key, client_version)) { MQTT_DEBUG_PRINTLN("Created auth token for EU server"); eu_token_created = true; } else { @@ -1302,9 +1326,199 @@ void MQTTBridge::publishStatusToAnalyzerClient(PsychicMqttClient* client, const } void MQTTBridge::maintainAnalyzerConnections() { +<<<<<<< HEAD // PsychicMqttClient handles connection maintenance and reconnection automatically // No manual maintenance needed - the library manages this internally // Connection state changes are handled via the onConnect/onDisconnect callbacks +======= + if (!_identity) { + return; + } + + unsigned long current_time = time(nullptr); + // If time is not synced (time() returns 0 or very small value), skip expiration checks + // Tokens will still work but we can't track expiration properly + // If expiration time was set before time sync, it will be a small value, so we'll renew + bool time_synced = (current_time >= 1000000000); // After year 2001 + + const unsigned long RENEWAL_BUFFER = 3600; // Renew tokens 1 hour before expiration + const unsigned long RENEWAL_THROTTLE_MS = 60000; // Don't attempt renewal more than once per minute + + unsigned long now_millis = millis(); + + // Check and renew US server token if needed + if (_analyzer_us_enabled && _analyzer_us_client) { + // Check if token is expired or will expire soon + // If time wasn't synced when token was created, expiration time will be invalid, so renew + bool token_needs_renewal = (_token_us_expires_at == 0) || + !time_synced || + !(_token_us_expires_at >= 1000000000) || // Expiration time invalid + (time_synced && current_time >= _token_us_expires_at) || + (time_synced && current_time >= (_token_us_expires_at - RENEWAL_BUFFER)); + + // Throttle renewal attempts - don't try more than once per minute to avoid blocking + bool can_attempt_renewal = (now_millis - _last_token_renewal_attempt_us) >= RENEWAL_THROTTLE_MS; + + // Check if client is disconnected and needs reconnection with new token + bool needs_reconnect = !_analyzer_us_client->connected(); + + if (token_needs_renewal && can_attempt_renewal) { + _last_token_renewal_attempt_us = now_millis; + MQTT_DEBUG_PRINTLN("US token expired or expiring soon (expires_at: %lu, current: %lu), renewing...", + _token_us_expires_at, current_time); + + // Prepare owner public key (if set) - convert to uppercase hex + const char* owner_key = nullptr; + char owner_key_uppercase[65]; + if (_prefs->mqtt_owner_public_key[0] != '\0') { + // Copy and convert to uppercase + strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1); + owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0'; + for (int i = 0; owner_key_uppercase[i]; i++) { + owner_key_uppercase[i] = toupper(owner_key_uppercase[i]); + } + owner_key = owner_key_uppercase; + } + + // Build client version string (same format as used in status messages) + char client_version[64]; + snprintf(client_version, sizeof(client_version), "meshcoretomqtt/%s", _build_date); + + // Renew the token + if (JWTHelper::createAuthToken( + *_identity, "mqtt-us-v1.letsmesh.net", + 0, 86400, _auth_token_us, sizeof(_auth_token_us), + owner_key, client_version)) { + unsigned long expires_in = 86400; // 24 hours + _token_us_expires_at = current_time + expires_in; + MQTT_DEBUG_PRINTLN("US token renewed, new expiration: %lu", _token_us_expires_at); + + // Update client credentials with new token + _analyzer_us_client->setCredentials(_analyzer_username, _auth_token_us); + + // Reconnect to apply new token (whether currently connected or not) + // If connected, disconnect first to ensure new token is used + if (_analyzer_us_client->connected()) { + MQTT_DEBUG_PRINTLN("Disconnecting US server to apply new token..."); + _analyzer_us_client->disconnect(); + } + MQTT_DEBUG_PRINTLN("Reconnecting to US server with renewed token..."); + _last_reconnect_attempt_us = now_millis; // Update reconnect timestamp to throttle subsequent attempts + _analyzer_us_client->connect(); + } else { + MQTT_DEBUG_PRINTLN("Failed to renew US token"); + _token_us_expires_at = 0; + } + } else if (needs_reconnect) { + // Token is still valid but connection is lost - reconnect with existing token + // Throttle reconnection attempts to avoid spamming + unsigned long reconnect_elapsed = (now_millis >= _last_reconnect_attempt_us) ? + (now_millis - _last_reconnect_attempt_us) : + (ULONG_MAX - _last_reconnect_attempt_us + now_millis + 1); + if (reconnect_elapsed >= RECONNECT_THROTTLE_MS) { + _last_reconnect_attempt_us = now_millis; + MQTT_DEBUG_PRINTLN("US server disconnected but token still valid, reconnecting..."); + _analyzer_us_client->connect(); + } else { + // Throttled - only log periodically to avoid spam (every 5 minutes max) + static unsigned long last_throttle_log_us = 0; + if (now_millis - last_throttle_log_us > 300000) { + MQTT_DEBUG_PRINTLN("US server reconnection throttled (last attempt %lu ms ago, need %lu ms)", + reconnect_elapsed, RECONNECT_THROTTLE_MS); + last_throttle_log_us = now_millis; + } + } + } + } + + // Check and renew EU server token if needed + if (_analyzer_eu_enabled && _analyzer_eu_client) { + // Check if token is expired or will expire soon + // If time wasn't synced when token was created, expiration time will be invalid, so renew + bool token_needs_renewal = (_token_eu_expires_at == 0) || + !time_synced || + !(_token_eu_expires_at >= 1000000000) || // Expiration time invalid + (time_synced && current_time >= _token_eu_expires_at) || + (time_synced && current_time >= (_token_eu_expires_at - RENEWAL_BUFFER)); + + // Throttle renewal attempts - don't try more than once per minute to avoid blocking + bool can_attempt_renewal = (now_millis - _last_token_renewal_attempt_eu) >= RENEWAL_THROTTLE_MS; + + // Check if client is disconnected and needs reconnection with new token + bool needs_reconnect = !_analyzer_eu_client->connected(); + + if (token_needs_renewal && can_attempt_renewal) { + _last_token_renewal_attempt_eu = now_millis; + MQTT_DEBUG_PRINTLN("EU token expired or expiring soon (expires_at: %lu, current: %lu), renewing...", + _token_eu_expires_at, current_time); + + // Prepare owner public key (if set) - convert to uppercase hex + const char* owner_key = nullptr; + char owner_key_uppercase[65]; + if (_prefs->mqtt_owner_public_key[0] != '\0') { + // Copy and convert to uppercase + strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1); + owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0'; + for (int i = 0; owner_key_uppercase[i]; i++) { + owner_key_uppercase[i] = toupper(owner_key_uppercase[i]); + } + owner_key = owner_key_uppercase; + } + + // Build client version string (same format as used in status messages) + char client_version[64]; + snprintf(client_version, sizeof(client_version), "meshcoretomqtt/%s", _build_date); + + // Renew the token + if (JWTHelper::createAuthToken( + *_identity, "mqtt-eu-v1.letsmesh.net", + 0, 86400, _auth_token_eu, sizeof(_auth_token_eu), + owner_key, client_version)) { + unsigned long expires_in = 86400; // 24 hours + _token_eu_expires_at = current_time + expires_in; + MQTT_DEBUG_PRINTLN("EU token renewed, new expiration: %lu", _token_eu_expires_at); + + // Update client credentials with new token + _analyzer_eu_client->setCredentials(_analyzer_username, _auth_token_eu); + + // Reconnect to apply new token (whether currently connected or not) + // If connected, disconnect first to ensure new token is used + if (_analyzer_eu_client->connected()) { + MQTT_DEBUG_PRINTLN("Disconnecting EU server to apply new token..."); + _analyzer_eu_client->disconnect(); + } + MQTT_DEBUG_PRINTLN("Reconnecting to EU server with renewed token..."); + _last_reconnect_attempt_eu = now_millis; // Update reconnect timestamp to throttle subsequent attempts + _analyzer_eu_client->connect(); + } else { + MQTT_DEBUG_PRINTLN("Failed to renew EU token"); + _token_eu_expires_at = 0; + } + } else if (needs_reconnect) { + // Token is still valid but connection is lost - reconnect with existing token + // Throttle reconnection attempts to avoid spamming + unsigned long reconnect_elapsed = (now_millis >= _last_reconnect_attempt_eu) ? + (now_millis - _last_reconnect_attempt_eu) : + (ULONG_MAX - _last_reconnect_attempt_eu + now_millis + 1); + if (reconnect_elapsed >= RECONNECT_THROTTLE_MS) { + _last_reconnect_attempt_eu = now_millis; + MQTT_DEBUG_PRINTLN("EU server disconnected but token still valid, reconnecting..."); + _analyzer_eu_client->connect(); + } else { + // Throttled - only log periodically to avoid spam (every 5 minutes max) + static unsigned long last_throttle_log_eu = 0; + if (now_millis - last_throttle_log_eu > 300000) { + MQTT_DEBUG_PRINTLN("EU server reconnection throttled (last attempt %lu ms ago, need %lu ms)", + reconnect_elapsed, RECONNECT_THROTTLE_MS); + last_throttle_log_eu = now_millis; + } + } + } + } + + // Note: PsychicMqttClient handles automatic reconnection internally, + // but we need to ensure tokens are renewed before reconnection attempts +>>>>>>> 1ebe417c (Enhance CommonCLI and JWTHelper for MQTT owner public key support) } void MQTTBridge::setMessageTypes(bool status, bool packets, bool raw) { diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 387ff0e3..89b62be0 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -117,8 +117,8 @@ private: // Let's Mesh Analyzer support bool _analyzer_us_enabled; bool _analyzer_eu_enabled; - char _auth_token_us[512]; // JWT token for US server authentication - char _auth_token_eu[512]; // JWT token for EU server authentication + char _auth_token_us[768]; // JWT token for US server authentication (increased for owner/client fields) + char _auth_token_eu[768]; // JWT token for EU server authentication (increased for owner/client fields) char _analyzer_username[70]; // Username in format v1_{UPPERCASE_PUBLIC_KEY} // Device identity for JWT token creation