diff --git a/core/startos/src/bins/tunnel.rs b/core/startos/src/bins/tunnel.rs index 0afcb0cf1..5c505551d 100644 --- a/core/startos/src/bins/tunnel.rs +++ b/core/startos/src/bins/tunnel.rs @@ -1,6 +1,6 @@ -use std::collections::BTreeMap; use std::ffi::OsString; use std::net::SocketAddr; +use std::sync::Arc; use std::time::Duration; use clap::Parser; @@ -14,11 +14,12 @@ use visit_rs::Visit; use crate::context::config::ClientConfig; use crate::context::CliContext; use crate::net::gateway::{Bind, BindTcp}; -use crate::net::tls::{ChainedHandler, TlsListener}; +use crate::net::tls::TlsListener; use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer}; use crate::prelude::*; use crate::tunnel::context::{TunnelConfig, TunnelContext}; use crate::tunnel::tunnel_router; +use crate::tunnel::web::TunnelCertHandler; use crate::util::logger::LOGGER; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -47,14 +48,19 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> { let mut sub = https_db.subscribe("/webserver".parse().unwrap()).await; while sub.recv().await.is_some() { while let Err(e) = async { - if let Some(addr) = https_db.peek().await.as_webserver().de()? { + let webserver = https_db.peek().await.into_webserver(); + if webserver.as_enabled().de()? { + let addr = webserver.as_listen().de()?.or_not_found("listen address")?; acceptor_setter.send_if_modified(|a| { let key = WebserverListener::Https(addr); if !a.contains_key(&key) { match (|| { Ok::<_, Error>(TlsListener::new( BindTcp.bind(addr)?, - BasicCertHandler(https_db.clone()), + TunnelCertHandler { + db: https_db.clone(), + crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()), + }, )) })() { Ok(l) => { @@ -130,8 +136,8 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> { .await .with_kind(crate::ErrorKind::Unknown)?; - sig_handler.wait_for_abort().await; - https_thread.wait_for_abort().await; + sig_handler.wait_for_abort().await.with_kind(ErrorKind::Unknown)?; + https_thread.wait_for_abort().await.with_kind(ErrorKind::Unknown)?; Ok::<_, Error>(server) } diff --git a/core/startos/src/net/acme.rs b/core/startos/src/net/acme.rs index 22514fcb7..134a70af8 100644 --- a/core/startos/src/net/acme.rs +++ b/core/startos/src/net/acme.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use async_acme::acme::{Identifier, ACME_TLS_ALPN_NAME}; use clap::builder::ValueParserFactory; use clap::Parser; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; use imbl_value::InternedString; use itertools::Itertools; use models::{ErrorData, FromStrParser}; diff --git a/core/startos/src/net/gateway.rs b/core/startos/src/net/gateway.rs index 884ae9c52..8f07147dc 100644 --- a/core/startos/src/net/gateway.rs +++ b/core/startos/src/net/gateway.rs @@ -552,7 +552,6 @@ async fn watch_ip( let managed = device_proxy.managed().await?; if !managed { - dbg!("unmanaged", &iface); return Ok(()); } let dac = device_proxy.active_connection().await?; diff --git a/core/startos/src/net/tls.rs b/core/startos/src/net/tls.rs index c6355fade..4a71453dd 100644 --- a/core/startos/src/net/tls.rs +++ b/core/startos/src/net/tls.rs @@ -18,6 +18,7 @@ use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor}; use crate::prelude::*; use crate::util::io::{BackTrackingIO, ReadWriter}; use crate::util::serde::MaybeUtf8String; +use crate::util::sync::SyncMutex; #[derive(Debug, Clone, VisitFields)] pub struct TlsMetadata { @@ -115,13 +116,15 @@ impl ResolvesServerCert for SingleCertResolver { pub struct TlsListener TlsHandler<'a, A>> { pub accept: A, pub tls_handler: H, - in_progress: Vec< - BoxFuture< - 'static, - ( - H, - Result, AcceptStream)>, Error>, - ), + in_progress: SyncMutex< + Vec< + BoxFuture< + 'static, + ( + H, + Result, AcceptStream)>, Error>, + ), + >, >, >, } @@ -130,7 +133,7 @@ impl TlsHandler<'a, A>> TlsListener { Self { accept, tls_handler: cert_handler, - in_progress: Vec::new(), + in_progress: SyncMutex::new(Vec::new()), } } } @@ -145,105 +148,103 @@ where &mut self, cx: &mut std::task::Context<'_>, ) -> Poll> { - loop { - if let Some((idx, (handler, res))) = - self.in_progress - .iter_mut() - .enumerate() - .find_map(|(idx, fut)| match fut.poll_unpin(cx) { - Poll::Ready(a) => Some((idx, a)), - Poll::Pending => None, - }) - { - drop(self.in_progress.swap_remove(idx)); - if let Some(res) = res.transpose() { - self.tls_handler = handler; - return Poll::Ready(res); - } - continue; - } - - if let Poll::Ready((metadata, stream)) = self.accept.poll_accept(cx)? { - crate::dbg!("ACCEPTED"); - let mut tls_handler = self.tls_handler.clone(); - self.in_progress.push( - async move { - let res = async { - let mut acceptor = LazyConfigAcceptor::new( - Acceptor::default(), - BackTrackingIO::new(stream), - ); - let mut mid: tokio_rustls::StartHandshake< - BackTrackingIO, - > = match (&mut acceptor).await { - Ok(a) => a, - Err(e) => { - let mut stream = - acceptor.take_io().or_not_found("acceptor io")?; - let (_, buf) = stream.rewind(); - if std::str::from_utf8(buf) - .ok() - .and_then(|buf| { - buf.lines() - .map(|l| l.trim()) - .filter(|l| !l.is_empty()) - .next() - }) - .map_or(false, |buf| { - regex::Regex::new("[A-Z]+ (.+) HTTP/1") - .unwrap() - .is_match(buf) - }) - { - handle_http_on_https(stream).await.log_err(); - - return Ok(None); - } else { - return Err(e).with_kind(ErrorKind::Network); - } - } - }; - let hello = mid.client_hello(); - crate::dbg!("getting config"); - if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { - crate::dbg!("config gotten"); - let metadata = TlsMetadata { - inner: metadata, - tls_info: TlsHandshakeInfo { - sni: hello.server_name().map(InternedString::intern), - alpn: hello - .alpn() - .into_iter() - .flatten() - .map(|a| MaybeUtf8String(a.to_vec())) - .collect(), - }, - }; - let buffered = mid.io.stop_buffering(); - mid.io - .write_all(&buffered) - .await - .with_kind(ErrorKind::Network)?; - return Ok(Some(( - metadata, - Box::pin(mid.into_stream(Arc::new(cfg)).await?) as AcceptStream, - ))); - } - crate::dbg!("no config"); - - Ok(None) + self.in_progress.mutate(|in_progress| { + loop { + if let Some((idx, (handler, res))) = + in_progress.iter_mut().enumerate().find_map(|(idx, fut)| { + match fut.poll_unpin(cx) { + Poll::Ready(a) => Some((idx, a)), + Poll::Pending => None, } - .await; - (tls_handler, res) + }) + { + drop(in_progress.swap_remove(idx)); + if let Some(res) = res.transpose() { + self.tls_handler = handler; + return Poll::Ready(res); } - .boxed(), - ); - continue; - } - break; - } + continue; + } - Poll::Pending + if let Poll::Ready((metadata, stream)) = self.accept.poll_accept(cx)? { + let mut tls_handler = self.tls_handler.clone(); + in_progress.push( + async move { + let res = async { + let mut acceptor = LazyConfigAcceptor::new( + Acceptor::default(), + BackTrackingIO::new(stream), + ); + let mut mid: tokio_rustls::StartHandshake< + BackTrackingIO, + > = match (&mut acceptor).await { + Ok(a) => a, + Err(e) => { + let mut stream = + acceptor.take_io().or_not_found("acceptor io")?; + let (_, buf) = stream.rewind(); + if std::str::from_utf8(buf) + .ok() + .and_then(|buf| { + buf.lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .next() + }) + .map_or(false, |buf| { + regex::Regex::new("[A-Z]+ (.+) HTTP/1") + .unwrap() + .is_match(buf) + }) + { + handle_http_on_https(stream).await.log_err(); + + return Ok(None); + } else { + return Err(e).with_kind(ErrorKind::Network); + } + } + }; + let hello = mid.client_hello(); + if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { + let metadata = TlsMetadata { + inner: metadata, + tls_info: TlsHandshakeInfo { + sni: hello.server_name().map(InternedString::intern), + alpn: hello + .alpn() + .into_iter() + .flatten() + .map(|a| MaybeUtf8String(a.to_vec())) + .collect(), + }, + }; + let buffered = mid.io.stop_buffering(); + mid.io + .write_all(&buffered) + .await + .with_kind(ErrorKind::Network)?; + return Ok(Some(( + metadata, + Box::pin(mid.into_stream(Arc::new(cfg)).await?) + as AcceptStream, + ))); + } + + Ok(None) + } + .await; + (tls_handler, res) + } + .boxed(), + ); + continue; + } + break; + } + + Poll::Pending + }) } } diff --git a/core/startos/src/tunnel/api.rs b/core/startos/src/tunnel/api.rs index 46d352a09..91af3b6d6 100644 --- a/core/startos/src/tunnel/api.rs +++ b/core/startos/src/tunnel/api.rs @@ -331,7 +331,7 @@ pub async fn show_config( } }) { wan_addr - } else if let Some(webserver) = peek.as_webserver().de()? { + } else if let Some(webserver) = peek.as_webserver().as_listen().de()? { webserver.ip() } else { ctx.net_iface diff --git a/core/startos/src/tunnel/auth.rs b/core/startos/src/tunnel/auth.rs index 371dceb62..bcbd1286f 100644 --- a/core/startos/src/tunnel/auth.rs +++ b/core/startos/src/tunnel/auth.rs @@ -31,15 +31,42 @@ impl SignatureAuthContext for TunnelContext { ) -> impl IntoIterator + Send, Error>> + Send { let peek = self.db().peek().await; peek.as_webserver() + .as_listen() .de() .map(|a| a.as_ref().map(InternedString::from_display)) .transpose() .into_iter() .chain( - std::iter::from_fn(move || Some(peek.as_certificates().keys())) - .flatten_ok() - .map_ok(|h| h.0) - .flatten_ok(), + std::iter::once_with(move || { + peek.as_webserver() + .as_certificate() + .de() + .ok() + .flatten() + .and_then(|cert_data| cert_data.cert.0.first().cloned()) + .and_then(|cert| cert.subject_alt_names()) + .into_iter() + .flatten() + .filter_map(|san| { + san.dnsname().map(InternedString::from).or_else(|| { + san.ipaddress().and_then(|ip_bytes| { + let ip: std::net::IpAddr = match ip_bytes.len() { + 4 => std::net::IpAddr::V4(std::net::Ipv4Addr::from( + <[u8; 4]>::try_from(ip_bytes).ok()?, + )), + 16 => std::net::IpAddr::V6(std::net::Ipv6Addr::from( + <[u8; 16]>::try_from(ip_bytes).ok()?, + )), + _ => return None, + }; + Some(InternedString::from_display(&ip)) + }) + }) + }) + .map(Ok) + .collect::>() + }) + .flatten(), ) } fn check_pubkey( diff --git a/core/startos/src/tunnel/db.rs b/core/startos/src/tunnel/db.rs index ec7cba6e0..64d20b99d 100644 --- a/core/startos/src/tunnel/db.rs +++ b/core/startos/src/tunnel/db.rs @@ -1,5 +1,5 @@ use std::collections::BTreeMap; -use std::net::{SocketAddr, SocketAddrV4}; +use std::net::SocketAddrV4; use std::path::PathBuf; use clap::builder::ValueParserFactory; @@ -23,7 +23,7 @@ use crate::prelude::*; use crate::sign::AnyVerifyingKey; use crate::tunnel::auth::SignerInfo; use crate::tunnel::context::TunnelContext; -use crate::tunnel::web::TunnelCertData; +use crate::tunnel::web::WebserverInfo; use crate::tunnel::wg::WgServer; use crate::util::serde::{apply_expr, deserialize_from_str, serialize_display, HandlerExtSerde}; @@ -76,11 +76,10 @@ impl ValueParserFactory for GatewayPort { #[serde(rename_all = "camelCase")] #[model = "Model"] pub struct TunnelDatabase { - pub webserver: Option, + pub webserver: WebserverInfo, pub sessions: Sessions, pub password: Option, pub auth_pubkeys: HashMap, - pub certificates: Option, pub gateways: OrdMap, pub wg: WgServer, pub port_forwards: PortForwards, diff --git a/core/startos/src/tunnel/mod.rs b/core/startos/src/tunnel/mod.rs index 9855eff38..97ef93d4f 100644 --- a/core/startos/src/tunnel/mod.rs +++ b/core/startos/src/tunnel/mod.rs @@ -7,7 +7,6 @@ use rpc_toolkit::Server; use crate::middleware::auth::Auth; use crate::middleware::cors::Cors; use crate::net::static_server::{bad_request, not_found, server_error}; -use crate::net::web_server::{Accept, WebServer}; use crate::rpc_continuations::Guid; use crate::tunnel::context::TunnelContext; diff --git a/core/startos/src/tunnel/web.rs b/core/startos/src/tunnel/web.rs index 6eb4312af..893c27f69 100644 --- a/core/startos/src/tunnel/web.rs +++ b/core/startos/src/tunnel/web.rs @@ -1,6 +1,7 @@ -use std::collections::{BTreeSet, VecDeque}; +use std::collections::VecDeque; use std::io::Write; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::sync::Arc; use clap::Parser; use imbl_value::{json, InternedString}; @@ -10,6 +11,8 @@ use openssl::x509::X509; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio_rustls::rustls::crypto::CryptoProvider; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use tokio_rustls::rustls::server::ClientHello; use tokio_rustls::rustls::ServerConfig; @@ -20,124 +23,281 @@ use crate::net::web_server::Accept; use crate::prelude::*; use crate::tunnel::context::TunnelContext; use crate::tunnel::db::TunnelDatabase; -use crate::util::serde::Pem; +use crate::util::serde::{HandlerExtSerde, Pem}; -#[derive(Debug, Deserialize, Serialize, Parser)] +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct WebserverInfo { + pub enabled: bool, + pub listen: Option, + pub certificate: Option, +} + +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct TunnelCertData { - #[arg(long)] pub key: Pem>, - #[arg(long)] pub cert: Pem>, } -pub struct TunnelCertHandler(TypedPatchDb); +#[derive(Clone)] +pub struct TunnelCertHandler { + pub db: TypedPatchDb, + pub crypto_provider: Arc, +} impl<'a, A> TlsHandler<'a, A> for TunnelCertHandler where - A: Accept, + A: Accept + 'a, + ::Metadata: Send + Sync, { async fn get_config( &'a mut self, - hello: &'a ClientHello<'a>, - metadata: &'a ::Metadata, + _: &'a ClientHello<'a>, + _: &'a ::Metadata, ) -> Option { + let cert_info = self + .db + .peek() + .await + .as_webserver() + .as_certificate() + .de() + .log_err()??; + let cert_chain: Vec<_> = cert_info + .cert + .0 + .iter() + .map(|c| Ok::<_, Error>(CertificateDer::from(c.to_der()?))) + .collect::>() + .log_err()?; + let cert_key = cert_info.key.0.private_key_to_pkcs8().log_err()?; + + Some( + ServerConfig::builder_with_provider(self.crypto_provider.clone()) + .with_safe_default_protocol_versions() + .log_err()? + .with_no_client_auth() + .with_single_cert( + cert_chain, + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(cert_key)), + ) + .log_err()?, + ) } } pub fn web_api() -> ParentHandler { ParentHandler::new() - .subcommand("init", from_fn_async(init_web_rpc).no_cli()) .subcommand( "init", - from_fn_async(init_web_cli) - .with_about("Initialize the webserver") - .no_display(), + from_fn_async(init_web) + .no_display() + .with_about("Initialize the webserver"), + ) + .subcommand( + "set-listen", + from_fn_async(set_listen) + .no_display() + .with_about("Set the listen address for the webserver") + .with_call_remote::(), + ) + .subcommand( + "get-listen", + from_fn_async(get_listen) + .with_display_serializable() + .with_about("Get the listen address for the webserver") + .with_call_remote::(), + ) + .subcommand( + "get-available-ips", + from_fn_async(get_available_ips) + .with_display_serializable() + .with_about("Get available IP addresses to bind to") + .with_call_remote::(), ) .subcommand( "import-certificate", - from_fn_async(import_certificate) - .with_about("Import a certificate to use for the webserver") + from_fn_async(import_certificate_rpc).no_cli(), + ) + .subcommand( + "import-certificate", + from_fn_async(import_certificate_cli) + .no_display() + .with_about("Import a certificate to use for the webserver"), + ) + .subcommand( + "generate-certificate", + from_fn_async(generate_certificate) + .with_about("Generate a self signed certificaet to use for the webserver") + .with_call_remote::(), + ) + .subcommand( + "enable", + from_fn_async(enable_web) + .with_about("Enable the webserver") .no_display() .with_call_remote::(), ) - // .subcommand( - // "forget-certificate", - // from_fn_async(forget_certificate) - // .with_about("Forget a certificate that was imported into the webserver") - // .no_display() - // .with_call_remote::(), - // ) .subcommand( - "uninit", - from_fn_async(uninit_web) - .with_about("Disable the webserver") + "disable", + from_fn_async(disable_web) .no_display() + .with_about("Disable the webserver") + .with_call_remote::(), + ) + .subcommand( + "reset", + from_fn_async(reset_web) + .no_display() + .with_about("Reset the webserver") .with_call_remote::(), ) } -pub async fn import_certificate( +pub async fn import_certificate_rpc( ctx: TunnelContext, cert_data: TunnelCertData, ) -> Result<(), Error> { - let mut saninfo = BTreeSet::new(); - let leaf = cert_data.cert.get(0).ok_or_else(|| { - Error::new( - eyre!("certificate chain is empty"), - ErrorKind::InvalidRequest, - ) - })?; - for san in leaf.subject_alt_names().into_iter().flatten() { - if let Some(dns) = san.dnsname() { - saninfo.insert(dns.into()); - } - if let Some(ip) = san.ipaddress() { - if let Ok::<[u8; 4], _>(ip) = ip.try_into() { - saninfo.insert(InternedString::from_display(&Ipv4Addr::from_bits( - u32::from_be_bytes(ip), - ))); - } else if let Ok::<[u8; 16], _>(ip) = ip.try_into() { - saninfo.insert(InternedString::from_display(&Ipv6Addr::from_bits( - u128::from_be_bytes(ip), - ))); - } - } - } ctx.db .mutate(|db| { - db.as_certificates_mut() - .insert(&JsonKey(saninfo), &cert_data) + db.as_webserver_mut() + .as_certificate_mut() + .ser(&Some(cert_data)) }) .await .result?; Ok(()) } -#[derive(Debug, Deserialize, Serialize, Parser)] -pub struct InitWebParams { - listen: SocketAddr, +pub async fn import_certificate_cli( + HandlerArgs { + context, + parent_method, + method, + .. + }: HandlerArgs, +) -> Result<(), Error> { + println!("Please paste in your PEM encoded private key: "); + let mut stdin_lines = BufReader::new(tokio::io::stdin()).lines(); + let mut key_string = String::new(); + + while let Some(line) = stdin_lines.next_line().await? { + key_string.push_str(&line); + key_string.push_str("\n"); + if line.trim().starts_with("-----END") { + break; + } + } + + let key: Pem> = key_string.parse()?; + + println!("Please paste in your PEM encoded certificate (or certificate chain): "); + + let mut chain = Vec::::new(); + + loop { + let mut cert_string = String::new(); + + while let Some(line) = stdin_lines.next_line().await? { + cert_string.push_str(&line); + cert_string.push_str("\n"); + if line.trim().starts_with("-----END") { + break; + } + } + + let cert: Pem = cert_string.parse()?; + + let key = cert.0.public_key()?; + + if let Some(prev) = chain.last() { + if !prev.verify(&key)? { + return Err(Error::new( + eyre!( + "Invalid Fullchain: Previous cert was not signed by this certificate's key" + ), + ErrorKind::InvalidSignature, + )); + } + } + + let is_root = cert.0.verify(&key)?; + + chain.push(cert.0); + + if is_root { + break; + } + } + + context + .call_remote::( + &parent_method.iter().chain(method.iter()).join("."), + to_value(&TunnelCertData { + key, + cert: Pem(chain), + })?, + ) + .await?; + + Ok(()) } -pub async fn init_web_rpc( +#[derive(Debug, Deserialize, Serialize, Parser)] +pub struct GenerateCertParams { + #[arg(help = "Subject Alternative Name(s)")] + pub subject: Vec, +} + +pub async fn generate_certificate( ctx: TunnelContext, - InitWebParams { listen }: InitWebParams, -) -> Result<(), Error> { + GenerateCertParams { subject }: GenerateCertParams, +) -> Result, Error> { + let saninfo = SANInfo::new(&subject.into_iter().collect()); + + let key = crate::net::ssl::generate_key()?; + let cert = crate::net::ssl::make_self_signed((&key, &saninfo))?; + ctx.db .mutate(|db| { - if db.as_certificates().de()?.is_empty() { - return Err(Error::new( - eyre!("No certificate available"), - ErrorKind::OpenSsl, - )); - } - if db.as_password().transpose_ref().is_none() { - return Err(Error::new( - eyre!("Password not set"), - ErrorKind::Authorization, - )); - } + db.as_webserver_mut() + .as_certificate_mut() + .ser(&Some(TunnelCertData { + key: Pem(key), + cert: Pem(vec![cert.clone()]), + })) + }) + .await + .result?; - db.as_webserver_mut().ser(&Some(listen))?; + Ok(Pem(cert)) +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +pub struct SetListenParams { + pub listen: SocketAddr, +} + +pub async fn set_listen( + ctx: TunnelContext, + SetListenParams { listen }: SetListenParams, +) -> Result<(), Error> { + // Validate that the address is available to bind + tokio::net::TcpListener::bind(listen) + .await + .with_kind(ErrorKind::Network) + .with_ctx(|_| { + ( + ErrorKind::Network, + format!("{} is not available to bind to", listen), + ) + })?; + + ctx.db + .mutate(|db| { + db.as_webserver_mut().as_listen_mut().ser(&Some(listen))?; Ok(()) }) @@ -145,31 +305,130 @@ pub async fn init_web_rpc( .result } -pub async fn uninit_web(ctx: TunnelContext) -> Result<(), Error> { +pub async fn get_listen(ctx: TunnelContext) -> Result, Error> { + ctx.db.peek().await.as_webserver().as_listen().de() +} + +pub async fn get_available_ips(ctx: TunnelContext) -> Result, Error> { + let ips = ctx.net_iface.peek(|interfaces| { + interfaces + .values() + .filter_map(|info| { + info.ip_info + .as_ref() + .and_then(|ip_info| ip_info.subnets.iter().next().map(|subnet| subnet.addr())) + }) + .collect::>() + }); + + Ok(ips) +} + +pub async fn enable_web(ctx: TunnelContext) -> Result<(), Error> { ctx.db - .mutate(|db| db.as_webserver_mut().ser(&None)) + .mutate(|db| { + if db.as_webserver().as_listen().transpose_ref().is_none() { + return Err(Error::new( + eyre!("Listen is not set"), + ErrorKind::ParseNetAddress, + )); + } + if db.as_webserver().as_certificate().transpose_ref().is_none() { + return Err(Error::new( + eyre!("Certificate is not set"), + ErrorKind::OpenSsl, + )); + } + if db.as_password().transpose_ref().is_none() { + return Err(Error::new( + eyre!("Password is not set"), + ErrorKind::Authorization, + )); + }; + db.as_webserver_mut().as_enabled_mut().ser(&true) + }) .await .result } -pub async fn init_web_cli( - HandlerArgs { - context, - parent_method, - method, - params: InitWebParams { listen }, - .. - }: HandlerArgs, -) -> Result<(), Error> { +pub async fn disable_web(ctx: TunnelContext) -> Result<(), Error> { + ctx.db + .mutate(|db| db.as_webserver_mut().as_enabled_mut().ser(&false)) + .await + .result +} + +pub async fn reset_web(ctx: TunnelContext) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_webserver_mut().as_enabled_mut().ser(&false)?; + db.as_webserver_mut().as_listen_mut().ser(&None)?; + db.as_webserver_mut().as_certificate_mut().ser(&None)?; + db.as_password_mut().ser(&None)?; + Ok(()) + }) + .await + .result +} + +pub async fn init_web(ctx: CliContext) -> Result<(), Error> { loop { - match context - .call_remote::( - &parent_method.iter().chain(method.iter()).join("."), - to_value(&InitWebParams { listen })?, - ) + match ctx + .call_remote::("web.enable", json!({})) .await { Ok(_) => println!("Webserver Initialized"), + Err(e) if e.code == ErrorKind::ParseNetAddress as i32 => { + println!("A listen address has not been set yet. Setting one up now..."); + + let available_ips = from_value::>( + ctx.call_remote::("web.get-available-ips", json!({})) + .await?, + )?; + + let suggested_addr = available_ips + .into_iter() + .find(|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() + } + }) + .map(|ip| SocketAddr::new(ip, 8443)) + .unwrap_or_else(|| SocketAddr::from((Ipv6Addr::UNSPECIFIED, 8443))); + + let (mut readline, _writer) = rustyline_async::Readline::new(format!( + "Listen Address [{}]: ", + suggested_addr + )) + .with_kind(ErrorKind::Filesystem)?; + + let listen: SocketAddr = loop { + match readline.readline().await.with_kind(ErrorKind::Filesystem)? { + rustyline_async::ReadlineEvent::Line(l) if !l.trim().is_empty() => { + match l.trim().parse() { + Ok(addr) => break addr, + Err(_) => { + println!("Invalid socket address. Please enter in format IP:PORT (e.g., 0.0.0.0:8443)"); + readline.clear_history(); + } + } + } + rustyline_async::ReadlineEvent::Line(_) => { + break suggested_addr; + } + _ => return Err(Error::new(eyre!("Aborted"), ErrorKind::Unknown)), + } + }; + + ctx.call_remote::( + "web.set-listen", + to_value(&SetListenParams { listen })?, + ) + .await?; + } Err(e) if e.code == ErrorKind::OpenSsl as i32 => { println!( "StartTunnel has not been set up with an SSL Certificate yet. Setting one up now..." @@ -206,15 +465,25 @@ pub async fn init_web_cli( } } if self_signed { + let listen = from_value::>( + ctx.call_remote::("web.get-listen", json!({})) + .await?, + )? + .filter(|a| !a.ip().is_unspecified()); writeln!( writer, "Enter the name(s) to sign the certificate for, separated by commas." )?; readline.clear_history(); + let default_prompt = if let Some(listen) = listen { + format!("Subject Alternative Name(s) [{}]: ", listen.ip()) + } else { + "Subject Alternative Name(s): ".to_string() + }; readline - .update_prompt(&format!("Subject Alternative Name(s) [{}]: ", listen.ip())) + .update_prompt(&default_prompt) .with_kind(ErrorKind::Filesystem)?; - let mut saninfo = BTreeSet::new(); + let mut saninfo = Vec::new(); loop { match readline.readline().await.with_kind(ErrorKind::Filesystem)? { rustyline_async::ReadlineEvent::Line(l) if !l.trim().is_empty() => { @@ -222,83 +491,44 @@ pub async fn init_web_cli( break; } rustyline_async::ReadlineEvent::Line(_) => { + saninfo.extend( + listen + .map(|l| l.ip()) + .as_ref() + .map(InternedString::from_display), + ); readline.clear_history(); + if !saninfo.is_empty() { + break; + } } _ => return Err(Error::new(eyre!("Aborted"), ErrorKind::Unknown)), } } - let key = crate::net::ssl::gen_nistp256()?; - let cert = crate::net::ssl::make_self_signed((&key, &SANInfo::new(&saninfo)))?; - context - .call_remote::( - "web.import-certificate", - to_value(&TunnelCertData { - key: Pem(key), - cert: Pem(vec![cert]), - })?, - ) - .await?; + ctx.call_remote::( + "web.generate-certificate", + to_value(&GenerateCertParams { subject: saninfo })?, + ) + .await?; } else { drop((readline, writer)); - println!("Please paste in your PEM encoded private key: "); - let mut stdin_lines = BufReader::new(tokio::io::stdin()).lines(); - let mut key_string = String::new(); - - while let Some(line) = stdin_lines.next_line().await? { - key_string.push_str(&line); - key_string.push_str("\n"); - if line.trim().starts_with("-----END") { - break; - } - } - - let key: Pem> = key_string.parse()?; - - println!( - "Please paste in your PEM encoded certificate (or certificate chain): " - ); - - let mut chain = Vec::new(); - - loop { - let mut cert_string = String::new(); - - while let Some(line) = stdin_lines.next_line().await? { - cert_string.push_str(&line); - cert_string.push_str("\n"); - if line.trim().starts_with("-----END") { - break; - } - } - - let cert: Pem = cert_string.parse()?; - - let is_root = cert.0.authority_key_id().is_none(); - - chain.push(cert.0); - - if is_root { - break; - } - } - - context - .call_remote::( - "web.import-certificate", - to_value(&TunnelCertData { - key, - cert: Pem(chain), - })?, - ) - .await?; + import_certificate_cli(HandlerArgs { + context: ctx.clone(), + parent_method: vec!["web", "import-certificate"].into(), + method: VecDeque::new(), + params: Empty {}, + inherited_params: Empty {}, + raw_params: json!({}), + }) + .await?; } } Err(e) if e.code == ErrorKind::Authorization as i32 => { println!("A password has not been setup yet. Setting one up now..."); super::auth::set_password_cli(HandlerArgs { - context: context.clone(), + context: ctx.clone(), parent_method: vec!["auth", "set-password"].into(), method: VecDeque::new(), params: Empty {},