From 056a9ff9b65bb88e8016052b0a4c4c0096833201 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 4 Nov 2025 18:11:19 -0700 Subject: [PATCH] tui tweaks --- core/startos/src/tunnel/auth.rs | 2 +- core/startos/src/tunnel/web.rs | 77 +++++++++++++++++++++++---------- core/startos/src/util/tui.rs | 15 ++++--- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/core/startos/src/tunnel/auth.rs b/core/startos/src/tunnel/auth.rs index 03beab7f5..21482980f 100644 --- a/core/startos/src/tunnel/auth.rs +++ b/core/startos/src/tunnel/auth.rs @@ -305,7 +305,7 @@ pub async fn reset_password( let params = SetPasswordParams { password: base32::encode( base32::Alphabet::Rfc4648Lower { padding: false }, - &rand::random::<[u8; 10]>(), + &rand::random::<[u8; 16]>(), ), }; diff --git a/core/startos/src/tunnel/web.rs b/core/startos/src/tunnel/web.rs index eba414711..6d77f4309 100644 --- a/core/startos/src/tunnel/web.rs +++ b/core/startos/src/tunnel/web.rs @@ -28,7 +28,7 @@ use crate::tunnel::auth::SetPasswordParams; use crate::tunnel::context::TunnelContext; use crate::tunnel::db::TunnelDatabase; use crate::util::serde::{HandlerExtSerde, Pem, display_serializable}; -use crate::util::tui::{choose, parse_as, prompt, prompt_multiline}; +use crate::util::tui::{choose, choose_custom_display, parse_as, prompt, prompt_multiline}; #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] @@ -229,10 +229,19 @@ pub async fn import_certificate_cli( cert_string.truncate(0); let cert = cert?; - let key = cert.0.public_key()?; + let pubkey = cert.0.public_key()?; + + if chain.is_empty() { + if !key.public_eq(&pubkey) { + return Err(Error::new( + eyre!("Certificate does not match key!"), + ErrorKind::InvalidSignature, + )); + } + } if let Some(prev) = chain.last() { - if !prev.verify(&key)? { + if !prev.verify(&pubkey)? { return Err(Error::new( eyre!(concat!( "Invalid Fullchain: ", @@ -243,7 +252,7 @@ pub async fn import_certificate_cli( } } - let is_root = cert.0.verify(&key)?; + let is_root = cert.0.verify(&pubkey)?; chain.push(cert.0); @@ -494,8 +503,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { .await?, )?; println!("📝 SSL Certificate:"); - println!("{cert}"); - println!(); + print!("{cert}"); println!(concat!( "If you haven't already, ", @@ -507,25 +515,35 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { Err(e) if e.kind == ErrorKind::ParseNetAddress => { println!("Select the IP address at which to host the web interface:"); - let available_ips = from_value::>( + let mut suggested_addrs = from_value::>( ctx.call_remote::("web.get-available-ips", json!({})) .await?, )?; - let suggested_addrs = available_ips - .into_iter() - .filter(|ip| match ip { - IpAddr::V4(ipv4) => !ipv4.is_private() && !ipv4.is_loopback(), - IpAddr::V6(ipv6) => { - !ipv6.is_loopback() - && !ipv6.is_unique_local() - && !ipv6.is_unicast_link_local() + suggested_addrs.sort_by_cached_key(|a| match a { + IpAddr::V4(a) => { + if a.is_loopback() { + 3 + } else if a.is_private() { + 2 + } else { + 0 } - }) - .chain([Ipv6Addr::UNSPECIFIED.into()]) - .collect::>(); + } + IpAddr::V6(a) => { + if a.is_loopback() { + 5 + } else if a.is_unicast_link_local() { + 4 + } else { + 1 + } + } + }); - let ip = if suggested_addrs.len() > 16 { + let ip = if suggested_addrs.is_empty() { + prompt("Listen Address: ", parse_as::("IP Address"), None).await? + } else if suggested_addrs.len() > 16 { prompt( &format!("Listen Address [{}]: ", suggested_addrs[0]), parse_as::("IP Address"), @@ -533,7 +551,22 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { ) .await? } else { - *choose("Listen Address:", &suggested_addrs).await? + *choose_custom_display("Listen Address:", &suggested_addrs, |a| match a { + a if a.is_loopback() => { + format!("{a} (Loopback Address: only use if planning to proxy traffic)") + } + IpAddr::V4(a) if a.is_private() => { + format!("{a} (Private Address: only available from Local Area Network)") + } + IpAddr::V6(a) if a.is_unicast_link_local() => { + format!( + "[{a}] (Private Address: only available from Local Area Network)" + ) + } + IpAddr::V6(a) => format!("[{a}]"), + a => a.to_string(), + }) + .await? }; println!(concat!( @@ -638,8 +671,8 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { println!("Generating a random password..."); let params = SetPasswordParams { password: base32::encode( - base32::Alphabet::Rfc4648 { padding: false }, - &rand::random::<[u8; 10]>(), + base32::Alphabet::Rfc4648Lower { padding: false }, + &rand::random::<[u8; 16]>(), ), }; ctx.call_remote::("auth.set-password", to_value(¶ms)?) diff --git a/core/startos/src/util/tui.rs b/core/startos/src/util/tui.rs index ae30246b6..eb721c2bb 100644 --- a/core/startos/src/util/tui.rs +++ b/core/startos/src/util/tui.rs @@ -95,16 +95,14 @@ pub async fn prompt_multiline< Ok(res) } -pub async fn choose<'t, T: std::fmt::Display>( +pub async fn choose_custom_display<'t, T: std::fmt::Display>( prompt: &str, choices: &'t [T], + mut display: impl FnMut(&T) -> String, ) -> Result<&'t T, Error> { let mut io = DefaultIoDevices::default(); let style = r3bl_tui::readline_async::StyleSheet::default(); - let string_choices = choices - .into_iter() - .map(|c| c.to_string()) - .collect::>(); + let string_choices = choices.into_iter().map(|c| display(c)).collect::>(); let choice = r3bl_tui::readline_async::choose( prompt, string_choices.clone(), @@ -137,3 +135,10 @@ pub async fn choose<'t, T: std::fmt::Display>( println!("{prompt} {choice}"); Ok(&choice) } + +pub async fn choose<'t, T: std::fmt::Display>( + prompt: &str, + choices: &'t [T], +) -> Result<&'t T, Error> { + choose_custom_display(prompt, choices, |t| t.to_string()).await +}