Hostname fix & login page #1

Merged
thepetric merged 4 commits from feature/login-page into main 2026-06-21 13:38:07 +00:00
2 changed files with 154 additions and 3 deletions
+143 -3
View File
@@ -30,6 +30,7 @@
#include <WiFi.h> #include <WiFi.h>
#include <WebServer.h> #include <WebServer.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include "esp_netif.h"
// ---------------- USER SETTINGS ---------------- // ---------------- USER SETTINGS ----------------
@@ -82,6 +83,9 @@ static constexpr char BTPRIVACY_WIFI_HOSTNAME[] = "btprivacy";
static constexpr bool BTPRIVACY_MDNS_ENABLED = true; static constexpr bool BTPRIVACY_MDNS_ENABLED = true;
static constexpr uint16_t BTPRIVACY_WEB_PORT = 80; static constexpr uint16_t BTPRIVACY_WEB_PORT = 80;
static constexpr uint32_t BTPRIVACY_WIFI_TIMEOUT_MS = 12000; 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; 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 bool g_webStarted = false;
static uint32_t g_wifiConnectMs = 0; static uint32_t g_wifiConnectMs = 0;
static SlotState g_slots[BTPRIVACY_MAX_ADV_SETS]; static SlotState g_slots[BTPRIVACY_MAX_ADV_SETS];
static const char *WEB_HEADER_KEYS[] = {"Cookie"};
static void logBegin() { static void logBegin() {
@@ -652,6 +657,55 @@ static String boolJson(bool v) {
return v ? "true" : "false"; return v ? "true" : "false";
} }
static const char FAVICON_SVG[] PROGMEM = R"rawliteral(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" rx="24" fill="#111827"/>
<text x="64" y="80" text-anchor="middle" font-family="Arial,Helvetica,sans-serif" font-size="54" font-weight="900" fill="#ffffff">BT</text>
</svg>
)rawliteral";
static const char LOGIN_HTML[] PROGMEM = R"rawliteral(
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BTPrivacy Login</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="shortcut icon" href="/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
* { user-select: none; -webkit-user-select: none; cursor: default; }
body { min-height: 100vh; background: radial-gradient(circle at top left,#22355e 0,#111827 36%,#080b12 100%); }
input { user-select: text; -webkit-user-select: text; cursor: text; }
button { cursor: pointer; }
.glass { background: rgba(15, 23, 42, .74); border: 1px solid rgba(148, 163, 184, .25); box-shadow: 0 20px 70px rgba(0,0,0,.35); backdrop-filter: blur(10px); }
.muted { color: #94a3b8; }
</style>
</head>
<body>
<main class="container min-vh-100 d-flex align-items-center justify-content-center py-4">
<form method="post" action="/login" class="glass rounded-4 p-4 p-lg-5" style="width:100%;max-width:420px">
<h1 class="h3 fw-bold mb-4">BTPrivacy login</h1>
<div id="bad" class="alert alert-danger py-2 d-none">Invalid username or password</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input class="form-control form-control-lg" name="username" autocomplete="username" autofocus>
</div>
<div class="mb-4">
<label class="form-label">Password</label>
<input class="form-control form-control-lg" type="password" name="password" autocomplete="current-password">
</div>
<button class="btn btn-primary btn-lg w-100" type="submit">Login</button>
</form>
</main>
<script>
if(new URLSearchParams(location.search).has('bad')) document.getElementById('bad').classList.remove('d-none');
</script>
</body>
</html>
)rawliteral";
static const char INDEX_HTML[] PROGMEM = R"rawliteral( static const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!doctype html> <!doctype html>
<html lang="en" data-bs-theme="dark"> <html lang="en" data-bs-theme="dark">
@@ -659,9 +713,13 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>BTPrivacy</title> <title>BTPrivacy</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="shortcut icon" href="/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
* { user-select: none; -webkit-user-select: none; cursor: default; }
body { min-height: 100vh; background: radial-gradient(circle at top left,#22355e 0,#111827 36%,#080b12 100%); } body { min-height: 100vh; background: radial-gradient(circle at top left,#22355e 0,#111827 36%,#080b12 100%); }
button { cursor: pointer; }
.glass { background: rgba(15, 23, 42, .74); border: 1px solid rgba(148, 163, 184, .25); box-shadow: 0 20px 70px rgba(0,0,0,.35); backdrop-filter: blur(10px); } .glass { background: rgba(15, 23, 42, .74); border: 1px solid rgba(148, 163, 184, .25); box-shadow: 0 20px 70px rgba(0,0,0,.35); backdrop-filter: blur(10px); }
.metric { font-size: 2rem; font-weight: 700; letter-spacing: -.03em; } .metric { font-size: 2rem; font-weight: 700; letter-spacing: -.03em; }
.muted { color: #94a3b8; } .muted { color: #94a3b8; }
@@ -676,10 +734,14 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
<main class="container py-4 py-lg-5"> <main class="container py-4 py-lg-5">
<div class="d-flex flex-wrap align-items-center justify-content-between mb-4 gap-3"> <div class="d-flex flex-wrap align-items-center justify-content-between mb-4 gap-3">
<div> <div>
<div class="text-uppercase muted small fw-semibold">ESP32-S3 BLE privacy noise</div>
<h1 class="display-6 fw-bold mb-0">BTPrivacy</h1> <h1 class="display-6 fw-bold mb-0">BTPrivacy</h1>
</div> </div>
<span id="stateBadge" class="badge rounded-pill text-bg-secondary fs-6 px-3 py-2">loading</span> <div class="d-flex align-items-center gap-2">
<span id="stateBadge" class="badge rounded-pill text-bg-secondary fs-6 px-3 py-2">loading</span>
<form method="post" action="/logout" class="m-0">
<button class="btn btn-outline-light btn-sm rounded-pill px-3" type="submit">Logout</button>
</form>
</div>
</div> </div>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
@@ -792,11 +854,64 @@ setInterval(refresh, 2000);
</html> </html>
)rawliteral"; )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 handleFavicon() {
web.sendHeader("Cache-Control", "public, max-age=86400");
web.send(200, "image/svg+xml", FAVICON_SVG);
}
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() { static void handleIndex() {
if (!requireAuth()) return;
web.send(200, "text/html", INDEX_HTML); web.send(200, "text/html", INDEX_HTML);
} }
static void handleLogout() {
web.sendHeader("Set-Cookie", "BTPrivacyAuth=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict");
web.sendHeader("Location", "/login");
web.send(303, "text/plain", "");
}
static void sendStatusJson() { static void sendStatusJson() {
if (!requireAuthJson()) return;
String json; String json;
json.reserve(1800); json.reserve(1800);
@@ -856,6 +971,8 @@ static void sendActionResult(const char *message) {
} }
static void handleAction() { static void handleAction() {
if (!requireAuthJson()) return;
if (!web.hasArg("do")) { if (!web.hasArg("do")) {
web.send(400, "application/json", "{\"ok\":false,\"message\":\"missing action\"}"); web.send(400, "application/json", "{\"ok\":false,\"message\":\"missing action\"}");
return; return;
@@ -940,15 +1057,34 @@ static void handleAction() {
} }
static void setupWebRoutes() { static void setupWebRoutes() {
web.collectHeaders(WEB_HEADER_KEYS, sizeof(WEB_HEADER_KEYS) / sizeof(WEB_HEADER_KEYS[0]));
web.on("/", HTTP_GET, handleIndex); web.on("/", HTTP_GET, handleIndex);
web.on("/login", HTTP_GET, handleLogin);
web.on("/login", HTTP_POST, handleLogin);
web.on("/logout", HTTP_GET, handleLogout);
web.on("/logout", HTTP_POST, handleLogout);
web.on("/favicon.svg", HTTP_GET, handleFavicon);
web.on("/favicon.ico", HTTP_GET, handleFavicon);
web.on("/api/status", HTTP_GET, sendStatusJson); web.on("/api/status", HTTP_GET, sendStatusJson);
web.on("/api/action", HTTP_POST, handleAction); web.on("/api/action", HTTP_POST, handleAction);
web.on("/api/action", HTTP_GET, handleAction); web.on("/api/action", HTTP_GET, handleAction);
web.onNotFound([]() { web.onNotFound([]() {
if (!isAuthenticated()) {
web.sendHeader("Location", "/login");
web.send(303, "text/plain", "");
return;
}
web.send(404, "text/plain", "BTPrivacy: not found"); 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() { static void startWifiDashboard() {
if (!BTPRIVACY_WIFI_ENABLED || strlen(BTPRIVACY_WIFI_SSID) == 0) { if (!BTPRIVACY_WIFI_ENABLED || strlen(BTPRIVACY_WIFI_SSID) == 0) {
logPrintln("Wi-Fi dashboard: disabled / no SSID configured"); logPrintln("Wi-Fi dashboard: disabled / no SSID configured");
@@ -959,9 +1095,12 @@ static void startWifiDashboard() {
logPrint("Wi-Fi dashboard: connecting to "); logPrint("Wi-Fi dashboard: connecting to ");
logPrintln(BTPRIVACY_WIFI_SSID); logPrintln(BTPRIVACY_WIFI_SSID);
WiFi.persistent(false);
WiFi.disconnect(true, true);
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.setSleep(false); 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); WiFi.begin(BTPRIVACY_WIFI_SSID, BTPRIVACY_WIFI_PASSWORD);
const uint32_t start = millis(); const uint32_t start = millis();
@@ -982,6 +1121,7 @@ static void startWifiDashboard() {
g_wifiStarted = true; g_wifiStarted = true;
g_wifiConnectMs = millis() - start; g_wifiConnectMs = millis() - start;
applyWifiHostname();
setupWebRoutes(); setupWebRoutes();
web.begin(); web.begin();
+11
View File
@@ -108,6 +108,8 @@ static constexpr char BTPRIVACY_WIFI_HOSTNAME[] = "btprivacy";
static constexpr bool BTPRIVACY_MDNS_ENABLED = true; static constexpr bool BTPRIVACY_MDNS_ENABLED = true;
static constexpr uint16_t BTPRIVACY_WEB_PORT = 80; static constexpr uint16_t BTPRIVACY_WEB_PORT = 80;
static constexpr uint32_t BTPRIVACY_WIFI_TIMEOUT_MS = 12000; 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: `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/ 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: The page shows:
- advertising state - advertising state