From 2812f25e9e3fa5569de6741b0e5fd765b6f91f95 Mon Sep 17 00:00:00 2001 From: DDeLoachWG Date: Mon, 28 Nov 2022 12:54:35 -0800 Subject: [PATCH 1/3] Use ESP32's AsyncUDP when ESPALEXA_ASYNC requested When running on an ESP32 and choosing ESPALEXA_ASYNC, this PR changes the UDP implementation to async as well as TCP/web server. This change solves a memory fragmentation issue for ESP32 devices that results in WiFiUdp's parsePacket getting memory allocation errors causing CPU restarts. The async model also fixes the "flush" problem reported in PR#181 dropping packets since the async model delivers all packets it receives even on busy networks. --- src/Espalexa.h | 90 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/src/Espalexa.h b/src/Espalexa.h index ad0bcf6..16ad1e9 100644 --- a/src/Espalexa.h +++ b/src/Espalexa.h @@ -32,7 +32,9 @@ #ifdef ESPALEXA_ASYNC #ifdef ARDUINO_ARCH_ESP32 + #define USE_ESP32_ASYNC_UDP #include + #include #else #include #endif @@ -46,15 +48,19 @@ #include #endif #endif -#include +#ifndef USE_ESP32_ASYNC_UDP + #include +#endif #ifdef ESPALEXA_DEBUG #pragma message "Espalexa 2.7.0 debug mode" #define EA_DEBUG(x) Serial.print (x) #define EA_DEBUGLN(x) Serial.println (x) + #define EA_DEBUGBUF(buf,len) Serial.write(buf, len) #else #define EA_DEBUG(x) #define EA_DEBUGLN(x) + #define EA_DEBUGBUF(buf,len) #endif #include "EspalexaDevice.h" @@ -79,8 +85,11 @@ class Espalexa { EspalexaDevice* devices[ESPALEXA_MAXDEVICES] = {}; //Keep in mind that Device IDs go from 1 to DEVICES, cpp arrays from 0 to DEVICES-1!! - + #ifdef USE_ESP32_ASYNC_UDP + AsyncUDP espalexaUdp; + #else WiFiUDP espalexaUdp; + #endif IPAddress ipMulti; uint32_t mac24; //bottom 24 bits of mac String escapedMac=""; //lowercase mac address @@ -283,7 +292,7 @@ class Espalexa { } //respond to UDP SSDP M-SEARCH - void respondToSearch() + void respondToSearch(IPAddress remoteIP, uint16_t remotePort) { IPAddress localIP = WiFi.localIP(); char s[16]; @@ -301,13 +310,17 @@ class Espalexa { "USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n" // _uuid::_deviceType "\r\n"),s,escapedMac.c_str(),escapedMac.c_str()); - espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort()); - #ifdef ARDUINO_ARCH_ESP32 - espalexaUdp.write((uint8_t*)buf, strlen(buf)); + #ifdef USE_ESP32_ASYNC_UDP + espalexaUdp.writeTo((uint8_t*)buf, strlen(buf), remoteIP, remotePort); #else - espalexaUdp.write(buf); + espalexaUdp.beginPacket(remoteIP, remotePort); + #ifdef ARDUINO_ARCH_ESP32 + espalexaUdp.write((uint8_t*)buf, strlen(buf)); + #else + espalexaUdp.write(buf); + #endif + espalexaUdp.endPacket(); #endif - espalexaUdp.endPacket(); } public: @@ -337,10 +350,26 @@ class Espalexa { #else server = externalServer; #endif - #ifdef ARDUINO_ARCH_ESP32 - udpConnected = espalexaUdp.beginMulticast(IPAddress(239, 255, 255, 250), 1900); + #ifdef USE_ESP32_ASYNC_UDP + udpConnected = espalexaUdp.listenMulticast(IPAddress(239, 255, 255, 250), 1900); + if (udpConnected) + { + espalexaUdp.onPacket([this](AsyncUDPPacket packet) { + EA_DEBUG("UDP packet Length: "); + EA_DEBUG(packet.length()); + EA_DEBUGLN(", Data:"); + EA_DEBUGBUF(packet.data(), packet.length()); + EA_DEBUGLN(""); + + HandleUdpPacket(packet.data(), packet.length(), packet.remoteIP(), packet.remotePort()); + }); + } #else - udpConnected = espalexaUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 255, 255, 250), 1900); + #ifdef ARDUINO_ARCH_ESP32 + udpConnected = espalexaUdp.beginMulticast(IPAddress(239, 255, 255, 250), 1900); + #else + udpConnected = espalexaUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 255, 255, 250), 1900); + #endif #endif if (udpConnected){ @@ -352,15 +381,36 @@ class Espalexa { EA_DEBUGLN("Failed"); return false; } + + void HandleUdpPacket(uint8_t *packetBuffer, size_t packetSize, IPAddress remoteIp, uint16_t remotePort) + { + if (packetSize < 1) return; //no new udp packet + if (!discoverable) return; //do not reply to M-SEARCH if not discoverable + + const char* request = (const char *) packetBuffer; + if (strnstr(request, "M-SEARCH", packetSize) == nullptr) return; + + EA_DEBUGLN(request); + if (strnstr(request, "ssdp:disc", packetSize) != nullptr && //short for "ssdp:discover" + (strnstr(request, "upnp:rootd", packetSize) != nullptr || //short for "upnp:rootdevice" + strnstr(request, "ssdp:all", packetSize) != nullptr || + strnstr(request, "asic:1", packetSize) != nullptr )) //short for "device:basic:1" + { + EA_DEBUGLN("Responding search req..."); + respondToSearch(remoteIp, remotePort); + } + } - //service loop + //service loop for non-async modes void loop() { #ifndef ESPALEXA_ASYNC if (server == nullptr) return; //only if begin() was not called server->handleClient(); #endif + #ifndef USE_ESP32_ASYNC_UDP if (!udpConnected) return; + int packetSize = espalexaUdp.parsePacket(); if (packetSize < 1) return; //no new udp packet @@ -371,20 +421,8 @@ class Espalexa { packetBuffer[packetSize] = 0; espalexaUdp.flush(); - if (!discoverable) return; //do not reply to M-SEARCH if not discoverable - - const char* request = (const char *) packetBuffer; - if (strstr(request, "M-SEARCH") == nullptr) return; - - EA_DEBUGLN(request); - if (strstr(request, "ssdp:disc") != nullptr && //short for "ssdp:discover" - (strstr(request, "upnp:rootd") != nullptr || //short for "upnp:rootdevice" - strstr(request, "ssdp:all") != nullptr || - strstr(request, "asic:1") != nullptr )) //short for "device:basic:1" - { - EA_DEBUGLN("Responding search req..."); - respondToSearch(); - } + HandleUdpPacket(packetBuffer, packetSize, espalexaUdp.remoteIP(), espalexaUdp.remotePort()); + #endif } // returns device index or 0 on failure From 8058108c60a7262ed2a2b6fb6db8119efbd56be9 Mon Sep 17 00:00:00 2001 From: DDeLoachWG Date: Fri, 9 Dec 2022 14:04:15 -0800 Subject: [PATCH 2/3] Update Espalexa.h Fix debug buffer dump overflow and tweak debugging output --- src/Espalexa.h | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Espalexa.h b/src/Espalexa.h index 16ad1e9..0bbcbb7 100644 --- a/src/Espalexa.h +++ b/src/Espalexa.h @@ -356,10 +356,7 @@ class Espalexa { { espalexaUdp.onPacket([this](AsyncUDPPacket packet) { EA_DEBUG("UDP packet Length: "); - EA_DEBUG(packet.length()); - EA_DEBUGLN(", Data:"); - EA_DEBUGBUF(packet.data(), packet.length()); - EA_DEBUGLN(""); + EA_DEBUGLN(packet.length()); HandleUdpPacket(packet.data(), packet.length(), packet.remoteIP(), packet.remotePort()); }); @@ -390,7 +387,8 @@ class Espalexa { const char* request = (const char *) packetBuffer; if (strnstr(request, "M-SEARCH", packetSize) == nullptr) return; - EA_DEBUGLN(request); + EA_DEBUGBUF(request, packetSize); + EA_DEBUGLN(""); if (strnstr(request, "ssdp:disc", packetSize) != nullptr && //short for "ssdp:discover" (strnstr(request, "upnp:rootd", packetSize) != nullptr || //short for "upnp:rootdevice" strnstr(request, "ssdp:all", packetSize) != nullptr || @@ -479,7 +477,7 @@ class Espalexa { bool handleAlexaApiCall(AsyncWebServerRequest* request) { server = request; //copy request reference - String req = request->url(); //body from global variable + const String& req = request->url(); //body from global variable EA_DEBUGLN(request->contentType()); if (request->hasParam("body", true)) // This is necessary, otherwise ESP crashes if there is no body { @@ -492,7 +490,8 @@ class Espalexa { bool handleAlexaApiCall(String req, String body) { #endif - EA_DEBUGLN("AlexaApiCall"); + EA_DEBUG("AlexaApiCall "); + EA_DEBUGLN(req); if (req.indexOf("api") <0) return false; //return if not an API call EA_DEBUGLN("ok"); @@ -509,7 +508,7 @@ class Espalexa { server->send(200, "application/json", F("[{\"success\":{\"/lights/1/state/\": true}}]")); uint32_t devId = req.substring(req.indexOf("lights")+7).toInt(); - EA_DEBUG("ls"); EA_DEBUGLN(devId); + EA_DEBUG("ls "); EA_DEBUGLN(devId); unsigned idx = decodeLightKey(devId); if (idx >= currentDeviceCount) return true; //return if invalid ID From 08d58e3e819fe546bc928f5aa758c75b95e9d562 Mon Sep 17 00:00:00 2001 From: DDeLoachWG Date: Mon, 12 Dec 2022 13:25:51 -0800 Subject: [PATCH 3/3] Subnet filters, fixed state response Added an option for subnet filtering to avoid SSDP attack, fixed a light number issue in the state command response, tweaked debugging and added separate define to view all SSDP packet contents to limit debug log spamming. --- src/Espalexa.h | 161 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 104 insertions(+), 57 deletions(-) diff --git a/src/Espalexa.h b/src/Espalexa.h index 0bbcbb7..a57a0de 100644 --- a/src/Espalexa.h +++ b/src/Espalexa.h @@ -13,7 +13,7 @@ * @version 2.7.0 * @author Christian Schwinne * @license MIT - * @contributors d-999 + * @contributors d-999, DeLoachAero */ #include "Arduino.h" @@ -27,8 +27,12 @@ #ifndef ESPALEXA_MAXDEVICES #define ESPALEXA_MAXDEVICES 10 //this limit only has memory reasons, set it higher should you need to, max 128 #endif +#ifndef SSDP_INTERVAL + #define SSDP_INTERVAL 100 // cache control interval for SSDP Search response +#endif -//#define ESPALEXA_DEBUG +//#define ESPALEXA_DEBUG // for general debugging of ESPAlexa +//#define ESPALEXA_DEBUG_ALL_SSDP // if you want to see all the SSDP UDP discovery packet contents received when ESPALEXA_DEBUG is on #ifdef ESPALEXA_ASYNC #ifdef ARDUINO_ARCH_ESP32 @@ -82,6 +86,7 @@ class Espalexa { uint8_t currentDeviceCount = 0; bool discoverable = true; bool udpConnected = false; + bool enableSubnetFilter = false; // default false for backward compatibility EspalexaDevice* devices[ESPALEXA_MAXDEVICES] = {}; //Keep in mind that Device IDs go from 1 to DEVICES, cpp arrays from 0 to DEVICES-1!! @@ -302,13 +307,13 @@ class Espalexa { sprintf_P(buf,PSTR("HTTP/1.1 200 OK\r\n" "EXT:\r\n" - "CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL + "CACHE-CONTROL: max-age=%d\r\n" // SSDP_INTERVAL "LOCATION: http://%s:80/description.xml\r\n" "SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber "hue-bridgeid: %s\r\n" "ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType "USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n" // _uuid::_deviceType - "\r\n"),s,escapedMac.c_str(),escapedMac.c_str()); + "\r\n"),SSDP_INTERVAL,s,escapedMac.c_str(),escapedMac.c_str()); #ifdef USE_ESP32_ASYNC_UDP espalexaUdp.writeTo((uint8_t*)buf, strlen(buf), remoteIP, remotePort); @@ -355,9 +360,6 @@ class Espalexa { if (udpConnected) { espalexaUdp.onPacket([this](AsyncUDPPacket packet) { - EA_DEBUG("UDP packet Length: "); - EA_DEBUGLN(packet.length()); - HandleUdpPacket(packet.data(), packet.length(), packet.remoteIP(), packet.remotePort()); }); } @@ -387,15 +389,32 @@ class Espalexa { const char* request = (const char *) packetBuffer; if (strnstr(request, "M-SEARCH", packetSize) == nullptr) return; - EA_DEBUGBUF(request, packetSize); - EA_DEBUGLN(""); - if (strnstr(request, "ssdp:disc", packetSize) != nullptr && //short for "ssdp:discover" - (strnstr(request, "upnp:rootd", packetSize) != nullptr || //short for "upnp:rootdevice" - strnstr(request, "ssdp:all", packetSize) != nullptr || - strnstr(request, "asic:1", packetSize) != nullptr )) //short for "device:basic:1" + EA_DEBUG("Got UDP! Packet Length: "); + EA_DEBUGLN(packetSize); + + #if defined(ESPALEXA_DEBUG) && defined(ESPALEXA_DEBUG_ALL_SSDP) + EA_DEBUGBUF(request, packetSize); + EA_DEBUGLN(""); + #endif + // check remote IP if subnet filter enabled + if (!enableSubnetFilter || ((remoteIp & WiFi.subnetMask()) == (WiFi.localIP() & WiFi.subnetMask()) && remoteIp != WiFi.localIP())) + { + // remote caller is on same subnet, or subnet filter not enabled + if (strnstr(request, "ssdp:disc", packetSize) != nullptr && //short for "ssdp:discover" + (strnstr(request, "upnp:rootd", packetSize) != nullptr || //short for "upnp:rootdevice" + strnstr(request, "ssdp:all", packetSize) != nullptr || + strnstr(request, "asic:1", packetSize) != nullptr )) //short for "device:basic:1" + { + EA_DEBUG("Responding to search req from "); + EA_DEBUGLN(remoteIp.toString()); + respondToSearch(remoteIp, remotePort); + } + } + else { - EA_DEBUGLN("Responding search req..."); - respondToSearch(remoteIp, remotePort); + // remote caller not on same subnet, or sent my own local IP as the remote IP + EA_DEBUG("UDP remote IP not on same subnet or using device IP! "); + EA_DEBUGLN(remoteIp.toString()); } } @@ -477,9 +496,19 @@ class Espalexa { bool handleAlexaApiCall(AsyncWebServerRequest* request) { server = request; //copy request reference - const String& req = request->url(); //body from global variable + // note we may get body from global "body" variable or the request, depending on type of call + const String& req = request->url(); + #ifdef ESPALEXA_DEBUG + EA_DEBUG("Request from client IP "); + if (request->client() == nullptr) + EA_DEBUGLN("Null"); + else + EA_DEBUGLN(request->client()->remoteIP()); + #endif + EA_DEBUGLN(request->contentType()); - if (request->hasParam("body", true)) // This is necessary, otherwise ESP crashes if there is no body + // body may have been set by separate handler, but might instead be part of this request instance + if (request->hasParam("body", true)) { EA_DEBUG("BodyMethod2"); body = request->getParam("body", true)->value(); @@ -498,18 +527,17 @@ class Espalexa { if (body.indexOf("devicetype") > 0) //client wants a hue api username, we don't care and give static { EA_DEBUGLN("devType"); - body = ""; + body.clear(); server->send(200, "application/json", F("[{\"success\":{\"username\":\"2WLEDHardQrI3WHYTHoMcXHgEspsM8ZZRpSKtBQr\"}}]")); return true; } if ((req.indexOf("state") > 0) && (body.length() > 0)) //client wants to control light { - server->send(200, "application/json", F("[{\"success\":{\"/lights/1/state/\": true}}]")); - uint32_t devId = req.substring(req.indexOf("lights")+7).toInt(); EA_DEBUG("ls "); EA_DEBUGLN(devId); + unsigned idx = decodeLightKey(devId); if (idx >= currentDeviceCount) return true; //return if invalid ID EspalexaDevice* dev = devices[idx]; @@ -520,52 +548,60 @@ class Espalexa { { dev->setValue(0); dev->setPropertyChanged(EspalexaDeviceProperty::off); - dev->doCallback(); - return true; } - - if (body.indexOf("true") >0) //ON command + else { - dev->setValue(dev->getLastValue()); - dev->setPropertyChanged(EspalexaDeviceProperty::on); - } - - if (body.indexOf("bri") >0) //BRIGHTNESS command - { - uint8_t briL = body.substring(body.indexOf("bri") +5).toInt(); - if (briL == 255) + if (body.indexOf("true") >0) //ON command { - dev->setValue(255); - } else { - dev->setValue(briL+1); + dev->setValue(dev->getLastValue() ? dev->getLastValue() : 255); + dev->setPropertyChanged(EspalexaDeviceProperty::on); + } + + if (body.indexOf("bri") >0) //BRIGHTNESS command + { + uint8_t briL = body.substring(body.indexOf("bri") +5).toInt(); + if (briL == 255) + { + dev->setValue(255); + } else { + dev->setValue(briL+1); + } + dev->setPropertyChanged(EspalexaDeviceProperty::bri); + } + + if (body.indexOf("xy") >0) //COLOR command (XY mode) + { + dev->setColorXY(body.substring(body.indexOf("[") +1).toFloat(), body.substring(body.indexOf(",0") +1).toFloat()); + dev->setPropertyChanged(EspalexaDeviceProperty::xy); + } + + if (body.indexOf("hue") >0) //COLOR command (HS mode) + { + dev->setColor(body.substring(body.indexOf("hue") +5).toInt(), body.substring(body.indexOf("sat") +5).toInt()); + dev->setPropertyChanged(EspalexaDeviceProperty::hs); + } + + if (body.indexOf("ct") >0) //COLOR TEMP command (white spectrum) + { + dev->setColor(body.substring(body.indexOf("ct") +4).toInt()); + dev->setPropertyChanged(EspalexaDeviceProperty::ct); } - dev->setPropertyChanged(EspalexaDeviceProperty::bri); - } - - if (body.indexOf("xy") >0) //COLOR command (XY mode) - { - dev->setColorXY(body.substring(body.indexOf("[") +1).toFloat(), body.substring(body.indexOf(",0") +1).toFloat()); - dev->setPropertyChanged(EspalexaDeviceProperty::xy); - } - - if (body.indexOf("hue") >0) //COLOR command (HS mode) - { - dev->setColor(body.substring(body.indexOf("hue") +5).toInt(), body.substring(body.indexOf("sat") +5).toInt()); - dev->setPropertyChanged(EspalexaDeviceProperty::hs); - } - - if (body.indexOf("ct") >0) //COLOR TEMP command (white spectrum) - { - dev->setColor(body.substring(body.indexOf("ct") +4).toInt()); - dev->setPropertyChanged(EspalexaDeviceProperty::ct); } - dev->doCallback(); - #ifdef ESPALEXA_DEBUG if (dev->getLastChangedProperty() == EspalexaDeviceProperty::none) EA_DEBUGLN("STATE REQ WITHOUT BODY (likely Content-Type issue #6)"); #endif + + body.clear(); + char rsp[128]; + sprintf_P(rsp, PSTR("[{\"success\":{\"/lights/%d/state/on\": %s}}]"), devId, dev->getState() ? "true" : "false"); + server->send(200, "application/json", rsp); + EA_DEBUG("State Response: "); + EA_DEBUGLN(rsp); + + dev->doCallback(); + return true; } @@ -602,16 +638,20 @@ class Espalexa { char buf[512]; deviceJsonString(devices[idx], buf); server->send(200, "application/json", buf); + EA_DEBUG("Response: "); + EA_DEBUGLN(buf); } else { server->send(200, "application/json", "{}"); + EA_DEBUGLN("Response: {}"); } } - + body.clear(); return true; } //we don't care about other api commands at this time and send empty JSON server->send(200, "application/json", "{}"); + body.clear(); return true; } @@ -620,6 +660,13 @@ class Espalexa { { discoverable = d; } + + // set true to require remote UDP addresses be on the same subnet as the device, and not the same IP as the device, + // to avoid remote denial of service attacks (two common SSDP attacks) + void setEnableSubnetFilter(bool f) + { + enableSubnetFilter = f; + } //get EspalexaDevice at specific index EspalexaDevice* getDevice(uint8_t index)