From 07c99dc7d8a3eedd01a965d4b032bc5a636204f8 Mon Sep 17 00:00:00 2001 From: MaPaLo76 <72209721+MaPaLo76@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:32:47 +0100 Subject: [PATCH] feat(web): HTTP-Basic-Auth fuer alle Routen; webUser/webPassword in NVS; ElegantOTA.setAuth() --- Implementation-Plan.md | 17 +++++++++++---- README.md | 10 ++++++--- include/config.h | 6 ++++++ include/settings.h | 3 +++ include/web_server.h | 6 +++++- src/settings.cpp | 20 +++++++++++++++++ src/web_server.cpp | 48 +++++++++++++++++++++++++++++++++++++---- test_sketches/README.md | 18 +++++++++++++--- 8 files changed, 113 insertions(+), 15 deletions(-) diff --git a/Implementation-Plan.md b/Implementation-Plan.md index 45a3954..5d25431 100644 --- a/Implementation-Plan.md +++ b/Implementation-Plan.md @@ -157,9 +157,9 @@ - [x] **6.3** Publish-Methode `publishSession(sessionSec, summeSession, gratisSec)`: - Topic: `lasercutter/session` - JSON-Felder (aktualisiert): - - `session_minuten` (int, ceiling: 62 s = 2 min) + - `session_minutes` (int, ceiling: 62 s = 2 min) - `session_seconds` (int, Rohwert Netto-Sekunden) - - `gratiszeit_s` (int) + - `freetime_s` (int) - `ip` (string) - Kein `total_min`, kein `session_start_time` (NTP später) - Wird von LaserTracker-Logik in main aufgerufen @@ -262,6 +262,15 @@ - Reset während aktiver Session ✅ - Neustart nach akkumulierter Zeit → Zeit korrekt aus NVS geladen ✅ +- [x] **9.6** Webinterface mit HTTP-Basic-Auth abgesichert + - `webUser` + `webPassword` in `Settings`-Struct + NVS-Keys in `config.h` + - `saveWebCredentials()` in `SettingsManager` + - `WebServerManager::requireAuth()` – prüft Auth, sendet 401 wenn nicht autorisiert + - Standard: `webUser = "admin"`, `webPassword = ""` (leer = kein Schutz, rückwärtskompatibel) + - Alle Routen (`/`, `/config`, `/reset`) + ElegantOTA (`ElegantOTA.setAuth()`) geschützt + - Passwort änderbar über `/config`-Formular (neuer Abschnitt "Web-Zugang") + - Forward-Declaration `AsyncWebServerRequest` in `web_server.h` ergänzt (Fix Compile-Fehler) + --- ## Phase 10 – Dokumentation & Abschluss @@ -306,9 +315,9 @@ Phase 1 (Setup) **`lasercutter/session`** ```json { - "session_minuten": 2, + "session_minutes": 2, "session_seconds": 125, - "gratiszeit_s": 20, + "freetime_s": 20, "ip": "192.168.1.100" } ``` diff --git a/README.md b/README.md index 97898c1..6e60468 100644 --- a/README.md +++ b/README.md @@ -218,10 +218,10 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac ### JSON-Format `lasercutter/session` ```json { - "session_minuten": 2, + "session_minutes": 2, "session_seconds": 95, "session_start_time": "2026-02-23T12:34:56Z", - "gratiszeit_s": 20, + "freetime_s": 20, "ip": "192.168.2.62" } ``` @@ -261,10 +261,12 @@ Das Webinterface ist über die IP-Adresse des ESP32 im Browser erreichbar. | Seite | URL | Funktion | |------------------|-------------|--------------------------------------------------------------| | Status | `/` | Aktuelle Laserzeit, letzter Session-Wert, Systemstatus | -| Konfiguration | `/config` | MQTT-Broker (IP, Port, User, Passwort), Gratiszeit, Polarität | +| Konfiguration | `/config` | MQTT-Broker (IP, Port, User, Passwort), Gratiszeit, Polarität, Web-Passwort | | Reset | `/reset` | Setzt akkumulierte Laserzeit zurück | | OTA Update | `/update` | Firmware-Update über Browser | +> Alle Seiten sind per **HTTP Basic Auth** geschützt, wenn in der Konfiguration ein Web-Passwort gesetzt ist. + --- ## Konfiguration & Persistenz @@ -280,6 +282,8 @@ Alle Einstellungen werden im **NVS (Non-Volatile Storage)** des ESP32 gespeicher | Gratiszeit | `20` | Sekunden, 0–120 | | Signal-Polarität | `LOW_ACTIVE` | `LOW_ACTIVE` oder `HIGH_ACTIVE` | | Akkumulierte Zeit | `0.0` | Gespeicherte Laserzeit in Minuten (NVS) | +| Web-Benutzername | `admin` | HTTP-Basic-Auth Benutzername | +| Web-Passwort | *(leer)* | Leer = kein Passwortschutz; setzen über `/config` | --- diff --git a/include/config.h b/include/config.h index bf6356b..b50dda9 100644 --- a/include/config.h +++ b/include/config.h @@ -84,11 +84,17 @@ #define NVS_KEY_GRATIS_SEC "gratis_sec" #define NVS_KEY_SIGNAL_POL "signal_pol" // 0 = LOW_ACTIVE, 1 = HIGH_ACTIVE #define NVS_KEY_TOTAL_MINUTES "total_min" +#define NVS_KEY_WEB_USER "web_user" +#define NVS_KEY_WEB_PASSWORD "web_pass" // Signal-Polarität Werte #define SIGNAL_POL_LOW_ACTIVE 0 // LOW = Laser aktiv (INPUT_PULLUP Default) #define SIGNAL_POL_HIGH_ACTIVE 1 // HIGH = Laser aktiv +// Webinterface-Authentifizierung +#define DEFAULT_WEB_USER "admin" +#define DEFAULT_WEB_PASSWORD "" // leer = kein Passwortschutz + // ----------------------------------------------------------------------------- // Serielles Debugging // ----------------------------------------------------------------------------- diff --git a/include/settings.h b/include/settings.h index 954c458..192fdcb 100644 --- a/include/settings.h +++ b/include/settings.h @@ -26,6 +26,8 @@ struct Settings { uint8_t gratisSeconds; // Gratiszeit in Sekunden (0–120) uint8_t signalPolarity; // SIGNAL_POL_LOW_ACTIVE oder _HIGH_ACTIVE float totalMinutes; // akkumulierte Laserzeit in Minuten + char webUser[32]; // Webinterface Benutzername (Standard: "admin") + char webPassword[32]; // Webinterface Passwort (leer = kein Schutz) }; // ----------------------------------------------------------------------------- @@ -54,6 +56,7 @@ public: void saveSignalPolarity(uint8_t polarity); void saveMqttConfig(const char* broker, uint16_t port, const char* user, const char* password); + void saveWebCredentials(const char* user, const char* password); // ---- Zugriff auf Werte -------------------------------------------------- const Settings& get() const { return _s; } diff --git a/include/web_server.h b/include/web_server.h index c40a8e6..847d2b4 100644 --- a/include/web_server.h +++ b/include/web_server.h @@ -21,9 +21,10 @@ #include -// Forward-Declaration: ESPAsyncWebServer.h nur in web_server.cpp einbinden +// Forward-Declarations: ESPAsyncWebServer.h nur in web_server.cpp einbinden // (verhindert HTTP_GET/POST-Enum-Konflikt mit WiFiManager in main.cpp) class AsyncWebServer; +class AsyncWebServerRequest; class WebServerManager { public: @@ -37,6 +38,9 @@ private: void registerRoutes(); + // HTTP-Basic-Auth pruefen; gibt false zurueck und sendet 401 wenn nicht autorisiert + bool requireAuth(AsyncWebServerRequest* request); + // HTML-Seiten als Strings aufbauen String buildStatusPage(); String buildConfigPage(); diff --git a/src/settings.cpp b/src/settings.cpp index a0f44c4..6e53575 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -21,6 +21,8 @@ void SettingsManager::applyDefaults() { _s.gratisSeconds = DEFAULT_GRATIS_SECONDS; _s.signalPolarity = SIGNAL_POL_LOW_ACTIVE; _s.totalMinutes = 0.0f; + strlcpy(_s.webUser, DEFAULT_WEB_USER, sizeof(_s.webUser)); + strlcpy(_s.webPassword, DEFAULT_WEB_PASSWORD, sizeof(_s.webPassword)); } // --------------------------------------------------------------------------- @@ -64,6 +66,12 @@ void SettingsManager::load() { _s.gratisSeconds = _prefs.getUChar(NVS_KEY_GRATIS_SEC, DEFAULT_GRATIS_SECONDS); _s.signalPolarity = _prefs.getUChar(NVS_KEY_SIGNAL_POL, SIGNAL_POL_LOW_ACTIVE); _s.totalMinutes = _prefs.getFloat(NVS_KEY_TOTAL_MINUTES, 0.0f); + strlcpy(_s.webUser, + _prefs.getString(NVS_KEY_WEB_USER, DEFAULT_WEB_USER).c_str(), + sizeof(_s.webUser)); + strlcpy(_s.webPassword, + _prefs.getString(NVS_KEY_WEB_PASSWORD, DEFAULT_WEB_PASSWORD).c_str(), + sizeof(_s.webPassword)); validateAndClamp(); } @@ -77,6 +85,8 @@ void SettingsManager::save() { _prefs.putUChar (NVS_KEY_GRATIS_SEC, _s.gratisSeconds); _prefs.putUChar (NVS_KEY_SIGNAL_POL, _s.signalPolarity); _prefs.putFloat (NVS_KEY_TOTAL_MINUTES, _s.totalMinutes); + _prefs.putString(NVS_KEY_WEB_USER, _s.webUser); + _prefs.putString(NVS_KEY_WEB_PASSWORD, _s.webPassword); } // --------------------------------------------------------------------------- @@ -120,6 +130,14 @@ void SettingsManager::saveMqttConfig(const char* broker, uint16_t port, _prefs.putString(NVS_KEY_MQTT_PASSWORD, password); } +// --------------------------------------------------------------------------- +void SettingsManager::saveWebCredentials(const char* user, const char* password) { + strlcpy(_s.webUser, user, sizeof(_s.webUser)); + strlcpy(_s.webPassword, password, sizeof(_s.webPassword)); + _prefs.putString(NVS_KEY_WEB_USER, user); + _prefs.putString(NVS_KEY_WEB_PASSWORD, password); +} + // --------------------------------------------------------------------------- void SettingsManager::printToSerial() const { LOG_I("NVS", "=== Geladene Einstellungen ==="); @@ -131,5 +149,7 @@ void SettingsManager::printToSerial() const { LOG_I("NVS", " Signalpolarit : %s", _s.signalPolarity == SIGNAL_POL_LOW_ACTIVE ? "LOW_ACTIVE" : "HIGH_ACTIVE"); LOG_I("NVS", " Laserzeit ges.: %.2f min", _s.totalMinutes); + LOG_I("NVS", " Web User : %s", _s.webUser); + LOG_I("NVS", " Web Passwort : %s", _s.webPassword[0] ? "***" : "(kein Schutz)"); LOG_I("NVS", "=============================="); } diff --git a/src/web_server.cpp b/src/web_server.cpp index ac791bd..2529ea4 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -32,11 +32,30 @@ WebServerManager::WebServerManager() : _server(new AsyncWebServer(80)) {} // ----------------------------------------------------------------------------- void WebServerManager::begin() { registerRoutes(); + // ElegantOTA-Auth: nur wenn Passwort gesetzt + const Settings& s = settings.get(); + if (s.webPassword[0] != '\0') { + ElegantOTA.setAuth(s.webUser, s.webPassword); + } ElegantOTA.begin(_server); _server->begin(); Serial.println(F("[WebServer] gestartet auf Port 80")); Serial.print(F("[WebServer] URL: http://")); Serial.println(WiFi.localIP()); + LOG_I("WEB", "Auth: %s", s.webPassword[0] ? "aktiv" : "deaktiviert (kein Passwort)"); +} + +// ----------------------------------------------------------------------------- +// requireAuth() – HTTP Basic Auth pruefen (nur wenn Passwort konfiguriert) +// ----------------------------------------------------------------------------- +bool WebServerManager::requireAuth(AsyncWebServerRequest* request) { + const Settings& s = settings.get(); + if (s.webPassword[0] == '\0') return true; // kein Passwort = kein Schutz + if (!request->authenticate(s.webUser, s.webPassword)) { + request->requestAuthentication("LaserCutter Display"); + return false; + } + return true; } // ----------------------------------------------------------------------------- @@ -46,16 +65,19 @@ void WebServerManager::registerRoutes() { // --- GET / – Statusseite ------------------------------------------------- _server->on("/", HTTP_GET, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; request->send(200, "text/html; charset=utf-8", buildStatusPage()); }); // --- GET /config – Konfigurationsformular -------------------------------- _server->on("/config", HTTP_GET, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; request->send(200, "text/html; charset=utf-8", buildConfigPage()); }); // --- POST /config – Konfiguration speichern ------------------------------ - _server->on("/config", HTTP_POST, [](AsyncWebServerRequest* request) { + _server->on("/config", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; const Settings& s = settings.get(); // MQTT @@ -87,6 +109,13 @@ void WebServerManager::registerRoutes() { settings.saveSignalPolarity(pol); } + // Webinterface-Passwort + if (request->hasParam("web_user", true) && request->hasParam("web_password", true)) { + const char* wUser = request->getParam("web_user", true)->value().c_str(); + const char* wPass = request->getParam("web_password", true)->value().c_str(); + settings.saveWebCredentials(wUser, wPass); + } + Serial.println(F("[WebServer] Konfiguration gespeichert – Neustart empfohlen")); // Weiterleitung mit Hinweis @@ -102,7 +131,8 @@ void WebServerManager::registerRoutes() { }); // --- POST /reset – Session zurücksetzen --------------------------------- - _server->on("/reset", HTTP_POST, [](AsyncWebServerRequest* request) { + _server->on("/reset", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (!requireAuth(request)) return; laserTracker.resetSession(); Serial.println(F("[WebServer] Session zurueckgesetzt")); request->send(200, "text/html; charset=utf-8", @@ -157,10 +187,11 @@ String WebServerManager::buildStatusPage() { "" "

