Files
mumble-web2/proxy/src/main.rs
T

396 lines
13 KiB
Rust

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<String> {
vec!["localhost".into()]
}
#[derive(Deserialize)]
struct Config {
https_listen_address: SocketAddr,
http_listen_address: Option<SocketAddr>,
cert_path: Option<PathBuf>,
key_path: Option<PathBuf>,
#[serde(default = "default_cert_alt_names")]
cert_alt_names: Vec<String>,
mumble_server_url: String,
mumble_server_address: Option<SocketAddr>,
gui_path: PathBuf,
gui: Mutex<GuiConfig>,
}
static CONFIG: OnceCell<Config> = 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(
"</head>",
&format!(
"<script>window.config = {}</script>\n</head>",
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<u8>,
}
#[derive(Clone)]
pub struct ConfigCraft {
client_config: MumbleClientConfig,
}
#[craft]
impl ConfigCraft {
#[craft(handler)]
async fn get_config(&self) -> Json<MumbleClientConfig> {
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<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
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();
}