Blame | Last modification | View Log | RSS feed
#pragma once#include <Arduino.h>#include <LittleFS.h>#include <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson#include <MySQL.h> // https://github.com/cotestatnt/Arduino-MySQL#include <AsyncFsWebServer.h> // https://github.com/cotestatnt/async-esp-fs-webserver#include "mbedtls/md.h"#include "html_flash_files.h"extern MySQL sql;extern bool queryExecute(DataQuery_t&, const char*, ...);// Webserver classAsyncFsWebServer myWebServer(LittleFS, 80, "esp32rfid");int getUserLevel(const String& user, const String& hash) {DataQuery_t data;if (queryExecute(data, "SELECT password, level FROM users WHERE username = '%s';", user)) {sql.printResult(data, Serial);if (hash.equals(data.getRowValue(0, "password")))return atoi(data.getRowValue(0, "level"));}return 0;}String getSHA256(const char* payload) {String hashed = "";byte shaResult[32];mbedtls_md_context_t ctx;const size_t payloadLength = strlen(payload);mbedtls_md_init(&ctx);mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0);mbedtls_md_starts(&ctx);mbedtls_md_update(&ctx, (const unsigned char *) payload, payloadLength);mbedtls_md_finish(&ctx, shaResult);mbedtls_md_free(&ctx);for(int i= 0; i< sizeof(shaResult); i++){char str[3];sprintf(str, "%02x", (int)shaResult[i]);hashed += str;}return hashed;}void handleGetLogs(AsyncWebServerRequest *request) {String filter;if(request->hasArg("filter")) {filter = request->arg("firstname");}String SQL = "SELECT * FROM logs ";if (filter.length()) {SQL += filter;}SQL += " ORDER BY epoch DESC LIMIT 30;";Serial.println(SQL);delay(100);DataQuery_t data;if (queryExecute(data, SQL.c_str())) {sql.printResult(data, Serial);JsonDocument doc;JsonArray array = doc.to<JsonArray>();;for (Record_t &row : data.records) {JsonObject user = array.add<JsonObject>();user["id"] = row.record[0];user["epochTime"] = row.record[1];user["username"] = row.record[2];user["tagCode"] = row.record[3];user["readerID"] = row.record[4];}String json;serializeJsonPretty(doc, json);request->send(200, "application/json", json);return;}request->send(500, "text/plain", sql.getLastError());}void handleGetUsers(AsyncWebServerRequest *request) {DataQuery_t data;if (queryExecute(data, "SELECT id, username, name, email, tag_code, level FROM users")) {sql.printResult(data, Serial);JsonDocument doc;JsonArray array = doc.to<JsonArray>();for (Record_t &row : data.records) {JsonObject user = array.add<JsonObject>();user["id"] = row.record[0];user["username"] = row.record[1];user["name"] = row.record[2];user["email"] = row.record[3];user["tagCode"] = row.record[4];user["level"] = row.record[5];}String json;serializeJsonPretty(doc, json);request->send(200, "application/json", json);return;}request->send(500, "text/plain", sql.getLastError());}void handleNewUser(AsyncWebServerRequest *request) {String user = request->arg("username");String name = request->arg("name");String email = request->arg("email");String tagCode = request->arg("tagCode");String level = request->arg("level");String hashedPassword = getSHA256(request->arg("password").c_str());DataQuery_t data;if (queryExecute(data, newUpdateUser,request->arg("username").c_str(), hashedPassword.c_str(), request->arg("name").c_str(),request->arg("email").c_str(), request->arg("tagCode").c_str(), request->arg("level").c_str())){request->send(200, "text/plain", "OK");return;}request->send(500, "text/plain", sql.getLastError());}void handleRemoveUser(AsyncWebServerRequest *request) {DataQuery_t data;if (queryExecute(data, "DELETE FROM users WHERE username = '%s';", request->arg("username").c_str())) {request->send(200, "text/plain", "OK");return;}request->send(500, "text/plain", sql.getLastError());}void handleGetCode(AsyncWebServerRequest *request) {uint32_t timeout = millis();while (true) {if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {uint64_t tagCode = 0;// tagCode is swapped, but it doesn't matter We need only it's a unique numberfor(byte i = 0; i < mfrc522.uid.size; i++) {tagCode |= mfrc522.uid.uidByte[i] << (8*i);}// With 8 byte TAG code, the result integer could be too large since JavaScript// uses 64-bit floating-point numbers (IEEE 754), which have a maximum precision of 2^53 - 1String result = "{\"tagCode\": \"";result += String(tagCode);result += "\"}";Serial.printf("Tag code: 0x%llX", tagCode);request->send(200, "application/json", result);addLogRecord = true;return;}if (millis() - timeout > 5000) {request->send(500, "application/json", "{\"error\": \"timeout\"}");addLogRecord = true;return;}}}// This handler will be called from login page to check passwordvoid handleCheckHash(AsyncWebServerRequest *request) {// Even if user con login, only user with level >= 5 can edit users tableif (getUserLevel(request->arg("username"), request->arg("hash"))) {request->send(200, "text/plain", "OK");}else {request->send(401, "text/plain", "Wrong password");}}// This handler will be called from login page on login succesfullvoid handleMainPage(AsyncWebServerRequest *request) {// Check again user and password to avoid direct page loadingint level = getUserLevel(request->arg("username"), request->arg("hash"));if (level) {// Even if any user con login succesfully, only user with level >= 5 can edit users table// Username and user level is set here using cookie.String cookie = "username=" ;cookie += request->arg("username");cookie += ","; cookie += level; cookie += "; Path=/";AsyncWebServerResponse *response = request->beginResponse(200, "text/html", (const uint8_t*)index_htm, sizeof(index_htm));response->addHeader("Set-Cookie", cookie);request->send(response);}else {request->send(401, "text/plain", "Wrong password");}}//////////////////// Load application options from filesystem ////////////////////bool loadOptions() {if (LittleFS.exists(myWebServer.getConfiFileName())) {myWebServer.getOptionValue(MY_SQL_HOST, dbHost);myWebServer.getOptionValue(MY_SQL_PORT, dbPort);myWebServer.getOptionValue(MY_SQL_DB, database);myWebServer.getOptionValue(MY_SQL_USER, user);myWebServer.getOptionValue(MY_SQL_PASS, password);Serial.printf(MY_SQL_HOST ": %s\n", dbHost.c_str());Serial.printf(MY_SQL_PORT ": %d\n", dbPort);Serial.printf(MY_SQL_DB ": %s\n", database.c_str());Serial.printf(MY_SQL_USER ": %s\n", user.c_str());Serial.printf(MY_SQL_PASS ": %s\n", password.c_str());return true;}elseSerial.printf("File \"%s\" not exist\n", myWebServer.getConfiFileName());return false;}//////////////////////////////// Filesystem /////////////////////////////////////////// Configure and start webserverbool startWebServer(bool clear = false) {bool connected = false;// FILESYSTEM INITif (!LittleFS.begin()) {Serial.println("ERROR on mounting filesystem. It will be formmatted!");LittleFS.format();ESP.restart();}if (clear) {LittleFS.remove(myWebServer.getConfiFileName());}// Load configuration (if not present, default will be created when webserver will start)Serial.println("Load application otions:");if (!loadOptions()) Serial.println("Error!! Options NOT loaded!");Serial.println();// Try to connect to WiFi (will start AP if not connected after timeout)if (!myWebServer.startWiFi(20000)) {Serial.println("\nWiFi not connected! Starting AP mode...");myWebServer.startCaptivePortal("ESP32_LOGGER", "", "/setup");}elseconnected = true;// Configure /setup page and start Web ServermyWebServer.addOptionBox("MySQL setup");myWebServer.addOption(MY_SQL_HOST, dbHost);myWebServer.addOption(MY_SQL_PORT, dbPort);myWebServer.addOption(MY_SQL_DB, database);myWebServer.addOption(MY_SQL_USER, user);myWebServer.addOption(MY_SQL_PASS, password);// Add endpoints request handlersmyWebServer.on("/logs", HTTP_ANY, handleGetLogs);myWebServer.on("/users", HTTP_GET, handleGetUsers);myWebServer.on("/addUser", HTTP_POST, handleNewUser);myWebServer.on("/deleUser", HTTP_POST, handleRemoveUser);myWebServer.on("/getCode", HTTP_GET, handleGetCode);myWebServer.on("/waitCode", HTTP_GET, [](AsyncWebServerRequest *request){addLogRecord = false;request->send(200, "text/plain", "OK");});/** To avoid ugly and basic login prompt avalaible with "stardard" DIGEST_AUTH* let's use a custom login web page (from flash literal string). This web page* will send a POST request to /rfid enpoint passing username and password SHA256 hash*/myWebServer.on("/", HTTP_ANY, [](AsyncWebServerRequest *request){request->send(200, "text/html", (const uint8_t*)login_htm, sizeof(login_htm));});myWebServer.on("/login", HTTP_ANY, [](AsyncWebServerRequest *request){request->send(200, "text/html", (const uint8_t*)login_htm, sizeof(login_htm));});myWebServer.on("/rfid", HTTP_POST, handleCheckHash);/** If client calculated password SHA256 hash string match with the user DB stored* we can serve the /rfid web page (from flash literal string, same as login page)*/myWebServer.on("/rfid", HTTP_GET, handleMainPage);// To enable add/edit/delete buttons, user must be admin (level >= 5)myWebServer.on("/userLevel", HTTP_GET, [](AsyncWebServerRequest *request){DataQuery_t data;if (queryExecute(data, "SELECT password, level FROM users WHERE username = '%s';", request->arg("username"))) {String cookie = "user_level=" + String(data.getRowValue(0, "level")) + "; Path=/";AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "OK");response->addHeader("Set-Cookie", cookie);request->send(response);}});// Enable ACE FS file web editor and add FS info callback functionmyWebServer.enableFsCodeEditor();// Start the webservermyWebServer.init();Serial.print("\n\nESP Web Server started on IP Address: ");Serial.println(myWebServer.getServerIP());Serial.println("Open /setup page to configure optional parameters.\n""Open /edit page to view, edit or upload example or your custom webserver source files.");return connected;}