Просмотр исходного кода

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

Jared Dohrman 3 месяцев назад
Родитель
Сommit
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.
 - `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.
 - `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
 ## 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 password-gated local admin page at `/app`
 - a dedicated stats and trends page at `/stats`
 - a dedicated stats and trends page at `/stats`
 - quick `get` commands for common repeater and MQTT checks
 - 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 repeater settings
 - editable MQTT settings
 - editable MQTT settings
 - a historical stats view with trends, neighbours, and recent events
 - 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 panel uses the same admin password as the repeater CLI
 - the connection is HTTPS, but the certificate is self-signed
 - the connection is HTTPS, but the certificate is self-signed
 - browsers will warn the first time you connect
 - 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.
 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
 ## 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
 - press `Enter` to run the command
 - command history is shown in the terminal box below
 - command history is shown in the terminal box below
 - save buttons elsewhere in the page also show the generated command and the reply here
 - 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
 - `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.
 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.
 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
 ### 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;
     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];
   char command_buf[192];
   StrHelper::strncpy(command_buf, command, sizeof(command_buf));
   StrHelper::strncpy(command_buf, command, sizeof(command_buf));
   handleCommand(0, command_buf, reply);
   handleCommand(0, command_buf, reply);

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

@@ -256,10 +256,10 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
   </main>
   </main>
   <script>
   <script>
     const statusEl = document.getElementById("status");
     const statusEl = document.getElementById("status");
-    const LAST_PAGE_KEY = "repeater-last-page";
     function getPreferredPage() {
     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() {
     async function login() {
       const pwd = document.getElementById("password").value;
       const pwd = document.getElementById("password").value;
@@ -586,7 +586,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
         <input id="command" placeholder="get mqtt.status">
         <input id="command" placeholder="get mqtt.status">
         <button id="runBtn">Run</button>
         <button id="runBtn">Run</button>
       </div>
       </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>
       <div id="reply" class="terminal"></div>
     </section>
     </section>
 
 
@@ -993,7 +993,6 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
   <script>
   <script>
     const RADIO_PRESETS_URL = "https://api.meshcore.nz/api/v1/config";
     const RADIO_PRESETS_URL = "https://api.meshcore.nz/api/v1/config";
     const isStatsPage = window.location.pathname === "/stats";
     const isStatsPage = window.location.pathname === "/stats";
-    const LAST_PAGE_KEY = "repeater-last-page";
     const PANEL_TITLE_KEY = "repeater-panel-title";
     const PANEL_TITLE_KEY = "repeater-panel-title";
     let token = sessionStorage.getItem("repeater-token") || "";
     let token = sessionStorage.getItem("repeater-token") || "";
     let commandQueue = Promise.resolve();
     let commandQueue = Promise.resolve();
@@ -1020,16 +1019,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
         document.title = cachedTitle.trim();
         document.title = cachedTitle.trim();
       }
       }
     }
     }
-    function rememberCurrentPage() {
-      localStorage.setItem(LAST_PAGE_KEY, isStatsPage ? "/stats" : "/app");
-    }
     function redirectToLogin() {
     function redirectToLogin() {
-      rememberCurrentPage();
+      const next = isStatsPage ? "/stats" : "/app";
       sessionStorage.removeItem("repeater-token");
       sessionStorage.removeItem("repeater-token");
       token = "";
       token = "";
-      window.location.replace("/");
+      window.location.replace("/?next=" + encodeURIComponent(next));
     }
     }
-    rememberCurrentPage();
     applyCachedPanelTitle();
     applyCachedPanelTitle();
     function getPreferredTheme() {
     function getPreferredTheme() {
       const saved = localStorage.getItem("repeater-theme");
       const saved = localStorage.getItem("repeater-theme");
@@ -2466,7 +2461,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
 }  // namespace
 }  // namespace
 
 
 WebPanelServer::WebPanelServer()
 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) {
 void WebPanelServer::setCommandRunner(WebPanelCommandRunner* runner) {
@@ -2518,11 +2513,37 @@ bool WebPanelServer::start() {
   httpd_register_uri_handler(_server, &login_uri);
   httpd_register_uri_handler(_server, &login_uri);
   httpd_register_uri_handler(_server, &command_uri);
   httpd_register_uri_handler(_server, &command_uri);
   httpd_register_uri_handler(_server, &stats_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());
   WEB_PANEL_LOG("server started on https://%s/", WiFi.localIP().toString().c_str());
   return true;
   return true;
 }
 }
 
 
 void WebPanelServer::stop() {
 void WebPanelServer::stop() {
+  if (_redirect_server != nullptr) {
+    httpd_stop(_redirect_server);
+    _redirect_server = nullptr;
+  }
   if (_server != nullptr) {
   if (_server != nullptr) {
     WEB_PANEL_LOG("server stopped");
     WEB_PANEL_LOG("server stopped");
     httpd_ssl_stop(_server);
     httpd_ssl_stop(_server);
@@ -2563,6 +2584,21 @@ esp_err_t WebPanelServer::handleIndex(httpd_req_t* req) {
   return sendProgmemChunked(req, kWebPanelLoginHtml);
   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) {
 esp_err_t WebPanelServer::handleApp(httpd_req_t* req) {
   auto* ctx = static_cast<RouteContext*>(req->user_ctx);
   auto* ctx = static_cast<RouteContext*>(req->user_ctx);
   if (ctx == nullptr || ctx->self == nullptr) {
   if (ctx == nullptr || ctx->self == nullptr) {

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

@@ -9,6 +9,7 @@
   #endif
   #endif
   #if WITH_WEB_PANEL
   #if WITH_WEB_PANEL
     #include <esp_https_server.h>
     #include <esp_https_server.h>
+    #include <esp_http_server.h>
   #endif
   #endif
 #endif
 #endif
 
 
@@ -53,11 +54,13 @@ private:
 
 
   WebPanelCommandRunner* _runner;
   WebPanelCommandRunner* _runner;
   httpd_handle_t _server;
   httpd_handle_t _server;
+  httpd_handle_t _redirect_server;
   char _token[33];
   char _token[33];
   unsigned long _last_activity_ms;
   unsigned long _last_activity_ms;
   RouteContext _route_context;
   RouteContext _route_context;
 
 
   static esp_err_t handleIndex(httpd_req_t* req);
   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 handleApp(httpd_req_t* req);
   static esp_err_t handleStatsPage(httpd_req_t* req);
   static esp_err_t handleStatsPage(httpd_req_t* req);
   static esp_err_t handleLogin(httpd_req_t* req);
   static esp_err_t handleLogin(httpd_req_t* req);