feat: open full web CLI access and add panel redirect flow
Cette révision appartient à :
@@ -82,93 +82,15 @@ Legacy dotted aliases are also accepted:
|
||||
- `get battery.reporting`: shows whether board battery reporting is enabled. Support is board-dependent.
|
||||
- `set battery.reporting on|off`: enables or disables battery voltage reporting on supported boards. This is currently useful for Heltec V3 boards where USB-only power can produce misleading battery readings. If your board needs this too, open an issue and support can be added board-by-board.
|
||||
|
||||
## Web Panel Allowlisted Commands
|
||||
## Web Panel CLI Access
|
||||
|
||||
When the repeater web panel is enabled, it only allows a limited command set.
|
||||
When the repeater web panel is enabled and you are authenticated, the browser CLI panel can run the same CLI commands accepted by the repeater.
|
||||
|
||||
That allowlist currently includes:
|
||||
Notes:
|
||||
|
||||
- `clock`
|
||||
- `region default`
|
||||
- `region default <name>`
|
||||
- `region default <null>`
|
||||
- `get mqtt.status`
|
||||
- `get web`
|
||||
- `get web.status`
|
||||
- `get web.stats.status`
|
||||
- `advert`
|
||||
- `reboot`
|
||||
- `start ota`
|
||||
- `memory`
|
||||
- `stats-core`
|
||||
- `stats-radio`
|
||||
- `stats-packets`
|
||||
- `get wifi.status`
|
||||
- `get wifi.powersaving`
|
||||
- `set wifi.ssid <ssid>`
|
||||
- `set wifi.pwd <password>`
|
||||
- `set wifi.powersaving on|off`
|
||||
- `get mqtt.iata`
|
||||
- `set mqtt.iata <code>`
|
||||
- `get mqtt.owner`
|
||||
- `set mqtt.owner <64-hex-char-public-key>`
|
||||
- `get mqtt.email`
|
||||
- `set mqtt.email <email>`
|
||||
- `get mqtt.packets`
|
||||
- `set mqtt.packets on|off`
|
||||
- `get mqtt.raw`
|
||||
- `set mqtt.raw on|off`
|
||||
- `get mqtt.statuscfg`
|
||||
- `set mqtt.status on|off`
|
||||
- `get mqtt.tx`
|
||||
- `set mqtt.tx on|off`
|
||||
- `get mqtt.eastmesh-au`
|
||||
- `set mqtt.eastmesh-au on|off`
|
||||
- `get mqtt.eastmesh.au`
|
||||
- `set mqtt.eastmesh.au on|off`
|
||||
- `get mqtt.letsmesh-eu`
|
||||
- `set mqtt.letsmesh-eu on|off`
|
||||
- `get mqtt.letsmesh.eu`
|
||||
- `set mqtt.letsmesh.eu on|off`
|
||||
- `get mqtt.letsmesh-us`
|
||||
- `set mqtt.letsmesh-us on|off`
|
||||
- `get mqtt.letsmesh.us`
|
||||
- `set mqtt.letsmesh.us on|off`
|
||||
- `set web on|off`
|
||||
- `set.web on|off`
|
||||
- `set web.stats on|off`
|
||||
- `set.web.stats on|off`
|
||||
- `get name`
|
||||
- `set name <device-name>`
|
||||
- `get lat`
|
||||
- `set lat <latitude>`
|
||||
- `get lon`
|
||||
- `set lon <longitude>`
|
||||
- `get radio`
|
||||
- `set radio <freq> <bw> <sf> <cr>`
|
||||
- `get prv.key`
|
||||
- `get guest.password`
|
||||
- `password <admin-password>`
|
||||
- `set guest.password <password>`
|
||||
- `set prv.key <64-hex-char-private-key>`
|
||||
- `get role`
|
||||
- `get public.key`
|
||||
- `get advert.interval`
|
||||
- `set advert.interval <minutes>`
|
||||
- `get agc.reset.interval`
|
||||
- `set agc.reset.interval <seconds>`
|
||||
- `get flood.advert.interval`
|
||||
- `set flood.advert.interval <hours>`
|
||||
- `get repeat`
|
||||
- `set repeat on|off`
|
||||
- `get flood.max`
|
||||
- `set flood.max <count>`
|
||||
- `get path.hash.mode`
|
||||
- `set path.hash.mode <mode>`
|
||||
- `get owner.info`
|
||||
- `set owner.info <text>`
|
||||
- `time <iso-or-epoch>`
|
||||
- `time.force <iso-or-epoch>`
|
||||
- the panel still uses the repeater admin password for access
|
||||
- commands run with the same care as if you typed them into the repeater CLI directly
|
||||
- this is intended for local admin use on a trusted network
|
||||
|
||||
## Companion WiFi Rescue Commands
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ It gives you:
|
||||
- a password-gated local admin page at `/app`
|
||||
- a dedicated stats and trends page at `/stats`
|
||||
- quick `get` commands for common repeater and MQTT checks
|
||||
- a terminal-style CLI panel for allowlisted commands
|
||||
- a terminal-style CLI panel for full repeater CLI access
|
||||
- editable repeater settings
|
||||
- editable MQTT settings
|
||||
- a historical stats view with trends, neighbours, and recent events
|
||||
@@ -71,7 +71,7 @@ Example:
|
||||
- the panel uses the same admin password as the repeater CLI
|
||||
- the connection is HTTPS, but the certificate is self-signed
|
||||
- browsers will warn the first time you connect
|
||||
- the panel only exposes an allowlisted subset of CLI commands
|
||||
- the panel exposes the repeater CLI after login
|
||||
|
||||
This is intended for local admin use on a trusted network, not for open internet exposure.
|
||||
|
||||
@@ -134,13 +134,13 @@ These are useful for quick checks without typing into the CLI field.
|
||||
|
||||
## Run CLI Command
|
||||
|
||||
This is a small terminal for allowlisted commands.
|
||||
This is a small terminal for the repeater CLI.
|
||||
|
||||
- press `Enter` to run the command
|
||||
- command history is shown in the terminal box below
|
||||
- save buttons elsewhere in the page also show the generated command and the reply here
|
||||
- `clock` is available here if you want to check the repeater's current board time
|
||||
- the full current allowlist is documented in `docs/custom-cli.md` under `Web Panel Allowlisted Commands`
|
||||
- authenticated sessions can run the same CLI commands accepted by the repeater
|
||||
|
||||
This makes it easy to see exactly what the panel sent to the repeater.
|
||||
|
||||
@@ -310,9 +310,9 @@ Check:
|
||||
|
||||
For fixed installations where MQTT uptime matters more than browser access, use the panel briefly and then disable it again.
|
||||
|
||||
### A command says it is not allowlisted
|
||||
### HTTP opens instead of HTTPS
|
||||
|
||||
The panel intentionally limits what can be run from the browser. Use the serial CLI for commands outside the web allowlist. `clock` is included, but most maintenance and debug commands are still serial-only.
|
||||
The repeater now redirects plain `http://` requests to the local `https://` panel URL. If the browser still shows a connection problem after redirecting, open `https://<repeater-ip>/` directly and accept the self-signed certificate warning first.
|
||||
|
||||
### Stats or settings do not refresh
|
||||
|
||||
|
||||
@@ -1992,102 +1992,6 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
||||
return;
|
||||
}
|
||||
|
||||
auto matches_exact = [command](const char* candidate) -> bool {
|
||||
return strcmp(command, candidate) == 0;
|
||||
};
|
||||
auto matches_prefix = [command](const char* candidate) -> bool {
|
||||
size_t len = strlen(candidate);
|
||||
return strncmp(command, candidate, len) == 0;
|
||||
};
|
||||
|
||||
bool allowed =
|
||||
matches_exact("clock") ||
|
||||
matches_exact("region default") ||
|
||||
matches_exact("get mqtt.status") ||
|
||||
matches_exact("get web.status") ||
|
||||
matches_exact("get web.stats.status") ||
|
||||
matches_exact("get web") ||
|
||||
matches_exact("advert") ||
|
||||
matches_exact("reboot") ||
|
||||
matches_exact("start ota") ||
|
||||
matches_exact("get wifi.status") ||
|
||||
matches_exact("get wifi.powersaving") ||
|
||||
matches_exact("stats-core") ||
|
||||
matches_exact("stats-radio") ||
|
||||
matches_exact("stats-packets") ||
|
||||
matches_exact("memory") ||
|
||||
matches_exact("get mqtt.iata") ||
|
||||
matches_exact("get mqtt.owner") ||
|
||||
matches_exact("get mqtt.email") ||
|
||||
matches_exact("get mqtt.packets") ||
|
||||
matches_exact("get mqtt.raw") ||
|
||||
matches_exact("get mqtt.statuscfg") ||
|
||||
matches_exact("get mqtt.tx") ||
|
||||
matches_exact("get mqtt.eastmesh-au") ||
|
||||
matches_exact("get mqtt.eastmesh.au") ||
|
||||
matches_exact("get mqtt.letsmesh-eu") ||
|
||||
matches_exact("get mqtt.letsmesh.eu") ||
|
||||
matches_exact("get mqtt.letsmesh-us") ||
|
||||
matches_exact("get mqtt.letsmesh.us") ||
|
||||
matches_exact("get name") ||
|
||||
matches_exact("get lat") ||
|
||||
matches_exact("get lon") ||
|
||||
matches_exact("get radio") ||
|
||||
matches_exact("get prv.key") ||
|
||||
matches_exact("get role") ||
|
||||
matches_exact("get public.key") ||
|
||||
matches_exact("get advert.interval") ||
|
||||
matches_exact("get agc.reset.interval") ||
|
||||
matches_exact("get flood.advert.interval") ||
|
||||
matches_exact("get repeat") ||
|
||||
matches_exact("get flood.max") ||
|
||||
matches_exact("get path.hash.mode") ||
|
||||
matches_exact("get owner.info") ||
|
||||
matches_exact("get guest.password") ||
|
||||
matches_prefix("set wifi.ssid ") ||
|
||||
matches_prefix("set wifi.pwd ") ||
|
||||
matches_prefix("set wifi.powersaving ") ||
|
||||
matches_prefix("set mqtt.iata ") ||
|
||||
matches_prefix("set mqtt.owner ") ||
|
||||
matches_prefix("set mqtt.email ") ||
|
||||
matches_prefix("set mqtt.packets ") ||
|
||||
matches_prefix("set mqtt.raw ") ||
|
||||
matches_prefix("set mqtt.status ") ||
|
||||
matches_prefix("set mqtt.tx ") ||
|
||||
matches_prefix("set web ") ||
|
||||
matches_prefix("set.web ") ||
|
||||
matches_prefix("set web.stats ") ||
|
||||
matches_prefix("set.web.stats ") ||
|
||||
matches_prefix("set mqtt.eastmesh-au ") ||
|
||||
matches_prefix("set mqtt.eastmesh.au ") ||
|
||||
matches_prefix("set mqtt.letsmesh-eu ") ||
|
||||
matches_prefix("set mqtt.letsmesh.eu ") ||
|
||||
matches_prefix("set mqtt.letsmesh-us ") ||
|
||||
matches_prefix("set mqtt.letsmesh.us ") ||
|
||||
matches_prefix("set name ") ||
|
||||
matches_prefix("set lat ") ||
|
||||
matches_prefix("set lon ") ||
|
||||
matches_prefix("set radio ") ||
|
||||
matches_prefix("password ") ||
|
||||
matches_prefix("set guest.password ") ||
|
||||
matches_prefix("set prv.key ") ||
|
||||
matches_prefix("set advert.interval ") ||
|
||||
matches_prefix("set agc.reset.interval ") ||
|
||||
matches_prefix("set flood.advert.interval ") ||
|
||||
matches_prefix("set repeat ") ||
|
||||
matches_prefix("set flood.max ") ||
|
||||
matches_prefix("set path.hash.mode ") ||
|
||||
matches_prefix("region default ") ||
|
||||
matches_prefix("time ") ||
|
||||
matches_prefix("time.force ") ||
|
||||
matches_prefix("set owner.info ");
|
||||
|
||||
if (!allowed) {
|
||||
strncpy(reply, "Err - command not allowlisted for web access", reply_size - 1);
|
||||
reply[reply_size - 1] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
char command_buf[192];
|
||||
StrHelper::strncpy(command_buf, command, sizeof(command_buf));
|
||||
handleCommand(0, command_buf, reply);
|
||||
|
||||
@@ -256,10 +256,10 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
|
||||
</main>
|
||||
<script>
|
||||
const statusEl = document.getElementById("status");
|
||||
const LAST_PAGE_KEY = "repeater-last-page";
|
||||
function getPreferredPage() {
|
||||
const stored = localStorage.getItem(LAST_PAGE_KEY);
|
||||
return stored === "/stats" ? "/stats" : "/app";
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const next = params.get("next");
|
||||
return next === "/stats" ? "/stats" : "/app";
|
||||
}
|
||||
async function login() {
|
||||
const pwd = document.getElementById("password").value;
|
||||
@@ -586,7 +586,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
<input id="command" placeholder="get mqtt.status">
|
||||
<button id="runBtn">Run</button>
|
||||
</div>
|
||||
<p class="panel-copy">Only the allowlisted commands exposed by this panel will run here.</p>
|
||||
<p class="panel-copy">Authenticated sessions can run repeater CLI commands here.</p>
|
||||
<div id="reply" class="terminal"></div>
|
||||
</section>
|
||||
|
||||
@@ -993,7 +993,6 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
<script>
|
||||
const RADIO_PRESETS_URL = "https://api.meshcore.nz/api/v1/config";
|
||||
const isStatsPage = window.location.pathname === "/stats";
|
||||
const LAST_PAGE_KEY = "repeater-last-page";
|
||||
const PANEL_TITLE_KEY = "repeater-panel-title";
|
||||
let token = sessionStorage.getItem("repeater-token") || "";
|
||||
let commandQueue = Promise.resolve();
|
||||
@@ -1020,16 +1019,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
document.title = cachedTitle.trim();
|
||||
}
|
||||
}
|
||||
function rememberCurrentPage() {
|
||||
localStorage.setItem(LAST_PAGE_KEY, isStatsPage ? "/stats" : "/app");
|
||||
}
|
||||
function redirectToLogin() {
|
||||
rememberCurrentPage();
|
||||
const next = isStatsPage ? "/stats" : "/app";
|
||||
sessionStorage.removeItem("repeater-token");
|
||||
token = "";
|
||||
window.location.replace("/");
|
||||
window.location.replace("/?next=" + encodeURIComponent(next));
|
||||
}
|
||||
rememberCurrentPage();
|
||||
applyCachedPanelTitle();
|
||||
function getPreferredTheme() {
|
||||
const saved = localStorage.getItem("repeater-theme");
|
||||
@@ -2466,7 +2461,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
} // namespace
|
||||
|
||||
WebPanelServer::WebPanelServer()
|
||||
: _runner(nullptr), _server(nullptr), _token{0}, _last_activity_ms(0), _route_context{this} {
|
||||
: _runner(nullptr), _server(nullptr), _redirect_server(nullptr), _token{0}, _last_activity_ms(0), _route_context{this} {
|
||||
}
|
||||
|
||||
void WebPanelServer::setCommandRunner(WebPanelCommandRunner* runner) {
|
||||
@@ -2518,11 +2513,37 @@ bool WebPanelServer::start() {
|
||||
httpd_register_uri_handler(_server, &login_uri);
|
||||
httpd_register_uri_handler(_server, &command_uri);
|
||||
httpd_register_uri_handler(_server, &stats_uri);
|
||||
|
||||
httpd_config_t redirect_config = HTTPD_DEFAULT_CONFIG();
|
||||
redirect_config.server_port = 80;
|
||||
redirect_config.ctrl_port = 32768;
|
||||
redirect_config.max_open_sockets = 2;
|
||||
redirect_config.max_uri_handlers = 1;
|
||||
redirect_config.max_resp_headers = 4;
|
||||
redirect_config.backlog_conn = 2;
|
||||
redirect_config.recv_wait_timeout = 2;
|
||||
redirect_config.send_wait_timeout = 2;
|
||||
redirect_config.stack_size = kWebServerStackSize;
|
||||
redirect_config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
|
||||
rc = httpd_start(&_redirect_server, &redirect_config);
|
||||
if (rc == ESP_OK) {
|
||||
httpd_uri_t redirect_uri = {.uri = "/*", .method = HTTP_GET, .handler = &WebPanelServer::handleHttpRedirect, .user_ctx = &_route_context};
|
||||
httpd_register_uri_handler(_redirect_server, &redirect_uri);
|
||||
} else {
|
||||
_redirect_server = nullptr;
|
||||
WEB_PANEL_LOG("redirect server start failed rc=0x%x", static_cast<unsigned>(rc));
|
||||
}
|
||||
|
||||
WEB_PANEL_LOG("server started on https://%s/", WiFi.localIP().toString().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebPanelServer::stop() {
|
||||
if (_redirect_server != nullptr) {
|
||||
httpd_stop(_redirect_server);
|
||||
_redirect_server = nullptr;
|
||||
}
|
||||
if (_server != nullptr) {
|
||||
WEB_PANEL_LOG("server stopped");
|
||||
httpd_ssl_stop(_server);
|
||||
@@ -2563,6 +2584,21 @@ esp_err_t WebPanelServer::handleIndex(httpd_req_t* req) {
|
||||
return sendProgmemChunked(req, kWebPanelLoginHtml);
|
||||
}
|
||||
|
||||
esp_err_t WebPanelServer::handleHttpRedirect(httpd_req_t* req) {
|
||||
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
|
||||
if (ctx == nullptr || ctx->self == nullptr) {
|
||||
return httpd_resp_send_500(req);
|
||||
}
|
||||
|
||||
char location[160];
|
||||
const char* path = (req->uri != nullptr && req->uri[0] != 0) ? req->uri : "/";
|
||||
snprintf(location, sizeof(location), "https://%s%s", WiFi.localIP().toString().c_str(), path);
|
||||
httpd_resp_set_status(req, "302 Found");
|
||||
httpd_resp_set_hdr(req, "Location", location);
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||
return httpd_resp_send(req, "", 0);
|
||||
}
|
||||
|
||||
esp_err_t WebPanelServer::handleApp(httpd_req_t* req) {
|
||||
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
|
||||
if (ctx == nullptr || ctx->self == nullptr) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#endif
|
||||
#if WITH_WEB_PANEL
|
||||
#include <esp_https_server.h>
|
||||
#include <esp_http_server.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -53,11 +54,13 @@ private:
|
||||
|
||||
WebPanelCommandRunner* _runner;
|
||||
httpd_handle_t _server;
|
||||
httpd_handle_t _redirect_server;
|
||||
char _token[33];
|
||||
unsigned long _last_activity_ms;
|
||||
RouteContext _route_context;
|
||||
|
||||
static esp_err_t handleIndex(httpd_req_t* req);
|
||||
static esp_err_t handleHttpRedirect(httpd_req_t* req);
|
||||
static esp_err_t handleApp(httpd_req_t* req);
|
||||
static esp_err_t handleStatsPage(httpd_req_t* req);
|
||||
static esp_err_t handleLogin(httpd_req_t* req);
|
||||
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur