Web Server con esp8266 e esp32: gestione sicurezza ed autenticazione – 4

Spread the love

WebServer Esp8266 ESP32 security authentication realm and token
WebServer Esp8266 ESP32 security authentication realm and token

Uno dei tipi di autenticazione più semplici è la Basic Auth, ed anche l’implementazione è abbastanza semplice.

Per esp32 è necessario modificare queste righe

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
[...]
ESP8266WebServer server(80);

// Comment this line for esp8266
// httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");

in

#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
[...]
WebServer server(80);

Ma fai attenzione ora sul WiFiClient di ESP32 c’è un grosso bug, puoi controllare qui , quindi se provi ad avviare questo server avrai qualche problema in presenza di molte richieste simultanee. Perciò ho scritto un porting di questo sketch usando la libreria ESPAsyncWebServer che usa l’AsyncTCP  dove non si presenta questo problema. Ma ora spiego la soluzione attuale per l’esp8266 e alla fine dell’articolo la soluzione equivalente per ESP32.

Risolto sulla versione 1.0.5 dell’esp32 core.

Autenticazione base

Prendi l’implementazione precedente del server web e aggiungi l’implementazione dell’autenticazione

/*
 *  WeMos D1 mini (esp8266)
 *  Simple web server that read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also now authentication in basic
 *  WITH BASIC AUTHENTICATION
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/it/category/guide/come-creare-un-server-web-esp8266-esp32/
 *
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

// allows you to set the realm of authentication Default:"Login Required"
const char* www_realm = "Custom Auth Realm";
// the Content of the HTML response in case of Unautherized Access Default:empty
String authFailResponse = "Authentication Failed";

const char* www_username = "admin";
const char* www_password = "esp8266";

ESP8266WebServer httpServer(80);


bool loadFromSPIFFS(String path, String dataType) {
  Serial.print("Requested page -> ");
  Serial.println(path);
  if (SPIFFS.exists(path)){
	  File dataFile = SPIFFS.open(path, "r");
	  if (!dataFile) {
		  handleNotFound();
		  return false;
	  }

	  if (httpServer.streamFile(dataFile, dataType) != dataFile.size()) {
	    Serial.println("Sent less data than expected!");
	  }else{
		  Serial.println("Page served!");
	  }

	  dataFile.close();
  }else{
	  handleNotFound();
	  return false;
  }
  return true;
}

void serverRouting();

void setup(void) {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

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

void loop(void) {
	httpServer.handleClient();
}

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 (!httpServer.authenticate(www_username, www_password)) {
	  httpServer.requestAuthentication();
  }

  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 serverRouting() {
	  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
      // Comment this line for esp8266
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");
}

Ora all’accesso avrai un box di login:

Basic Auth esp8266 WebServer
Basic Auth esp8266 WebServer

AsyncWebServer

Ora lo sketch precedente ora una libreria molto popolare: AsyncWebServer, esiste una versione per esp8266 ed esp32, è necessario modificare solo la libreria di base.

Per l’ESP8266 va usata la libreria ESPAsyncTCP e per utilizzare questa libreria potrebbe essere necessario disporre delle ultime versioni git di  ESP8266  Arduino Core

Per l’ESP32 sarà richiesta per funzionare l’AsyncTCP. Per utilizzare questa libreria potrebbe essere necessario disporre delle ultime versioni git del core Arduino dell’ESP32.

Questo è un server completamente asincrono e come tale non viene eseguito sul thread del ciclo.

Ecco l’esempio precedente per esp32 con questa libreria.

/*
 *  ESP32
 *  Simple web server that read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also
 *  WITH BASIC AUTHENTICATION
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/it/category/guide/come-creare-un-server-web-esp8266-esp32/
 *
 */

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

// allows you to set the realm of authentication Default:"Login Required"
const char* www_realm = "Custom Auth Realm";
// the Content of the HTML response in case of Unautherized Access Default:empty
String authFailResponse = "Authentication Failed";

