diff --git a/examples/SmartSwitch/README.md b/examples/SmartSwitch/README.md index fd8cd40..4ce4e9a 100644 --- a/examples/SmartSwitch/README.md +++ b/examples/SmartSwitch/README.md @@ -4,14 +4,16 @@ ## SmartSwitch * Remote Temperature Control application with schedule (example car block heater or battery charger) -* Based on ESP_AsyncFSBrowser example +* Based on ESP_AsyncFSBrowser example with ACE editor * Wide browser compatibility, no extra server-side needed * HTTP server and WebSocket, single port * Standalone, no JS dependencies for the browser from Internet (I hope), ace editor included * Added ESPAsyncWiFiManager -* Fallback to an own WIFI_AP, no Internet to sync but by a browser the clock can be set once * Real Time (NTP) w/ Time Zones * Memorized settings to EEPROM * Multiple clients can be connected at same time, they see each other' requests +* Base Authentication of the editor, static content, WS +* Or Cookie Authentication including WS part, need lib src changes taken from https://github.com/me-no-dev/ESPAsyncWebServer/pull/684 * Default credentials smart:switch -* Use latest ESP8266 or ESP32 core(from GitHub) +* Use latest ESP8266 ESP32 cores from GitHub + diff --git a/examples/SmartSwitch/SmartSwitch.ino b/examples/SmartSwitch/SmartSwitch.ino index 0307cd2..9609e9d 100644 --- a/examples/SmartSwitch/SmartSwitch.ino +++ b/examples/SmartSwitch/SmartSwitch.ino @@ -16,9 +16,21 @@ Use latest ESP core lib (from Github) #define USE_WFM // to use ESPAsyncWiFiManager //#define DEL_WFM // delete Wifi credentials stored //(use once then comment and flash again), also HTTP /erase-wifi can do the same live - -#define USE_AUTH_STAT // .setAuthentication also for static (editor always requires auth) -//#define USE_AUTH_WS // .setAuthentication also for ws, broken for Safari iOS + +// AUTH COOKIE uses only the password, Base uses both +#define http_username "smart" +#define http_password "switch" + +//See https://github.com/me-no-dev/ESPAsyncWebServer/pull/684 +#define USE_AUTH_COOKIE +#define MY_COOKIE_FULL "LLKQ=7;max-age=31536000;" +#define MY_COOKIE_DEL "LLKQ=" +#define MY_COOKIE "LLKQ=7" + +#ifndef USE_AUTH_COOKIE + #define USE_AUTH_STAT //Base Auth for stat, /commands and SPIFFSEditor + //#define USE_AUTH_WS //Base Auth also for WS, not very supported +#endif #include #ifdef ESP32 @@ -55,7 +67,10 @@ Use latest ESP core lib (from Github) // DHT #define DHTTYPE DHT22 // DHT 11 // DHT 22, AM2302, AM2321 // DHT 21, AM2301 -#define DHTPIN 4 //D2 +#define DHTPIN 4 //D2 + +#define DHT_T_CORR -0.5 //Temperature offset compensation of the sensor (can be -) +#define DHT_H_CORR 1.5 //Humidity offset compensation of the sensor DHT dht(DHTPIN, DHTTYPE); @@ -80,9 +95,7 @@ AsyncWebSocket ws("/ws"); const char* ssid = "MYROUTERSSD"; const char* password = "MYROUTERPASSWD"; #endif -const char* hostName = "smartsw"; -const char* http_username = "smart"; -const char* http_password = "switch"; + const char* hostName = "smartsw32"; // RTC static timeval tv; @@ -91,6 +104,15 @@ static time_t now; // HW I/O const int btnPin = 0; //D3 const int ledPin = 2; //D4 + +#ifdef ESP32 + #define LED_ON 0x1 + #define LED_OFF 0x0 +#elif defined(ESP8266) + #define LED_ON 0x0 + #define LED_OFF 0x1 +#endif + int btnState = HIGH; // Globals @@ -101,7 +123,7 @@ float t = 0; float h = 0; bool udht = false; bool heat_enabled_prev = false; -int ledState; +int ledState = LED_OFF; struct EE_bl { byte memid; //here goes the EEMARK stamp @@ -178,30 +200,32 @@ void showTime() } if (heat_enabled_prev) { // smart control (delayed one cycle) - if (((t - HYST) < ee.tempe)&&(ledState == HIGH)) { // OFF->ON once - ledState = LOW; + if (((t - HYST) < ee.tempe)&&(ledState == LED_OFF)) { // OFF->ON once + ledState = LED_ON; digitalWrite(ledPin, ledState); // apply change ws.textAll("led,ledon"); } - if ((((t + HYST) > ee.tempe)&&(ledState == LOW))||(!heat_enabled)) { // ON->OFF once, also turn off at end of period. - ledState = HIGH; + if ((((t + HYST) > ee.tempe)&&(ledState == LED_ON))||(!heat_enabled)) { // ON->OFF once, also turn off at end of period. + ledState = LED_OFF; digitalWrite(ledPin, ledState); // apply change ws.textAll("led,ledoff"); } - Serial.printf(ledState ? "LED OFF" : "LED ON"); + + Serial.printf(ledState == LED_ON ? "LED ON" : "LED OFF"); Serial.print(F(", Smart enabled\n")); } heat_enabled_prev = heat_enabled; //update } void updateDHT(){ - h = dht.readHumidity(); - t = dht.readTemperature(); //Celsius or dht.readTemperature(true) for Fahrenheit - if (isnan(h) || isnan(t)) { + float h1 = dht.readHumidity(); + float t1 = dht.readTemperature(); //Celsius or dht.readTemperature(true) for Fahrenheit + if (isnan(h1) || isnan(t1)) { Serial.print(F("Failed to read from DHT sensor!")); - h = 0; // debug w/o sensor - t = 0; - } + } else { + h = h1 + DHT_H_CORR; + t = t1 + DHT_T_CORR; + } } void analogSample() @@ -216,7 +240,7 @@ void checkPhysicalButton() if (btnState != LOW) { // btnState is used to avoid sequential toggles ledState = !ledState; digitalWrite(ledPin, ledState); - if (ledState) ws.textAll("led,ledoff"); + if (ledState == LED_OFF) ws.textAll("led,ledoff"); else ws.textAll("led,ledon"); } btnState = LOW; @@ -241,6 +265,16 @@ void mytimer() { } } +#ifdef USE_AUTH_COOKIE +bool myHandshake(AsyncWebServerRequest *request){ // false will 401 + if (request->hasHeader("Cookie")){ + String cookie = request->header("Cookie"); + if (cookie.indexOf(MY_COOKIE) != -1) return true; + else return false; + } else return false; +} +#endif + // server void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ if(type == WS_EVT_CONNECT){ @@ -252,7 +286,7 @@ void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventT Serial.printf("[%u] Connected from %d.%d.%d.%d\n", client->id(), ip[0], ip[1], ip[2], ip[3]); showTime(); analogSample(); - if (ledState) ws.textAll("led,ledoff"); + if (ledState == LED_OFF) ws.textAll("led,ledoff"); else ws.textAll("led,ledon"); ws.printfAll("Now,Setting,%02d:%02d,%02d:%02d,%+2.1f", ee.hstart, ee.mstart, ee.hstop, ee.mstop, ee.tempe); @@ -279,11 +313,11 @@ void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventT } if(data[0] == 'L') { // LED if(data[1] == '1') { - ledState = LOW; + ledState = LED_ON; ws.textAll("led,ledon"); // for others } else if(data[1] == '0') { - ledState = HIGH; + ledState = LED_OFF; ws.textAll("led,ledoff"); } digitalWrite(ledPin, ledState); // apply change @@ -444,31 +478,74 @@ void setup(){ #ifdef USE_AUTH_WS ws.setAuthentication(http_username,http_password); #endif + +#ifdef USE_AUTH_COOKIE + ws.handleHandshake(myHandshake); +#endif + ws.onEvent(onWsEvent); server.addHandler(&ws); #ifdef ESP32 + #ifdef USE_AUTH_STAT server.addHandler(new SPIFFSEditor(SPIFFS, http_username,http_password)); + #elif defined(USE_AUTH_COOKIE) + server.addHandler(new SPIFFSEditor(SPIFFS)).setFilter(myHandshake); + #endif #elif defined(ESP8266) + #ifdef USE_AUTH_STAT server.addHandler(new SPIFFSEditor(http_username,http_password)); + #elif defined(USE_AUTH_COOKIE) + server.addHandler(new SPIFFSEditor()).setFilter(myHandshake); + #endif #endif - - server.on("/free-ram", HTTP_GET, [](AsyncWebServerRequest *request){ // direct request->answer - request->send(200, "text/plain", String(ESP.getFreeHeap())); + +#ifdef USE_AUTH_COOKIE + server.on("/lg2n", HTTP_POST, [](AsyncWebServerRequest *request){ // cookie test + if((request->hasParam("pa2w",true) && (String(request->getParam("pa2w",true)->value().c_str()) == String(http_password)))||(request->hasParam("lg0f",true))){ + AsyncWebServerResponse *response = request->beginResponse(301); + response->addHeader("Location", "/"); + response->addHeader("Cache-Control", "no-cache"); + if(request->hasParam("lg0f",true)) response->addHeader("Set-Cookie", MY_COOKIE_DEL); + else response->addHeader("Set-Cookie", MY_COOKIE_FULL); + request->send(response); + } else request->send(200, "text/plain","Wrong Password!"); }); +#endif +// below paths need individual auth //////////////////////////////////////////////// - server.on("/get-time", HTTP_GET, [](AsyncWebServerRequest *request){ + server.on("/free-ram", HTTP_GET, [](AsyncWebServerRequest *request){ // direct request->answer +#ifdef USE_AUTH_STAT + if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); +#endif + request->send(200, "text/plain", String(ESP.getFreeHeap())); +#ifdef USE_AUTH COOKIE + }).setFilter(myHandshake); +#else + }); +#endif + + server.on("/get-time", HTTP_GET, [](AsyncWebServerRequest *request){ +#ifdef USE_AUTH_STAT + if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); +#endif if(request->hasParam("btime")){ time_t rtc = (request->getParam("btime")->value()).toInt(); timeval tv = { rtc, 0 }; settimeofday(&tv, nullptr); } request->send(200, "text/plain","Got browser time ..."); +#ifdef USE_AUTH COOKIE + }).setFilter(myHandshake); +#else }); - +#endif server.on("/hw-reset", HTTP_GET, [](AsyncWebServerRequest *request){ +#ifdef USE_AUTH_STAT + if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); +#endif request->onDisconnect([]() { #ifdef ESP32 ESP.restart(); @@ -477,9 +554,16 @@ void setup(){ #endif }); request->send(200, "text/plain","Restarting ..."); +#ifdef USE_AUTH COOKIE + }).setFilter(myHandshake); +#else }); +#endif server.on("/erase-wifi", HTTP_GET, [](AsyncWebServerRequest *request){ +#ifdef USE_AUTH_STAT + if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); +#endif request->onDisconnect([]() { WiFi.disconnect(true); #ifdef ESP32 @@ -489,12 +573,23 @@ void setup(){ #endif }); request->send(200, "text/plain","Erasing WiFi data ..."); - }); - -#ifdef USE_AUTH_STAT - server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.htm").setAuthentication(http_username,http_password); +#ifdef USE_AUTH COOKIE + }).setFilter(myHandshake); #else - server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.htm"); + }); +#endif + +// above paths need individual auth //////////////////////////////////////////////// + +#ifdef USE_AUTH_COOKIE + server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.htm").setFilter(myHandshake); + server.serveStatic("/", SPIFFS, "/login/").setDefaultFile("index.htm").setFilter(!myHandshake); +#else + #ifdef USE_AUTH_STAT + server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.htm").setAuthentication(http_username,http_password); + #else + server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.htm"); + #endif #endif server.onNotFound([](AsyncWebServerRequest *request){ // nothing known diff --git a/examples/SmartSwitch/data/acefull.js.gz b/examples/SmartSwitch/data/acefull.js.gz index e08df27..00cdd5f 100644 Binary files a/examples/SmartSwitch/data/acefull.js.gz and b/examples/SmartSwitch/data/acefull.js.gz differ diff --git a/examples/SmartSwitch/data/login/favicon.ico.gz b/examples/SmartSwitch/data/login/favicon.ico.gz new file mode 100644 index 0000000..9e60546 Binary files /dev/null and b/examples/SmartSwitch/data/login/favicon.ico.gz differ diff --git a/examples/SmartSwitch/data/login/index.htm b/examples/SmartSwitch/data/login/index.htm new file mode 100644 index 0000000..c522ea4 --- /dev/null +++ b/examples/SmartSwitch/data/login/index.htm @@ -0,0 +1,20 @@ + + + + + Login + + + + + +
+

