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.
This commit is contained in:
agessaman
2026-07-19 23:45:48 -07:00
parent 26db31f625
commit 5cb3fc1867
3 changed files with 58 additions and 17 deletions
+6
View File
@@ -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 <airport_code>' 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 <pass>' 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')) {
+30 -4
View File
@@ -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
+22 -13
View File
@@ -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 <user>", index + 1);
return false;
}
if (_obs->mqtt_slot_password[index][0] == '\0') {
if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.password <pass>", 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 <user>", 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 <pass>", index + 1);
return false;
}
} else {
// Custom slot without a topic template uses meshcore format, needs IATA