LaserCutter Display

" "" - "" - "" + "" + "" "" "" + "" "" "" "" @@ -179,6 +210,7 @@ String WebServerManager::buildStatusPage() { ); html.replace("%ALLSESSIONS%", String(laserTracker.getAllSessionsSumMinutes())); + html.replace("%CURRENTSESSION%", String(laserTracker.getRunningSessionSeconds())); html.replace("%TOTAL%", String(totalBuf)); html.replace("%LASTSESSION%", String(laserTracker.getLastSessionSeconds())); html.replace("%LASER%", laserStatus); @@ -234,6 +266,13 @@ String WebServerManager::buildConfigPage() { "" + "
" + "

Web-Zugang

" + "" + "" + "" + "" "
" "" "
" @@ -248,6 +287,7 @@ String WebServerManager::buildConfigPage() { html.replace("%GRATIS%", String(s.gratisSeconds)); html.replace("%POL_LOW%", s.signalPolarity == 0 ? "checked" : ""); html.replace("%POL_HIGH%", s.signalPolarity == 1 ? "checked" : ""); + html.replace("%WEB_USER%", String(s.webUser)); return html; } diff --git a/test_sketches/README.md b/test_sketches/README.md index 13893c6..4cc94b4 100644 --- a/test_sketches/README.md +++ b/test_sketches/README.md @@ -297,7 +297,7 @@ pio device monitor -e test-mqtt 3. Serial: `SessionEnd -> publishSession` 4. MQTT Explorer: `lasercutter/session` erscheint mit JSON: ```json - {"session_minuten": 1, "session_seconds": 5, "gratiszeit_s": 20, "ip": "192.168.x.x"} + {"session_minutes": 1, "session_seconds": 5, "freetime_s": 20, "ip": "192.168.x.x"} ``` **C) Gratis-Session (< 20 s):** @@ -342,7 +342,7 @@ pio device monitor -e test-mqtt ... [LaserTracker] SessionEnd: gesamt=35s netto=15s sessionNetSec=15 total=0.58min [I][TEST-MQTT] SessionEnd -> publishSession (lastSession=15s) -[MQTT] publishSession: {"session_minuten":1,"session_seconds":15,"gratiszeit_s":20,"ip":"192.168.x.x"} -> OK +[MQTT] publishSession: {"session_minutes":1,"session_seconds":15,"freetime_s":20,"ip":"192.168.x.x"} -> OK ``` ### Checkliste @@ -352,7 +352,7 @@ pio device monitor -e test-mqtt | Verbindung zu mqtt.majufilo.eu | `[MQTT] Verbunden!` im Serial | [x] | | Heartbeat beim Connect | `lasercutter/status` retained im MQTT Explorer | [x] | | Heartbeat alle 60 s | Topic aktualisiert sich periodisch | [x] | -| Session-Publish nach Session-Ende | `lasercutter/session` mit `session_minuten`, `session_seconds` | [x] | +| Session-Publish nach Session-Ende | `lasercutter/session` mit `session_minutes`, `session_seconds` | [x] | | Gratis-Session publiziert NICHT | kein Topic-Update bei lastSession=0 | [ ] | | Reset via MQTT (`lasercutter/reset` = `{"reset":true}`) | `total_min` springt auf 0 | [x] | | LWT `{"online":false}` nach Verbindungsabbruch | Nach 60 s im MQTT Explorer sichtbar | [x] | @@ -409,6 +409,14 @@ IP-Adresse aus dem Serial Monitor ablesen (erscheint nach WiFi-Connect): 2. ElegantOTA-Seite erscheint 3. (Optional) Test-Firmware hochladen und Neustart beobachten +**E) HTTP-Basic-Auth:** +1. In `/config` Abschnitt "Web-Zugang": Passwort setzen (z.B. `geheim`) und speichern +2. ESP32 neu starten +3. Browser: `http:///` → Browser-Login-Dialog erscheint +4. Benutzername `admin`, Passwort `geheim` eingeben → Zugang gewährt +5. Falsches Passwort eingeben → Browser zeigt 401 / erneuten Dialog +6. In `/config` Passwort wieder leeren → nach Neustart kein Schutz mehr + ### Checkliste | Pruefpunkt | Erwartung | Ergebnis | @@ -421,6 +429,10 @@ IP-Adresse aus dem Serial Monitor ablesen (erscheint nach WiFi-Connect): | Polaritaet aendern + speichern | NVS-Wert nach Neustart korrekt | [ ] | | Session-Reset per Browser | Session = 0 min, Gesamtzeit (NVS) unveraendert | [ ] | | `/update` erreichbar | ElegantOTA-Seite erscheint | [ ] | +| Passwort setzen + Neustart | Browser-Login-Dialog erscheint | [ ] | +| Korrektes Passwort | Zugang gewaehrt | [ ] | +| Falsches Passwort | 401 / erneuter Dialog | [ ] | +| Passwort leeren + Neustart | Kein Login-Dialog | [ ] | ---
Summe Session%ALLSESSIONS% min
Maschinenlaufzeit (gesamt)%TOTAL% min
Summe Laserzeit%ALLSESSIONS% min
Aktuelle Laserzeit%CURRENTSESSION% s
Letzte Laserzeit%LASTSESSION% s
Laser%LASER%
Maschinenlaufzeit (gesamt)%TOTAL% min
IP-Adresse%IP%
MQTT%MQTT%
Broker%BROKER%:%PORT%