mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
finish api
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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<M> {
|
||||
@@ -115,13 +116,15 @@ impl ResolvesServerCert for SingleCertResolver {
|
||||
pub struct TlsListener<A: Accept, H: for<'a> TlsHandler<'a, A>> {
|
||||
pub accept: A,
|
||||
pub tls_handler: H,
|
||||
in_progress: Vec<
|
||||
BoxFuture<
|
||||
'static,
|
||||
(
|
||||
H,
|
||||
Result<Option<(TlsMetadata<A::Metadata>, AcceptStream)>, Error>,
|
||||
),
|
||||
in_progress: SyncMutex<
|
||||
Vec<
|
||||
BoxFuture<
|
||||
'static,
|
||||
(
|
||||
H,
|
||||
Result<Option<(TlsMetadata<A::Metadata>, AcceptStream)>, Error>,
|
||||
),
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
@@ -130,7 +133,7 @@ impl<A: Accept, H: for<'a> TlsHandler<'a, A>> TlsListener<A, H> {
|
||||
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<Result<(Self::Metadata, AcceptStream), Error>> {
|
||||
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<AcceptStream>,
|
||||
> = 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<AcceptStream>,
|
||||
> = 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,15 +31,42 @@ impl SignatureAuthContext for TunnelContext {
|
||||
) -> impl IntoIterator<Item = Result<impl AsRef<str> + 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::<Vec<_>>()
|
||||
})
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
fn check_pubkey(
|
||||
|
||||
@@ -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<Self>"]
|
||||
pub struct TunnelDatabase {
|
||||
pub webserver: Option<SocketAddr>,
|
||||
pub webserver: WebserverInfo,
|
||||
pub sessions: Sessions,
|
||||
pub password: Option<String>,
|
||||
pub auth_pubkeys: HashMap<AnyVerifyingKey, SignerInfo>,
|
||||
pub certificates: Option<TunnelCertData>,
|
||||
pub gateways: OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||
pub wg: WgServer,
|
||||
pub port_forwards: PortForwards,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Self>"]
|
||||
pub struct WebserverInfo {
|
||||
pub enabled: bool,
|
||||
pub listen: Option<SocketAddr>,
|
||||
pub certificate: Option<TunnelCertData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TunnelCertData {
|
||||
#[arg(long)]
|
||||
pub key: Pem<PKey<Private>>,
|
||||
#[arg(long)]
|
||||
pub cert: Pem<Vec<X509>>,
|
||||
}
|
||||
|
||||
pub struct TunnelCertHandler(TypedPatchDb<TunnelDatabase>);
|
||||
#[derive(Clone)]
|
||||
pub struct TunnelCertHandler {
|
||||
pub db: TypedPatchDb<TunnelDatabase>,
|
||||
pub crypto_provider: Arc<CryptoProvider>,
|
||||
}
|
||||
impl<'a, A> TlsHandler<'a, A> for TunnelCertHandler
|
||||
where
|
||||
A: Accept,
|
||||
A: Accept + 'a,
|
||||
<A as Accept>::Metadata: Send + Sync,
|
||||
{
|
||||
async fn get_config(
|
||||
&'a mut self,
|
||||
hello: &'a ClientHello<'a>,
|
||||
metadata: &'a <A as Accept>::Metadata,
|
||||
_: &'a ClientHello<'a>,
|
||||
_: &'a <A as Accept>::Metadata,
|
||||
) -> Option<ServerConfig> {
|
||||
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::<Result<_, _>>()
|
||||
.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<C: Context>() -> ParentHandler<C> {
|
||||
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::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"get-listen",
|
||||
from_fn_async(get_listen)
|
||||
.with_display_serializable()
|
||||
.with_about("Get the listen address for the webserver")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.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::<CliContext>(),
|
||||
)
|
||||
.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::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"enable",
|
||||
from_fn_async(enable_web)
|
||||
.with_about("Enable the webserver")
|
||||
.no_display()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
// .subcommand(
|
||||
// "forget-certificate",
|
||||
// from_fn_async(forget_certificate)
|
||||
// .with_about("Forget a certificate that was imported into the webserver")
|
||||
// .no_display()
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
.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::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"reset",
|
||||
from_fn_async(reset_web)
|
||||
.no_display()
|
||||
.with_about("Reset the webserver")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
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<CliContext>,
|
||||
) -> 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<PKey<Private>> = key_string.parse()?;
|
||||
|
||||
println!("Please paste in your PEM encoded certificate (or certificate chain): ");
|
||||
|
||||
let mut chain = Vec::<X509>::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<X509> = 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::<TunnelContext>(
|
||||
&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<InternedString>,
|
||||
}
|
||||
|
||||
pub async fn generate_certificate(
|
||||
ctx: TunnelContext,
|
||||
InitWebParams { listen }: InitWebParams,
|
||||
) -> Result<(), Error> {
|
||||
GenerateCertParams { subject }: GenerateCertParams,
|
||||
) -> Result<Pem<X509>, 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<Option<SocketAddr>, Error> {
|
||||
ctx.db.peek().await.as_webserver().as_listen().de()
|
||||
}
|
||||
|
||||
pub async fn get_available_ips(ctx: TunnelContext) -> Result<Vec<IpAddr>, 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::<Vec<IpAddr>>()
|
||||
});
|
||||
|
||||
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<CliContext, InitWebParams>,
|
||||
) -> 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::<TunnelContext>(
|
||||
&parent_method.iter().chain(method.iter()).join("."),
|
||||
to_value(&InitWebParams { listen })?,
|
||||
)
|
||||
match ctx
|
||||
.call_remote::<TunnelContext>("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::<Vec<IpAddr>>(
|
||||
ctx.call_remote::<TunnelContext>("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::<TunnelContext>(
|
||||
"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::<Option<SocketAddr>>(
|
||||
ctx.call_remote::<TunnelContext>("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::<TunnelContext>(
|
||||
"web.import-certificate",
|
||||
to_value(&TunnelCertData {
|
||||
key: Pem(key),
|
||||
cert: Pem(vec![cert]),
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
ctx.call_remote::<TunnelContext>(
|
||||
"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<PKey<Private>> = 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<X509> = cert_string.parse()?;
|
||||
|
||||
let is_root = cert.0.authority_key_id().is_none();
|
||||
|
||||
chain.push(cert.0);
|
||||
|
||||
if is_root {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
.call_remote::<TunnelContext>(
|
||||
"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 {},
|
||||
|
||||
Reference in New Issue
Block a user