use color_eyre::eyre::{anyhow, bail, Context, Result}; use color_eyre::owo_colors::OwoColorize; use mumble_web2_common::GuiConfig; use once_cell::sync::OnceCell; use rcgen::date_time_ymd; use salvo::conn::rustls::{Keycert, RustlsConfig}; use salvo::cors::{AllowOrigin, Cors}; use salvo::logging::Logger; use salvo::prelude::*; use salvo::proto::quic::BidiStream; use serde::{Deserialize, Serialize}; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tokio::fs; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::pin; use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier}; use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime}; use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct}; use tokio_rustls::{rustls, TlsConnector}; use tracing::info; use tracing::info_span; use tracing::Instrument; use tracing::{error, instrument}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::EnvFilter; fn default_cert_alt_names() -> Vec { vec!["localhost".into()] } #[derive(Deserialize)] struct Config { https_listen_address: SocketAddr, http_listen_address: Option, cert_path: Option, key_path: Option, #[serde(default = "default_cert_alt_names")] cert_alt_names: Vec, mumble_server_url: String, mumble_server_address: Option, gui_path: PathBuf, gui: Mutex, } static CONFIG: OnceCell = OnceCell::new(); #[handler] #[instrument] async fn serve_gui_index_html(req: &Request, res: &mut Response) { let config = CONFIG.get().unwrap(); // Load the HTML file let path = config.gui_path.join("index.html"); let html = match fs::read_to_string(&path).await { Ok(content) => content, Err(err) => { error!("could not load {}: {:?}", path.display(), err); res.status_code(StatusCode::INTERNAL_SERVER_ERROR); return; } }; // Insert the script tag with configuration let modified_html = html.replace( "", &format!( "\n", serde_json::to_string(&config.gui).unwrap(), ), ); res.render(Text::Html(modified_html)); } async fn init_config() -> Result<()> { let mut config: Config = toml::from_str( &fs::read_to_string("./config.toml") .await .context("reading config.toml (try making a copy of config.toml.example)")?, )?; let mumble_server_addr = config .mumble_server_url .to_socket_addrs() .context(format!( "parsing mumble_server_url={}", config.mumble_server_url ))? .next() .ok_or(anyhow!( "no socket addrs in mumble_server_url={}", config.mumble_server_url ))?; config.mumble_server_address = Some(mumble_server_addr); CONFIG .set(config) .map_err(|_| anyhow!("config already initialized"))?; Ok(()) } #[tokio::main] async fn main() -> Result<()> { init_logging(); init_config().await?; let config = CONFIG.get().unwrap(); info!("config\n{}", toml::to_string_pretty(&config.gui)?); info!("gui config\n{}", serde_json::to_string_pretty(&config.gui)?); rustls::crypto::aws_lc_rs::default_provider() .install_default() .map_err(|e| anyhow!("could not install crypto provider {e:?}"))?; let (cert, key) = match (&config.cert_path, &config.key_path) { (None, None) => { info!("generating self-signed cert"); // FIXME: redo every <14 days let mut dname = rcgen::DistinguishedName::new(); dname.push(rcgen::DnType::CommonName, "mumble-web self-signed"); let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; let mut cert_params = rcgen::CertificateParams::new(config.cert_alt_names.clone())?; cert_params.distinguished_name = dname; cert_params.not_before = time::OffsetDateTime::now_utc(); cert_params.not_after = cert_params.not_before + time::Duration::days(12); let cert = cert_params.self_signed(&key_pair)?; let hash = hmac_sha256::Hash::hash(cert.der().as_ref()); { let mut gui_config = config.gui.lock().unwrap(); gui_config.cert_hash = Some(hash.into()); } (cert.pem().into(), key_pair.serialize_pem().into()) } (Some(cert_path), Some(key_path)) => { // Read server certs let cert = fs::read(cert_path) .await .context(format!("reading cert {}", cert_path.display()))?; let key = fs::read(key_path) .await .context(format!("reading key {}", key_path.display()))?; (cert, key) } _ => { bail!("please supply both cert_path and key_path (or neither to generate a self-signed cert)") } }; let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice())); let config_craft = ConfigCraft { client_config: MumbleClientConfig { force_proxy: true, proxy_url: "https://localhost:4433".to_string(), cert_hash: config.gui.lock().unwrap().cert_hash.clone().unwrap(), }, }; // Server routing let router = Router::new() .get(serve_gui_index_html) .push(Router::with_path("/proxy").goal(connect_proxy)) .push(Router::with_path("/config").get(config_craft.get_config())) .push(Router::with_path("/<*+rest>").get(StaticDir::new(config.gui_path.clone()))) .hoop(Logger::new()); let cors = Cors::new().allow_origin(AllowOrigin::any()).into_handler(); let service = Service::new(router).hoop(cors); // Create http listeners let http_listener = config.http_listen_address.map(TcpListener::new); let https_listener = TcpListener::new(config.https_listen_address).rustls(rustls_config.clone()); let http3_listener = QuinnListener::new(rustls_config, config.https_listen_address); // Start server match (http_listener, https_listener, http3_listener) { (Some(a), b, c) => { let accepter = a.join(b).join(c).bind().await; Server::new(accepter).serve(service).await; } (None, b, c) => { let accepter = b.join(c).bind().await; Server::new(accepter).serve(service).await; } } Ok(()) } #[derive(Serialize, Clone)] struct MumbleClientConfig { force_proxy: bool, proxy_url: String, cert_hash: Vec, } #[derive(Clone)] pub struct ConfigCraft { client_config: MumbleClientConfig, } #[craft] impl ConfigCraft { #[craft(handler)] async fn get_config(&self) -> Json { Json(self.client_config.clone()) } } #[handler] #[instrument] async fn connect_proxy(req: &mut Request, res: &mut Response) { info!("received proxy request"); let mumble_server_address = CONFIG.get().unwrap().mumble_server_address.unwrap(); let wt = match req.web_transport_mut().await { Ok(wt) => wt, Err(err) => { res.status_code(StatusCode::BAD_REQUEST); res.render(format!("error with webtransport: {err:?}")); return; } }; info!("got webtransport for connection"); use salvo::webtransport::server::AcceptedBi; let (id, bi) = match wt.accept_bi().await { Ok(Some(AcceptedBi::BidiStream(id, bi))) => (id, bi), Ok(Some(AcceptedBi::Request(req, _))) => { res.status_code(StatusCode::BAD_REQUEST); res.render(format!( "expected webtransport stream but got request {req:?}" )); return; } Ok(None) => { res.status_code(StatusCode::BAD_REQUEST); res.render(format!("no bidirectional connection requested")); return; } Err(err) => { res.status_code(StatusCode::INTERNAL_SERVER_ERROR); res.render(format!("error with bidirectional connection: {err:?}")); return; } }; /* let id = wt.session_id(); let bi = match wt.open_bi(id).await { Ok(bi) => bi, Err(err) => { res.status_code(StatusCode::BAD_REQUEST); res.render(format!("could not open bidirectional stream: {err:?}")); return; } }; */ let (outgoing, incoming) = bi.split(); let res = tokio::spawn(async move { if let Err(error) = connect_proxy_impl(mumble_server_address, incoming, outgoing).await { error!("error connecting proxy {error:?}") } }) .await; if let Err(err) = res { error!("crash in connected proxy {err:?}"); } } #[instrument(skip(incoming, outgoing))] async fn connect_proxy_impl( mumble_server_address: SocketAddr, incoming: impl AsyncRead + Send + Sync + 'static, outgoing: impl AsyncWrite + Send + Sync + 'static, ) -> Result<()> { info!("connecting to Mumble server..."); let config = ClientConfig::builder() .dangerous() .with_custom_certificate_verifier(Arc::new(NoCertificateVerification)) .with_no_client_auth(); let connector = TlsConnector::from(Arc::new(config)); let server_tcp = TcpStream::connect(mumble_server_address).await?; let server_stream = connector .connect("example.com".try_into()?, server_tcp) .await?; let (read_server, write_server) = tokio::io::split(server_stream); info!("connected to Mumble server"); // Spawn tasks to handle transmitting data between the WebTransport client and Mumble TCP Server let c2s = tokio::spawn( pass_bytes_loop(incoming, write_server) .instrument(info_span!("Handler", "Client to server")), ); let s2c = tokio::spawn( pass_bytes_loop(read_server, outgoing) .instrument(info_span!("Handler", "Server to client")), ); tokio::select! { res = c2s => res??, res = s2c => res??, }; Ok(()) } #[derive(Debug)] struct NoCertificateVerification; impl ServerCertVerifier for NoCertificateVerification { fn verify_server_cert( &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>, _ocsp: &[u8], _now: UnixTime, ) -> Result { Ok(rustls::client::danger::ServerCertVerified::assertion()) } fn verify_tls12_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn verify_tls13_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn supported_verify_schemes(&self) -> Vec { vec![ rustls::SignatureScheme::RSA_PKCS1_SHA1, rustls::SignatureScheme::ECDSA_SHA1_Legacy, rustls::SignatureScheme::RSA_PKCS1_SHA256, rustls::SignatureScheme::ECDSA_NISTP256_SHA256, rustls::SignatureScheme::RSA_PKCS1_SHA384, rustls::SignatureScheme::ECDSA_NISTP384_SHA384, rustls::SignatureScheme::RSA_PKCS1_SHA512, rustls::SignatureScheme::ECDSA_NISTP521_SHA512, rustls::SignatureScheme::RSA_PSS_SHA256, rustls::SignatureScheme::RSA_PSS_SHA384, rustls::SignatureScheme::RSA_PSS_SHA512, rustls::SignatureScheme::ED25519, rustls::SignatureScheme::ED448, ] } } async fn pass_bytes_loop( client_stream: impl AsyncRead + Sync + Send + 'static, server_stream: impl AsyncWrite + Send + Sync + 'static, ) -> Result<()> { let mut buffer = vec![0; 65536].into_boxed_slice(); pin!(client_stream); pin!(server_stream); loop { let bytes_read = client_stream.read(&mut buffer).await?; if bytes_read == 0 { break Ok(()); } server_stream.write_all(&buffer[..bytes_read]).await?; server_stream.flush().await?; } } fn init_logging() { let env_filter = EnvFilter::builder() .with_default_directive(LevelFilter::DEBUG.into()) .from_env_lossy(); tracing_subscriber::fmt() .with_target(true) .with_level(true) .with_env_filter(env_filter) .init(); }