+

Password

+
+ +

+ +
+ + \ No newline at end of file diff --git a/examples/SmartSwitch/data/worker-css.js.gz b/examples/SmartSwitch/data/worker-css.js.gz index 6e826b6..3684985 100644 Binary files a/examples/SmartSwitch/data/worker-css.js.gz and b/examples/SmartSwitch/data/worker-css.js.gz differ diff --git a/examples/SmartSwitch/data/worker-html.js.gz b/examples/SmartSwitch/data/worker-html.js.gz index 5245c5e..f9a03ea 100644 Binary files a/examples/SmartSwitch/data/worker-html.js.gz and b/examples/SmartSwitch/data/worker-html.js.gz differ diff --git a/examples/SmartSwitch/data/worker-javascript.js.gz b/examples/SmartSwitch/data/worker-javascript.js.gz index a5fce7e..c5ca200 100644 Binary files a/examples/SmartSwitch/data/worker-javascript.js.gz and b/examples/SmartSwitch/data/worker-javascript.js.gz differ diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp index 8230f0e..1325310 100644 --- a/src/AsyncEventSource.cpp +++ b/src/AsyncEventSource.cpp @@ -263,6 +263,10 @@ void AsyncEventSource::onConnect(ArEventHandlerFunction cb){ _connectcb = cb; } +void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb){ + _authorizeConnectHandler = cb; +} + void AsyncEventSource::_addClient(AsyncEventSourceClient * client){ /*char * temp = (char *)malloc(2054); if(temp != NULL){ @@ -333,6 +337,7 @@ bool AsyncEventSource::canHandle(AsyncWebServerRequest *request){ return false; } request->addInterestingHeader(F("Last-Event-ID")); + request->addInterestingHeader("Cookie"); return true; } @@ -340,6 +345,11 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request){ if((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())) { return request->requestAuthentication(); } + if(_authorizeConnectHandler != NULL){ + if(!_authorizeConnectHandler(request)){ + return request->send(401); + } + } request->send(new AsyncEventSourceResponse(this)); } diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index b097fa6..a350e7f 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -49,6 +49,7 @@ class AsyncEventSource; class AsyncEventSourceResponse; class AsyncEventSourceClient; typedef std::function ArEventHandlerFunction; +typedef std::function ArAuthorizeConnectHandler; class AsyncEventSourceMessage { private: @@ -100,6 +101,7 @@ class AsyncEventSource: public AsyncWebHandler { String _url; LinkedList _clients; ArEventHandlerFunction _connectcb; + ArAuthorizeConnectHandler _authorizeConnectHandler; public: AsyncEventSource(const String& url); ~AsyncEventSource(); @@ -107,6 +109,7 @@ class AsyncEventSource: public AsyncWebHandler { const char * url() const { return _url.c_str(); } void close(); void onConnect(ArEventHandlerFunction cb); + void authorizeConnect(ArAuthorizeConnectHandler cb); void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); size_t count() const; //number clinets connected size_t avgPacketsWaiting() const; diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 04e1a6f..9ce9385 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -1146,6 +1146,7 @@ void AsyncWebSocket::binaryAll(const __FlashStringHelper *message, size_t len){ const char __WS_STR_CONNECTION[] PROGMEM = { "Connection" }; const char __WS_STR_UPGRADE[] PROGMEM = { "Upgrade" }; const char __WS_STR_ORIGIN[] PROGMEM = { "Origin" }; +const char __WS_STR_COOKIE[] PROGMEM = { "Cookie" }; const char __WS_STR_VERSION[] PROGMEM = { "Sec-WebSocket-Version" }; const char __WS_STR_KEY[] PROGMEM = { "Sec-WebSocket-Key" }; const char __WS_STR_PROTOCOL[] PROGMEM = { "Sec-WebSocket-Protocol" }; @@ -1155,6 +1156,7 @@ const char __WS_STR_UUID[] PROGMEM = { "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" }; #define WS_STR_CONNECTION FPSTR(__WS_STR_CONNECTION) #define WS_STR_UPGRADE FPSTR(__WS_STR_UPGRADE) #define WS_STR_ORIGIN FPSTR(__WS_STR_ORIGIN) +#define WS_STR_COOKIE FPSTR(__WS_STR_COOKIE) #define WS_STR_VERSION FPSTR(__WS_STR_VERSION) #define WS_STR_KEY FPSTR(__WS_STR_KEY) #define WS_STR_PROTOCOL FPSTR(__WS_STR_PROTOCOL) @@ -1171,6 +1173,7 @@ bool AsyncWebSocket::canHandle(AsyncWebServerRequest *request){ request->addInterestingHeader(WS_STR_CONNECTION); request->addInterestingHeader(WS_STR_UPGRADE); request->addInterestingHeader(WS_STR_ORIGIN); + request->addInterestingHeader(WS_STR_COOKIE); request->addInterestingHeader(WS_STR_VERSION); request->addInterestingHeader(WS_STR_KEY); request->addInterestingHeader(WS_STR_PROTOCOL); @@ -1185,6 +1188,14 @@ void AsyncWebSocket::handleRequest(AsyncWebServerRequest *request){ if((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())){ return request->requestAuthentication(); } +////////////////////////////////////////// + if(_handshakeHandler != nullptr){ + if(!_handshakeHandler(request)){ + request->send(401); + return; + } + } +////////////////////////////////////////// AsyncWebHeader* version = request->getHeader(WS_STR_VERSION); if(version->value().toInt() != 13){ AsyncWebServerResponse *response = request->beginResponse(400); diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index 5b03ace..f06af2c 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -237,6 +237,7 @@ class AsyncWebSocketClient { void _onData(void *pbuf, size_t plen); }; +typedef std::function AwsHandshakeHandler; typedef std::function AwsEventHandler; //WebServer Handler implementation that plays the role of a socket server @@ -248,6 +249,7 @@ class AsyncWebSocket: public AsyncWebHandler { AsyncWebSocketClientLinkedList _clients; uint32_t _cNextId; AwsEventHandler _eventHandler; + AwsHandshakeHandler _handshakeHandler; bool _enabled; AsyncWebLock _lock; @@ -316,6 +318,11 @@ class AsyncWebSocket: public AsyncWebHandler { _eventHandler = handler; } + // Handshake Handler + void handleHandshake(AwsHandshakeHandler handler){ + _handshakeHandler = handler; + } + //system callbacks (do not call) uint32_t _getNextId(){ return _cNextId++; } void _addClient(AsyncWebSocketClient * client); diff --git a/src/edit.htm.gz.h b/src/edit.htm.gz.h index c2d7f53..3f352e0 100644 --- a/src/edit.htm.gz.h +++ b/src/edit.htm.gz.h @@ -2,7 +2,7 @@ //File: edit.htm.gz, Size: 4408 #define edit_htm_gz_len 4408 const uint8_t edit_htm_gz[] PROGMEM = { -0x1F,0x8B,0x08,0x08,0x52,0x4A,0xB0,0x5E,0x02,0x00,0x65,0x64,0x69,0x74,0x2E,0x68,0x74,0x6D,0x00,0xB5, +0x1F,0x8B,0x08,0x08,0x42,0x13,0xB7,0x5E,0x02,0x00,0x65,0x64,0x69,0x74,0x2E,0x68,0x74,0x6D,0x00,0xB5, 0x1A,0x0B,0x5B,0xDB,0x36,0xF0,0xAF,0x18,0x6F,0x63,0xF6,0xE2,0x38,0x0E,0xA5,0xAC,0x73,0x30,0x2C,0x50, 0x56,0xFA,0x02,0x4A,0x42,0x3B,0xCA,0xD8,0x3E,0xC5,0x56,0x62,0x15,0x5B,0xF6,0x2C,0x99,0x40,0xB3,0xFC, 0xF7,0x9D,0x24,0x3F,0x43,0xE8,0x1E,0xDF,0xD6,0x6E,0x8D,0xA4,0xD3,0x9D,0xEE,0x4E,0xF7,0x54,0xB2,0xBB,