Do you remember the project created in the “How to create a web server with esp8266 and esp32” guide? The time has come to place it in the modern era, then we will remove polling and add push for live data modification.
This mode is the best way, normal applications retrieve initial data from REST calls and then they are updated via WebSocket.
First we are going to add the WebSocket server.
#include <WebSocketsServer.h>
[...]
const uint8_t wsPort = 81;
WebSocketsServer webSocket = WebSocketsServer(wsPort);
[...]
// In the setup
Serial.print("Starting WebSocket..");
webSocket.begin();
webSocket.onEvent(webSocketEvent);
Serial.println("OK");
[...]
// In the loop
webSocket.loop();
So in the cycle we will add a broadcast message (for all clients) every 2 seconds which updates the temperature and humidity
if (connectionNumber>0 && lastUpdate+messageInterval<millis()){
Serial.println("[WSc] SENT: Broadcast message!!");
const size_t capacity = 1024;
DynamicJsonDocument doc(capacity);
doc["humidity"] = dht12.readHumidity();
doc["temp"] = dht12.readTemperature();
// If you don't have a DHT12 put only the library
// comment upper line and decomment this line
doc["humidity"] = random(10,80);
doc["temp"] = random(1000,3500)/100.;
String buf;
serializeJson(doc, buf);
webSocket.broadcastTXT(buf);
lastUpdate = millis();
}
The code is quite simple, but pay attention to the connectionNumber> 0 condition which prevents WebSocket messages from being sent if no clients are connected.
It’s a simple variable that I update every connection/disconnection with the number of connected clients.
So all the management can be said to be completed
Here in the webSocketEvent you can see the management of the number of connected clients.
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_DISCONNECTED:
Serial.printf("[%u] Disconnected!\n", num);
webSocket.sendTXT(num, "{\"connected\":false}");
connectionNumber = webSocket.connectedClients();
Serial.print("Connected devices ");
Serial.println(connectionNumber);
break;
case WStype_CONNECTED:
{
IPAddress ip = webSocket.remoteIP(num);
Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
// send message to client
webSocket.sendTXT(num, "{\"connected\":true}");
connectionNumber = webSocket.connectedClients();
Serial.print("Connected devices ");
Serial.println(connectionNumber);
}
break;
case WStype_TEXT:
Serial.printf("[%u] RECEIVE TXT: %s\n", num, payload);
// send message to client
webSocket.sendTXT(num, "(ECHO MESSAGE) "+String((char *)payload));
// send data to all connected clients
// webSocket.broadcastTXT("message here");
break;
case WStype_BIN:
Serial.printf("[%u] get binary length: %u\n", num, length);
hexdump(payload, length);
// send message to client
// webSocket.sendBIN(num, payload, length);
break;
}
}
So we must add the event listener to the front-end to manage the message
function onOpen(evt) {
console.log("CONNECTED");
}
function onClose(evt) {
console.log("DISCONNECTED");
}
function onMessage(evt) {
var res = JSON.parse(evt.data);
hw.setHumidity(res.humidity);
tw.setTemperature(res.temp);
}
function onError(evt) {
alert(evt.data);
}
function initWebSocket() {
var uri = 'ws://'+location.hostname+':81/';
websocket = new WebSocket(uri);
websocket.onopen = function (evt) {
onOpen(evt)
};
websocket.onclose = function (evt) {
onClose(evt)
};
websocket.onmessage = function (evt) {
onMessage(evt)
};
websocket.onerror = function (evt) {
onError(evt)
};
}
initWebSocket();
The code is very simple and work correctly.
Authentication
Simple work workaround
This is not so beautiful solution, but It’s quite simple to implement without the use of extra knowledge (like header, cookie, etc.), and It’s quite powerful.
You can add a token like authorization, in this way token is passed like the login.
// Simple way to invalidate session every restart
// ws_auth_code = sha1(String(www_username) + ":" + String(www_password) + ":" + String(random(1000)))+":";
ws_auth_code = sha1(String(www_username) + ":" + String(www_password))+":";
Serial.println(base64::encode(ws_auth_code).c_str());
webSocket.setAuthorization(base64::encode(ws_auth_code).c_str());
Than I add new token in the authentication phase (without clientIP)
httpServer.sendHeader("Set-Cookie", "ESPWSSESSIONID=" + String(ws_auth_code));
than I insert the token in the uri of ws, like described in the previous article.
Here retrieve cookie
var getCookie = function (name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
};
if (ENABLE_WS_AUTHORIZATION) {
// Example
var cookieVal = getCookie('ESPWSSESSIONID');
initWebSocket(cookieVal);
}else{
initWebSocket();
}
and it will be passed into the uri in this way
ws://937c7478aaa380b87242dd7aba8ff9304e5cf089@192.168.1.136:81/
if (authToken){
var newUri = uri.substring(0,uri.indexOf("//")+2)+authToken+'@'+uri.substring(uri.indexOf("//")+2);
websocket = new WebSocket(newUri,'arduino');
}else {
websocket = new WebSocket(uri);
}
Check the complete code on GitHub
Token Based Authentication
Last time we did a token-based authentication, we can now retrieve that implementation and integrate the WebSoket.
Has you can see in the article “Web Server with esp8266 and esp32: manage security and authentication” I add the token in ESPSESSIONID cookie,
void handleLogin() {
Serial.println("Handle login");
String msg;
if (httpServer.hasHeader("Cookie")) {
// Print cookies
Serial.print("Found cookie: ");
String cookie = httpServer.header("Cookie");
Serial.println(cookie);
}
if (httpServer.hasArg("username") && httpServer.hasArg("password")) {
Serial.print("Found parameter: ");
if (httpServer.arg("username") == String(www_username) && httpServer.arg("password") == String(www_password)) {
httpServer.sendHeader("Location", "/");
httpServer.sendHeader("Cache-Control", "no-cache");
String token = sha1(String(www_username) + ":" + String(www_password) + ":" + httpServer.client().remoteIP().toString());
httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=" + token);
httpServer.sendHeader("Set-Cookie", "CLIENTIP=" + httpServer.client().remoteIP().toString());
httpServer.send(301);
Serial.println("Log in Successful");
return;
}
msg = "Wrong username/password! try again.";
Serial.println("Log in Failed");
httpServer.sendHeader("Location", "/login.html?msg=" + msg);
httpServer.sendHeader("Cache-Control", "no-cache");
httpServer.send(301);
return;
}
}
then every REST call I check if It’s present.
//Check if header is present and correct
bool is_authenticated() {
Serial.println("Enter is_authenticated");
if (httpServer.hasHeader("Cookie")) {
Serial.print("Found cookie: ");
String cookie = httpServer.header("Cookie");
Serial.println(cookie);
String token = sha1(String(www_username) + ":" +
String(www_password) + ":" +
httpServer.client().remoteIP().toString());
// token = sha1(token);
if (cookie.indexOf("ESPSESSIONID=" + token) != -1) {
Serial.println("Authentication Successful");
return true;
}
}
Serial.println("Authentication Failed");
return false;
}
As you can see I use IP client also to generate a specified token for device, so to resolve this problem I add the client IP to the cookie.
String token = sha1(String(www_username) + ":" + String(www_password) + ":" + httpServer.client().remoteIP().toString());
httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=" + token);
httpServer.sendHeader("Set-Cookie", "CLIENTIP=" + httpServer.client().remoteIP().toString());
Than we must add the custom validator to the WebSoket
const char * headerkeys[] = { "Cookie" };
size_t headerKeyCount = sizeof(headerkeys) / sizeof(char*);
webSocket.onValidateHttpHeader(validateHttpHeader, headerkeys, headerKeyCount);
The onValidateHttpHeader requires 3 parameters:
- validateHttpHeader: is the function that do the validation;
- headerkeys: is the list of headers that are mandatory;
- headerKeyCount: is the size of the headers.
The bool validateHttpHeader(String headerName, String headerValue)
function has 2 parameter
- headerName: the name of the cookie;
- headerValue: the value of the cookie.
We now have the specific header to validate the cookie, so the mandatory header is the cookie too,
/*
* The WebSocketServerHttpHeaderValFunc delegate passed to webSocket.onValidateHttpHeader
*/
bool validateHttpHeader(String headerName, String headerValue) {
//assume a true response for any headers not handled by this validator
bool valid = true;
if(headerName.equalsIgnoreCase("Cookie")) {
//if the header passed is the Cookie header, validate it according to the rules in 'isCookieValid' function
valid = isCookieValid(headerValue);
}
return valid;
}
and when we find this header we must the check of the value
bool isCookieValid(String rawCookieHeaderValue) {
Serial.print("Cookie validation ");
Serial.println(rawCookieHeaderValue);
String clientIP = "";
if (rawCookieHeaderValue.indexOf("CLIENTIP") != -1) {
clientIP = rawCookieHeaderValue.substring(rawCookieHeaderValue.indexOf("CLIENTIP=") + 9, rawCookieHeaderValue.indexOf("|"));
Serial.print("clientIP ");
Serial.println(clientIP);
}
if (rawCookieHeaderValue.indexOf("ESPSESSIONID") != -1) {
String tokenStr = rawCookieHeaderValue.substring(rawCookieHeaderValue.indexOf("ESPSESSIONID=") + 13, rawCookieHeaderValue.indexOf(";"));
Serial.print("ESPSESSIONID ");
Serial.println(tokenStr);
String token = sha1(String(www_username) + ":" + String(www_password) + ":" + clientIP);
Serial.print("token ");
Serial.println(token);
return tokenStr == token;
}
return false;
}
To the front-end you don’t need other change, te take advantage of the existing cookie.
And here is the complete server code
/*
* WeMos D1 mini (esp8266)
* Simple web server that read from SPIFFS and
* stream on browser various type of file
* and manage gzip format also.
* Here I add a management of a token authentication
* with a custom login form, and relative logout.
* Add WebSocket data update with the
* integration with token authentication handshake
*
* DHT12 library https://mischianti.org/dht12-library-en/
*
* DHT12 ----- Esp8266
* SDL ----- D1
* SDA ----- D2
*
* by Mischianti Renzo <https://mischianti.org>
*
* https://mischianti.org/websocket-on-arduino-esp8266-and-esp32-temperature-and-humidity-realtime-update-3/
*
*/
#define ENABLE_WS_AUTHORIZATION
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WebSocketsServer.h>
#include <base64.h>
#include <FS.h>
#include "Hash.h"
#include <DHT12.h>
#include <ArduinoJson.h>
const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";
const char* www_username = "admin";
const char* www_password = "esp8266";
String ws_auth_code;
const uint8_t wsPort = 81;
int8_t connectionNumber = 0;
unsigned long messageInterval = 2000;
// Set dht12 i2c comunication on default Wire pin
DHT12 dht12;
ESP8266WebServer httpServer(80);
WebSocketsServer webSocket = WebSocketsServer(wsPort);
void serverRouting();
void manageSecurity();
#ifdef ENABLE_WS_AUTHORIZATION
bool validateHttpHeader(String headerName, String headerValue);
#endif
void setup(void) {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
// Start sensor
dht12.begin();
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Serial.print(F("Inizializing FS..."));
if (SPIFFS.begin()) {
Serial.println(F("done."));
} else {
Serial.println(F("fail."));
}
Serial.println("Set routing for http server!");
serverRouting();
httpServer.begin();
Serial.println("HTTP server started");
Serial.print("Starting WebSocket..");
#ifdef ENABLE_WS_AUTHORIZATION
// Simple way to invalidate session every restart
// ws_auth_code = sha1(String(www_username) + ":" + String(www_password) + ":" + String(random(1000)))+":";
// ws_auth_code = sha1(String(www_username) + ":" + String(www_password))+":";
//
// Serial.println(base64::encode(ws_auth_code).c_str());
//
// webSocket.setAuthorization(base64::encode(ws_auth_code).c_str());
const char * headerkeys[] = { "Cookie" };
size_t headerKeyCount = sizeof(headerkeys) / sizeof(char*);
webSocket.onValidateHttpHeader(validateHttpHeader, headerkeys, headerKeyCount);
#endif
webSocket.begin();
webSocket.onEvent(webSocketEvent);
Serial.println("OK");
}
unsigned long lastUpdate = millis()+messageInterval;
void loop(void) {
httpServer.handleClient();
webSocket.loop();
if (connectionNumber>0 && lastUpdate+messageInterval<millis()){
Serial.println("[WSc] SENT: Broadcast message!!");
const size_t capacity = 1024;
DynamicJsonDocument doc(capacity);
doc["humidity"] = dht12.readHumidity();
doc["temp"] = dht12.readTemperature();
// If you don't have a DHT12 put only the library
// comment upper line and decomment this line
doc["humidity"] = random(10,80);
doc["temp"] = random(1000,3500)/100.;
String buf;
serializeJson(doc, buf);
webSocket.broadcastTXT(buf);
lastUpdate = millis();
}
}
String getContentType(String filename) {
if (filename.endsWith(F(".htm"))) return F("text/html");
else if (filename.endsWith(F(".html"))) return F("text/html");
else if (filename.endsWith(F(".css"))) return F("text/css");
else if (filename.endsWith(F(".js"))) return F("application/javascript");
else if (filename.endsWith(F(".json"))) return F("application/json");
else if (filename.endsWith(F(".png"))) return F("image/png");
else if (filename.endsWith(F(".gif"))) return F("image/gif");
else if (filename.endsWith(F(".jpg"))) return F("image/jpeg");
else if (filename.endsWith(F(".jpeg"))) return F("image/jpeg");
else if (filename.endsWith(F(".ico"))) return F("image/x-icon");
else if (filename.endsWith(F(".xml"))) return F("text/xml");
else if (filename.endsWith(F(".pdf"))) return F("application/x-pdf");
else if (filename.endsWith(F(".zip"))) return F("application/x-zip");
else if (filename.endsWith(F(".gz"))) return F("application/x-gzip");
return F("text/plain");
}
bool handleFileRead(String path) {
Serial.print(F("handleFileRead: "));
Serial.println(path);
if (!is_authenticated()) {
Serial.println(F("Go on not login!"));
path = F("/login.html");
handleLogout();
return true;
} else {
if (path.endsWith("/")) path += F("index.html"); // If a folder is requested, send the index file
}
String contentType = getContentType(path); // Get the MIME type
String pathWithGz = path + F(".gz");
if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { // If the file exists, either as a compressed archive, or normal
if (SPIFFS.exists(pathWithGz)) { // If there's a compressed version available
path += F(".gz"); // Use the compressed version
}
fs::File file = SPIFFS.open(path, "r"); // Open the file
size_t sent = httpServer.streamFile(file, contentType); // Send it to the client
file.close(); // Close the file again
Serial.println(
String(F("\tSent file: ")) + path + String(F(" of size "))
+ sent);
return true;
}
Serial.println(String(F("\tFile Not Found: ")) + path);
return false; // If the file doesn't exist, return false
}
void handleNotFound() {
String message = "File Not Found\n\n";
message += "URI: ";
message += httpServer.uri();
message += "\nMethod: ";
message += (httpServer.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += httpServer.args();
message += "\n";
for (uint8_t i = 0; i < httpServer.args(); i++) {
message += " " + httpServer.argName(i) + ": " + httpServer.arg(i)
+ "\n";
}
httpServer.send(404, "text/plain", message);
}
void handleLogin() {
Serial.println("Handle login");
String msg;
if (httpServer.hasHeader("Cookie")) {
// Print cookies
Serial.print("Found cookie: ");
String cookie = httpServer.header("Cookie");
Serial.println(cookie);
}
if (httpServer.hasArg("username") && httpServer.hasArg("password")) {
Serial.print("Found parameter: ");
if (httpServer.arg("username") == String(www_username) && httpServer.arg("password") == String(www_password)) {
httpServer.sendHeader("Location", "/");
httpServer.sendHeader("Cache-Control", "no-cache");
String token = sha1(String(www_username) + ":" + String(www_password) + ":" + httpServer.client().remoteIP().toString());
httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=" + token);
httpServer.sendHeader("Set-Cookie", "CLIENTIP=" + httpServer.client().remoteIP().toString());
httpServer.send(301);
Serial.println("Log in Successful");
return;
}
msg = "Wrong username/password! try again.";
Serial.println("Log in Failed");
httpServer.sendHeader("Location", "/login.html?msg=" + msg);
httpServer.sendHeader("Cache-Control", "no-cache");
httpServer.send(301);
return;
}
}
/**
* Manage logout (simply remove correct token and redirect to login form)
*/
void handleLogout() {
Serial.println("Disconnection");
httpServer.sendHeader("Location", "/login.html?msg=User disconnected");
httpServer.sendHeader("Cache-Control", "no-cache");
httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=0");
// httpServer.sendHeader("Set-Cookie", "ESPWSSESSIONID=0");
httpServer.send(301);
return;
}
/**
* Retrieve temperature humidity realtime data
*/
void handleTemperatureHumidity(){
Serial.println("handleTemperatureHumidity");
manageSecurity();
Serial.println("handleTemperatureHumidity security pass!");
const size_t capacity = 1024;
DynamicJsonDocument doc(capacity);
doc["humidity"] = dht12.readHumidity();
doc["temp"] = dht12.readTemperature();
// If you don't have a DHT12 put only the library
// comment upper line and decomment this line
doc["humidity"] = random(10,80);
doc["temp"] = random(1000,3500)/100.;
String buf;
serializeJson(doc, buf);
httpServer.send(200, F("application/json"), buf);
}
//Check if header is present and correct
bool is_authenticated() {
Serial.println("Enter is_authenticated");
if (httpServer.hasHeader("Cookie")) {
Serial.print("Found cookie: ");
String cookie = httpServer.header("Cookie");
Serial.println(cookie);
String token = sha1(String(www_username) + ":" +
String(www_password) + ":" +
httpServer.client().remoteIP().toString());
if (cookie.indexOf("ESPSESSIONID=" + token) != -1) {
Serial.println("Authentication Successful");
return true;
}
}
Serial.println("Authentication Failed");
return false;
}
void manageSecurity(){
if (!is_authenticated()) {
httpServer.send(401, F("application/json"), "{\"msg\": \"You must authenticate!\"}");
return;
}
}
void restEndPoint(){
// External rest end point (out of authentication)
httpServer.on("/login", HTTP_POST, handleLogin);
httpServer.on("/logout", HTTP_GET, handleLogout);
httpServer.on("/temperatureHumidity", HTTP_GET, handleTemperatureHumidity);
}
void serverRouting() {
restEndPoint();
// Manage Web Server
Serial.println(F("Go on not found!"));
httpServer.onNotFound([]() { // If the client requests any URI
Serial.println(F("On not found"));
if (!handleFileRead(httpServer.uri())) { // send it if it exists
handleNotFound();// otherwise, respond with a 404 (Not Found) error
}
});
Serial.println(F("Set cache!"));
// Serve a file with no cache so every tile It's downloaded
httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json", "no-cache, no-store, must-revalidate");
// Server all other page with long cache so browser chaching they
httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");
//here the list of headers to be recorded
const char * headerkeys[] = { "User-Agent", "Cookie" };
size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*);
//ask server to track these headers
httpServer.collectHeaders(headerkeys, headerkeyssize);
}
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_DISCONNECTED:
Serial.printf("[%u] Disconnected!\n", num);
webSocket.sendTXT(num, "{\"connected\":false}");
connectionNumber = webSocket.connectedClients();
Serial.print("Connected devices ");
Serial.println(connectionNumber);
break;
case WStype_CONNECTED:
{
IPAddress ip = webSocket.remoteIP(num);
Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
// send message to client
webSocket.sendTXT(num, "{\"connected\":true}");
connectionNumber = webSocket.connectedClients();
Serial.print("Connected devices ");
Serial.println(connectionNumber);
}
break;
case WStype_TEXT:
Serial.printf("[%u] RECEIVE TXT: %s\n", num, payload);
// send message to client
webSocket.sendTXT(num, "(ECHO MESSAGE) "+String((char *)payload));
// send data to all connected clients
// webSocket.broadcastTXT("message here");
break;
case WStype_BIN:
Serial.printf("[%u] get binary length: %u\n", num, length);
hexdump(payload, length);
// send message to client
// webSocket.sendBIN(num, payload, length);
break;
}
}
#ifdef ENABLE_WS_AUTHORIZATION
bool isCookieValid(String rawCookieHeaderValue) {
Serial.print("Cookie validation ");
Serial.println(rawCookieHeaderValue);
String clientIP = "";
if (rawCookieHeaderValue.indexOf("CLIENTIP") != -1) {
clientIP = rawCookieHeaderValue.substring(rawCookieHeaderValue.indexOf("CLIENTIP=") + 9, rawCookieHeaderValue.indexOf("|"));
Serial.print("clientIP ");
Serial.println(clientIP);
}
if (rawCookieHeaderValue.indexOf("ESPSESSIONID") != -1) {
String tokenStr = rawCookieHeaderValue.substring(rawCookieHeaderValue.indexOf("ESPSESSIONID=") + 13, rawCookieHeaderValue.indexOf("ESPSESSIONID=") + 13+40);
Serial.print("ESPSESSIONID ");
Serial.println(tokenStr);
String token = sha1(String(www_username) + ":" + String(www_password) + ":" + clientIP);
Serial.print("token ");
Serial.println(token);
return tokenStr == token;
}
return false;
}
/*
* The WebSocketServerHttpHeaderValFunc delegate passed to webSocket.onValidateHttpHeader
*/
bool validateHttpHeader(String headerName, String headerValue) {
//assume a true response for any headers not handled by this validator
bool valid = true;
if(headerName.equalsIgnoreCase("Cookie")) {
//if the header passed is the Cookie header, validate it according to the rules in 'isCookieValid' function
valid = isCookieValid(headerValue);
}
return valid;
}
#endif
Thanks
- WebSocket on Arduino, esp8266 and esp32: client
- WebSocket on Arduino, esp8266 and esp32: server and authentication
- WebSocket on Arduino, esp8266 and esp32: temperature and humidity realtime update
Complete code on GitHub