diff --git a/platformio.ini b/platformio.ini index bc03b404..837f7ca3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -57,7 +57,7 @@ build_flags = -D BUILD_TARGET=\"$PIOENV\" -D APP_NAME=\"MoonLight\" ; ๐ŸŒ™ Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename -D APP_VERSION=\"0.9.1\" ; semver compatible version string - -D APP_DATE=\"20260504\" ; ๐ŸŒ™ + -D APP_DATE=\"20260505\" ; ๐ŸŒ™ -D PLATFORM_VERSION=\"pioarduino-55.03.37\" ; ๐ŸŒ™ make sure it matches with above plaftform diff --git a/src/MoonBase/Modules/ModuleDevices.h b/src/MoonBase/Modules/ModuleDevices.h index e5f09e3e..a747b8c6 100644 --- a/src/MoonBase/Modules/ModuleDevices.h +++ b/src/MoonBase/Modules/ModuleDevices.h @@ -85,6 +85,14 @@ class ModuleDevices : public Module { false); } + void begin() override { + Module::begin(); // loads state from filesystem + // Device list is dynamic โ€” rebuilt from UDP every 10 s. + // Clear any stale or garbled entries that may have been persisted. + _state.data["devices"].to(); + EXT_LOGD(MB_TAG, "cleared persisted device list โ€” will rebuild from UDP"); + } + void setupDefinition(const JsonArray& controls) override { EXT_LOGV(MB_TAG, ""); JsonObject control; // state.data has one or more properties @@ -206,16 +214,18 @@ class ModuleDevices : public Module { // Update device table from a full MoonLight discovery packet void updateDevices(const UDPMessage& message, IPAddress ip) { + // Validate here (not just in receiveUDP) so the sendUDP(false) self-update path is also guarded. + if (!isValidHostname(message.header.name, sizeof(message.header.name))) { + EXT_LOGW(MB_TAG, "Skipping device update with invalid name from ...%d", ip[3]); + return; + } + // EXT_LOGD(MB_TAG, "updateDevices ...%d %s", ip[3], message.header.name); if (_state.data["devices"].isNull()) _state.data["devices"].to(); - // set the doc + // deep-copy current state so we can modify it independently of _state.data JsonDocument doc; - if (_sveltekit->getSocket()->getActiveClients()) { // rebuild the devices array - doc.set(_state.data); // copy - } else { - doc = _state.data; // reference - } + doc.set(_state.data); // set the devices array JsonArray devices = doc["devices"]; @@ -240,9 +250,12 @@ class ModuleDevices : public Module { device["ip"] = ip.toString(); device["lastSync"] = time(nullptr); // time will change, triggering update - device["name"] = message.header.name; - device["version"] = message.versionStr; - device["build"] = message.build; + // String() forces ArduinoJson to copy bytes into its pool rather than linking a const char* + // pointer to the stack-allocated message struct. A linked pointer becomes dangling after + // updateDevices() returns, causing compareRecursive() to read garbage on the next update cycle. + device["name"] = String(message.header.name); + device["version"] = String(message.versionStr); + device["build"] = String(message.build); device["uptime"] = message.uptime; device["packageSize"] = message.packageSize; device["lightsOn"] = (message.header.type & 0x80) != 0; @@ -257,13 +270,9 @@ class ModuleDevices : public Module { void updateDevicesWLED(const UDPWLEDHeader& header, IPAddress ip) { if (_state.data["devices"].isNull()) _state.data["devices"].to(); - // set the doc + // deep-copy current state so we can modify it independently of _state.data JsonDocument doc; - if (_sveltekit->getSocket()->getActiveClients()) { // rebuild the devices array - doc.set(_state.data); // copy - } else { - doc = _state.data; // reference - } + doc.set(_state.data); // set the devices array JsonArray devices = doc["devices"]; @@ -287,7 +296,7 @@ class ModuleDevices : public Module { device["ip"] = ip.toString(); device["lastSync"] = time(nullptr); // time will change, triggering update - device["name"] = header.name; + device["name"] = String(header.name); // force copy โ€” same reason as updateDevices() char verBuf[12]; snprintf(verBuf, sizeof(verBuf), "%lu", (unsigned long)header.version); device["version"] = verBuf; @@ -342,10 +351,17 @@ class ModuleDevices : public Module { message.header.name[sizeof(message.header.name) - 1] = '\0'; message.versionStr[sizeof(message.versionStr) - 1] = '\0'; message.build[sizeof(message.build) - 1] = '\0'; - if (message.header.token == 255 && message.header.id == 1) + if (message.header.token != 255 || message.header.id != 1) { + EXT_LOGW(MB_TAG, "Bad MoonLight header from ...%d (token=%d id=%d)", deviceUDP.remoteIP()[3], message.header.token, message.header.id); + } else if (message.packageSize != sizeof(UDPMessage)) { + // packageSize is a self-describing field: rejects foreign devices that happen to send + // exactly 101 bytes but with a different struct layout (wrong field at bytes 96-97) + EXT_LOGW(MB_TAG, "Struct mismatch from ...%d: got packageSize=%d, expected %d", deviceUDP.remoteIP()[3], message.packageSize, sizeof(UDPMessage)); + } else if (isValidHostname(message.header.name, sizeof(message.header.name))) { updateDevices(message, deviceUDP.remoteIP()); - else - EXT_LOGW(MB_TAG, "Bad MoonLight header from ...%d", deviceUDP.remoteIP()[3]); + } else { + EXT_LOGW(MB_TAG, "Garbled name in packet from ...%d, rejecting", deviceUDP.remoteIP()[3]); + } } else { EXT_LOGW(MB_TAG, "Unknown packet size on port %d: %d (WLED=%d ML=%d)", deviceUDPPort, packetSize, sizeof(UDPWLEDHeader), sizeof(UDPMessage)); @@ -409,6 +425,19 @@ class ModuleDevices : public Module { } private: + // Validate hostname: non-empty, [a-zA-Z0-9-] only. + // isprint() is NOT used โ€” ESP32 newlib treats 0xA0-0xFF as printable (ISO-8859-1 locale), + // so garbled high-byte chars like รฒ/รด/รฑ would pass an isprint() check. + static bool isValidHostname(const char* name, size_t maxLen) { + if (name[0] == '\0') return false; + for (size_t j = 0; j < maxLen - 1 && name[j]; j++) { + uint8_t c = (uint8_t)name[j]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-')) + return false; + } + return true; + } + // Fill the 44-byte WLED-compatible header from local device info void infoToHeader(UDPWLEDHeader& header, bool lightsOn) { IPAddress localIP = networkLocalIP();