Przeglądaj źródła

feat: open full web CLI access and add panel redirect flow

Jared Dohrman 3 miesięcy temu
rodzic
commit
bcce0071f1

+ 6 - 84
docs/custom-cli.md

@@ -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
 

+ 6 - 6
docs/web-panel.md

@@ -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
 

+ 0 - 96
examples/simple_repeater/MyMesh.cpp

@@ -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);

+ 48 - 12
src/helpers/web/WebPanelServer.cpp

@@ -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) {

+ 3 - 0
src/helpers/web/WebPanelServer.h

@@ -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);