const char* www_username = "admin";
const char* www_password = "esp8266";

AsyncWebServer httpServer(80);

void serverRouting();

void setup(void) {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

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

void loop(void) {

}

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(AsyncWebServerRequest *request, String path){
  Serial.print(F("handleFileRead: "));
  Serial.println(path);

  if (!request->authenticate(www_username, www_password)) {
	  Serial.print(F("NOT AUTHENTICATE!"));
	  request->requestAuthentication();
  }

  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
	bool gzipped = false;

    if(SPIFFS.exists(pathWithGz)) {                        	// If there's a compressed version available
      path += F(".gz");                                     // Use the compressed version
      gzipped = true;
    }
    AsyncWebServerResponse *response = request->beginResponse(SPIFFS, path, contentType);
    if (gzipped){
    	response->addHeader("Content-Encoding", "gzip");
    }
    Serial.print("Real file path: ");
    Serial.println(path);

    request->send(response);

    return true;
  }
  Serial.println(String(F("\tFile Not Found: ")) + path);
  return false;                                          	// If the file doesn't exist, return false
}

void handleNotFound(AsyncWebServerRequest *request) {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += request->url();
  message += "\nMethod: ";
  message += (request->method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += request->args();
  message += "\n";

  for (uint8_t i = 0; i < request->args(); i++) {
    message += " " + request->argName(i) + ": " + request->arg(i) + "\n";
  }

  request->send(404, "text/plain", message);
}

void serverRouting() {
	  httpServer.onNotFound([](AsyncWebServerRequest *request) {                              // If the client requests any URI
		  Serial.println(F("On not found"));
	    if (!handleFileRead(request, request->url())){                  // send it if it exists
	    	handleNotFound(request); // 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
      // Comment this line for esp8266
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");
}

Semplice autenticazione tramite token

Ok, questa era una soluzione, ma il modulo di accesso per l’autenticazione di base è davvero pessimo, quindi la mia idea è di gestire una semplice autenticazione a token e utilizzare un cookie per memorizzare il token.

Il token verrà generato con una semplice funzione hash crittografica (SHA1), e sarà utile per non far decodificare il token in maniera agevole. Per ESP32 Hash.h non esiste, quindi useremo la funzione principale per ricreare la sha1 funzione.

Implementazione SHA1 per ESP32

Ora per l’ESP32 sarà necessario rimuovere "hash.h" e importare "mbedtls/md.h".
Qui il codice esp8266.

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include "Hash.h"
[...]
ESP8266WebServer httpServer(80);

Qui il codice risultante.

#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
#include "mbedtls/md.h"
[...]
WebServer httpServer(80);
[...]
String sha1(String payloadStr){
	const char *payload = payloadStr.c_str();

	int size = 20;

	byte shaResult[size];

	mbedtls_md_context_t ctx;
	mbedtls_md_type_t md_type = MBEDTLS_MD_SHA1;

	const size_t payloadLength = strlen(payload);

	mbedtls_md_init(&ctx);
	mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
	mbedtls_md_starts(&ctx);
	mbedtls_md_update(&ctx, (const unsigned char *) payload, payloadLength);
	mbedtls_md_finish(&ctx, shaResult);
	mbedtls_md_free(&ctx);

    String hashStr = "";

    for(uint16_t i = 0; i < size; i++) {
        String hex = String(shaResult[i], HEX);
        if(hex.length() < 2) {
            hex = "0" + hex;
        }
        hashStr += hex;
    }

    return hashStr;
}

Aggiungo il codice completo ed andrò spiegare lo schizzo completo ed il modulo di accesso.

/*
 *  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.
 *
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/it/category/guide/come-creare-un-server-web-esp8266-esp32/
 *
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include "Hash.h"

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

const char* www_username = "admin";
const char* www_password = "esp8266";

ESP8266WebServer httpServer(80);

void serverRouting();

void setup(void) {
	Serial.begin(115200);
	WiFi.mode(WIFI_STA);
	WiFi.begin(ssid, password);
	Serial.println("");

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

void loop(void) {
	httpServer.handleClient();
}

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 = "/login.html";
	} 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.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.send(301);
	return;
}
//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;
}

void serverRouting() {
	// External rest end point (out of authentication)
	httpServer.on("/login", HTTP_POST, handleLogin);
	httpServer.on("/logout", HTTP_GET, handleLogout);

	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
      // Comment this line for esp8266
	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);
}

E la login page

<!DOCTYPE html>
<html lang="en">
<head>
    <style>
        @import url(https://fonts.googleapis.com/css?family=Roboto:300);

        .login-page {
            width: 360px;
            padding: 8% 0 0;
            margin: auto;
        }

        .form {
            position: relative;
            z-index: 1;
            background: #FFFFFF;
            max-width: 360px;
            margin: 0 auto 100px;
            padding: 45px;
            text-align: center;
            box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
        }

        .form input {
            font-family: "Roboto", sans-serif;
            outline: 0;
            background: #f2f2f2;
            width: 100%;
            border: 0;
            margin: 0 0 15px;
            padding: 15px;
            box-sizing: border-box;
            font-size: 14px;
        }

        .form button {
            font-family: "Roboto", sans-serif;
            text-transform: uppercase;
            outline: 0;
            background: #4CAF50;
            width: 100%;
            border: 0;
            padding: 15px;
            color: #FFFFFF;
            font-size: 14px;
            -webkit-transition: all 0.3 ease;
            transition: all 0.3 ease;
            cursor: pointer;
        }

        .form button:hover, .form button:active, .form button:focus {
            background: #43A047;
        }

        .form .message {
            margin: 15px 0 0;
            color: #b3b3b3;
            font-size: 12px;
        }

        .form .message a {
            color: #4CAF50;
            text-decoration: none;
        }

        .form .register-form {
            display: none;
        }

        .container {
            position: relative;
            z-index: 1;
            max-width: 300px;
            margin: 0 auto;
        }

        .container:before, .container:after {
            content: "";
            display: block;
            clear: both;
        }

        .container .info {
            margin: 50px auto;
            text-align: center;
        }

        .container .info h1 {
            margin: 0 0 15px;
            padding: 0;
            font-size: 36px;
            font-weight: 300;
            color: #1a1a1a;
        }

        .container .info span {
            color: #4d4d4d;
            font-size: 12px;
        }

        .container .info span a {
            color: #000000;
            text-decoration: none;
        }

        .container .info span .fa {
            color: #EF3B3A;
        }

        body {
            background: #76b852; /* fallback for old browsers */
            background: -webkit-linear-gradient(right, #76b852, #8DC26F);
            background: -moz-linear-gradient(right, #76b852, #8DC26F);
            background: -o-linear-gradient(right, #76b852, #8DC26F);
            background: linear-gradient(to left, #76b852, #8DC26F);
            font-family: "Roboto", sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
        }

        .logo-image {
            background-image: url(./logo256.jpg);
            background-position: center;
            background-size: contain;
            background-repeat: no-repeat;
            height: 120px;
        }
    </style>
    <script>
    </script>
</head>
    <body>
    <div class="login-page">
        <div class="form">
            <div class="logo-image"></div>

            <form action="/login" method="POST" class="login-form" style="padding-top: 20px;">
                <input id="username" name="username" type="text" placeholder="username"/>
                <input id="password" name="password" type="password" placeholder="password"/>
                <button type='submit' name='SUBMIT' value='Submit' >Login</button>
                <div style="margin-top: 10px; color: red;font-size: small;" id="message"></div>
            </form>
        </div>
    </div>
    </body>
<script>
    function getURLParameter(sParam)
    {
        var sPageURL = window.location.search.substring(1);
        var sURLVariables = sPageURL.split('&');
        for (var i = 0; i < sURLVariables.length; i++)
        {
            var sParameterName = sURLVariables[i].split('=');
            if (sParameterName[0] == sParam)
            {
                return sParameterName[1];
            }
        }
    }
    if (getURLParameter("msg")) {
        document.getElementById("message").innerText = decodeURI(getURLParameter("msg"));
    }
</script>
</html>

Ecco il risultato:

WebServer esp8266 login page
WebServer esp8266 login page

L’autenticazione, come gli altri linguaggi di programmazione, deve funzionare come un filtro su tutte le richieste, in questo caso quel codice è:

	if (!is_authenticated()) {
		Serial.println(F("Go on not login!"));
		path = "/login.html";
	} else {
		if (path.endsWith("/")) path += F("index.html"); // If a folder is requested, send the index file
	}

controlla se è autenticato e se non lo è forza la pagina da servire come login.html.

La pagina di login semplicemente manda in POST un nome utente e una password dalla form di submit all’end point /login, con httpServer.hasArg("username") puoi controllare se il parametro è presente, con httpServer.arg("username") ottieni il valore del parametro .

	// External rest end point (out of authentication)
	httpServer.on("/login", HTTP_POST, handleLogin);
	httpServer.on("/logout", HTTP_GET, handleLogout);

la login, come il logout è fuori dal filtro di autenticazione e viene elaborato senza cookie, ed all’interno del metodo controlla se il nome utente e la password sono corretti

	if (httpServer.hasArg("username") && httpServer.hasArg("password")) {
		Serial.print("Found parameter: ");

		if (httpServer.arg("username") == String(www_username) && httpServer.arg("password") == String(www_password)) {

se sono corretti, genera un Set-Cookiee che inserisci il token per il dominio, quindi invia un codice di stato http 301 ad una Location che innesca una redirect.

			httpServer.sendHeader("Location", "/");
			httpServer.sendHeader("Cache-Control", "no-cache");

			String token = sha1(String(www_username) + ":" + String(www_password) + ":" + WiFi.localIP().toString());
			httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=" + token);

			httpServer.send(301);
			Serial.println("Log in Successful");
			return;

il valore del cookie è un token generato con l’hash sha1 con l’utente, la password e l’indirizzo ip del client, per una maggiore sicurezza è possibile aggiungere un indirizzo mac o un giorno.

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

Il risultato di questa operazione è che ora hai un cookie per il tuo dominio con un token.

esp8266 Login cookies management
esp8266 Login cookies management

Come puoi vedere se inserisci una password sbagliata verrai reindirizzato a una pagina di login con un messaggio da mostrare su di essa.

Il controllo dell’autenticazione, a questo punto, è abbastanza semplice

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

recupera il token dai cookie e controlla se va bene con le credenziali locali e l’IP del client.

Il cookie che utilizzo qui è un cookie di sessione, quindi è persistente solo per la sessione corrente del browser. Ogni volta che fai una richiesta da questo browser e dal dominio WebServer, il cookie, viene automaticamente allegato all’header e può essere recuperato dal server.

Come la login che aggiunge, logout rimuove/modifica semplicemente il cookie originale e annulla la sessione di autenticazione.

esp8266 Logout cookies management
esp8266 Logout cookies management

Quindi ora hai un login/logout con handshake personalizzato completo, con una gestione della sicurezza abbastanza buona. Ma per una soluzione completa è necessario aggiungere https con certificato.

Sconsiglio di eseguire questo passaggio per più di un motivo:

  • aggiungi solo un certificato auto firmato nella tua LAN, quindi più di un browser moderno blocca il tuo sito e la richiesta di autorizzazione è piuttosto noiosa.
  • probabilmente usi questo dispositivo solo nella rete locale (LAN), ed è improbabile che altre persone inizino a fiutare il tuo traffico.
  • se vuoi usare questo sito in Internet puoi usare un apache di confine (non gli indiani d’America) o NGINX, che funziona come un proxy e puoi allegare il certificato a questo PC e gestirlo per fare il cambio di porta (da 433 a 80) e relativo riscrittura url (se vuoi).

AsyncWebServer

Qui l’implementazione con AsyncWebServer.

/*
 *  ESP32
 *  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.
 *
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/it/category/guide/come-creare-un-server-web-esp8266-esp32/
 *
 */
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include "mbedtls/md.h"

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

const char* www_username = "admin";
const char* www_password = "esp8266";

AsyncWebServer httpServer(80);

void serverRouting();

String sha1(String payloadStr){
	const char *payload = payloadStr.c_str();

	int size = 20;

	byte shaResult[size];

	mbedtls_md_context_t ctx;
	mbedtls_md_type_t md_type = MBEDTLS_MD_SHA1;

	const size_t payloadLength = strlen(payload);

	mbedtls_md_init(&ctx);
	mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
	mbedtls_md_starts(&ctx);
	mbedtls_md_update(&ctx, (const unsigned char *) payload, payloadLength);
	mbedtls_md_finish(&ctx, shaResult);
	mbedtls_md_free(&ctx);

    String hashStr = "";

    for(uint16_t i = 0; i < size; i++) {
        String hex = String(shaResult[i], HEX);
        if(hex.length() < 2) {
            hex = "0" + hex;
        }
        hashStr += hex;
    }

    return hashStr;
}

void setup(void) {
	Serial.begin(115200);
	WiFi.mode(WIFI_STA);
	WiFi.begin(ssid, password);
	Serial.println("");

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

void loop(void) {

}

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(AsyncWebServerRequest *request, String path) {
	Serial.print(F("handleFileRead: "));
	Serial.println(path);

	if (!is_authenticated(request)) {
		Serial.println(F("Go on not login!"));
		path = "/login.html";
	} 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
		bool gzipped = false;

	    if(SPIFFS.exists(pathWithGz)) {                        	// If there's a compressed version available
	      path += F(".gz");                                     // Use the compressed version
	      gzipped = true;
	    }
	    AsyncWebServerResponse *response = request->beginResponse(SPIFFS, path, contentType);
	    if (gzipped){
	    	response->addHeader("Content-Encoding", "gzip");
	    }
	    Serial.print("Real file path: ");
	    Serial.println(path);

	    request->send(response);

	    return true;
	  }
	Serial.println(String(F("\tFile Not Found: ")) + path);
	return false;                     // If the file doesn't exist, return false
}

void handleNotFound(AsyncWebServerRequest *request) {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += request->url();
  message += "\nMethod: ";
  message += (request->method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += request->args();
  message += "\n";

  for (uint8_t i = 0; i < request->args(); i++) {
    message += " " + request->argName(i) + ": " + request->arg(i) + "\n";
  }

  request->send(404, "text/plain", message);
}

void handleLogin(AsyncWebServerRequest *request) {
	Serial.println("Handle login");
	String msg;
	if (request->hasHeader("Cookie")) {
		// Print cookies
		Serial.print("Found cookie: ");
		String cookie = request->header("Cookie");
		Serial.println(cookie);
	}

	if (request->hasArg("username") && request->hasArg("password")) {
		Serial.print("Found parameter: ");

		if (request->arg("username") == String(www_username) && request->arg("password") == String(www_password)) {
			AsyncWebServerResponse *response = request->beginResponse(301); //Sends 301 redirect

			response->addHeader("Location", "/");
			response->addHeader("Cache-Control", "no-cache");

			String token = sha1(String(www_username) + ":" + String(www_password) + ":" + request->client()->remoteIP().toString());
			Serial.print("Token: ");
			Serial.println(token);
			response->addHeader("Set-Cookie", "ESPSESSIONID=" + token);

			request->send(response);
			Serial.println("Log in Successful");
			return;
		}
		msg = "Wrong username/password! try again.";
		Serial.println("Log in Failed");
		AsyncWebServerResponse *response = request->beginResponse(301); //Sends 301 redirect

		response->addHeader("Location", "/login.html?msg=" + msg);
		response->addHeader("Cache-Control", "no-cache");
		request->send(response);
		return;
	}
}

/**
 * Manage logout (simply remove correct token and redirect to login form)
 */
void handleLogout(AsyncWebServerRequest *request) {
	Serial.println("Disconnection");
	AsyncWebServerResponse *response = request->beginResponse(301); //Sends 301 redirect

	response->addHeader("Location", "/login.html?msg=User disconnected");
	response->addHeader("Cache-Control", "no-cache");
	response->addHeader("Set-Cookie", "ESPSESSIONID=0");
	request->send(response);
	return;
}
//Check if header is present and correct
bool is_authenticated(AsyncWebServerRequest *request) {
	Serial.println("Enter is_authenticated");
	if (request->hasHeader("Cookie")) {
		Serial.print("Found cookie: ");
		String cookie = request->header("Cookie");
		Serial.println(cookie);

		String token = sha1(String(www_username) + ":" +
				String(www_password) + ":" +
				request->client()->remoteIP().toString());
//	token = sha1(token);

		if (cookie.indexOf("ESPSESSIONID=" + token) != -1) {
			Serial.println("Authentication Successful");
			return true;
		}
	}
	Serial.println("Authentication Failed");
	return false;
}

void serverRouting() {
	// External rest end point (out of authentication)
	httpServer.on("/login", HTTP_POST, handleLogin);
	httpServer.on("/logout", HTTP_GET, handleLogout);

	Serial.println(F("Go on not found!"));
	httpServer.onNotFound([](AsyncWebServerRequest *request) {               // If the client requests any URI
				Serial.println(F("On not found"));
			    if (!handleFileRead(request, request->url())){                  // send it if it exists
			    	handleNotFound(request); // 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
      // Comment this line for esp8266
	httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");
}

Come puoi vedere l’implementazione è simile e non è difficile passare dall’una all’altra.

Grazie

Ma come puoi vedere non c’è una parte fondamentale: non interagiamo con i dati del microcontrollore, quindi il prossimo passo è gestire questo scambio di dati.

  1. Web Server su esp8266 e esp32: servire pagine e gestire LEDs
  2. Web Server su esp8266 e esp32: servire pagine compresse come byte array e SPIFFS
  3. Web Server su esp8266 e esp32: web server generico multiuso
  4. Web Server su esp8266 e esp32: gestione sicurezza ed autenticazione
  5. Web Server su esp8266 e esp32: aggiunta di un back-end REST protetto
  6. Web Server su esp8266 e esp32: Interfaccia Web sicura per temperatura ed umidità di un DHT

Codice ed esempio su repository GitHub


Spread the love

2 Risposte

  1. Stefano Zack ha detto:

    Buongiorno Sig. Mischianti. Sono un Amatore di codice a cui mancano parecchie basi, ma seguo con passione i suoi tutorial. Mi trovo ora bloccato da mesi e chiedo il suo aiuto.
    Cerco di integrare l’autenticazione tramite TOKEN usando AsyncTCP del suo tutorial con il CameraWebServer di esempio.
    Non riesco a capire come poter indirizzare, dopo aver ottenuto le credenziali di Login verso la pagina http. Principalmente riesco a caricare la pagina html, ma non gli stili. Come procederebbe lei? Grazie, apprezzao il suo lavoro, distinti saluti, Stefano.

    • Renzo Mischianti ha detto:

      Buondì,
      il reindirizzamento è garantito dall’HTTP_STATUS a 301, nel dettaglio questo codice

                  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.send(301);
      

      formalmente sarebbe meglio la 307 ma in un sito non pubblico poco conta.

      Per gli stili immagino sia un problema di url impostata nella pagina, andrebbe verificata quella.

      Ciao RM

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *