2 Commits

Author SHA1 Message Date
sam 55a91b1459 actually read the config maybe 2025-10-25 20:03:19 -06:00
sam d9695be153 proper reactivity on config load 2025-10-25 19:42:08 -06:00
8 changed files with 80 additions and 82 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Serialize, Default)] #[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct GuiConfig { pub struct GuiConfig {
#[serde(default)] #[serde(default)]
pub force_proxy: bool, pub force_proxy: bool,
-1
View File
@@ -1,7 +1,6 @@
https_listen_address = "127.0.0.1:4433" https_listen_address = "127.0.0.1:4433"
http_listen_address = "127.0.0.1:8080" http_listen_address = "127.0.0.1:8080"
mumble_server_url = "[SERVER_URL_HERE]" mumble_server_url = "[SERVER_URL_HERE]"
gui_path = "target/dx/mumble-web2-gui/release/web/public"
[gui] [gui]
force_proxy = true force_proxy = true
-2
View File
@@ -4,8 +4,6 @@ http_listen_address = "127.0.0.1:4400"
#key_path = "./key.pem" #key_path = "./key.pem"
#mumble_server_url = "voip.ohea.xyz:64738" #mumble_server_url = "voip.ohea.xyz:64738"
mumble_server_url = "127.0.0.1:64738" mumble_server_url = "127.0.0.1:64738"
#gui_path = "./target/dx/mumble-web2-gui/release/web/public"
gui_path = "./target/dx/mumble-web2-gui/debug/web/public"
[gui] [gui]
force_proxy = true force_proxy = true
+25 -23
View File
@@ -9,7 +9,7 @@ use sir::{css, global_css};
use std::collections::HashMap; use std::collections::HashMap;
use tracing::error; use tracing::error;
use crate::{imp, CONFIG}; use crate::imp;
pub type ChannelId = u32; pub type ChannelId = u32;
pub type UserId = u32; pub type UserId = u32;
@@ -26,6 +26,7 @@ pub enum Command {
Connect { Connect {
address: String, address: String,
username: String, username: String,
config: GuiConfig,
}, },
SendChat { SendChat {
markdown: String, markdown: String,
@@ -436,9 +437,7 @@ pub fn ChatView() -> Element {
//}, //},
#[component] #[component]
pub fn ControlView() -> Element { pub fn ControlView(config: Resource<GuiConfig>) -> Element {
let config_future = use_resource(|| CONFIG.get());
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let status = &STATE.status; let status = &STATE.status;
let server = STATE.server.read(); let server = STATE.server.read();
@@ -457,7 +456,7 @@ pub fn ControlView() -> Element {
let current_channel_name = server.channels[&channel].name.clone(); let current_channel_name = server.channels[&channel].name.clone();
let Some(proxy_url) = config_future let Some(proxy_url) = config
.read_unchecked() .read_unchecked()
.as_ref() .as_ref()
.and_then(|gui_config| gui_config.proxy_url.clone()) .and_then(|gui_config| gui_config.proxy_url.clone())
@@ -706,7 +705,7 @@ pub fn ControlView() -> Element {
} }
#[component] #[component]
pub fn ServerView() -> Element { pub fn ServerView(config: Resource<GuiConfig>) -> 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 Some(&UserState { let Some(&UserState {
@@ -797,25 +796,26 @@ pub fn ServerView() -> Element {
} }
div { div {
class: "{control_box}", class: "{control_box}",
ControlView {} ControlView { config }
} }
} }
) )
} }
#[component] #[component]
pub fn LoginView() -> Element { pub fn LoginView(config: Resource<GuiConfig>) -> Element {
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let config_future = use_resource(|| CONFIG.get()); let mut address_input = use_signal(|| None::<String>);
let mut address = use_memo(move || {
let default_address = &*config_future if let Some(addr) = address_input() {
.read_unchecked() addr.clone()
.as_ref() } else {
.and_then(|gui_config| gui_config.proxy_url.clone()) config()
.unwrap_or("".to_string()); .and_then(|c| c.proxy_url.clone())
.unwrap_or_default()
let mut address = use_signal(|| default_address.to_string()); }
});
let previous_username = imp::load_username(); let previous_username = imp::load_username();
let mut username = use_signal(|| previous_username.unwrap_or(String::new())); let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
@@ -869,6 +869,7 @@ pub fn LoginView() -> Element {
net.send(Connect { net.send(Connect {
address: address.read().clone(), address: address.read().clone(),
username: username.read().clone(), username: username.read().clone(),
config: config.read().clone().unwrap_or_default(),
}) })
}; };
let status = &STATE.status; let status = &STATE.status;
@@ -918,7 +919,7 @@ pub fn LoginView() -> Element {
placeholder: "server address", placeholder: "server address",
value: "{address.read()}", value: "{address.read()}",
autofocus: "true", autofocus: "true",
oninput: move |evt| address.set(evt.value().clone()), oninput: move |evt| address_input.set(Some(evt.value().clone())),
} }
{bottom} {bottom}
} }
@@ -927,9 +928,10 @@ pub fn LoginView() -> Element {
pub fn app() -> Element { pub fn app() -> Element {
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx)); use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
use_future(|| async move { let config = use_resource(|| async move {
if let Err(err) = imp::load_config().await { match imp::load_config().await {
error!("{}", err) Ok(config) => config,
Err(_) => GuiConfig::default(),
} }
}); });
@@ -1006,8 +1008,8 @@ pub fn app() -> Element {
sir::AppStyle { } sir::AppStyle { }
match *STATE.status.read() { match *STATE.status.read() {
Connected => rsx!(ServerView {}), Connected => rsx!(ServerView { config }),
_ => rsx!(LoginView {}), _ => rsx!(LoginView { config }),
} }
) )
} }
+5 -1
View File
@@ -17,7 +17,7 @@ use tokio_rustls::rustls::ClientConfig;
use tokio_rustls::rustls::DigitallySignedStruct; use tokio_rustls::rustls::DigitallySignedStruct;
use tokio_rustls::TlsConnector; use tokio_rustls::TlsConnector;
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
use tracing::{error, warn}; use tracing::{error, info, instrument, warn};
pub use tokio::task::spawn; pub use tokio::task::spawn;
pub use tokio::time::sleep; pub use tokio::time::sleep;
@@ -183,11 +183,15 @@ impl ServerCertVerifier for NoCertificateVerification {
} }
} }
#[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>,
gui_config: &GuiConfig,
) -> Result<(), Error> { ) -> Result<(), Error> {
info!("connecting");
let config = ClientConfig::builder() let config = ClientConfig::builder()
.dangerous() .dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification)) .with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
+21 -24
View File
@@ -1,5 +1,4 @@
use crate::app::Command; use crate::app::Command;
use crate::CONFIG;
use color_eyre::eyre::{bail, eyre, Error}; use color_eyre::eyre::{bail, eyre, Error};
use dioxus::prelude::*; use dioxus::prelude::*;
use futures::{AsyncRead, AsyncWrite}; use futures::{AsyncRead, AsyncWrite};
@@ -9,7 +8,9 @@ use mumble_protocol::control::{ClientControlCodec, ControlPacket};
use mumble_protocol::voice::{VoicePacket, VoicePacketPayload}; use mumble_protocol::voice::{VoicePacket, VoicePacketPayload};
use mumble_protocol::Serverbound; use mumble_protocol::Serverbound;
use mumble_web2_common::GuiConfig; use mumble_web2_common::GuiConfig;
use reqwest::Url;
use std::time::Duration; use std::time::Duration;
use tracing::level_filters::LevelFilter;
use tracing::{debug, error, info, instrument}; use tracing::{debug, error, info, instrument};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
@@ -308,8 +309,9 @@ pub async fn network_connect(
address: String, address: String,
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
gui_config: &GuiConfig,
) -> Result<(), Error> { ) -> Result<(), Error> {
info!("Rust via WASM!"); info!("connecting");
let object = web_sys::js_sys::Object::new(); let object = web_sys::js_sys::Object::new();
@@ -320,8 +322,7 @@ pub async fn network_connect(
) )
.ey()?; .ey()?;
if let Some(server_hash) = &CONFIG.try_get().and_then(|cfg| cfg.cert_hash) { if let Some(server_hash) = &gui_config.cert_hash {
error!("{:?}", server_hash);
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice()); let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?; web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
} }
@@ -387,27 +388,20 @@ pub fn load_username() -> Option<String> {
.ok()? .ok()?
} }
fn load_config_from_window() -> Option<GuiConfig> { pub async fn load_config() -> color_eyre::Result<GuiConfig> {
serde_wasm_bindgen::from_value(Reflect::get(window()?.as_ref(), &"config".into()).ok()?).ok() let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
} Some(url) => Url::parse(url)?,
None => {
let window: web_sys::Window = web_sys::window().expect("no global `window` exists");
let location = window.location();
Url::parse(&location.href().ey()?)?.join("config")?
}
};
info!("loading config from {}", config_url);
fn load_config_from_env() -> Option<GuiConfig> { let config = reqwest::get(config_url).await?.json::<GuiConfig>().await?;
serde_json::from_str(option_env!("MUMBLE_WEB2_GUI_CONFIG")?).ok()?
}
pub async fn load_config() -> color_eyre::Result<()> { Ok(config)
let config_url = option_env!("MUMBLE_WEB2_GUI_CONFIG_URL").ok_or(eyre!("foo"))?;
let config = reqwest::get(config_url)
.await
.unwrap()
.json::<GuiConfig>()
.await
.unwrap();
crate::CONFIG.set(config);
Ok(())
} }
pub fn init_logging() { pub fn init_logging() {
@@ -420,11 +414,14 @@ pub fn init_logging() {
let fmt_layer = tracing_subscriber::fmt::layer() let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false) // Only partially supported across browsers .with_ansi(false) // Only partially supported across browsers
.without_time() // std::time is not available in browsers .without_time() // std::time is not available in browsers
.with_writer(MakeWebConsoleWriter::new()); // write events to the console .with_writer(MakeWebConsoleWriter::new()) // write events to the console
.with_filter(LevelFilter::DEBUG);
let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); let perf_layer = performance_layer().with_details_from_fields(Pretty::default());
tracing_subscriber::registry() tracing_subscriber::registry()
.with(fmt_layer) .with(fmt_layer)
.with(perf_layer) .with(perf_layer)
.init(); .init();
info!("logging initiated");
} }
+7 -5
View File
@@ -33,18 +33,20 @@ pub mod app;
pub mod imp; pub mod imp;
mod msghtml; mod msghtml;
//pub static CONFIG: Lazy<GuiConfig> = Lazy::new(|| imp::load_config().unwrap_or_default());
pub static CONFIG: async_cell::sync::AsyncCell<GuiConfig> = async_cell::sync::AsyncCell::new();
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,
config,
}) = 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, &config).await {
error!("could not connect {:?}", error); error!("could not connect {:?}", error);
*STATE.status.write() = ConnectionState::Failed(error.to_string()); *STATE.status.write() = ConnectionState::Failed(error.to_string());
} else { } else {
+21 -25
View File
@@ -31,7 +31,7 @@ fn default_cert_alt_names() -> Vec<String> {
vec!["localhost".into()] vec!["localhost".into()]
} }
#[derive(Deserialize)] #[derive(Debug, Deserialize, Serialize)]
struct Config { struct Config {
https_listen_address: SocketAddr, https_listen_address: SocketAddr,
http_listen_address: Option<SocketAddr>, http_listen_address: Option<SocketAddr>,
@@ -41,7 +41,7 @@ struct Config {
cert_alt_names: Vec<String>, cert_alt_names: Vec<String>,
mumble_server_url: String, mumble_server_url: String,
mumble_server_address: Option<SocketAddr>, mumble_server_address: Option<SocketAddr>,
gui_path: PathBuf, gui_path: Option<PathBuf>,
gui: Mutex<GuiConfig>, gui: Mutex<GuiConfig>,
} }
@@ -52,8 +52,15 @@ static CONFIG: OnceCell<Config> = OnceCell::new();
async fn serve_gui_index_html(req: &Request, res: &mut Response) { async fn serve_gui_index_html(req: &Request, res: &mut Response) {
let config = CONFIG.get().unwrap(); let config = CONFIG.get().unwrap();
let path = match &config.gui_path {
Some(p) => p.join("index.html"),
None => {
res.status_code(StatusCode::NOT_FOUND);
return;
}
};
// Load the HTML file // Load the HTML file
let path = config.gui_path.join("index.html");
let html = match fs::read_to_string(&path).await { let html = match fs::read_to_string(&path).await {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
@@ -74,10 +81,9 @@ async fn serve_gui_index_html(req: &Request, res: &mut Response) {
res.render(Text::Html(modified_html)); res.render(Text::Html(modified_html));
} }
async fn init_config() -> Result<()> { fn init_config() -> Result<()> {
let mut config: Config = toml::from_str( let mut config: Config = toml::from_str(
&fs::read_to_string("./config.toml") &std::fs::read_to_string("./config.toml")
.await
.context("reading config.toml (try making a copy of config.toml.example)")?, .context("reading config.toml (try making a copy of config.toml.example)")?,
)?; )?;
let mumble_server_addr = config let mumble_server_addr = config
@@ -102,10 +108,9 @@ async fn init_config() -> Result<()> {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
init_logging(); init_logging();
init_config().await?; init_config()?;
let config = CONFIG.get().unwrap(); let config = CONFIG.get().unwrap();
info!("config\n{}", toml::to_string_pretty(&config.gui)?); info!("config\n{}", toml::to_string_pretty(&config)?);
info!("gui config\n{}", serde_json::to_string_pretty(&config.gui)?);
rustls::crypto::aws_lc_rs::default_provider() rustls::crypto::aws_lc_rs::default_provider()
.install_default() .install_default()
@@ -150,20 +155,18 @@ async fn main() -> Result<()> {
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice())); let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
let config_craft = ConfigCraft { let config_craft = ConfigCraft {
client_config: MumbleClientConfig { client_config: config.gui.lock().unwrap().clone(),
force_proxy: true,
proxy_url: "https://localhost:4433".to_string(),
cert_hash: config.gui.lock().unwrap().cert_hash.clone().unwrap(),
},
}; };
// Server routing // Server routing
let router = Router::new() let mut router = Router::new()
.get(serve_gui_index_html) .get(serve_gui_index_html)
.push(Router::with_path("/proxy").goal(connect_proxy)) .push(Router::with_path("/proxy").goal(connect_proxy))
.push(Router::with_path("/config").get(config_craft.get_config())) .push(Router::with_path("/config").get(config_craft.get_config()))
.push(Router::with_path("/<*+rest>").get(StaticDir::new(config.gui_path.clone())))
.hoop(Logger::new()); .hoop(Logger::new());
if let Some(gui_path) = config.gui_path.clone() {
router = router.push(Router::with_path("/<*+rest>").get(StaticDir::new(gui_path)));
}
let cors = Cors::new().allow_origin(AllowOrigin::any()).into_handler(); let cors = Cors::new().allow_origin(AllowOrigin::any()).into_handler();
@@ -190,22 +193,15 @@ async fn main() -> Result<()> {
Ok(()) Ok(())
} }
#[derive(Serialize, Clone)]
struct MumbleClientConfig {
force_proxy: bool,
proxy_url: String,
cert_hash: Vec<u8>,
}
#[derive(Clone)] #[derive(Clone)]
pub struct ConfigCraft { pub struct ConfigCraft {
client_config: MumbleClientConfig, client_config: GuiConfig,
} }
#[craft] #[craft]
impl ConfigCraft { impl ConfigCraft {
#[craft(handler)] #[craft(handler)]
async fn get_config(&self) -> Json<MumbleClientConfig> { async fn get_config(&self) -> Json<GuiConfig> {
Json(self.client_config.clone()) Json(self.client_config.clone())
} }
} }