Files
mumble-web2/proxy/src/main.rs
T
sam e72bb6d4c4
Build Mumble Web 2 / macos_build (push) Failing after 2s
Build Mumble Web 2 / linux_build (push) Failing after 0s
Build Mumble Web 2 / android_build (push) Failing after 3s
Build Mumble Web 2 / windows_build (push) Failing after 10s
Move mumble UDP ping into common crate (#33)
The desktop GUI doesn't go through the proxy to reach the Mumble server,
so its login screen needs to ping directly. Rather than duplicate the
ping logic, move it into the common crate behind an optional
`networking` feature (so common stays lightweight when the feature
isn't requested).

- common: add `ping_server(address, port)` behind `networking` feature
- proxy: replace the inline UdpSocket ping + ping.rs codec with a call
  to common::ping_server; drop the rand dep
- client: PlatformInterface::get_status now takes an address; desktop
  and mobile call common::ping_server directly, web still uses the
  proxy's /status endpoint
- gui: thread the address from the login input through get_status, so
  it re-pings when the user edits the address

Assisted-by: claude-opus-4-7
Reviewed-on: #33
Reviewed-by: restitux <restitux@ohea.xyz>
Co-authored-by: Sam Sartor <me@samsartor.com>
Co-committed-by: Sam Sartor <me@samsartor.com>
2026-05-05 04:37:19 +00:00

368 lines
12 KiB
Rust

use color_eyre::eyre::{anyhow, bail, Context, Result};
use mumble_web2_common::{ping_server, ProxyOverrides, ServerStatus};
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;
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;
use url::Url;
fn default_cert_alt_names() -> Vec<String> {
vec!["localhost".into()]
}
#[derive(Debug, Deserialize, Serialize)]
struct Config {
proxy_url: Option<Url>,
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: Option<PathBuf>,
}
fn init_config() -> Result<Config> {
let mut config: Config = toml::from_str(
&std::fs::read_to_string("./config.toml")
.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);
Ok(config)
}
#[tokio::main]
async fn main() -> Result<()> {
init_logging();
let server_config = Arc::new(init_config()?);
info!("config:\n{}", toml::to_string_pretty(&*server_config)?);
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
let mut overrides = ProxyOverrides {
proxy_url: match &server_config.proxy_url {
Some(url) => Some(url.to_string()),
None => None,
},
cert_hash: None,
any_server: false,
};
let (cert, key) = match (&server_config.cert_path, &server_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(server_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());
overrides.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()));
info!("proxy overrides:\n{}", toml::to_string_pretty(&overrides)?);
let config_craft = ConfigCraft {
server_config: server_config.clone(),
overrides,
};
let status_craft = StatusCraft {
mumble_server_address: server_config.mumble_server_address.unwrap().clone(),
};
// Server routing
let mut router = Router::new()
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
.push(Router::with_path("/overrides").get(config_craft.get_overrides()))
.push(Router::with_path("/status").get(status_craft.get_status()))
.hoop(Logger::new());
if let Some(gui_path) = server_config.gui_path.clone() {
router =
router.push(Router::with_path("/").get(StaticFile::new(gui_path.join("index.html"))));
router = router.push(Router::with_path("/<*+rest>").get(StaticDir::new(gui_path)));
}
let cors = Cors::new().allow_origin(AllowOrigin::any()).into_handler();
let service = Service::new(router).hoop(cors);
// Create http listeners
let http_listener = server_config.http_listen_address.map(TcpListener::new);
let https_listener =
TcpListener::new(server_config.https_listen_address).rustls(rustls_config.clone());
let http3_listener = QuinnListener::new(rustls_config, server_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(Clone)]
pub struct StatusCraft {
mumble_server_address: SocketAddr,
}
#[craft]
impl StatusCraft {
#[craft(handler)]
async fn get_status(&self) -> Json<ServerStatus> {
let addr = self.mumble_server_address;
match ping_server(&addr.ip().to_string(), addr.port()).await {
Ok(status) => Json(status),
Err(e) => {
error!("ping failed: {e:#}");
Json(ServerStatus::default())
}
}
}
}
#[derive(Clone)]
pub struct ConfigCraft {
server_config: Arc<Config>,
overrides: ProxyOverrides,
}
#[craft]
impl ConfigCraft {
#[craft(handler)]
async fn get_overrides(&self) -> Json<ProxyOverrides> {
Json(self.overrides.clone())
}
#[craft(handler)]
async fn connect_proxy(&self, req: &mut Request, res: &mut Response) {
info!("received proxy request");
let mumble_server_address = self.server_config.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 (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");
// Handle transmitting data between the WebTransport client and Mumble TCP Server
// When one direction completes/fails, the other is dropped and its streams are closed
tokio::select! {
res = pass_bytes_loop(incoming, write_server)
.instrument(info_span!("Handler", "Client to server")) => res?,
res = pass_bytes_loop(read_server, outgoing)
.instrument(info_span!("Handler", "Server to client")) => 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();
}