Browse Source

Changed WS auth and added REST API key

fastled
Xose Pérez 8 years ago
parent
commit
4da4affdb7
12 changed files with 249 additions and 114 deletions
  1. +4
    -0
      code/html/custom.css
  2. +92
    -62
      code/html/custom.js
  3. +1
    -1
      code/html/fsversion
  4. +14
    -1
      code/html/index.html
  5. +2
    -1
      code/src/defaults.h
  6. +1
    -1
      code/src/dht.ino
  7. +1
    -1
      code/src/emon.ino
  8. +2
    -2
      code/src/mqtt.ino
  9. +1
    -1
      code/src/pow.ino
  10. +2
    -2
      code/src/relay.ino
  11. +1
    -1
      code/src/version.h
  12. +128
    -41
      code/src/web.ino

+ 4
- 0
code/html/custom.css View File

@ -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;
}


+ 92
- 62
code/html/custom.js View File

@ -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();
});
}


+ 1
- 1
code/html/fsversion View File

@ -1 +1 @@
1.0.1
1.0.2

+ 14
- 1
code/html/index.html View File

@ -172,12 +172,25 @@
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="adminPass">Administrator password</label>
<label class="pure-u-1 pure-u-md-1-4" for="adminPass">Admin password</label>
<input name="adminPass" class="pure-u-1 pure-u-md-3-4" type="text" tabindex="3" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">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).</div>
</div>
<div class="pure-g">
<div class="pure-u-1 pure-u-sm-1-4"><label for="apiEnabled">Enable HTTP API</label></div>
<div class="pure-u-1 pure-u-sm-1-4"><input type="checkbox" name="apiEnabled" /></div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="apiKey">HTTP API Key</label>
<input name="apiKey" class="pure-u-3-4 pure-u-md-1-2" type="text" tabindex="4" />
<div class=" pure-u-1-4 pure-u-md-1-4"><button class="pure-button button-apikey pure-u-23-24">Generate</button></div>
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">This is the key you will have to pass with every HTTP request to the API, either to get or write values.</div>
</div>
</fieldset>
</div>
</div>


+ 2
- 1
code/src/defaults.h View File

@ -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


+ 1
- 1
code/src/dht.ino View File

@ -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);
}


+ 1
- 1
code/src/emon.ino View File

@ -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) {


+ 2
- 2
code/src/mqtt.ino View File

@ -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}");
}


+ 1
- 1
code/src/pow.ino View File

@ -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());


+ 2
- 2
code/src/relay.ino View File

@ -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}");
}


+ 1
- 1
code/src/version.h View File

@ -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"

+ 128
- 41
code/src/web.ino View File

@ -18,23 +18,28 @@ Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
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; i<config.size(); i++) {
@ -97,6 +93,12 @@ void webSocketParse(uint32_t client_id, uint8_t * payload, size_t length) {
if (value.length() == 0) continue;
}
// Checkboxes
if (key == "apiEnabled") {
apiEnabled = true;
continue;
}
if (key == "ssid") {
key = key + String(network);
}
@ -113,6 +115,12 @@ void webSocketParse(uint32_t client_id, uint8_t * payload, size_t length) {
}
// Checkboxes
if (apiEnabled != (getSetting("apiEnabled").toInt() == 1)) {
setSetting("apiEnabled", String() + (apiEnabled ? 1 : 0));
dirty = true;
}
// Save settings
if (dirty) {
@ -146,7 +154,7 @@ void webSocketParse(uint32_t client_id, uint8_t * payload, size_t length) {
}
void webSocketStart(uint32_t client_id) {
void _wsStart(uint32_t client_id) {
char app[64];
sprintf(app, "%s %s", APP_NAME, APP_VERSION);
@ -157,12 +165,6 @@ void webSocketStart(uint32_t client_id) {
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
// CSRF
if (client_id < CSRF_BUFFER_SIZE) {
_csrf[client_id] = random(0x7fffffff);
}
root["csrf"] = _csrf[client_id % CSRF_BUFFER_SIZE];
root["app"] = app;
root["manufacturer"] = String(MANUFACTURER);
root["chipid"] = chipid;
@ -171,7 +173,7 @@ void webSocketStart(uint32_t client_id) {
root["hostname"] = getSetting("hostname", HOSTNAME);
root["network"] = getNetwork();
root["ip"] = getIP();
root["mqttStatus"] = mqttConnected() ? "1" : "0";
root["mqttStatus"] = mqttConnected();
root["mqttServer"] = getSetting("mqttServer", MQTT_SERVER);
root["mqttPort"] = getSetting("mqttPort", String(MQTT_PORT));
root["mqttUser"] = getSetting("mqttUser");
@ -179,6 +181,8 @@ void webSocketStart(uint32_t client_id) {
root["mqttTopic"] = getSetting("mqttTopic", MQTT_TOPIC);
root["relayStatus"] = relayStatus(0);
root["relayMode"] = getSetting("relayMode", String(RELAY_MODE));
root["apiEnabled"] = getSetting("apiEnabled").toInt() == 1;
root["apiKey"] = getSetting("apiKey");
#if ENABLE_DHT
root["dhtVisible"] = 1;
@ -217,15 +221,37 @@ void webSocketStart(uint32_t client_id) {
}
void webSocketEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
bool _wsAuth(AsyncWebSocketClient * client) {
IPAddress ip = client->remoteIP();
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);


Loading…
Cancel
Save