16 Commits

Author SHA1 Message Date
sam a25cf64681 salvo is working 2024-11-12 15:42:01 -07:00
sam 3c6a436690 wip salvo server 2024-11-11 17:14:53 -07:00
sam 80aedc7269 make proxy part of the project structure 2024-11-11 14:35:04 -07:00
sam efe842f671 Add 'proxy/' from commit 'e1f3bca708f1f5e8ecadc2becb95360a5a9ada13'
git-subtree-dir: proxy
git-subtree-mainline: 70fcd18690
git-subtree-split: e1f3bca708
2024-11-11 14:10:38 -07:00
sam 70fcd18690 actually make logging work 2024-11-11 14:09:05 -07:00
sam e1f3bca708 built-in https 2024-11-09 14:34:12 -07:00
sam 105deab45d use config.toml & send gui config to client 2024-11-09 14:08:16 -07:00
restitux 4055bf24ab Add injected config file 2024-11-09 13:27:27 -07:00
restitux 95c57c4850 Add bundle script 2024-11-09 13:27:15 -07:00
restitux b19f629605 Add static asset serving 2024-11-09 12:50:56 -07:00
restitux 1d8f3fd791 cleanup 2024-08-30 23:18:22 -06:00
restitux 206bf23bdf Fix imports and try_write 2024-05-21 23:52:44 -06:00
restitux 2d3f31754b Add TCP connection to server 2024-05-21 23:29:13 -06:00
sam abd2a2f81c forgot length check 2024-05-21 18:10:17 -04:00
restitux b2cae01bf8 Add debug logging and global connection map 2024-05-21 00:22:41 -06:00
restitux 725db06703 Initial commit...it works? 2024-05-20 00:11:15 -06:00
15 changed files with 4064 additions and 66 deletions
Generated
+1069 -50
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = [ "common","gui"]
members = [ "common","gui", "proxy"]
[workspace.dependencies]
serde = { version = "1.0.214", features = ["derive"] }
+24
View File
@@ -0,0 +1,24 @@
# GUI Development
## Running Desktop
1. `cargo install dioxus-cli --version 0.6.0-alpha.4`
2. `dx build -p mumble-web2-gui --platform desktop`
## Running Web
1. `cargo install dioxus-cli --version 0.6.0-alpha.4`
2. `cargo install cargo install wtransport --example gencert`
3. in the proxy directory:
1. `cp config.toml.example config.toml`
2. run `gencert` and copy the certificate hash into config.toml
3. `cargo run -p mumble-web2-proxy` in the background
## with `dx serve`
4. in the gui directory
1. `export 'MUMBLE_WEB2_GUI_CONFIG={"cert_hash": <CERTIFICATE HASH HERE>, "proxy_url": "https://localhost:4433"}'`
2. `dx serve -p mumble-web2-gui --platform web`
5. connect to `localhost:8080` (most common)
## with `mumble-web2-proxy` only
4. in the gui directory:
1. `dx build -p mumble-web2-gui --platform web`
5. connect to `localhost:4434` (most common)
+1 -1
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Serialize, Default)]
pub struct GuiConfig {
#[serde(default)]
pub force_proxy: bool,
+1 -1
View File
@@ -83,7 +83,7 @@ futures-channel = "0.3.30"
sir = { git = "https://gitlab.com/samsartor/sir", features = ["dioxus"] } # dioxus 0.6
mumble-web2-common = { workspace = true }
serde = { workspace = true }
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["ansi"] }
tracing-subscriber = { version = "0.3.18", features = ["ansi"] }
tracing = "0.1.40"
color-eyre = "0.6.3"
+3 -2
View File
@@ -10,13 +10,14 @@ asset_dir = "public"
[web.app]
# HTML title tag content
title = "mumble-web"
title = "Mumble Web 2"
base_path = "gui"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "public"]
watch_path = ["src", "assets"]
# include `assets` in web platform
[web.resource]
+9 -4
View File
@@ -10,7 +10,7 @@ use mumble_protocol::voice::{VoicePacket, VoicePacketPayload};
use mumble_protocol::Serverbound;
use mumble_web2_common::GuiConfig;
use std::time::Duration;
use tracing::{debug, error, info};
use tracing::{debug, error, info, instrument};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::js_sys::{Promise, Reflect, Uint8Array};
@@ -324,6 +324,7 @@ async fn create_encoder_worklet(
Ok(worklet_node)
}
#[instrument]
pub async fn network_connect(
address: String,
username: String,
@@ -351,16 +352,20 @@ pub async fn network_connect(
debug!("created option object: {:?}", &object);
let mut options = WebTransportOptions::new();
options.server_certificate_hashes(&array);
options.set_server_certificate_hashes(&array);
debug!("created WebTransportOptions");
console::log_1(&options.clone().into());
let transport = WebTransport::new_with_options(&address, &options).ey()?;
debug!("created WebTransport connection object");
console::log_1(&transport.clone().into());
if let Err(e) = wasm_bindgen_futures::JsFuture::from(transport.ready()).await {
bail!("could not connect to transport: {e:?}");
if let Err(e) = wasm_bindgen_futures::JsFuture::from(transport.ready())
.await
.ey()
{
bail!("could not connect to transport: {e}");
}
info!("transport is ready");
+7 -6
View File
@@ -25,6 +25,7 @@ use std::collections::HashMap;
use std::time::Duration;
use tracing::debug;
use tracing::error;
use tracing::info;
pub mod app;
pub mod imp;
@@ -58,10 +59,10 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
spawn(async move {
while let Some(msg) = writer_recv_chan.next().await {
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
debug!("sending {:#?}", msg);
info!("sending packet {:#?}", msg);
}
if let Err(e) = writer.send(msg).await {
error!("error sending message {:?}", e);
error!("error sending packet {:?}", e);
break;
}
}
@@ -73,7 +74,7 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
Some(Err(err)) => bail!("bad version packet: {err:?}"),
None => bail!("no version was recieved"),
};
debug!("got version packet {:#?}", version);
info!("got version packet {:#?}", version);
// Send version packet
let mut msg = msgs::Version::new();
@@ -117,7 +118,7 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
match packet {
Some(Ok(msg)) => {
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
debug!("receiving {:#?}", msg);
info!("receiving packet {:#?}", msg);
}
let res = accept_packet(msg, &mut audio, &mut decoder_map);
if let Err(err) = res {
@@ -133,14 +134,14 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
command = command_future => {
command_future = event_rx.next();
if let Some(command) = &command {
debug!("commanding {:#?}", command);
info!("issuing command {:#?}", command);
}
match command {
Some(Command::Disconnect) => break,
Some(command) => {
let res = accept_command(command, &mut send_chan);
if let Err(err) = res {
error!("error accepting command {:?}", err)
info!("error accepting command {:?}", err)
}
}
None => continue,
+2 -1
View File
@@ -1,4 +1,4 @@
use mumble_web2_gui::app;
use mumble_web2_gui::{app, imp::init_logging};
pub fn main() {
#[cfg(feature = "desktop")]
@@ -7,5 +7,6 @@ pub fn main() {
.build()
.unwrap()
.enter();
init_logging();
dioxus::launch(app::app);
}
+4
View File
@@ -0,0 +1,4 @@
cert.pem
key.pem
bundle
config.toml
+2567
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "mumble-web2-proxy"
version = "0.1.0"
edition = "2021"
[dependencies]
color-eyre = "0.6.3"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
tokio = { version = "1.37.0", features = ["full"] }
tokio-rustls = "^0.26"
toml = "0.8.19"
tracing = { version = "0.1.40", features = ["async-await"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
mumble-web2-common = { workspace = true }
salvo = { version = "0.74.2", features = ["quinn", "eyre", "rustls", "serve-static", "logging"] }
once_cell = "1.20.2"
rustls = { version = "^0.23", features = ["aws_lc_rs"] }
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
cargo build --release
rm -rf mumble-web2
git clone https://git.ohea.xyz/mumble/mumble-web2
cd mumble-web2
echo "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]" >server_hash.txt
dx build --release
cd ..
rm -rf bundle
mkdir bundle
cp target/release/mumble-webtransport-proxy bundle
cp -r mumble-web2/dist bundle/gui
+11
View File
@@ -0,0 +1,11 @@
https_listen_address = "127.0.0.1:4433"
http_listen_address = "127.0.0.1:8080"
cert_path = "./cert.pem"
key_path = "./key.pem"
mumble_server_url = "voip.ohea.xyz:64738"
gui_path = "../target/dx/mumble-web2-gui/release/web/public"
[gui]
force_proxy = true
proxy_url = "https://127.0.0.1:4433/proxy"
# cert_hash = [...]
+332
View File
@@ -0,0 +1,332 @@
use color_eyre::eyre::{anyhow, Context, Error, Result};
use mumble_web2_common::GuiConfig;
use once_cell::sync::OnceCell;
use salvo::conn::rustls::{Keycert, RustlsConfig};
use salvo::logging::Logger;
use salvo::prelude::*;
use salvo::proto::quic::BidiStream;
use serde::Deserialize;
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;
#[derive(Deserialize)]
struct Config {
https_listen_address: SocketAddr,
http_listen_address: Option<SocketAddr>,
cert_path: PathBuf,
key_path: PathBuf,
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));
}
#[handler]
async fn redirect_to_gui(res: &mut Response) {
res.render(Redirect::permanent("/gui"));
}
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();
// Server routing
let router = Router::new()
.get(redirect_to_gui)
.push(Router::with_path("/proxy").goal(connect_proxy))
.push(Router::with_path("/gui").get(serve_gui_index_html))
.push(Router::with_path("/gui/<*+rest>").get(StaticDir::new(config.gui_path.clone())))
// right now dioxus assets don't properly handle base_url, so we are stuck with this
.push(
Router::with_path("/assets/<*+rest>")
.get(StaticDir::new(config.gui_path.join("assets"))),
)
.hoop(Logger::new());
// Read server certs
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
let cert = fs::read(&config.cert_path)
.await
.context(format!("reading cert {}", config.cert_path.display()))?;
let key = fs::read(&config.key_path)
.await
.context(format!("reading key {}", config.key_path.display()))?;
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
// 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(router).await;
}
(None, b, c) => {
let accepter = b.join(c).bind().await;
Server::new(accepter).serve(router).await;
}
}
Ok(())
}
#[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();
}