mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 06:22:41 +00:00
Integrate compatible observer board, preset, WebConfig, display, CI, and policy updates. Preserve the checkout's newer MQTT binary preferences and WiFi bridge runtime where the forked implementations conflict. Keep ThinkNode observer networking WiFi-based until the Ethernet transport is wired into that runtime.
146 lines
6.4 KiB
C
146 lines
6.4 KiB
C
#pragma once
|
|
|
|
#include <string.h>
|
|
#include "MQTTPresets.h" // MAX_MQTT_SLOTS
|
|
|
|
// Classification of the config keys the web portal is allowed to drive through
|
|
// the CLI `set` handlers. Factored out of WebConfigServer.cpp so the allowlist
|
|
// and the (attacker-facing) key parsing can be unit-tested on the host without
|
|
// pulling in the whole ESP32 web server (see test/test_webconfig_keys).
|
|
//
|
|
// Everything here is pure string logic. The functions are `static inline` so
|
|
// each translation unit that includes this gets its own copy (there are only
|
|
// two: WebConfigServer.cpp and the test), avoiding any ODR concern.
|
|
|
|
// Keys mapping to CLI `set <key> <value>` handlers. Everything not listed here
|
|
// is rejected, so a crafted request can't reach arbitrary commands (`erase`,
|
|
// etc.) through the batch. The portal's admin-password field is classified
|
|
// separately, see wcIsAdminPasswordKey below.
|
|
static const char* const WC_ALLOWED_SET_KEYS[] = {
|
|
// NodePrefs (radio / node)
|
|
"name", "bluetooth.name", "bluetooth.mac", "bluetooth.stealth",
|
|
"lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
|
|
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.fem.txgain", "radio.rxps", "powersaving",
|
|
"repeat",
|
|
"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout",
|
|
"display.inbox",
|
|
"advert.interval", "flood.advert.interval",
|
|
"flood.max", "flood.max.advert", "flood.max.unscoped", "loop.detect",
|
|
// MQTTPrefs (WiFi / MQTT / misc observer)
|
|
"wifi.ssid", "wifi.pwd", "wifi.powersave", "espnow.channel",
|
|
"mqtt.enabled", "mqtt.origin", "mqtt.iata", "mqtt.status", "mqtt.packets", "mqtt.raw",
|
|
"mqtt.tx", "mqtt.rx", "mqtt.interval", "mqtt.neighbors", "mqtt.neighbors.interval",
|
|
"mqtt.ntp", "mqtt.owner", "mqtt.email",
|
|
"timezone", "timezone.offset", "snmp", "snmp.community",
|
|
};
|
|
static const char* const WC_ALLOWED_SLOT_KEYS[] = {
|
|
"preset", "server", "port", "username", "password", "token", "topic", "audience",
|
|
"filter",
|
|
};
|
|
|
|
// True when `key` is a well-formed per-slot key ("mqttN.<field>" with N in
|
|
// 1..MAX_MQTT_SLOTS). The shortest such key is "mqttN.x" (7 chars), and this
|
|
// probes key[4..6], so the length guard must come first -- an attacker-supplied
|
|
// "mqtt" or "m" would otherwise read past the terminator.
|
|
static inline bool wcIsSlotKeyPrefix(const char* key) {
|
|
return strlen(key) >= 7 && memcmp(key, "mqtt", 4) == 0
|
|
&& key[4] >= '1' && key[4] <= ('0' + MAX_MQTT_SLOTS) && key[5] == '.';
|
|
}
|
|
|
|
static inline bool wcIsAllowedSetKey(const char* key) {
|
|
for (size_t i = 0; i < sizeof(WC_ALLOWED_SET_KEYS) / sizeof(WC_ALLOWED_SET_KEYS[0]); i++) {
|
|
if (strcmp(key, WC_ALLOWED_SET_KEYS[i]) == 0) return true;
|
|
}
|
|
// mqtt<1-6>.<field>
|
|
if (wcIsSlotKeyPrefix(key)) {
|
|
for (size_t i = 0; i < sizeof(WC_ALLOWED_SLOT_KEYS) / sizeof(WC_ALLOWED_SLOT_KEYS[0]); i++) {
|
|
if (strcmp(&key[6], WC_ALLOWED_SLOT_KEYS[i]) == 0) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static inline bool wcIsEspNowChannelKey(const char* key) {
|
|
return key != NULL && strcmp(key, "espnow.channel") == 0;
|
|
}
|
|
|
|
// These settings cannot be applied to a running transport. The server enforces
|
|
// a reboot even when a crafted client omits the browser's reboot flag.
|
|
static inline bool wcSetKeyRequiresReboot(const char* key) {
|
|
return key != NULL
|
|
&& (wcIsEspNowChannelKey(key)
|
|
|| strcmp(key, "bluetooth.mac") == 0
|
|
|| strcmp(key, "bluetooth.stealth") == 0);
|
|
}
|
|
|
|
// The admin password maps to the top-level `password` command, not a setter, so
|
|
// it is classified apart from the `set` allowlist. It is the only key that gets
|
|
// this treatment, which is what keeps the allowlist the sole route to `set` and
|
|
// leaves no general path from a batch to arbitrary top-level CLI commands.
|
|
static inline bool wcIsAdminPasswordKey(const char* key) {
|
|
return strcmp(key, "password") == 0;
|
|
}
|
|
|
|
static inline bool wcIsValidAdminPassword(const char* value) {
|
|
if (value == NULL) return false;
|
|
const size_t len = strlen(value);
|
|
if (len == 0 || len > 15) return false; // NodePrefs::password[16], including NUL
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (value[i] == '\r' || value[i] == '\n') return false; // reject, never silently strip
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Keys carrying a secret whose stored value is masked with the placeholder in
|
|
// the UI; a POST echoing the placeholder for one of these is dropped (unchanged).
|
|
static inline bool wcIsSecretKey(const char* key) {
|
|
if (strcmp(key, "wifi.pwd") == 0) return true;
|
|
if (wcIsSlotKeyPrefix(key)
|
|
&& (strcmp(&key[6], "password") == 0 || strcmp(&key[6], "token") == 0)) return true;
|
|
return false;
|
|
}
|
|
|
|
// Identify explicit secret getters separately from config-form fields. Local
|
|
// CLI connections may read these values; configuration snapshots remain masked.
|
|
static inline bool wcIsSecretReadCommand(const char* cmd) {
|
|
if (strncmp(cmd, "get ", 4) != 0) return false;
|
|
const char* key = cmd + 4;
|
|
while (*key == ' ') key++;
|
|
if (strcmp(key, "prv.key") == 0) return true; // this node's identity
|
|
if (strcmp(key, "password") == 0) return true;
|
|
if (strcmp(key, "guest.password") == 0) return true;
|
|
if (strcmp(key, "alert.psk") == 0) return true;
|
|
if (strcmp(key, "bridge.secret") == 0) return true;
|
|
return wcIsSecretKey(key); // wifi.pwd, mqttN.password, mqttN.token
|
|
}
|
|
|
|
// Browser-generated request IDs are exactly eight random bytes encoded as
|
|
// hexadecimal. Keeping the grammar deliberately small makes the ID safe to
|
|
// echo in JSON/logs and prevents an empty or truncated ID from weakening the
|
|
// save/result correlation contract.
|
|
static inline bool wcIsValidReqId(const char* reqid) {
|
|
if (reqid == NULL || strlen(reqid) != 16) return false;
|
|
for (size_t i = 0; i < 16; i++) {
|
|
char c = reqid[i];
|
|
if (!((c >= '0' && c <= '9') ||
|
|
(c >= 'a' && c <= 'f') ||
|
|
(c >= 'A' && c <= 'F'))) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The browser terminal submits exactly one command through the existing
|
|
// 160-byte CLI buffer. Reject blank, oversized, or multiline input so a JSON
|
|
// request cannot smuggle a second command into the dispatcher.
|
|
static inline bool wcIsValidCliCommand(const char* command) {
|
|
if (command == NULL) return false;
|
|
const size_t len = strnlen(command, 161);
|
|
if (len == 0 || len > 159) return false;
|
|
bool has_text = false;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (command[i] == '\r' || command[i] == '\n') return false;
|
|
if (command[i] != ' ' && command[i] != '\t') has_text = true;
|
|
}
|
|
return has_text;
|
|
}
|