From d90e55e8949f7f3dbfe6c9bbbbc0ce4d6fa7bdcc Mon Sep 17 00:00:00 2001 From: thepetric Date: Sun, 21 Jun 2026 14:40:15 +0200 Subject: [PATCH 1/4] Hostname fix & login page --- BTPrivacyApp.cpp | 108 ++++++++++++++++++++++++++++++++++++++++++++++- README.md | 11 +++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/BTPrivacyApp.cpp b/BTPrivacyApp.cpp index 95e3f4b..0ae509d 100644 --- a/BTPrivacyApp.cpp +++ b/BTPrivacyApp.cpp @@ -30,6 +30,7 @@ #include #include #include +#include "esp_netif.h" // ---------------- USER SETTINGS ---------------- @@ -82,6 +83,9 @@ static constexpr char BTPRIVACY_WIFI_HOSTNAME[] = "btprivacy"; static constexpr bool BTPRIVACY_MDNS_ENABLED = true; static constexpr uint16_t BTPRIVACY_WEB_PORT = 80; static constexpr uint32_t BTPRIVACY_WIFI_TIMEOUT_MS = 12000; +static constexpr char BTPRIVACY_WEB_USERNAME[] = "admin"; +static constexpr char BTPRIVACY_WEB_PASSWORD[] = "btprivacy"; +static constexpr char BTPRIVACY_AUTH_COOKIE[] = "BTPrivacyAuth=1"; static constexpr uint8_t BLE_LEGACY_ADV_MAX_LEN = 31; @@ -184,6 +188,7 @@ static bool g_wifiStarted = false; static bool g_webStarted = false; static uint32_t g_wifiConnectMs = 0; static SlotState g_slots[BTPRIVACY_MAX_ADV_SETS]; +static const char *WEB_HEADER_KEYS[] = {"Cookie"}; static void logBegin() { @@ -652,6 +657,44 @@ static String boolJson(bool v) { return v ? "true" : "false"; } +static const char LOGIN_HTML[] PROGMEM = R"rawliteral( + + + + + + BTPrivacy Login + + + + +
+
+
ESP32-S3 BLE privacy noise
+

BTPrivacy login

