mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-28 00:44:06 +00:00
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:
@@ -415,6 +415,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')) {
|
||||
|
||||
@@ -43,10 +43,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)
|
||||
@@ -108,7 +130,7 @@ static const char ISRG_ROOT_X1[] PROGMEM =
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
// Number of built-in presets
|
||||
static const int MQTT_PRESET_COUNT = 26;
|
||||
static const int MQTT_PRESET_COUNT = 28;
|
||||
|
||||
// Built-in preset definitions (stored in flash)
|
||||
static const MQTTPresetDef MQTT_PRESETS[MQTT_PRESET_COUNT] = {
|
||||
@@ -144,6 +166,10 @@ static const MQTTPresetDef MQTT_PRESETS[MQTT_PRESET_COUNT] = {
|
||||
{ "ipnt.uk", "wss://mqtt.ipnt.uk:443", "mqtt.ipnt.uk", ISRG_ROOT_X1, MQTT_AUTH_JWT, MQTT_TOPIC_MESHCORE, 0, true, 55, nullptr, nullptr },
|
||||
{ "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 },
|
||||
// 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
|
||||
|
||||
@@ -1648,10 +1648,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 {
|
||||
@@ -2507,15 +2516,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
|
||||
|
||||
@@ -878,7 +878,11 @@ void WebConfigServer::handlePresets(AsyncWebServerRequest* req) {
|
||||
// What the UI must collect for this preset to connect
|
||||
if (p.topic_style == MQTT_TOPIC_MESHRANK) {
|
||||
o["needs"] = "token";
|
||||
} else if (mqttPresetNeedsSlotCredentials(&p)) {
|
||||
} else if (mqttPresetNeedsSlotUsername(&p) && mqttPresetNeedsSlotPassword(&p)) {
|
||||
o["needs"] = "userpass";
|
||||
} else if (mqttPresetNeedsSlotPassword(&p)) {
|
||||
o["needs"] = "password";
|
||||
} else if (mqttPresetNeedsSlotUsername(&p)) {
|
||||
o["needs"] = "userpass";
|
||||
} else {
|
||||
o["needs"] = "none";
|
||||
|
||||
@@ -105,6 +105,9 @@ TEST(MQTTPresets, UserpassWithoutEmbeddedCredsNeedsSlotCredentials) {
|
||||
ASSERT_NE(nullptr, p);
|
||||
EXPECT_EQ(MQTT_AUTH_USERPASS, p->auth_type);
|
||||
EXPECT_TRUE(mqttPresetNeedsSlotCredentials(p));
|
||||
EXPECT_TRUE(mqttPresetNeedsSlotUsername(p));
|
||||
EXPECT_TRUE(mqttPresetNeedsSlotPassword(p));
|
||||
EXPECT_FALSE(mqttPresetUsesDevicePubkeyUsername(p));
|
||||
}
|
||||
|
||||
TEST(MQTTPresets, NonUserpassNeverNeedsSlotCredentials) {
|
||||
@@ -124,6 +127,34 @@ TEST(MQTTPresets, MeshrankIsTokenStyleNoAuth) {
|
||||
EXPECT_EQ(MQTT_AUTH_NONE, p->auth_type);
|
||||
}
|
||||
|
||||
TEST(MQTTPresets, MeshChaun14UsesPubkeyUsernameAndNeedsPassword) {
|
||||
const MQTTPresetDef* p = findMQTTPreset("mesh-chaun14");
|
||||
ASSERT_NE(nullptr, p);
|
||||
EXPECT_EQ(MQTT_AUTH_USERPASS, p->auth_type);
|
||||
EXPECT_EQ(MQTT_TOPIC_MESHCORE, p->topic_style);
|
||||
EXPECT_STREQ("mqtt://mqtt.mesh.chaun14.fr:1884", p->server_url);
|
||||
EXPECT_EQ(nullptr, p->ca_cert);
|
||||
EXPECT_EQ(60, p->keepalive);
|
||||
EXPECT_TRUE(mqttPresetUsesDevicePubkeyUsername(p));
|
||||
EXPECT_STREQ(MQTT_USERPASS_USERNAME_PUBKEY, p->userpass_username);
|
||||
EXPECT_FALSE(mqttPresetNeedsSlotUsername(p));
|
||||
EXPECT_TRUE(mqttPresetNeedsSlotPassword(p));
|
||||
EXPECT_TRUE(mqttPresetNeedsSlotCredentials(p));
|
||||
}
|
||||
|
||||
TEST(MQTTPresets, WcmeshIsJwtWithIsrgRootX1) {
|
||||
const MQTTPresetDef* p = findMQTTPreset("wcmesh");
|
||||
ASSERT_NE(nullptr, p);
|
||||
EXPECT_EQ(MQTT_AUTH_JWT, p->auth_type);
|
||||
EXPECT_EQ(MQTT_TOPIC_MESHCORE, p->topic_style);
|
||||
EXPECT_STREQ("wss://mqtt.wcmesh.com:443", p->server_url);
|
||||
EXPECT_STREQ("mqtt.wcmesh.com", p->jwt_audience);
|
||||
EXPECT_EQ(ISRG_ROOT_X1, p->ca_cert);
|
||||
EXPECT_NE(GTS_ROOT_R4, p->ca_cert);
|
||||
EXPECT_FALSE(mqttPresetNeedsSlotCredentials(p));
|
||||
EXPECT_FALSE(mqttPresetUsesDevicePubkeyUsername(p));
|
||||
}
|
||||
|
||||
// ---- slot count constants -------------------------------------------------
|
||||
|
||||
TEST(MQTTPresets, SlotCountsAreSane) {
|
||||
|
||||
+1
-1
@@ -639,7 +639,7 @@ function refreshSlotFields(host,i){
|
||||
var def=null;st.presets.forEach(function(p){if(p.name===name)def=p});
|
||||
var needs=name==="custom"?"custom":(def?def.needs:"none");
|
||||
var showMap={token:needs==="token",username:needs==="userpass"||needs==="custom",
|
||||
password:needs==="userpass"||needs==="custom",server:needs==="custom",
|
||||
password:needs==="userpass"||needs==="password"||needs==="custom",server:needs==="custom",
|
||||
port:needs==="custom",topic:needs==="custom",audience:needs==="custom"};
|
||||
Object.keys(showMap).forEach(function(f){
|
||||
var w=card.querySelector(".sf-"+f);if(w)w.classList.toggle("hide",!showMap[f]);
|
||||
|
||||
Reference in New Issue
Block a user