From 9f88c5926224500cfe3db4b7e23711a651433990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xose=20P=C3=A9rez?= Date: Thu, 17 Nov 2016 09:30:47 +0100 Subject: [PATCH 1/2] REST entry point for relay management, supporting GET and PUT --- code/src/button.ino | 2 +- code/src/emon.ino | 2 +- code/src/mqtt.ino | 8 ++--- code/src/relay.ino | 28 +++++++++-------- code/src/rf.ino | 4 +-- code/src/web.ino | 75 ++++++++++++++++++++++++++++++++++++--------- 6 files changed, 84 insertions(+), 35 deletions(-) diff --git a/code/src/button.ino b/code/src/button.ino index 6fc2aa3e..bc79e8a0 100644 --- a/code/src/button.ino +++ b/code/src/button.ino @@ -21,7 +21,7 @@ void buttonSetup() { void buttonLoop() { if (button1.loop()) { - if (button1.getEvent() == EVENT_SINGLE_CLICK) toggleRelay(); + if (button1.getEvent() == EVENT_SINGLE_CLICK) relayToggle(0); if (button1.getEvent() == EVENT_DOUBLE_CLICK) createAP(); if (button1.getEvent() == EVENT_LONG_CLICK) ESP.reset(); } diff --git a/code/src/emon.ino b/code/src/emon.ino index e278b082..31871a94 100644 --- a/code/src/emon.ino +++ b/code/src/emon.ino @@ -71,7 +71,7 @@ void powerMonitorLoop() { if (millis() > next_measurement) { // Safety check: do not read current if relay is OFF - if (!digitalRead(RELAY_PIN)) { + if (!relayStatus(0)) { current = 0; } else { current = emon.getCurrent(EMON_SAMPLES); diff --git a/code/src/mqtt.ino b/code/src/mqtt.ino index aceb501b..f595c744 100644 --- a/code/src/mqtt.ino +++ b/code/src/mqtt.ino @@ -59,7 +59,7 @@ void _mqttOnConnect(bool sessionPresent) { mqttSend((char *) MQTT_FSVERSION_TOPIC, buffer); // Publish current relay status - mqttSend((char *) MQTT_STATUS_TOPIC, (char *) (digitalRead(RELAY_PIN) ? "1" : "0")); + mqttSend((char *) MQTT_STATUS_TOPIC, (char *) (relayStatus(0) ? "1" : "0")); // Subscribe to topic DEBUG_MSG("[MQTT] Subscribing to %s\n", (char *) mqttTopic.c_str()); @@ -91,14 +91,14 @@ void _mqttOnMessage(char* topic, char* payload, AsyncMqttClientMessageProperties // Action to perform if ((char)payload[0] == '0') { isCallbackMessage = true; - switchRelayOff(); + relayStatus(0, false); } if ((char)payload[0] == '1') { isCallbackMessage = true; - switchRelayOn(); + relayStatus(0, true); } if ((char)payload[0] == '2') { - toggleRelay(); + relayToggle(0); } isCallbackMessage = false; diff --git a/code/src/relay.ino b/code/src/relay.ino index f857f8c5..6868adaa 100644 --- a/code/src/relay.ino +++ b/code/src/relay.ino @@ -13,7 +13,7 @@ Copyright (C) 2016 by Xose Pérez // RELAY // ----------------------------------------------------------------------------- -void switchRelayOn() { +void _relayOn(unsigned char id) { if (!digitalRead(RELAY_PIN)) { DEBUG_MSG("[RELAY] ON\n"); @@ -24,10 +24,10 @@ void switchRelayOn() { } webSocketSend((char *) "{\"relayStatus\": true}"); - + } -void switchRelayOff() { +void _relayOff(unsigned char id) { if (digitalRead(RELAY_PIN)) { DEBUG_MSG("[RELAY] OFF\n"); @@ -41,19 +41,23 @@ void switchRelayOff() { } -void toggleRelay() { - if (digitalRead(RELAY_PIN)) { - switchRelayOff(); - } else { - switchRelayOn(); - } +void relayStatus(unsigned char id, bool status) { + status ? _relayOn(id) : _relayOff(id); +} + +bool relayStatus(unsigned char id) { + return (digitalRead(RELAY_PIN) == HIGH); +} + +void relayToggle(unsigned char id) { + relayStatus(id, !relayStatus(id)); } void relaySetup() { pinMode(RELAY_PIN, OUTPUT); EEPROM.begin(4096); byte relayMode = getSetting("relayMode", String(RELAY_MODE)).toInt(); - if (relayMode == 0) switchRelayOff(); - if (relayMode == 1) switchRelayOn(); - if (relayMode == 2) EEPROM.read(0) == 1 ? switchRelayOn() : switchRelayOff(); + if (relayMode == 0) relayStatus(0, false); + if (relayMode == 1) relayStatus(0, true); + if (relayMode == 2) relayStatus(0, EEPROM.read(0) == 1); } diff --git a/code/src/rf.ino b/code/src/rf.ino index b5711e8d..8a07f9d7 100644 --- a/code/src/rf.ino +++ b/code/src/rf.ino @@ -23,8 +23,8 @@ void rfLoop() { return; if (rfCode == 0) return; DEBUG_MSG("[RF] Received code: %lu\n", rfCode); - if (rfCode == rfCodeON) switchRelayOn(); - if (rfCode == rfCodeOFF) switchRelayOff(); + if (rfCode == rfCodeON) relayStatus(0, true); + if (rfCode == rfCodeOFF) relayStatus(0, false); rfCode = 0; } diff --git a/code/src/web.ino b/code/src/web.ino index 31bff6a2..eb5bea78 100644 --- a/code/src/web.ino +++ b/code/src/web.ino @@ -63,8 +63,8 @@ void webSocketParse(uint32_t client_id, uint8_t * payload, size_t length) { if (action.equals("reset")) ESP.reset(); if (action.equals("reconnect")) wifiDisconnect(); - if (action.equals("on")) switchRelayOn(); - if (action.equals("off")) switchRelayOff(); + if (action.equals("on")) relayStatus(0, true); + if (action.equals("off")) relayStatus(0, false); }; @@ -177,7 +177,7 @@ void webSocketStart(uint32_t client_id) { root["mqttUser"] = getSetting("mqttUser"); root["mqttPassword"] = getSetting("mqttPassword"); root["mqttTopic"] = getSetting("mqttTopic", MQTT_TOPIC); - root["relayStatus"] = digitalRead(RELAY_PIN) == HIGH; + root["relayStatus"] = relayStatus(0); root["relayMode"] = getSetting("relayMode", String(RELAY_MODE)); #if ENABLE_DHT @@ -241,8 +241,14 @@ void webSocketEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsE // WEBSERVER // ----------------------------------------------------------------------------- -void onHome(AsyncWebServerRequest *request) { - DEBUG_MSG("[WEBSERVER] Request: %s\n", request->url().c_str()); +void _logRequest(AsyncWebServerRequest *request) { + DEBUG_MSG("[WEBSERVER] Request: %s %s\n", request->methodToString(), request->url().c_str()); +} + +void _onHome(AsyncWebServerRequest *request) { + + _logRequest(request); + String password = getSetting("adminPass", ADMIN_PASS); char httpPassword[password.length() + 1]; password.toCharArray(httpPassword, password.length() + 1); @@ -252,18 +258,55 @@ void onHome(AsyncWebServerRequest *request) { request->send(SPIFFS, "/index.html"); } -void onRelayOn(AsyncWebServerRequest *request) { - DEBUG_MSG("[WEBSERVER] Request: %s\n", request->url().c_str()); - switchRelayOn(); +void _onRelayOn(AsyncWebServerRequest *request) { + + _logRequest(request); + + relayStatus(0, true); request->send(200, "text/plain", "ON"); + }; -void onRelayOff(AsyncWebServerRequest *request) { - DEBUG_MSG("[WEBSERVER] Request: %s\n", request->url().c_str()); - switchRelayOff(); +void _onRelayOff(AsyncWebServerRequest *request) { + + _logRequest(request); + + relayStatus(0, false); request->send(200, "text/plain", "OFF"); + }; +ArRequestHandlerFunction _onRelayStatusWrapper(bool relayID) { + + return [&](AsyncWebServerRequest *request) { + + _logRequest(request); + + if (request->method() == HTTP_PUT) { + if (request->hasParam("status", true)) { + AsyncWebParameter* p = request->getParam("status", true); + relayStatus(relayID, p->value().toInt() == 1); + } + } + + bool asJson = false; + if (request->hasHeader("Accept")) { + AsyncWebHeader* h = request->getHeader("Accept"); + asJson = h->value().equals("application/json"); + } + + if (asJson) { + char buffer[40]; + sprintf(buffer, "{\"status\": %d}", relayStatus(relayID) ? 1 : 0); + request->send(200, "application/json", buffer); + } else { + request->send(200, "text/plain", relayStatus(relayID) ? "1" : "0"); + } + + }; + +} + void webSetup() { // Setup websocket plugin @@ -271,12 +314,14 @@ void webSetup() { server.addHandler(&ws); // Serve home (password protected) - server.on("/", HTTP_GET, onHome); - server.on("/index.html", HTTP_GET, onHome); + server.on("/", HTTP_GET, _onHome); + server.on("/index.html", HTTP_GET, _onHome); // API entry points (non protected) - server.on("/relay/on", HTTP_GET, onRelayOn); - server.on("/relay/off", HTTP_GET, onRelayOff); + server.on("/relay/on", HTTP_GET, _onRelayOn); + server.on("/relay/off", HTTP_GET, _onRelayOff); + server.on("/relay/0/status", HTTP_GET + HTTP_PUT, _onRelayStatusWrapper(0)); + //server.on("/relay/1/status", HTTP_GET + HTTP_PUT, _onRelayStatusWrapper(1)); // Serve static files server.serveStatic("/", SPIFFS, "/"); From 4da4affdb75d0bf24a37508d78d14c38a7842b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xose=20P=C3=A9rez?= Date: Fri, 18 Nov 2016 11:12:21 +0100 Subject: [PATCH 2/2] Changed WS auth and added REST API key --- code/html/custom.css | 4 + code/html/custom.js | 154 +++++++++++++++++++++++---------------- code/html/fsversion | 2 +- code/html/index.html | 15 +++- code/src/defaults.h | 3 +- code/src/dht.ino | 2 +- code/src/emon.ino | 2 +- code/src/mqtt.ino | 4 +- code/src/pow.ino | 2 +- code/src/relay.ino | 4 +- code/src/version.h | 2 +- code/src/web.ino | 169 ++++++++++++++++++++++++++++++++----------- 12 files changed, 249 insertions(+), 114 deletions(-) diff --git a/code/html/custom.css b/code/html/custom.css index 9263f9da..0d544a6a 100644 --- a/code/html/custom.css +++ b/code/html/custom.css @@ -37,6 +37,10 @@ .button-reconnect { background: rgb(202, 60, 60); } +.button-apikey { + background: rgb(0, 202, 0); + margin-left: 5px; +} .pure-g { margin-bottom: 20px; } diff --git a/code/html/custom.js b/code/html/custom.js index 581a192e..486ce000 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -1,9 +1,8 @@ var websock; -var csrf; function doUpdate() { var data = $("#formSave").serializeArray(); - websock.send(JSON.stringify({'csrf': csrf, 'config': data})); + websock.send(JSON.stringify({'config': data})); $(".powExpected").val(0); return false; } @@ -11,19 +10,37 @@ function doUpdate() { function doReset() { var response = window.confirm("Are you sure you want to reset the device?"); if (response == false) return false; - websock.send(JSON.stringify({'csrf': csrf, 'action': 'reset'})); + websock.send(JSON.stringify({'action': 'reset'})); return false; } function doReconnect() { var response = window.confirm("Are you sure you want to disconnect from the current WIFI network?"); if (response == false) return false; - websock.send(JSON.stringify({'csrf': csrf, 'action': 'reconnect'})); + websock.send(JSON.stringify({'action': 'reconnect'})); return false; } function doToggle(element, value) { - websock.send(JSON.stringify({'csrf': csrf, 'action': value ? 'on' : 'off'})); + websock.send(JSON.stringify({'action': value ? 'on' : 'off'})); + return false; +} + +function randomString(length, chars) { + var mask = ''; + if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; + if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + if (chars.indexOf('#') > -1) mask += '0123456789'; + if (chars.indexOf('@') > -1) mask += 'ABCDEF'; + if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; + var result = ''; + for (var i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))]; + return result; +} + +function doGenerateAPIKey() { + var apikey = randomString(16, '@#'); + $("input[name=\"apiKey\"]").val(apikey); return false; } @@ -31,6 +48,7 @@ function showPanel() { $(".panel").hide(); $("#" + $(this).attr("data")).show(); if ($("#layout").hasClass('active')) toggleMenu(); + $("input[type='checkbox']").iphoneStyle("calculateDimensions").iphoneStyle("refresh"); }; function toggleMenu() { @@ -41,36 +59,6 @@ function toggleMenu() { function processData(data) { - // CSRF - if ("csrf" in data) { - csrf = data.csrf; - } - - // messages - if ("message" in data) { - window.alert(data.message); - } - - // pre-process - if ("network" in data) { - data.network = data.network.toUpperCase(); - } - if ("mqttStatus" in data) { - data.mqttStatus = data.mqttStatus ? "CONNECTED" : "NOT CONNECTED"; - } - - // relay - if ("relayStatus" in data) { - $("input[name='relayStatus']") - .prop("checked", data.relayStatus) - .iphoneStyle({ - checkedLabel: 'ON', - uncheckedLabel: 'OFF', - onChange: doToggle - }) - .iphoneStyle("refresh"); - } - // title if ("app" in data) { $(".pure-menu-heading").html(data.app); @@ -81,9 +69,27 @@ function processData(data) { document.title = title; } - // automatic assign Object.keys(data).forEach(function(key) { + // Wifi + if (key == "wifi") { + var groups = $("#panel-wifi .pure-g"); + for (var i in data.wifi) { + var wifi = data.wifi[i]; + Object.keys(wifi).forEach(function(key) { + var id = "input[name=" + key + "]"; + if ($(id, groups[i]).length) $(id, groups[i]).val(wifi[key]); + }); + }; + return; + } + + // Messages + if (key == "message") { + window.alert(data.message); + return; + } + // Enable options if (key.endsWith("Visible")) { var module = key.slice(0,-7); @@ -92,41 +98,40 @@ function processData(data) { return; } + // Pre-process + if (key == "network") { + data.network = data.network.toUpperCase(); + } + if (key == "mqttStatus") { + data.mqttStatus = data.mqttStatus ? "CONNECTED" : "NOT CONNECTED"; + } + // Look for INPUTs var element = $("input[name=" + key + "]"); if (element.length > 0) { if (element.attr('type') == 'checkbox') { element - .prop("checked", data[key] == 1) - .iphoneStyle({ - resizeContainer: false, - resizeHandle: false, - checkedLabel: 'ON', - uncheckedLabel: 'OFF' - }) + .prop("checked", data[key]) .iphoneStyle("refresh"); } else { element.val(data[key]); } + return; } // Look for SELECTs var element = $("select[name=" + key + "]"); if (element.length > 0) { element.val(data[key]); + return; } }); - // WIFI - var groups = $("#panel-wifi .pure-g"); - for (var i in data.wifi) { - var wifi = data.wifi[i]; - Object.keys(wifi).forEach(function(key) { - var id = "input[name=" + key + "]"; - if ($(id, groups[i]).length) $(id, groups[i]).val(wifi[key]); - }); - }; + // Auto generate an APIKey if none defined yet + if ($("input[name='apiKey']").val() == "") { + doGenerateAPIKey(); + } } @@ -138,16 +143,10 @@ function getJson(str) { } } -function init() { - - $("#menuLink").on('click', toggleMenu); - $(".button-update").on('click', doUpdate); - $(".button-reset").on('click', doReset); - $(".button-reconnect").on('click', doReconnect); - $(".pure-menu-link").on('click', showPanel); - - var host = window.location.hostname; - //host = '192.168.1.115'; +function initWebSocket(host) { + if (host === undefined) { + host = window.location.hostname; + } websock = new WebSocket('ws://' + host + '/ws'); websock.onopen = function(evt) {}; websock.onclose = function(evt) {}; @@ -156,6 +155,37 @@ function init() { var data = getJson(evt.data); if (data) processData(data); }; +} + +function init() { + + $("#menuLink").on('click', toggleMenu); + $(".button-update").on('click', doUpdate); + $(".button-reset").on('click', doReset); + $(".button-reconnect").on('click', doReconnect); + $(".button-apikey").on('click', doGenerateAPIKey); + $(".pure-menu-link").on('click', showPanel); + + $("input[name='relayStatus']") + .iphoneStyle({ + onChange: doToggle + }); + $("input[type='checkbox']") + .iphoneStyle({ + resizeContainer: true, + resizeHandle: true, + checkedLabel: 'ON', + uncheckedLabel: 'OFF' + }) + .iphoneStyle("refresh"); + + + $.ajax({ + 'method': 'GET', + 'url': '/auth' + }).done(function(data) { + initWebSocket(); + }); } diff --git a/code/html/fsversion b/code/html/fsversion index 7dea76ed..6d7de6e6 100644 --- a/code/html/fsversion +++ b/code/html/fsversion @@ -1 +1 @@ -1.0.1 +1.0.2 diff --git a/code/html/index.html b/code/html/index.html index fbbad511..0bad0b49 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -172,12 +172,25 @@
- +
 
The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).
+
+
+
+
+ +
+ + +
+
 
+
This is the key you will have to pass with every HTTP request to the API, either to get or write values.
+
+ diff --git a/code/src/defaults.h b/code/src/defaults.h index 60be3d66..04e2567c 100644 --- a/code/src/defaults.h +++ b/code/src/defaults.h @@ -75,7 +75,8 @@ #define WIFI_MAX_NETWORKS 3 #define ADMIN_PASS "fibonacci" #define HTTP_USERNAME "admin" -#define CSRF_BUFFER_SIZE 5 +#define WS_BUFFER_SIZE 5 +#define WS_TIMEOUT 1800000 // ----------------------------------------------------------------------------- // OTA & NOFUSS diff --git a/code/src/dht.ino b/code/src/dht.ino index fe412fc2..b7fd2cf8 100644 --- a/code/src/dht.ino +++ b/code/src/dht.ino @@ -66,7 +66,7 @@ void dhtLoop() { // Update websocket clients char buffer[100]; sprintf_P(buffer, PSTR("{\"dhtVisible\": 1, \"dhtTmp\": %s, \"dhtHum\": %s}"), temperature, humidity); - webSocketSend(buffer); + wsSend(buffer); } diff --git a/code/src/emon.ino b/code/src/emon.ino index 31871a94..6202e996 100644 --- a/code/src/emon.ino +++ b/code/src/emon.ino @@ -95,7 +95,7 @@ void powerMonitorLoop() { // Update websocket clients char text[20]; sprintf_P(text, PSTR("{\"emonPower\": %d}"), int(current * mainsVoltage)); - webSocketSend(text); + wsSend(text); // Send MQTT messages averaged every EMON_MEASUREMENTS if (measurements == EMON_MEASUREMENTS) { diff --git a/code/src/mqtt.ino b/code/src/mqtt.ino index f595c744..73c53268 100644 --- a/code/src/mqtt.ino +++ b/code/src/mqtt.ino @@ -46,7 +46,7 @@ void _mqttOnConnect(bool sessionPresent) { DEBUG_MSG("[MQTT] Connected!\n"); // Send status via webSocket - webSocketSend((char *) "{\"mqttStatus\": true}"); + wsSend((char *) "{\"mqttStatus\": true}"); // Build MQTT topics buildTopics(); @@ -70,7 +70,7 @@ void _mqttOnConnect(bool sessionPresent) { void _mqttOnDisconnect(AsyncMqttClientDisconnectReason reason) { // Send status via webSocket - webSocketSend((char *) "{\"mqttStatus\": false}"); + wsSend((char *) "{\"mqttStatus\": false}"); } diff --git a/code/src/pow.ino b/code/src/pow.ino index 93260332..e631e17b 100644 --- a/code/src/pow.ino +++ b/code/src/pow.ino @@ -146,7 +146,7 @@ void powLoop() { char buffer[100]; sprintf_P(buffer, PSTR("{\"powVisible\": 1, \"powActivePower\": %d}"), power); - webSocketSend(buffer); + wsSend(buffer); if (--report_count == 0) { mqttSend((char *) getSetting("powPowerTopic", POW_POWER_TOPIC).c_str(), (char *) String(power).c_str()); diff --git a/code/src/relay.ino b/code/src/relay.ino index 6868adaa..e72efdaf 100644 --- a/code/src/relay.ino +++ b/code/src/relay.ino @@ -23,7 +23,7 @@ void _relayOn(unsigned char id) { mqttSend((char *) MQTT_STATUS_TOPIC, (char *) "1"); } - webSocketSend((char *) "{\"relayStatus\": true}"); + wsSend((char *) "{\"relayStatus\": true}"); } @@ -37,7 +37,7 @@ void _relayOff(unsigned char id) { mqttSend((char *) MQTT_STATUS_TOPIC, (char *) "0"); } - webSocketSend((char *) "{\"relayStatus\": false}"); + wsSend((char *) "{\"relayStatus\": false}"); } diff --git a/code/src/version.h b/code/src/version.h index afd3ce45..5e910f49 100644 --- a/code/src/version.h +++ b/code/src/version.h @@ -1,4 +1,4 @@ #define APP_NAME "Espurna" -#define APP_VERSION "1.0.1" +#define APP_VERSION "1.0.2" #define APP_AUTHOR "xose.perez@gmail.com" #define APP_WEBSITE "http://tinkerman.cat" diff --git a/code/src/web.ino b/code/src/web.ino index eb5bea78..4a9c7e96 100644 --- a/code/src/web.ino +++ b/code/src/web.ino @@ -18,23 +18,28 @@ Copyright (C) 2016 by Xose Pérez AsyncWebServer server(80); AsyncWebSocket ws("/ws"); -unsigned long _csrf[CSRF_BUFFER_SIZE]; +typedef struct { + IPAddress ip; + unsigned long timestamp = 0; +} ws_ticket_t; + +ws_ticket_t _ticket[WS_BUFFER_SIZE]; // ----------------------------------------------------------------------------- // WEBSOCKETS // ----------------------------------------------------------------------------- -bool webSocketSend(char * payload) { +bool wsSend(char * payload) { //DEBUG_MSG("[WEBSOCKET] Broadcasting '%s'\n", payload); ws.textAll(payload); } -bool webSocketSend(uint32_t client_id, char * payload) { +bool wsSend(uint32_t client_id, char * payload) { //DEBUG_MSG("[WEBSOCKET] Sending '%s' to #%ld\n", payload, client_id); ws.text(client_id, payload); } -void webSocketParse(uint32_t client_id, uint8_t * payload, size_t length) { +void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) { // Parse JSON input DynamicJsonBuffer jsonBuffer; @@ -45,16 +50,6 @@ void webSocketParse(uint32_t client_id, uint8_t * payload, size_t length) { return; } - // CSRF - unsigned long csrf = 0; - if (root.containsKey("csrf")) csrf = root["csrf"]; - if (csrf != _csrf[client_id % CSRF_BUFFER_SIZE]) { - DEBUG_MSG("[WEBSOCKET] CSRF check failed\n"); - ws.text(client_id, "{\"message\": \"Session expired, please reload page...\"}"); - return; - } - - // Check actions if (root.containsKey("action")) { @@ -76,6 +71,7 @@ void webSocketParse(uint32_t client_id, uint8_t * payload, size_t length) { bool dirty = false; bool dirtyMQTT = false; + bool apiEnabled = false; unsigned int network = 0; for (unsigned int i=0; iremoteIP(); + unsigned long now = millis(); + unsigned short index = 0; + + for (index = 0; index < WS_BUFFER_SIZE; index++) { + if ((_ticket[index].ip == ip) && (now - _ticket[index].timestamp < WS_TIMEOUT)) break; + } + + if (index == WS_BUFFER_SIZE) { + DEBUG_MSG("[WEBSOCKET] Validation check failed\n"); + ws.text(client->id(), "{\"message\": \"Session expired, please reload page...\"}"); + return false; + } + + return true; + +} + +void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + + // Authorize + #ifndef NOWSAUTH + if (!_wsAuth(client)) return; + #endif + if (type == WS_EVT_CONNECT) { - #if DEBUG_PORT - { - IPAddress ip = server.remoteIP(client->id()); - DEBUG_MSG("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n", client->id(), ip[0], ip[1], ip[2], ip[3], server->url()); - } - #endif - webSocketStart(client->id()); + IPAddress ip = client->remoteIP(); + DEBUG_MSG("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n", client->id(), ip[0], ip[1], ip[2], ip[3], server->url()); + _wsStart(client->id()); } else if(type == WS_EVT_DISCONNECT) { DEBUG_MSG("[WEBSOCKET] #%u disconnected\n", client->id()); } else if(type == WS_EVT_ERROR) { @@ -233,8 +259,9 @@ void webSocketEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsE } else if(type == WS_EVT_PONG) { DEBUG_MSG("[WEBSOCKET] #%u pong(%u): %s\n", client->id(), len, len ? (char*) data : ""); } else if(type == WS_EVT_DATA) { - webSocketParse(client->id(), data, len); + _wsParse(client->id(), data, len); } + } // ----------------------------------------------------------------------------- @@ -245,16 +272,43 @@ void _logRequest(AsyncWebServerRequest *request) { DEBUG_MSG("[WEBSERVER] Request: %s %s\n", request->methodToString(), request->url().c_str()); } -void _onHome(AsyncWebServerRequest *request) { - - _logRequest(request); - +bool _authenticate(AsyncWebServerRequest *request) { String password = getSetting("adminPass", ADMIN_PASS); char httpPassword[password.length() + 1]; password.toCharArray(httpPassword, password.length() + 1); - if (!request->authenticate(HTTP_USERNAME, httpPassword)) { - return request->requestAuthentication(); + return request->authenticate(HTTP_USERNAME, httpPassword); +} + +void _onAuth(AsyncWebServerRequest *request) { + + _logRequest(request); + + if (!_authenticate(request)) return request->requestAuthentication(); + + IPAddress ip = request->client()->remoteIP(); + unsigned long now = millis(); + unsigned short index; + for (index = 0; index < WS_BUFFER_SIZE; index++) { + if (_ticket[index].ip == ip) break; + if (_ticket[index].timestamp == 0) break; + if (now - _ticket[index].timestamp > WS_TIMEOUT) break; } + if (index == WS_BUFFER_SIZE) { + request->send(423); + } else { + _ticket[index].ip = ip; + _ticket[index].timestamp = now; + request->send(204); + } + +} + +void _onHome(AsyncWebServerRequest *request) { + + _logRequest(request); + + if (!_authenticate(request)) return request->requestAuthentication(); + request->send(SPIFFS, "/index.html"); } @@ -276,16 +330,48 @@ void _onRelayOff(AsyncWebServerRequest *request) { }; -ArRequestHandlerFunction _onRelayStatusWrapper(bool relayID) { +bool _apiAuth(AsyncWebServerRequest *request) { + + if (getSetting("apiEnabled").toInt() == 0) { + DEBUG_MSG("[WEBSERVER] HTTP API is not enabled\n"); + request->send(403); + return false; + } + + if (!request->hasParam("apikey", (request->method() == HTTP_PUT))) { + DEBUG_MSG("[WEBSERVER] Missing apikey parameter\n"); + request->send(403); + return false; + } + + AsyncWebParameter* p = request->getParam("apikey", (request->method() == HTTP_PUT)); + if (!p->value().equals(getSetting("apiKey"))) { + DEBUG_MSG("[WEBSERVER] Wrong apikey parameter\n"); + request->send(403); + return false; + } + + return true; + +} + +ArRequestHandlerFunction _onRelayStatusWrapper(unsigned int relayID) { return [&](AsyncWebServerRequest *request) { _logRequest(request); + if (!_apiAuth(request)) return; + if (request->method() == HTTP_PUT) { if (request->hasParam("status", true)) { AsyncWebParameter* p = request->getParam("status", true); - relayStatus(relayID, p->value().toInt() == 1); + unsigned int value = p->value().toInt(); + if (value == 2) { + relayToggle(relayID); + } else { + relayStatus(relayID, value == 1); + } } } @@ -296,7 +382,7 @@ ArRequestHandlerFunction _onRelayStatusWrapper(bool relayID) { } if (asJson) { - char buffer[40]; + char buffer[20]; sprintf(buffer, "{\"status\": %d}", relayStatus(relayID) ? 1 : 0); request->send(200, "application/json", buffer); } else { @@ -310,12 +396,13 @@ ArRequestHandlerFunction _onRelayStatusWrapper(bool relayID) { void webSetup() { // Setup websocket plugin - ws.onEvent(webSocketEvent); + ws.onEvent(_wsEvent); server.addHandler(&ws); // Serve home (password protected) server.on("/", HTTP_GET, _onHome); server.on("/index.html", HTTP_GET, _onHome); + server.on("/auth", HTTP_GET, _onAuth); // API entry points (non protected) server.on("/relay/on", HTTP_GET, _onRelayOn);