From 7b692002447441011a73dd3039ef1c00ef4e6d30 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Tue, 28 Jul 2026 13:39:00 -0400 Subject: [PATCH] adding some buttons --- core/archipelago/src/api/rpc/dispatcher.rs | 1 + core/archipelago/src/api/rpc/openwrt.rs | 53 +++++++++++++ core/openwrt/src/router.rs | 16 ++++ core/openwrt/src/uci.rs | 2 +- neode-ui/package-lock.json | 25 +----- neode-ui/src/views/server/OpenWrtGateway.vue | 81 ++++++++++++++++++++ 6 files changed, 155 insertions(+), 23 deletions(-) diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index 521ea1b0..bfa7c8a3 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -245,6 +245,7 @@ impl RpcHandler { "openwrt.scan" => self.handle_openwrt_scan(params).await, "openwrt.get-status" => self.handle_openwrt_get_status(params).await, "openwrt.forget" => self.handle_openwrt_forget().await, + "openwrt.set-password" => self.handle_openwrt_set_password(params).await, "openwrt.provision-tollgate" => self.handle_openwrt_provision_tollgate(params).await, "openwrt.scan-wifi" => self.handle_openwrt_scan_wifi(params).await, "openwrt.configure-wan" => self.handle_openwrt_configure_wan(params).await, diff --git a/core/archipelago/src/api/rpc/openwrt.rs b/core/archipelago/src/api/rpc/openwrt.rs index 1a87b2cd..0a9ceb91 100644 --- a/core/archipelago/src/api/rpc/openwrt.rs +++ b/core/archipelago/src/api/rpc/openwrt.rs @@ -172,6 +172,59 @@ impl RpcHandler { Ok(serde_json::json!({ "ok": true })) } + /// Set the router's SSH login password from the app — no manual SSH/ + /// console session on the router required. Works for a fresh flash + /// (root has no password yet — connect with `current_password: ""`) + /// or to rotate an existing one. On success the new credentials are + /// persisted the same way a normal login does, so the app is fully + /// connected afterward with no separate "Connect" step needed. + /// + /// Params: `{ "host": "192.168.1.1", "ssh_user": "root", + /// "current_password": "", "new_password": "..." }` + pub(super) async fn handle_openwrt_set_password( + &self, + params: Option, + ) -> Result { + let p = params.unwrap_or_default(); + let host = p + .get("host") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("host is required"))? + .to_string(); + let ssh_user = p + .get("ssh_user") + .and_then(|v| v.as_str()) + .unwrap_or("root") + .to_string(); + let current_password = p + .get("current_password") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let new_password = p + .get("new_password") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .ok_or_else(|| anyhow::anyhow!("new_password is required"))? + .to_string(); + + let router = Router::connect_password(&host, 22, &ssh_user, ¤t_password)?; + router.verify_openwrt()?; + router.set_password(&ssh_user, &new_password)?; + + net_router::configure_router( + &self.config.data_dir, + net_router::RouterType::OpenWrt, + &host, + None, + Some(&ssh_user), + Some(&new_password), + ) + .await?; + + Ok(serde_json::json!({ "ok": true, "host": host })) + } + /// Provision TollGate on an OpenWrt router and create the "archipelago" SSID. /// /// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "", diff --git a/core/openwrt/src/router.rs b/core/openwrt/src/router.rs index d0234178..0c8535e8 100644 --- a/core/openwrt/src/router.rs +++ b/core/openwrt/src/router.rs @@ -82,6 +82,22 @@ impl Router { Ok(out) } + /// Set `user`'s login password via BusyBox `passwd`, non-interactively. + /// + /// BusyBox's passwd applet reads the new password twice from stdin even + /// without a tty (unlike shadow-utils' passwd, which requires one), so + /// piping two lines in works. Root can always set its own (or another + /// user's) password without supplying the old one first. This is what + /// lets a fresh-flash router (root has no password yet) be fully set up + /// from the app — no manual SSH/console session required. + pub fn set_password(&self, user: &str, new_password: &str) -> Result<()> { + let q = crate::uci::shell_quote(new_password); + let uq = crate::uci::shell_quote(user); + let cmd = format!("printf '%s\\n%s\\n' {q} {q} | passwd {uq} 2>&1"); + self.run_ok(&cmd)?; + Ok(()) + } + /// Verify the remote device is actually running OpenWrt. pub fn verify_openwrt(&self) -> Result { let release = self diff --git a/core/openwrt/src/uci.rs b/core/openwrt/src/uci.rs index e74f3e27..14003dc0 100644 --- a/core/openwrt/src/uci.rs +++ b/core/openwrt/src/uci.rs @@ -60,6 +60,6 @@ impl Router { } /// Wrap a value in single quotes, escaping any embedded single quotes. -fn shell_quote(s: &str) -> String { +pub(crate) fn shell_quote(s: &str) -> String { format!("'{}'", s.replace('\'', r"'\''")) } diff --git a/neode-ui/package-lock.json b/neode-ui/package-lock.json index f25e7ff8..41b11e6d 100644 --- a/neode-ui/package-lock.json +++ b/neode-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "neode-ui", - "version": "1.7.115-alpha", + "version": "1.7.116-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neode-ui", - "version": "1.7.115-alpha", + "version": "1.7.116-alpha", "dependencies": { "@scure/bip39": "^2.2.0", "@types/dompurify": "^3.0.5", @@ -150,7 +150,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1812,7 +1811,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1836,7 +1834,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3923,7 +3920,6 @@ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/geojson": "*" } @@ -3973,7 +3969,6 @@ "integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cac": "^6.7.14", "colorette": "^2.0.20", @@ -4474,7 +4469,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4960,7 +4954,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5979,7 +5972,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -8172,7 +8164,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8222,7 +8213,6 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -8325,8 +8315,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/leven": { "version": "3.1.0", @@ -9082,7 +9071,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9744,7 +9732,6 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -11000,7 +10987,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11256,7 +11242,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11498,7 +11483,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11661,7 +11645,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11675,7 +11658,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -11768,7 +11750,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/compiler-sfc": "3.5.30", diff --git a/neode-ui/src/views/server/OpenWrtGateway.vue b/neode-ui/src/views/server/OpenWrtGateway.vue index c245c136..b0bf9523 100644 --- a/neode-ui/src/views/server/OpenWrtGateway.vue +++ b/neode-ui/src/views/server/OpenWrtGateway.vue @@ -87,6 +87,15 @@ const detecting = ref(false) const detectError = ref('') const detectedCandidates = ref([]) +// Set/change router password — lets a fresh-flash router (root has no +// password yet) get fully connected without the user ever opening a +// terminal or LuCI themselves. +const showSetPassword = ref(false) +const newPassword = ref('') +const confirmPassword = ref('') +const settingPassword = ref(false) +const setPasswordError = ref('') + const provisioning = ref(false) const provisionError = ref('') const provisionSuccess = ref(false) @@ -148,6 +157,39 @@ async function connect() { } } +async function setPasswordAndConnect() { + if (!host.value.trim() || !newPassword.value) return + setPasswordError.value = '' + if (newPassword.value !== confirmPassword.value) { + setPasswordError.value = 'Passwords do not match.' + return + } + settingPassword.value = true + try { + await rpcClient.call({ + method: 'openwrt.set-password', + params: { + host: host.value.trim(), + ssh_user: sshUser.value, + current_password: sshPassword.value, + new_password: newPassword.value, + }, + timeout: 20000, + }) + // Router accepted the new password — log in with it right away so the + // user never has to separately "Connect" after this. + sshPassword.value = newPassword.value + showSetPassword.value = false + newPassword.value = '' + confirmPassword.value = '' + await connect() + } catch (e) { + setPasswordError.value = e instanceof Error ? e.message : String(e) + } finally { + settingPassword.value = false + } +} + interface WiredInterface { name: string; type: string; state: string; ipv4: string[] } async function detectRouter() { @@ -464,6 +506,45 @@ onMounted(() => load()) > {{ connecting ? 'Connecting…' : 'Connect' }} + +
+

+ Sets the router's SSH password directly — no need to SSH in yourself first. + Leave "Password" above blank if this is a fresh flash (no password set yet). +

+
+
+ + +
+
+ + +
+
+ +

{{ setPasswordError }}

+

{{ error }}