Hostname fix & login page #1
+143
-3
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user