Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a25cf64681 | |||
| 3c6a436690 | |||
| 80aedc7269 | |||
| efe842f671 | |||
| 70fcd18690 | |||
| 2211be5324 | |||
| 30a94323b3 | |||
| e1f3bca708 | |||
| 7308a210e2 | |||
| 105deab45d | |||
| 4055bf24ab | |||
| 95c57c4850 | |||
| b19f629605 | |||
| 1d8f3fd791 | |||
| 206bf23bdf | |||
| 2d3f31754b | |||
| abd2a2f81c | |||
| b2cae01bf8 | |||
| 725db06703 |
@@ -1,2 +1,16 @@
|
|||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||||
|
|
||||||
|
[profile]
|
||||||
|
|
||||||
|
[profile.dioxus-wasm]
|
||||||
|
inherits = "dev"
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
[profile.dioxus-server]
|
||||||
|
inherits = "dev"
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
[profile.dioxus-android]
|
||||||
|
inherits = "dev"
|
||||||
|
opt-level = 2
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[language-server.rust-analyzer]
|
||||||
|
config = { cargo = { features = "all" } }
|
||||||
@@ -1,90 +1,16 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "mumble-web2"
|
resolver = "2"
|
||||||
version = "0.1.0"
|
members = [ "common","gui", "proxy"]
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
[workspace.dependencies]
|
||||||
dioxus = { version = "0.5.6" }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
dioxus-web = { version = "0.5.6", optional = true }
|
|
||||||
manganis = "0.2.2"
|
|
||||||
once_cell = "1.19.0"
|
|
||||||
asynchronous-codec = "0.6.2"
|
asynchronous-codec = "0.6.2"
|
||||||
futures = "0.3.30"
|
mumble-web2-common = { path = "common" }
|
||||||
merge-io = "0.3.0"
|
|
||||||
mumble-protocol = { version = "0.5.0", package = "mumble-protocol-2x", default-features = false, features = [
|
|
||||||
"asynchronous-codec",
|
|
||||||
] }
|
|
||||||
serde_json = "1.0.117"
|
|
||||||
tokio-util = { version = "0.7.11", features = ["codec", "compat"] }
|
|
||||||
wasm-bindgen = { version = "0.2.92", optional = true }
|
|
||||||
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
|
||||||
wasm-streams = { version = "0.4.0", optional = true }
|
|
||||||
serde-wasm-bindgen = { version = "0.6.5", optional = true }
|
|
||||||
js-sys = { version = "0.3.70", optional = true }
|
|
||||||
web-sys = { version = "0.3.72", features = [
|
|
||||||
"WebTransport",
|
|
||||||
"console",
|
|
||||||
"WebTransportOptions",
|
|
||||||
"WebTransportBidirectionalStream",
|
|
||||||
"WebTransportSendStream",
|
|
||||||
"WebTransportReceiveStream",
|
|
||||||
"Navigator",
|
|
||||||
"MediaDevices",
|
|
||||||
"AudioDecoder",
|
|
||||||
"AudioDecoderInit",
|
|
||||||
"AudioData",
|
|
||||||
"AudioEncoderConfig",
|
|
||||||
"AudioDecoderConfig",
|
|
||||||
"EncodedAudioChunk",
|
|
||||||
"EncodedAudioChunkInit",
|
|
||||||
"EncodedAudioChunkType",
|
|
||||||
"CodecState",
|
|
||||||
"MediaStreamTrackGenerator",
|
|
||||||
"MediaStreamTrackGeneratorInit",
|
|
||||||
"AudioContext",
|
|
||||||
"AudioContextOptions",
|
|
||||||
"MediaStream",
|
|
||||||
"GainNode",
|
|
||||||
"MediaStreamAudioSourceNode",
|
|
||||||
"BaseAudioContext",
|
|
||||||
"AudioDestinationNode",
|
|
||||||
"AudioWorkletNode",
|
|
||||||
"AudioWorklet",
|
|
||||||
"AudioWorkletProcessor",
|
|
||||||
"MediaStreamConstraints",
|
|
||||||
"WorkletOptions",
|
|
||||||
"AudioEncoder",
|
|
||||||
"AudioEncoderInit",
|
|
||||||
"AudioDataInit",
|
|
||||||
"HtmlAnchorElement",
|
|
||||||
"Url",
|
|
||||||
"Blob",
|
|
||||||
"AudioDataCopyToOptions",
|
|
||||||
"AudioSampleFormat",
|
|
||||||
"Storage",
|
|
||||||
], optional = true}
|
|
||||||
anyhow = "1.0.86"
|
|
||||||
byteorder = "1.5.0"
|
|
||||||
ogg = "0.9.1"
|
|
||||||
ordermap = "0.5.3"
|
|
||||||
html-purifier = "0.3.0"
|
|
||||||
markdown = "0.3.0"
|
|
||||||
gloo-timers = { version = "0.3.0", features = ["futures"], optional = true }
|
|
||||||
futures-channel = "0.3.30"
|
|
||||||
sir = { version = "0.5.0", features = ["dioxus"] }
|
|
||||||
tokio = { version = "1.41.1", features = ["net", "rt"], optional = true }
|
|
||||||
tokio-rustls = { version = "0.26.0", optional = true }
|
|
||||||
|
|
||||||
[features]
|
[workspace.dependencies.mumble-protocol]
|
||||||
web = [
|
version = "0.5.0"
|
||||||
"dioxus/web",
|
package = "mumble-protocol-2x"
|
||||||
"dioxus-web",
|
default-features = false
|
||||||
"wasm-bindgen",
|
features = [
|
||||||
"wasm-bindgen-futures",
|
"asynchronous-codec",
|
||||||
"wasm-streams",
|
|
||||||
"serde-wasm-bindgen",
|
|
||||||
"js-sys",
|
|
||||||
"web-sys",
|
|
||||||
"gloo-timers",
|
|
||||||
]
|
]
|
||||||
desktop = ["dioxus/desktop", "tokio", "tokio-rustls"]
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "mumble-web2-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Default)]
|
||||||
|
pub struct GuiConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub force_proxy: bool,
|
||||||
|
pub proxy_url: Option<String>,
|
||||||
|
pub cert_hash: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
[package]
|
||||||
|
name = "mumble-web2-gui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Web Dependencies
|
||||||
|
# ================
|
||||||
|
dioxus-web = { version = "0.6.0-alpha.4", optional = true }
|
||||||
|
wasm-bindgen = { version = "0.2.92", optional = true }
|
||||||
|
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
||||||
|
wasm-streams = { version = "0.4.0", optional = true }
|
||||||
|
serde-wasm-bindgen = { version = "0.6.5", optional = true }
|
||||||
|
js-sys = { version = "0.3.70", optional = true }
|
||||||
|
web-sys = { version = "0.3.72", features = [
|
||||||
|
"WebTransport",
|
||||||
|
"console",
|
||||||
|
"WebTransportOptions",
|
||||||
|
"WebTransportBidirectionalStream",
|
||||||
|
"WebTransportSendStream",
|
||||||
|
"WebTransportReceiveStream",
|
||||||
|
"Navigator",
|
||||||
|
"MediaDevices",
|
||||||
|
"AudioDecoder",
|
||||||
|
"AudioDecoderInit",
|
||||||
|
"AudioData",
|
||||||
|
"AudioEncoderConfig",
|
||||||
|
"AudioDecoderConfig",
|
||||||
|
"EncodedAudioChunk",
|
||||||
|
"EncodedAudioChunkInit",
|
||||||
|
"EncodedAudioChunkType",
|
||||||
|
"CodecState",
|
||||||
|
"MediaStreamTrackGenerator",
|
||||||
|
"MediaStreamTrackGeneratorInit",
|
||||||
|
"AudioContext",
|
||||||
|
"AudioContextOptions",
|
||||||
|
"MediaStream",
|
||||||
|
"GainNode",
|
||||||
|
"MediaStreamAudioSourceNode",
|
||||||
|
"BaseAudioContext",
|
||||||
|
"AudioDestinationNode",
|
||||||
|
"AudioWorkletNode",
|
||||||
|
"AudioWorklet",
|
||||||
|
"AudioWorkletProcessor",
|
||||||
|
"MediaStreamConstraints",
|
||||||
|
"WorkletOptions",
|
||||||
|
"AudioEncoder",
|
||||||
|
"AudioEncoderInit",
|
||||||
|
"AudioDataInit",
|
||||||
|
"HtmlAnchorElement",
|
||||||
|
"Url",
|
||||||
|
"Blob",
|
||||||
|
"AudioDataCopyToOptions",
|
||||||
|
"AudioSampleFormat",
|
||||||
|
"Storage",
|
||||||
|
], optional = true}
|
||||||
|
gloo-timers = { version = "0.3.0", features = ["futures"], optional = true }
|
||||||
|
tracing-web = { version = "0.1.3", optional = true }
|
||||||
|
|
||||||
|
# Desktop Dependecies
|
||||||
|
# ===================
|
||||||
|
dioxus-desktop = { version = "0.6.0-alpha.4", optional = true}
|
||||||
|
tokio = { version = "1.41.1", features = ["net", "rt"], optional = true }
|
||||||
|
tokio-rustls = { version = "0.26.0", optional = true }
|
||||||
|
|
||||||
|
|
||||||
|
# Base Dependencies
|
||||||
|
# ================
|
||||||
|
dioxus = { version = "0.6.0-alpha.4" }
|
||||||
|
once_cell = "1.19.0"
|
||||||
|
asynchronous-codec = { workspace = true }
|
||||||
|
futures = "0.3.30"
|
||||||
|
merge-io = "0.3.0"
|
||||||
|
mumble-protocol = { workspace = true }
|
||||||
|
serde_json = "1.0.117"
|
||||||
|
tokio-util = { version = "0.7.11", features = ["codec", "compat"] }
|
||||||
|
byteorder = "1.5.0"
|
||||||
|
ogg = "0.9.1"
|
||||||
|
ordermap = "0.5.3"
|
||||||
|
html-purifier = "0.3.0"
|
||||||
|
markdown = "0.3.0"
|
||||||
|
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", features = ["ansi"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
web = [
|
||||||
|
"dioxus/web",
|
||||||
|
"dioxus-web",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"js-sys",
|
||||||
|
"web-sys",
|
||||||
|
"gloo-timers",
|
||||||
|
"tracing-web",
|
||||||
|
]
|
||||||
|
desktop = ["dioxus/desktop", "tokio", "tokio-rustls", "tracing-subscriber/env-filter"]
|
||||||
@@ -10,13 +10,14 @@ asset_dir = "public"
|
|||||||
|
|
||||||
[web.app]
|
[web.app]
|
||||||
# HTML title tag content
|
# HTML title tag content
|
||||||
title = "mumble-web"
|
title = "Mumble Web 2"
|
||||||
|
base_path = "gui"
|
||||||
|
|
||||||
[web.watcher]
|
[web.watcher]
|
||||||
# when watcher trigger, regenerate the `index.html`
|
# when watcher trigger, regenerate the `index.html`
|
||||||
reload_html = true
|
reload_html = true
|
||||||
# which files or dirs will be watcher monitoring
|
# which files or dirs will be watcher monitoring
|
||||||
watch_path = ["src", "public"]
|
watch_path = ["src", "assets"]
|
||||||
|
|
||||||
# include `assets` in web platform
|
# include `assets` in web platform
|
||||||
[web.resource]
|
[web.resource]
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 737 B After Width: | Height: | Size: 737 B |
@@ -1,12 +1,11 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use manganis::mg;
|
|
||||||
use ordermap::OrderSet;
|
use ordermap::OrderSet;
|
||||||
use sir::{css, global_css};
|
use sir::{css, global_css};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::imp;
|
use crate::{imp, CONFIG};
|
||||||
|
|
||||||
pub type ChannelId = u32;
|
pub type ChannelId = u32;
|
||||||
pub type UserId = u32;
|
pub type UserId = u32;
|
||||||
@@ -111,15 +110,15 @@ pub enum UserIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UserIcon {
|
impl UserIcon {
|
||||||
pub fn url(self) -> Option<&'static str> {
|
pub fn url(self) -> Option<Asset> {
|
||||||
// speaker from https://www.svgrepo.com/collection/ikono-bold-line-icons/
|
// speaker from https://www.svgrepo.com/collection/ikono-bold-line-icons/
|
||||||
// mic from https://www.svgrepo.com/collection/hashicorp-line-interface-icons/
|
// mic from https://www.svgrepo.com/collection/hashicorp-line-interface-icons/
|
||||||
|
|
||||||
use UserIcon::*;
|
use UserIcon::*;
|
||||||
Some(match self {
|
Some(match self {
|
||||||
Normal => "/mic-svgrepo-com.svg",
|
Normal => asset!("assets/mic-svgrepo-com.svg"),
|
||||||
Muted => "/mic-off-svgrepo-com.svg",
|
Muted => asset!("assets/mic-off-svgrepo-com.svg"),
|
||||||
Deafened => "/speaker-muted-svgrepo-com.svg",
|
Deafened => asset!("assets/speaker-muted-svgrepo-com.svg"),
|
||||||
None => return Option::None,
|
None => return Option::None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -160,19 +159,26 @@ pub fn UserPill(name: String, icon: UserIcon) -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn User(id: UserId) -> Element {
|
pub fn User(id: UserId) -> Element {
|
||||||
let server = STATE.server.read();
|
let server = STATE.server.read();
|
||||||
let state = server.users.get(&id)?;
|
match server.users.get(&id) {
|
||||||
rsx!(UserPill {
|
Some(state) => rsx!(UserPill {
|
||||||
name: state.name.clone(),
|
name: state.name.clone(),
|
||||||
icon: state.icon(),
|
icon: state.icon(),
|
||||||
})
|
}),
|
||||||
|
None => rsx!(UserPill {
|
||||||
|
name: format!("unknown user ({id})"),
|
||||||
|
icon: UserIcon::None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Channel(id: ChannelId) -> Element {
|
pub fn Channel(id: ChannelId) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let server = STATE.server.read();
|
let server = STATE.server.read();
|
||||||
let user = server.session?;
|
let user = server.session.unwrap();
|
||||||
let state = server.channels.get(&id)?;
|
let Some(state) = server.channels.get(&id) else {
|
||||||
|
return rsx!("missing channel {id}");
|
||||||
|
};
|
||||||
|
|
||||||
let channel_details = css!(
|
let channel_details = css!(
|
||||||
"
|
"
|
||||||
@@ -318,13 +324,16 @@ pub fn ChatView() -> Element {
|
|||||||
pub fn ServerView() -> Element {
|
pub fn ServerView() -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let server = STATE.server.read();
|
let server = STATE.server.read();
|
||||||
let &UserState {
|
let Some(&UserState {
|
||||||
deaf,
|
deaf,
|
||||||
self_deaf,
|
self_deaf,
|
||||||
mute,
|
mute,
|
||||||
self_mute,
|
self_mute,
|
||||||
..
|
..
|
||||||
} = server.this_user()?;
|
}) = server.this_user()
|
||||||
|
else {
|
||||||
|
return rsx!();
|
||||||
|
};
|
||||||
|
|
||||||
let grid = css!(
|
let grid = css!(
|
||||||
r#"
|
r#"
|
||||||
@@ -408,8 +417,8 @@ pub fn ServerView() -> Element {
|
|||||||
disabled: mute,
|
disabled: mute,
|
||||||
onclick: move |_| net.send(SetMute { mute: !self_mute }),
|
onclick: move |_| net.send(SetMute { mute: !self_mute }),
|
||||||
match mute || self_mute {
|
match mute || self_mute {
|
||||||
true => rsx!(img { src: "/mic-off-svgrepo-com.svg" }),
|
true => rsx!(img { src: asset!("assets/mic-off-svgrepo-com.svg") }),
|
||||||
false => rsx!(img { src: "/mic-svgrepo-com.svg" }),
|
false => rsx!(img { src: asset!("assets/mic-svgrepo-com.svg") }),
|
||||||
}
|
}
|
||||||
"\u{00A0}Mute"
|
"\u{00A0}Mute"
|
||||||
}
|
}
|
||||||
@@ -419,8 +428,8 @@ pub fn ServerView() -> Element {
|
|||||||
disabled: deaf,
|
disabled: deaf,
|
||||||
onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }),
|
onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }),
|
||||||
match deaf || self_deaf {
|
match deaf || self_deaf {
|
||||||
true => rsx!(img { src: "/speaker-muted-svgrepo-com.svg" }),
|
true => rsx!(img { src: asset!("assets/speaker-muted-svgrepo-com.svg") }),
|
||||||
false => rsx!(img { src: "/speaker-medium-svgrepo-com.svg" }),
|
false => rsx!(img { src: asset!("assets/speaker-medium-svgrepo-com.svg") }),
|
||||||
}
|
}
|
||||||
"\u{00A0}Deafen"
|
"\u{00A0}Deafen"
|
||||||
}
|
}
|
||||||
@@ -444,7 +453,7 @@ pub fn ServerView() -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn LoginView() -> Element {
|
pub fn LoginView() -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let default_address = option_env!("MUMBLEWEB2_WEBTRANSPORT_SERVER_ADDRESS").unwrap_or("");
|
let default_address = CONFIG.proxy_url.as_deref().unwrap_or("");
|
||||||
let mut address = use_signal(|| default_address.to_string());
|
let mut address = use_signal(|| default_address.to_string());
|
||||||
|
|
||||||
let previous_username = imp::load_username();
|
let previous_username = imp::load_username();
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
use crate::app::Command;
|
use crate::app::Command;
|
||||||
use anyhow::Result;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::{UnboundedReceiver, UnboundedSender};
|
use dioxus::hooks::{UnboundedReceiver, UnboundedSender};
|
||||||
use futures::io::{AsyncRead, AsyncWrite};
|
use futures::io::{AsyncRead, AsyncWrite};
|
||||||
use mumble_protocol::control::{ClientControlCodec, ControlPacket};
|
use mumble_protocol::control::{ClientControlCodec, ControlPacket};
|
||||||
use mumble_protocol::Serverbound;
|
use mumble_protocol::Serverbound;
|
||||||
|
use mumble_web2_common::GuiConfig;
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
use std::{fmt, io, sync::Arc};
|
use std::{fmt, io, sync::Arc};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::task::LocalSet;
|
|
||||||
use tokio_rustls::rustls;
|
use tokio_rustls::rustls;
|
||||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
@@ -19,60 +19,22 @@ use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt
|
|||||||
pub use tokio::task::spawn;
|
pub use tokio::task::spawn;
|
||||||
pub use tokio::time::sleep;
|
pub use tokio::time::sleep;
|
||||||
|
|
||||||
pub struct Error(anyhow::Error);
|
|
||||||
|
|
||||||
pub trait ImpRead: AsyncRead + Unpin + Send + 'static {}
|
pub trait ImpRead: AsyncRead + Unpin + Send + 'static {}
|
||||||
impl<T: AsyncRead + Unpin + Send + 'static> ImpRead for T {}
|
impl<T: AsyncRead + Unpin + Send + 'static> ImpRead for T {}
|
||||||
|
|
||||||
pub trait ImpWrite: AsyncWrite + Unpin + Send + 'static {}
|
pub trait ImpWrite: AsyncWrite + Unpin + Send + 'static {}
|
||||||
impl<T: AsyncWrite + Unpin + Send + 'static> ImpWrite for T {}
|
impl<T: AsyncWrite + Unpin + Send + 'static> ImpWrite for T {}
|
||||||
|
|
||||||
impl From<anyhow::Error> for Error {
|
|
||||||
fn from(value: anyhow::Error) -> Self {
|
|
||||||
Error(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
Error(value.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub fn new(text: String) -> Self {
|
|
||||||
Self(anyhow::Error::msg(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log(&self) {
|
|
||||||
eprintln!("{}", self.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt::Display::fmt(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt::Debug::fmt(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AudioSystem();
|
pub struct AudioSystem();
|
||||||
|
|
||||||
impl AudioSystem {
|
impl AudioSystem {
|
||||||
pub fn new(sender: UnboundedSender<ControlPacket<Serverbound>>) -> Result<Self, Error> {
|
pub fn new(sender: UnboundedSender<ControlPacket<Serverbound>>) -> Result<Self, Error> {
|
||||||
// dbg!("todo");
|
// TODO
|
||||||
Ok(AudioSystem())
|
Ok(AudioSystem())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
|
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
|
||||||
// dbg!("todo");
|
// TODO
|
||||||
Ok(AudioPlayer())
|
Ok(AudioPlayer())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +43,7 @@ pub struct AudioPlayer();
|
|||||||
|
|
||||||
impl AudioPlayer {
|
impl AudioPlayer {
|
||||||
pub fn play_opus(&mut self, payload: &[u8]) {
|
pub fn play_opus(&mut self, payload: &[u8]) {
|
||||||
// dbg!("todo");
|
// TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,17 +119,17 @@ pub async fn network_connect(
|
|||||||
let server_tcp = TcpStream::connect(addr).await?;
|
let server_tcp = TcpStream::connect(addr).await?;
|
||||||
let server_stream = connector
|
let server_stream = connector
|
||||||
//.connect("127.0.0.1".try_into()?, server_tcp)
|
//.connect("127.0.0.1".try_into()?, server_tcp)
|
||||||
.connect(address.try_into().map_err(anyhow::Error::from)?, server_tcp)
|
.connect(address.try_into()?, server_tcp)
|
||||||
.await?;
|
.await?;
|
||||||
let (read_server, write_server) = tokio::io::split(server_stream);
|
let (read_server, write_server) = tokio::io::split(server_stream);
|
||||||
|
|
||||||
let read_codec = ClientControlCodec::new();
|
let read_codec = ClientControlCodec::new();
|
||||||
let write_codec = ClientControlCodec::new();
|
let write_codec = ClientControlCodec::new();
|
||||||
|
|
||||||
let mut reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
||||||
let mut writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
||||||
|
|
||||||
super::network_loop(username, event_rx, reader, writer).await
|
crate::network_loop(username, event_rx, reader, writer).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_default_username(username: &str) -> Option<()> {
|
pub fn set_default_username(username: &str) -> Option<()> {
|
||||||
@@ -177,3 +139,22 @@ pub fn set_default_username(username: &str) -> Option<()> {
|
|||||||
pub fn load_username() -> Option<String> {
|
pub fn load_username() -> Option<String> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_config() -> Option<GuiConfig> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_logging() {
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
|
|
||||||
|
let env_filter = EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy();
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_target(true)
|
||||||
|
.with_level(true)
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#[cfg(feature = "web")]
|
||||||
|
mod web;
|
||||||
|
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
mod desktop;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "web", not(feature = "desktop")))]
|
||||||
|
pub use web::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
pub use desktop::*;
|
||||||
@@ -1,26 +1,19 @@
|
|||||||
use crate::app::Command;
|
use crate::app::Command;
|
||||||
use crate::bail;
|
use crate::CONFIG;
|
||||||
|
use color_eyre::eyre::{bail, eyre, Error};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use futures::AsyncRead;
|
use futures::{AsyncRead, AsyncWrite};
|
||||||
use futures::AsyncWrite;
|
|
||||||
use futures_channel::mpsc::UnboundedSender;
|
use futures_channel::mpsc::UnboundedSender;
|
||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use manganis::mg;
|
use mumble_protocol::control::{ClientControlCodec, ControlPacket};
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::voice::{VoicePacket, VoicePacketPayload};
|
||||||
use mumble_protocol::control::ControlPacket;
|
|
||||||
use mumble_protocol::voice::VoicePacket;
|
|
||||||
use mumble_protocol::voice::VoicePacketPayload;
|
|
||||||
use mumble_protocol::Serverbound;
|
use mumble_protocol::Serverbound;
|
||||||
use std::fmt;
|
use mumble_web2_common::GuiConfig;
|
||||||
use std::io;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tracing::{debug, error, info, instrument};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::console;
|
use web_sys::js_sys::{Promise, Reflect, Uint8Array};
|
||||||
use web_sys::js_sys::Promise;
|
|
||||||
use web_sys::js_sys::Reflect;
|
|
||||||
use web_sys::js_sys::Uint8Array;
|
|
||||||
use web_sys::window;
|
|
||||||
use web_sys::AudioContext;
|
use web_sys::AudioContext;
|
||||||
use web_sys::AudioContextOptions;
|
use web_sys::AudioContextOptions;
|
||||||
use web_sys::AudioData;
|
use web_sys::AudioData;
|
||||||
@@ -43,6 +36,7 @@ use web_sys::WebTransport;
|
|||||||
use web_sys::WebTransportBidirectionalStream;
|
use web_sys::WebTransportBidirectionalStream;
|
||||||
use web_sys::WebTransportOptions;
|
use web_sys::WebTransportOptions;
|
||||||
use web_sys::WorkletOptions;
|
use web_sys::WorkletOptions;
|
||||||
|
use web_sys::{console, window};
|
||||||
|
|
||||||
pub use wasm_bindgen_futures::spawn_local as spawn;
|
pub use wasm_bindgen_futures::spawn_local as spawn;
|
||||||
|
|
||||||
@@ -56,58 +50,27 @@ pub async fn sleep(d: Duration) {
|
|||||||
TimeoutFuture::new(d.as_millis() as u32).await
|
TimeoutFuture::new(d.as_millis() as u32).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Error(JsValue);
|
trait ResultExt<T> {
|
||||||
|
fn ey(self) -> Result<T, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
impl From<anyhow::Error> for Error {
|
impl<T> ResultExt<T> for Result<T, JsValue> {
|
||||||
fn from(value: anyhow::Error) -> Self {
|
fn ey(self) -> Result<T, Error> {
|
||||||
Error(JsError::new(&value.to_string()).into())
|
match self {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(e) => match e.dyn_into::<js_sys::Error>() {
|
||||||
|
Ok(e) => Err(eyre!("{}: {}", e.name(), e.message())),
|
||||||
|
Err(e) => Err(eyre!("{:?}", e)),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl<T> ResultExt<T> for Result<T, JsError> {
|
||||||
fn from(value: io::Error) -> Self {
|
fn ey(self) -> Result<T, Error> {
|
||||||
Error(JsError::new(&value.to_string()).into())
|
self.map_err(|e| JsValue::from(e)).ey()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<JsValue> for Error {
|
|
||||||
fn from(value: JsValue) -> Self {
|
|
||||||
Error(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<JsError> for Error {
|
|
||||||
fn from(value: JsError) -> Self {
|
|
||||||
Error(JsError::from(value).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub fn new(text: String) -> Self {
|
|
||||||
wasm_bindgen::JsError::new(&text).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log(&self) {
|
|
||||||
console::error_1(&self.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let text: String = js_sys::Object::from(self.0.clone()).to_string().into();
|
|
||||||
f.write_str(&text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let text: String = js_sys::Object::from(self.0.clone()).to_string().into();
|
|
||||||
f.write_str(&text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AudioSystem(AudioContext);
|
pub struct AudioSystem(AudioContext);
|
||||||
|
|
||||||
impl AudioSystem {
|
impl AudioSystem {
|
||||||
@@ -119,8 +82,8 @@ impl AudioSystem {
|
|||||||
let audio_context_worklet = audio_context.clone();
|
let audio_context_worklet = audio_context.clone();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
match create_encoder_worklet(&audio_context_worklet, sender).await {
|
match create_encoder_worklet(&audio_context_worklet, sender).await {
|
||||||
Ok(node) => console::log_2(&"Created audio worklet:".into(), &node),
|
Ok(node) => info!("created encoder worklet: {:?}", &node),
|
||||||
Err(err) => err.log(),
|
Err(err) => error!("could not create encoder worklet: {err}"),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,21 +94,25 @@ impl AudioSystem {
|
|||||||
let audio_context = &self.0;
|
let audio_context = &self.0;
|
||||||
|
|
||||||
let audio_stream_generator =
|
let audio_stream_generator =
|
||||||
MediaStreamTrackGenerator::new(&MediaStreamTrackGeneratorInit::new("audio"))?;
|
MediaStreamTrackGenerator::new(&MediaStreamTrackGeneratorInit::new("audio")).ey()?;
|
||||||
|
|
||||||
// Create MediaStream from MediaStreamTrackGenerator
|
// Create MediaStream from MediaStreamTrackGenerator
|
||||||
let js_tracks = web_sys::js_sys::Array::new();
|
let js_tracks = web_sys::js_sys::Array::new();
|
||||||
js_tracks.push(&audio_stream_generator);
|
js_tracks.push(&audio_stream_generator);
|
||||||
let media_stream = MediaStream::new_with_tracks(&js_tracks)?;
|
let media_stream = MediaStream::new_with_tracks(&js_tracks).ey()?;
|
||||||
|
|
||||||
// Create MediaStreamAudioSourceNode
|
// Create MediaStreamAudioSourceNode
|
||||||
let audio_source = audio_context.create_media_stream_source(&media_stream)?;
|
let audio_source = audio_context
|
||||||
|
.create_media_stream_source(&media_stream)
|
||||||
|
.ey()?;
|
||||||
// Connect output of audio_source to audio_context (browser audio)
|
// Connect output of audio_source to audio_context (browser audio)
|
||||||
audio_source.connect_with_audio_node(&audio_context.destination())?;
|
audio_source
|
||||||
|
.connect_with_audio_node(&audio_context.destination())
|
||||||
|
.ey()?;
|
||||||
|
|
||||||
// Create callback functions for AudioDecoder
|
// Create callback functions for AudioDecoder
|
||||||
let error = Closure::wrap(Box::new(move |e: JsValue| {
|
let decoder_error = Closure::wrap(Box::new(move |e: JsValue| {
|
||||||
console::error_1(&e);
|
error!("error decoding audio {:?}", e);
|
||||||
}) as Box<dyn FnMut(JsValue)>);
|
}) as Box<dyn FnMut(JsValue)>);
|
||||||
|
|
||||||
// This knows what MediaStreamTrackGenerator to use as it closes around it
|
// This knows what MediaStreamTrackGenerator to use as it closes around it
|
||||||
@@ -156,29 +123,33 @@ impl AudioSystem {
|
|||||||
}
|
}
|
||||||
if let Err(e) = writable.get_writer().map(|writer| {
|
if let Err(e) = writable.get_writer().map(|writer| {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
if let Err(e) = JsFuture::from(writer.ready()).await {
|
if let Err(e) = JsFuture::from(writer.ready()).await.ey() {
|
||||||
console::error_1(&format!("write chunk ready error {:?}", e).into());
|
error!("write chunk ready error {:?}", e);
|
||||||
}
|
}
|
||||||
if let Err(e) = JsFuture::from(writer.write_with_chunk(&audio_data)).await {
|
if let Err(e) = JsFuture::from(writer.write_with_chunk(&audio_data))
|
||||||
console::error_1(&format!("write chunk error {:?}", e).into());
|
.await
|
||||||
|
.ey()
|
||||||
|
{
|
||||||
|
error!("write chunk error {:?}", e);
|
||||||
};
|
};
|
||||||
writer.release_lock();
|
writer.release_lock();
|
||||||
});
|
});
|
||||||
}) {
|
}) {
|
||||||
console::error_1(&e);
|
error!("error writing audio data {:?}", e);
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(AudioData)>);
|
}) as Box<dyn FnMut(AudioData)>);
|
||||||
|
|
||||||
let audio_decoder = AudioDecoder::new(&AudioDecoderInit::new(
|
let audio_decoder = AudioDecoder::new(&AudioDecoderInit::new(
|
||||||
error.as_ref().unchecked_ref(),
|
decoder_error.as_ref().unchecked_ref(),
|
||||||
output.as_ref().unchecked_ref(),
|
output.as_ref().unchecked_ref(),
|
||||||
))?;
|
))
|
||||||
|
.ey()?;
|
||||||
|
|
||||||
audio_decoder.configure(&AudioDecoderConfig::new("opus", 1, 48000));
|
audio_decoder.configure(&AudioDecoderConfig::new("opus", 1, 48000));
|
||||||
console::log_1(&"Created Audio Decoder".into());
|
info!("created audio decoder");
|
||||||
|
|
||||||
// This is required to prevent these from being deallocated
|
// This is required to prevent these from being deallocated
|
||||||
error.forget();
|
decoder_error.forget();
|
||||||
output.forget();
|
output.forget();
|
||||||
|
|
||||||
Ok(AudioPlayer(audio_decoder))
|
Ok(AudioPlayer(audio_decoder))
|
||||||
@@ -227,32 +198,41 @@ async fn create_encoder_worklet(
|
|||||||
let stream = window()
|
let stream = window()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.navigator()
|
.navigator()
|
||||||
.media_devices()?
|
.media_devices()
|
||||||
.get_user_media_with_constraints(MediaStreamConstraints::new().audio(&JsValue::TRUE))?
|
.ey()?
|
||||||
|
.get_user_media_with_constraints(MediaStreamConstraints::new().audio(&JsValue::TRUE))
|
||||||
|
.ey()?
|
||||||
.into_future()
|
.into_future()
|
||||||
.await?
|
.await
|
||||||
|
.ey()?
|
||||||
.dyn_into()
|
.dyn_into()
|
||||||
.map_err(|e| JsError::new(&format!("not a stream: {e:?}")))?;
|
.map_err(|e| JsError::new(&format!("not a stream: {e:?}")))
|
||||||
|
.ey()?;
|
||||||
|
|
||||||
let options = WorkletOptions::new();
|
let options = WorkletOptions::new();
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&options,
|
&options,
|
||||||
&"processorOptions".into(),
|
&"processorOptions".into(),
|
||||||
&wasm_bindgen::module(),
|
&wasm_bindgen::module(),
|
||||||
)?;
|
)
|
||||||
|
.ey()?;
|
||||||
|
|
||||||
let module = "/rust_mic_worklet.js";
|
let module = asset!("assets/rust_mic_worklet.js").to_string();
|
||||||
console::log_1(&format!("Loading mic worklet from {module:?}").into());
|
info!("loading mic worklet from {module:?}");
|
||||||
audio_context
|
audio_context
|
||||||
.audio_worklet()?
|
.audio_worklet()
|
||||||
.add_module_with_options(module, &options)?
|
.ey()?
|
||||||
|
.add_module_with_options(&module, &options)
|
||||||
|
.ey()?
|
||||||
.into_future()
|
.into_future()
|
||||||
.await?;
|
.await
|
||||||
|
.ey()?;
|
||||||
|
|
||||||
let source = audio_context.create_media_stream_source(&stream)?;
|
let source = audio_context.create_media_stream_source(&stream).ey()?;
|
||||||
let worklet_node = AudioWorkletNode::new(audio_context, "rust_mic_worklet")?;
|
let worklet_node = AudioWorkletNode::new(audio_context, "rust_mic_worklet").ey()?;
|
||||||
|
|
||||||
let error: Closure<dyn FnMut(JsValue)> = Closure::new(|e| console::error_1(&e));
|
let encoder_error: Closure<dyn FnMut(JsValue)> =
|
||||||
|
Closure::new(|e| error!("error encoding audio {:?}", e));
|
||||||
|
|
||||||
let download_buffer = std::cell::RefCell::new(Vec::new());
|
let download_buffer = std::cell::RefCell::new(Vec::new());
|
||||||
|
|
||||||
@@ -286,13 +266,13 @@ async fn create_encoder_worklet(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let audio_encoder = AudioEncoder::new(&AudioEncoderInit::new(
|
let audio_encoder = AudioEncoder::new(&AudioEncoderInit::new(
|
||||||
error.as_ref().unchecked_ref(),
|
encoder_error.as_ref().unchecked_ref(),
|
||||||
output.as_ref().unchecked_ref(),
|
output.as_ref().unchecked_ref(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// This is required to prevent these from being deallocated
|
// This is required to prevent these from being deallocated
|
||||||
error.forget();
|
encoder_error.forget();
|
||||||
output.forget();
|
output.forget();
|
||||||
let encoder_config = AudioEncoderConfig::new("opus");
|
let encoder_config = AudioEncoderConfig::new("opus");
|
||||||
encoder_config.set_number_of_channels(1);
|
encoder_config.set_number_of_channels(1);
|
||||||
@@ -300,7 +280,7 @@ async fn create_encoder_worklet(
|
|||||||
encoder_config.set_bitrate(72_000.0);
|
encoder_config.set_bitrate(72_000.0);
|
||||||
|
|
||||||
audio_encoder.configure(&encoder_config);
|
audio_encoder.configure(&encoder_config);
|
||||||
console::log_1(&"Created Audio Encoder".into());
|
info!("created audio encoder");
|
||||||
|
|
||||||
let download_buffer = std::cell::RefCell::new(Vec::new());
|
let download_buffer = std::cell::RefCell::new(Vec::new());
|
||||||
|
|
||||||
@@ -321,41 +301,36 @@ async fn create_encoder_worklet(
|
|||||||
audio_encoder.encode(&data);
|
audio_encoder.encode(&data);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
console::error_1(&err);
|
error!(
|
||||||
console::debug_1(&event);
|
"error creating AudioData object {:?} during event {:?}",
|
||||||
|
err, event,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Reflect::set(
|
Reflect::set(
|
||||||
&Reflect::get(&worklet_node, &"port".into())?,
|
&Reflect::get(&worklet_node, &"port".into()).ey()?,
|
||||||
&"onmessage".into(),
|
&"onmessage".into(),
|
||||||
onmessage.as_ref(),
|
onmessage.as_ref(),
|
||||||
)?;
|
)
|
||||||
|
.ey()?;
|
||||||
onmessage.forget();
|
onmessage.forget();
|
||||||
|
|
||||||
source.connect_with_audio_node(&worklet_node)?;
|
source.connect_with_audio_node(&worklet_node).ey()?;
|
||||||
worklet_node.connect_with_audio_node(&audio_context.destination())?;
|
worklet_node
|
||||||
|
.connect_with_audio_node(&audio_context.destination())
|
||||||
|
.ey()?;
|
||||||
|
|
||||||
Ok(worklet_node)
|
Ok(worklet_node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
pub async fn network_connect(
|
pub async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
console::log_1(&"Rust via WASM!".into());
|
info!("Rust via WASM!");
|
||||||
|
|
||||||
let Ok(server_hash): Result<Vec<u8>, _> = include_str!("../../server_hash.txt")
|
|
||||||
.trim()
|
|
||||||
.trim_matches(&['[', ']'])
|
|
||||||
.split(',')
|
|
||||||
.map(|x| x.trim().parse())
|
|
||||||
.collect()
|
|
||||||
else {
|
|
||||||
bail!("could not parse server hash");
|
|
||||||
};
|
|
||||||
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
|
||||||
|
|
||||||
let object = web_sys::js_sys::Object::new();
|
let object = web_sys::js_sys::Object::new();
|
||||||
|
|
||||||
@@ -363,34 +338,42 @@ pub async fn network_connect(
|
|||||||
&object,
|
&object,
|
||||||
&JsValue::from_str("algorithm"),
|
&JsValue::from_str("algorithm"),
|
||||||
&JsValue::from_str("sha-256"),
|
&JsValue::from_str("sha-256"),
|
||||||
)?;
|
)
|
||||||
|
.ey()?;
|
||||||
|
|
||||||
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash)?;
|
if let Some(server_hash) = &CONFIG.cert_hash {
|
||||||
|
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
||||||
|
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
||||||
|
}
|
||||||
|
|
||||||
let array = web_sys::js_sys::Array::new();
|
let array = web_sys::js_sys::Array::new();
|
||||||
array.push(&object);
|
array.push(&object);
|
||||||
|
|
||||||
console::log_1(&object.clone().into());
|
debug!("created option object: {:?}", &object);
|
||||||
console::log_1(&"Created option object!".into());
|
|
||||||
|
|
||||||
let mut options = WebTransportOptions::new();
|
let mut options = WebTransportOptions::new();
|
||||||
options.server_certificate_hashes(&array);
|
options.set_server_certificate_hashes(&array);
|
||||||
|
|
||||||
console::log_1(&"Created WebTransportOptions!".into());
|
debug!("created WebTransportOptions");
|
||||||
|
console::log_1(&options.clone().into());
|
||||||
|
|
||||||
let transport = WebTransport::new_with_options(&address, &options)?;
|
let transport = WebTransport::new_with_options(&address, &options).ey()?;
|
||||||
console::log_1(&"Created WebTransport connection object.".into());
|
debug!("created WebTransport connection object");
|
||||||
console::log_1(&transport.clone().into());
|
console::log_1(&transport.clone().into());
|
||||||
|
|
||||||
if let Err(e) = wasm_bindgen_futures::JsFuture::from(transport.ready()).await {
|
if let Err(e) = wasm_bindgen_futures::JsFuture::from(transport.ready())
|
||||||
bail!("could not connect to transport: {e:?}");
|
.await
|
||||||
|
.ey()
|
||||||
|
{
|
||||||
|
bail!("could not connect to transport: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
console::log_1(&"Transport is ready.".into());
|
info!("transport is ready");
|
||||||
|
|
||||||
let stream: WebTransportBidirectionalStream =
|
let stream: WebTransportBidirectionalStream =
|
||||||
wasm_bindgen_futures::JsFuture::from(transport.create_bidirectional_stream())
|
wasm_bindgen_futures::JsFuture::from(transport.create_bidirectional_stream())
|
||||||
.await?
|
.await
|
||||||
|
.ey()?
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let wasm_stream_readable = wasm_streams::ReadableStream::from_raw(stream.readable().into());
|
let wasm_stream_readable = wasm_streams::ReadableStream::from_raw(stream.readable().into());
|
||||||
@@ -404,7 +387,7 @@ pub async fn network_connect(
|
|||||||
let writer =
|
let writer =
|
||||||
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
||||||
|
|
||||||
super::network_loop(username, event_rx, reader, writer).await
|
crate::network_loop(username, event_rx, reader, writer).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_default_username(username: &str) -> Option<()> {
|
pub fn set_default_username(username: &str) -> Option<()> {
|
||||||
@@ -423,3 +406,34 @@ pub fn load_username() -> Option<String> {
|
|||||||
.get_item("username")
|
.get_item("username")
|
||||||
.ok()?
|
.ok()?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_config_from_window() -> Option<GuiConfig> {
|
||||||
|
serde_wasm_bindgen::from_value(Reflect::get(window()?.as_ref(), &"config".into()).ok()?).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config_from_env() -> Option<GuiConfig> {
|
||||||
|
serde_json::from_str(option_env!("MUMBLE_WEB2_GUI_CONFIG")?).ok()?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config() -> Option<GuiConfig> {
|
||||||
|
load_config_from_window().or_else(load_config_from_env)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_logging() {
|
||||||
|
// copied from tracing_web example usage
|
||||||
|
|
||||||
|
use tracing_subscriber::fmt::format::Pretty;
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
use tracing_web::{performance_layer, MakeWebConsoleWriter};
|
||||||
|
|
||||||
|
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_ansi(false) // Only partially supported across browsers
|
||||||
|
.without_time() // std::time is not available in browsers
|
||||||
|
.with_writer(MakeWebConsoleWriter::new()); // write events to the console
|
||||||
|
let perf_layer = performance_layer().with_details_from_fields(Pretty::default());
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt_layer)
|
||||||
|
.with(perf_layer)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ use app::ConnectionState;
|
|||||||
use app::STATE;
|
use app::STATE;
|
||||||
use asynchronous_codec::FramedRead;
|
use asynchronous_codec::FramedRead;
|
||||||
use asynchronous_codec::FramedWrite;
|
use asynchronous_codec::FramedWrite;
|
||||||
|
use color_eyre::eyre::{bail, Error};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use futures::select;
|
use futures::select;
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
@@ -11,44 +12,36 @@ use futures::SinkExt as _;
|
|||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use futures_channel::mpsc::UnboundedSender;
|
use futures_channel::mpsc::UnboundedSender;
|
||||||
pub use imp::spawn;
|
pub use imp::spawn;
|
||||||
pub use imp::Error;
|
|
||||||
use mumble_protocol::control::msgs;
|
use mumble_protocol::control::msgs;
|
||||||
use mumble_protocol::control::ControlCodec;
|
use mumble_protocol::control::ControlCodec;
|
||||||
use mumble_protocol::control::ControlPacket;
|
use mumble_protocol::control::ControlPacket;
|
||||||
use mumble_protocol::voice::VoicePacketPayload;
|
use mumble_protocol::voice::VoicePacketPayload;
|
||||||
use mumble_protocol::Clientbound;
|
use mumble_protocol::Clientbound;
|
||||||
use mumble_protocol::Serverbound;
|
use mumble_protocol::Serverbound;
|
||||||
|
use mumble_web2_common::GuiConfig;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tracing::debug;
|
||||||
|
use tracing::error;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
#[path = "imp/web.rs"]
|
|
||||||
pub mod imp;
|
pub mod imp;
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
pub static CONFIG: Lazy<GuiConfig> = Lazy::new(|| imp::load_config().unwrap_or_default());
|
||||||
#[path = "imp/desktop.rs"]
|
|
||||||
pub mod imp;
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! bail {
|
|
||||||
($($x:tt)*) => {
|
|
||||||
return Err(Error::new(format!($($x)*)))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
||||||
loop {
|
loop {
|
||||||
let Some(Command::Connect { address, username }) = event_rx.next().await else {
|
let Some(Command::Connect { address, username }) = event_rx.next().await else {
|
||||||
panic!("Did not receive connect command")
|
panic!("did not receive connect command")
|
||||||
};
|
};
|
||||||
|
|
||||||
*STATE.server.write() = Default::default();
|
*STATE.server.write() = Default::default();
|
||||||
*STATE.status.write() = ConnectionState::Connecting;
|
*STATE.status.write() = ConnectionState::Connecting;
|
||||||
if let Err(error) = imp::network_connect(address, username, &mut event_rx).await {
|
if let Err(error) = imp::network_connect(address, username, &mut event_rx).await {
|
||||||
error.log();
|
error!("could not connect {:?}", error);
|
||||||
*STATE.status.write() = ConnectionState::Failed(error.to_string());
|
*STATE.status.write() = ConnectionState::Failed(error.to_string());
|
||||||
} else {
|
} else {
|
||||||
*STATE.status.write() = ConnectionState::Disconnected;
|
*STATE.status.write() = ConnectionState::Disconnected;
|
||||||
@@ -66,10 +59,10 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
|
|||||||
spawn(async move {
|
spawn(async move {
|
||||||
while let Some(msg) = writer_recv_chan.next().await {
|
while let Some(msg) = writer_recv_chan.next().await {
|
||||||
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
|
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
|
||||||
eprintln!("sending {:#?}", msg);
|
info!("sending packet {:#?}", msg);
|
||||||
}
|
}
|
||||||
if let Err(e) = writer.send(msg).await {
|
if let Err(e) = writer.send(msg).await {
|
||||||
eprintln!("ERROR: {}", e);
|
error!("error sending packet {:?}", e);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,8 +74,7 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
|
|||||||
Some(Err(err)) => bail!("bad version packet: {err:?}"),
|
Some(Err(err)) => bail!("bad version packet: {err:?}"),
|
||||||
None => bail!("no version was recieved"),
|
None => bail!("no version was recieved"),
|
||||||
};
|
};
|
||||||
println!("Got version packet");
|
info!("got version packet {:#?}", version);
|
||||||
println!("{:#?}", version);
|
|
||||||
|
|
||||||
// Send version packet
|
// Send version packet
|
||||||
let mut msg = msgs::Version::new();
|
let mut msg = msgs::Version::new();
|
||||||
@@ -126,28 +118,30 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
|
|||||||
match packet {
|
match packet {
|
||||||
Some(Ok(msg)) => {
|
Some(Ok(msg)) => {
|
||||||
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
||||||
println!("receiving {:#?}", msg);
|
info!("receiving packet {:#?}", msg);
|
||||||
}
|
}
|
||||||
let res = accept_packet(msg, &mut audio, &mut decoder_map);
|
let res = accept_packet(msg, &mut audio, &mut decoder_map);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
err.log();
|
error!("error accepting packet {:?}", err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Err(err)) => Error::from(err).log(),
|
Some(Err(err)) => {
|
||||||
|
error!("error receiving packet {:?}", err)
|
||||||
|
},
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
command = command_future => {
|
command = command_future => {
|
||||||
command_future = event_rx.next();
|
command_future = event_rx.next();
|
||||||
if let Some(command) = &command {
|
if let Some(command) = &command {
|
||||||
println!("commanding {:#?}", command);
|
info!("issuing command {:#?}", command);
|
||||||
}
|
}
|
||||||
match command {
|
match command {
|
||||||
Some(Command::Disconnect) => break,
|
Some(Command::Disconnect) => break,
|
||||||
Some(command) => {
|
Some(command) => {
|
||||||
let res = accept_command(command, &mut send_chan);
|
let res = accept_command(command, &mut send_chan);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
err.log();
|
info!("error accepting command {:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => continue,
|
None => continue,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use mumble_web2::app;
|
use mumble_web2_gui::{app, imp::init_logging};
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -7,5 +7,6 @@ pub fn main() {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.enter();
|
.enter();
|
||||||
|
init_logging();
|
||||||
dioxus::launch(app::app);
|
dioxus::launch(app::app);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
cert.pem
|
||||||
|
key.pem
|
||||||
|
bundle
|
||||||
|
config.toml
|
||||||
@@ -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"] }
|
||||||
@@ -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
|
||||||
@@ -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 = [...]
|
||||||
@@ -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();
|
||||||
|
}
|
||||||