+
Invalid username or password
+
+ + +
+
+ + +
+ +
+
+ + + +)rawliteral"; + static const char INDEX_HTML[] PROGMEM = R"rawliteral( @@ -792,11 +835,53 @@ setInterval(refresh, 2000); )rawliteral"; +static bool isAuthenticated() { + if (!web.hasHeader("Cookie")) return false; + const String cookie = web.header("Cookie"); + return cookie.indexOf(BTPRIVACY_AUTH_COOKIE) >= 0; +} + +static bool requireAuth() { + if (isAuthenticated()) return true; + web.sendHeader("Location", "/login"); + web.send(303, "text/plain", ""); + return false; +} + +static bool requireAuthJson() { + if (isAuthenticated()) return true; + web.send(401, "application/json", "{\"ok\":false,\"message\":\"login required\"}"); + return false; +} + +static void handleLogin() { + if (web.method() == HTTP_POST) { + const String username = web.arg("username"); + const String password = web.arg("password"); + + if (username == BTPRIVACY_WEB_USERNAME && password == BTPRIVACY_WEB_PASSWORD) { + web.sendHeader("Set-Cookie", String(BTPRIVACY_AUTH_COOKIE) + "; Path=/; HttpOnly; SameSite=Strict"); + web.sendHeader("Location", "/"); + web.send(303, "text/plain", ""); + return; + } + + web.sendHeader("Location", "/login?bad=1"); + web.send(303, "text/plain", ""); + return; + } + + web.send(200, "text/html", LOGIN_HTML); +} + static void handleIndex() { + if (!requireAuth()) return; web.send(200, "text/html", INDEX_HTML); } static void sendStatusJson() { + if (!requireAuthJson()) return; + String json; json.reserve(1800); @@ -856,6 +941,8 @@ static void sendActionResult(const char *message) { } static void handleAction() { + if (!requireAuthJson()) return; + if (!web.hasArg("do")) { web.send(400, "application/json", "{\"ok\":false,\"message\":\"missing action\"}"); return; @@ -940,15 +1027,30 @@ static void handleAction() { } static void setupWebRoutes() { + web.collectHeaders(WEB_HEADER_KEYS, sizeof(WEB_HEADER_KEYS) / sizeof(WEB_HEADER_KEYS[0])); web.on("/", HTTP_GET, handleIndex); + web.on("/login", HTTP_GET, handleLogin); + web.on("/login", HTTP_POST, handleLogin); web.on("/api/status", HTTP_GET, sendStatusJson); web.on("/api/action", HTTP_POST, handleAction); web.on("/api/action", HTTP_GET, handleAction); web.onNotFound([]() { + if (!isAuthenticated()) { + web.sendHeader("Location", "/login"); + web.send(303, "text/plain", ""); + return; + } web.send(404, "text/plain", "BTPrivacy: not found"); }); } +static void applyWifiHostname() { + if (strlen(BTPRIVACY_WIFI_HOSTNAME) == 0) return; + WiFi.setHostname(BTPRIVACY_WIFI_HOSTNAME); + esp_netif_t *staNetif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (staNetif) esp_netif_set_hostname(staNetif, BTPRIVACY_WIFI_HOSTNAME); +} + static void startWifiDashboard() { if (!BTPRIVACY_WIFI_ENABLED || strlen(BTPRIVACY_WIFI_SSID) == 0) { logPrintln("Wi-Fi dashboard: disabled / no SSID configured"); @@ -959,9 +1061,12 @@ static void startWifiDashboard() { logPrint("Wi-Fi dashboard: connecting to "); logPrintln(BTPRIVACY_WIFI_SSID); + WiFi.persistent(false); + WiFi.disconnect(true, true); WiFi.mode(WIFI_STA); WiFi.setSleep(false); - WiFi.setHostname(BTPRIVACY_WIFI_HOSTNAME); + WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); + applyWifiHostname(); WiFi.begin(BTPRIVACY_WIFI_SSID, BTPRIVACY_WIFI_PASSWORD); const uint32_t start = millis(); @@ -982,6 +1087,7 @@ static void startWifiDashboard() { g_wifiStarted = true; g_wifiConnectMs = millis() - start; + applyWifiHostname(); setupWebRoutes(); web.begin(); diff --git a/README.md b/README.md index 195b764..6bb6b39 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ static constexpr char BTPRIVACY_WIFI_HOSTNAME[] = "btprivacy"; static constexpr bool BTPRIVACY_MDNS_ENABLED = true; static constexpr uint16_t BTPRIVACY_WEB_PORT = 80; static constexpr uint32_t BTPRIVACY_WIFI_TIMEOUT_MS = 12000; +static constexpr char BTPRIVACY_WEB_USERNAME[] = "admin"; +static constexpr char BTPRIVACY_WEB_PASSWORD[] = "btprivacy"; ``` `BTPRIVACY_WIFI_HOSTNAME` is the hostname sent to the router. If mDNS works on your network, the dashboard should also be reachable at: @@ -122,6 +124,15 @@ The direct IP address is always printed in Serial Monitor when Wi-Fi connects: Wi-Fi dashboard: http://192.168.x.x/ ``` +The dashboard is protected by a local login page. The default credentials are: + +```text +Username: admin +Password: btprivacy +``` + +To change them, edit `BTPRIVACY_WEB_USERNAME` and `BTPRIVACY_WEB_PASSWORD` near the top of `BTPrivacyApp.cpp`. + The page shows: - advertising state -- 2.52.0 From b843c447d8ed39f831b3408efd2f1fe053776ed6 Mon Sep 17 00:00:00 2001 From: thepetric Date: Sun, 21 Jun 2026 14:59:27 +0200 Subject: [PATCH 2/4] Styling improvement --- BTPrivacyApp.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/BTPrivacyApp.cpp b/BTPrivacyApp.cpp index 0ae509d..357975c 100644 --- a/BTPrivacyApp.cpp +++ b/BTPrivacyApp.cpp @@ -674,7 +674,6 @@ static const char LOGIN_HTML[] PROGMEM = R"rawliteral(
-
ESP32-S3 BLE privacy noise

BTPrivacy login

Invalid username or password
@@ -719,7 +718,6 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
-
ESP32-S3 BLE privacy noise

BTPrivacy

loading -- 2.52.0 From e3287c2c2a44745dd11bd1afe8e0ad4d165d3ee7 Mon Sep 17 00:00:00 2001 From: thepetric Date: Sun, 21 Jun 2026 15:00:12 +0200 Subject: [PATCH 3/4] Favicon --- BTPrivacyApp.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/BTPrivacyApp.cpp b/BTPrivacyApp.cpp index 357975c..b18783f 100644 --- a/BTPrivacyApp.cpp +++ b/BTPrivacyApp.cpp @@ -657,6 +657,13 @@ static String boolJson(bool v) { return v ? "true" : "false"; } +static const char FAVICON_SVG[] PROGMEM = R"rawliteral( + + +BT + +)rawliteral"; + static const char LOGIN_HTML[] PROGMEM = R"rawliteral( @@ -664,6 +671,8 @@ static const char LOGIN_HTML[] PROGMEM = R"rawliteral( BTPrivacy Login + + @@ -714,7 +717,9 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(