From 5cb3fc18670faa310038a06cfdf98cf9e9e287c4 Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 19 Jul 2026 23:45:28 -0700 Subject: [PATCH] feat(mqtt): add mesh-chaun14 and wcmesh presets Support "{pubkey}" username sentinel so brokers can auth with the device public key without enlarging the prefs username field. --- src/helpers/CommonCLI_Observer.cpp | 6 +++++ src/helpers/MQTTPresets.h | 34 +++++++++++++++++++++++++---- src/helpers/bridges/MQTTBridge.cpp | 35 +++++++++++++++++++----------- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/helpers/CommonCLI_Observer.cpp b/src/helpers/CommonCLI_Observer.cpp index 002d6a15..37982da4 100644 --- a/src/helpers/CommonCLI_Observer.cpp +++ b/src/helpers/CommonCLI_Observer.cpp @@ -390,6 +390,12 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf } else if (p && p->topic_style == MQTT_TOPIC_MESHCORE && (strlen(_mqtt_prefs.mqtt_iata) == 0 || strcmp(_mqtt_prefs.mqtt_iata, "XXX") == 0)) { sprintf(reply, "OK - slot %d preset: %s (run 'set mqtt.iata ' to publish)", slot + 1, preset_name); + } else if (p && mqttPresetNeedsSlotPassword(p) && + _mqtt_prefs.mqtt_slot_password[slot][0] == '\0' && + !mqttPresetNeedsSlotUsername(p)) { + sprintf(reply, + "OK - slot %d preset: %s (run 'set mqtt%d.password ' to connect)", + slot + 1, preset_name, slot + 1); } else if (p && mqttPresetNeedsSlotCredentials(p) && (_mqtt_prefs.mqtt_slot_username[slot][0] == '\0' || _mqtt_prefs.mqtt_slot_password[slot][0] == '\0')) { diff --git a/src/helpers/MQTTPresets.h b/src/helpers/MQTTPresets.h index b3fcf5a8..bf263799 100644 --- a/src/helpers/MQTTPresets.h +++ b/src/helpers/MQTTPresets.h @@ -40,10 +40,32 @@ struct MQTTPresetDef { const char* userpass_password; // MQTT_AUTH_USERPASS: embedded password, or nullptr to use mqttN.password }; -// True when preset uses MQTT_AUTH_USERPASS but credentials come from slot prefs (mqttN.username/password). -static inline bool mqttPresetNeedsSlotCredentials(const MQTTPresetDef* preset) { +// Sentinel: resolve MQTT username from device public-key hex at connect time. +// Braces match topic placeholders ({device}/{iata}); never send this string to the broker. +static const char MQTT_USERPASS_USERNAME_PUBKEY[] = "{pubkey}"; + +static inline bool mqttPresetUsesDevicePubkeyUsername(const MQTTPresetDef* preset) { return preset && preset->auth_type == MQTT_AUTH_USERPASS && - (!preset->userpass_username || !preset->userpass_password); + preset->userpass_username && + strcmp(preset->userpass_username, MQTT_USERPASS_USERNAME_PUBKEY) == 0; +} + +// True when USERPASS username must come from mqttN.username (null embedded username). +// "{pubkey}" is an embedded sentinel, so it does not need a slot username. +static inline bool mqttPresetNeedsSlotUsername(const MQTTPresetDef* preset) { + return preset && preset->auth_type == MQTT_AUTH_USERPASS && + !preset->userpass_username; +} + +// True when USERPASS password must come from mqttN.password (null embedded password). +static inline bool mqttPresetNeedsSlotPassword(const MQTTPresetDef* preset) { + return preset && preset->auth_type == MQTT_AUTH_USERPASS && + !preset->userpass_password; +} + +// True when preset uses MQTT_AUTH_USERPASS but at least one credential comes from slot prefs. +static inline bool mqttPresetNeedsSlotCredentials(const MQTTPresetDef* preset) { + return mqttPresetNeedsSlotUsername(preset) || mqttPresetNeedsSlotPassword(preset); } // Google Trust Services - GTS Root R4 (used by LetsMesh Analyzer) @@ -105,7 +127,7 @@ static const char ISRG_ROOT_X1[] PROGMEM = "-----END CERTIFICATE-----\n"; // Number of built-in presets -static const int MQTT_PRESET_COUNT = 27; +static const int MQTT_PRESET_COUNT = 29; // Built-in preset definitions (stored in flash) static const MQTTPresetDef MQTT_PRESETS[MQTT_PRESET_COUNT] = { @@ -142,6 +164,10 @@ static const MQTTPresetDef MQTT_PRESETS[MQTT_PRESET_COUNT] = { { "flmesh", "wss://mcmqtt.jntconnections.com:443", "mcmqtt.jntconnections.com", GTS_ROOT_R4, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr }, { "corecomms", "wss://mqtt.corecomms.net:443/mqtt", "mqtt.corecomms.net", GTS_ROOT_R4, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr }, { "meshtexas", "wss://mqtt.meshtexas.org:443/mqtt", "mqtt.meshtexas.org", GTS_ROOT_R4, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr }, + // Username is device pubkey hex at connect; password from mqttN.password. No TLS. + { "mesh-chaun14", "mqtt://mqtt.mesh.chaun14.fr:1884", nullptr, nullptr, MQTT_AUTH_USERPASS, MQTT_TOPIC_MESHCORE, 0, true, 60, MQTT_USERPASS_USERNAME_PUBKEY, nullptr }, + // LetsMesh-compatible JWT; TLS is Let's Encrypt (ISRG Root X1), not GTS. + { "wcmesh", "wss://mqtt.wcmesh.com:443", "mqtt.wcmesh.com", ISRG_ROOT_X1, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr }, }; // Find a preset by name, returns nullptr if not found diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 9a9d4628..2066f9c6 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -1395,10 +1395,19 @@ void MQTTBridge::setupSlot(int index) { slot.client->setCredentials(_jwt_username, slot.auth_token); } } else if (slot.preset->auth_type == MQTT_AUTH_USERPASS) { - if (slot.preset->userpass_username && slot.preset->userpass_password) { - slot.client->setCredentials(slot.preset->userpass_username, slot.preset->userpass_password); - } else if (strlen(slot.username) > 0) { - slot.client->setCredentials(slot.username, slot.password); + const char* user = nullptr; + const char* pass = slot.preset->userpass_password + ? slot.preset->userpass_password + : slot.password; + if (mqttPresetUsesDevicePubkeyUsername(slot.preset)) { + user = _device_id; // never send "{pubkey}" literally + } else if (slot.preset->userpass_username) { + user = slot.preset->userpass_username; + } else if (slot.username[0] != '\0') { + user = slot.username; + } + if (user && user[0] != '\0' && pass && pass[0] != '\0') { + slot.client->setCredentials(user, pass); } } } else { @@ -2310,15 +2319,15 @@ bool MQTTBridge::isSlotReady(int index, char* reason_buf, size_t reason_size) co return false; } } - if (mqttPresetNeedsSlotCredentials(slot.preset)) { - if (_obs->mqtt_slot_username[index][0] == '\0') { - if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.username ", index + 1); - return false; - } - if (_obs->mqtt_slot_password[index][0] == '\0') { - if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.password ", index + 1); - return false; - } + if (mqttPresetNeedsSlotUsername(slot.preset) && + _obs->mqtt_slot_username[index][0] == '\0') { + if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.username ", index + 1); + return false; + } + if (mqttPresetNeedsSlotPassword(slot.preset) && + _obs->mqtt_slot_password[index][0] == '\0') { + if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.password ", index + 1); + return false; } } else { // Custom slot without a topic template uses meshcore format, needs IATA