feat: HTTPS support for web bridge (--tls flag)
Generates a self-signed certificate at startup for HTTPS. Required for mic access on Android/remote browsers (getUserMedia needs a secure context). Usage: wzp-web --port 9090 --relay 127.0.0.1:4433 --tls Browser: accept the self-signed cert warning, then mic works. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,11 @@ anyhow = "1"
|
|||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
tower-http = { version = "0.6", features = ["fs"] }
|
tower-http = { version = "0.6", features = ["fs"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
||||||
|
rcgen = "0.13"
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||||
|
rustls-pki-types = "1"
|
||||||
|
tokio-rustls = "0.26"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "wzp-web"
|
name = "wzp-web"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Serves a web page for browser-based voice calls and bridges
|
//! Serves a web page for browser-based voice calls and bridges
|
||||||
//! WebSocket audio to the wzp relay protocol.
|
//! WebSocket audio to the wzp relay protocol.
|
||||||
//!
|
//!
|
||||||
//! Usage: wzp-web [--port 8080] [--relay 127.0.0.1:4433]
|
//! Usage: wzp-web [--port 8080] [--relay 127.0.0.1:4433] [--tls]
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -35,6 +35,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let mut port: u16 = 8080;
|
let mut port: u16 = 8080;
|
||||||
let mut relay_addr: SocketAddr = "127.0.0.1:4433".parse()?;
|
let mut relay_addr: SocketAddr = "127.0.0.1:4433".parse()?;
|
||||||
|
let mut use_tls = false;
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let mut i = 1;
|
let mut i = 1;
|
||||||
@@ -48,12 +49,17 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
i += 1;
|
i += 1;
|
||||||
relay_addr = args[i].parse().expect("invalid relay address");
|
relay_addr = args[i].parse().expect("invalid relay address");
|
||||||
}
|
}
|
||||||
|
"--tls" => {
|
||||||
|
use_tls = true;
|
||||||
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
eprintln!("Usage: wzp-web [--port 8080] [--relay 127.0.0.1:4433]");
|
eprintln!("Usage: wzp-web [--port 8080] [--relay 127.0.0.1:4433] [--tls]");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Options:");
|
eprintln!("Options:");
|
||||||
eprintln!(" --port <port> HTTP/WebSocket port (default: 8080)");
|
eprintln!(" --port <port> HTTP/WebSocket port (default: 8080)");
|
||||||
eprintln!(" --relay <addr> WZP relay address (default: 127.0.0.1:4433)");
|
eprintln!(" --relay <addr> WZP relay address (default: 127.0.0.1:4433)");
|
||||||
|
eprintln!(" --tls Enable HTTPS with self-signed certificate");
|
||||||
|
eprintln!(" (required for mic access on Android/remote browsers)");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -63,13 +69,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let state = AppState { relay_addr };
|
let state = AppState { relay_addr };
|
||||||
|
|
||||||
// Determine static file path (relative to binary or cargo manifest)
|
|
||||||
let static_dir = if std::path::Path::new("crates/wzp-web/static").exists() {
|
let static_dir = if std::path::Path::new("crates/wzp-web/static").exists() {
|
||||||
"crates/wzp-web/static"
|
"crates/wzp-web/static"
|
||||||
} else if std::path::Path::new("static").exists() {
|
} else if std::path::Path::new("static").exists() {
|
||||||
"static"
|
"static"
|
||||||
} else {
|
} else {
|
||||||
// Fallback: look relative to executable
|
|
||||||
"static"
|
"static"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,11 +83,40 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let listen: SocketAddr = format!("0.0.0.0:{port}").parse()?;
|
let listen: SocketAddr = format!("0.0.0.0:{port}").parse()?;
|
||||||
info!(%listen, %relay_addr, "WarzonePhone web bridge starting");
|
|
||||||
info!("Open http://localhost:{port} in your browser");
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(listen).await?;
|
if use_tls {
|
||||||
axum::serve(listener, app).await?;
|
// Generate self-signed cert
|
||||||
|
let cert_key = rcgen::generate_simple_self_signed(vec![
|
||||||
|
"localhost".to_string(),
|
||||||
|
"wzp".to_string(),
|
||||||
|
])?;
|
||||||
|
let cert_der = rustls_pki_types::CertificateDer::from(cert_key.cert);
|
||||||
|
let key_der = rustls_pki_types::PrivateKeyDer::try_from(cert_key.key_pair.serialize_der())
|
||||||
|
.map_err(|e| anyhow::anyhow!("key error: {e}"))?;
|
||||||
|
|
||||||
|
let mut tls_config = rustls::ServerConfig::builder()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(vec![cert_der], key_der)?;
|
||||||
|
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||||
|
|
||||||
|
let tls_config = axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(tls_config));
|
||||||
|
|
||||||
|
info!(%listen, %relay_addr, "WarzonePhone web bridge starting (HTTPS)");
|
||||||
|
info!("Open https://localhost:{port} in your browser");
|
||||||
|
info!("NOTE: Accept the self-signed certificate warning in your browser");
|
||||||
|
|
||||||
|
axum_server::bind_rustls(listen, tls_config)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
info!(%listen, %relay_addr, "WarzonePhone web bridge starting (HTTP)");
|
||||||
|
info!("Open http://localhost:{port} in your browser");
|
||||||
|
info!("NOTE: Use --tls for mic access on Android/remote browsers");
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(listen).await?;
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +130,6 @@ async fn ws_handler(
|
|||||||
async fn handle_ws(socket: WebSocket, state: AppState) {
|
async fn handle_ws(socket: WebSocket, state: AppState) {
|
||||||
info!("WebSocket client connected");
|
info!("WebSocket client connected");
|
||||||
|
|
||||||
// Connect to wzp relay
|
|
||||||
let relay_addr = state.relay_addr;
|
let relay_addr = state.relay_addr;
|
||||||
let bind_addr: SocketAddr = if relay_addr.is_ipv6() {
|
let bind_addr: SocketAddr = if relay_addr.is_ipv6() {
|
||||||
"[::]:0".parse().unwrap()
|
"[::]:0".parse().unwrap()
|
||||||
@@ -132,7 +164,7 @@ async fn handle_ws(socket: WebSocket, state: AppState) {
|
|||||||
let encoder = Arc::new(Mutex::new(CallEncoder::new(&config)));
|
let encoder = Arc::new(Mutex::new(CallEncoder::new(&config)));
|
||||||
let decoder = Arc::new(Mutex::new(CallDecoder::new(&config)));
|
let decoder = Arc::new(Mutex::new(CallDecoder::new(&config)));
|
||||||
|
|
||||||
// --- Browser → Relay: receive PCM from WebSocket, encode, send to relay ---
|
// Browser -> Relay
|
||||||
let send_transport = transport.clone();
|
let send_transport = transport.clone();
|
||||||
let send_encoder = encoder.clone();
|
let send_encoder = encoder.clone();
|
||||||
let send_task = tokio::spawn(async move {
|
let send_task = tokio::spawn(async move {
|
||||||
@@ -140,9 +172,8 @@ async fn handle_ws(socket: WebSocket, state: AppState) {
|
|||||||
while let Some(Ok(msg)) = ws_receiver.next().await {
|
while let Some(Ok(msg)) = ws_receiver.next().await {
|
||||||
match msg {
|
match msg {
|
||||||
Message::Binary(data) => {
|
Message::Binary(data) => {
|
||||||
// data is raw s16le PCM from browser
|
|
||||||
if data.len() < FRAME_SAMPLES * 2 {
|
if data.len() < FRAME_SAMPLES * 2 {
|
||||||
continue; // incomplete frame
|
continue;
|
||||||
}
|
}
|
||||||
let pcm: Vec<i16> = data
|
let pcm: Vec<i16> = data
|
||||||
.chunks_exact(2)
|
.chunks_exact(2)
|
||||||
@@ -169,7 +200,7 @@ async fn handle_ws(socket: WebSocket, state: AppState) {
|
|||||||
}
|
}
|
||||||
frames_sent += 1;
|
frames_sent += 1;
|
||||||
if frames_sent % 250 == 0 {
|
if frames_sent % 250 == 0 {
|
||||||
info!(frames_sent, "browser → relay");
|
info!(frames_sent, "browser -> relay");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Close(_) => break,
|
Message::Close(_) => break,
|
||||||
@@ -179,7 +210,7 @@ async fn handle_ws(socket: WebSocket, state: AppState) {
|
|||||||
info!(frames_sent, "browser send loop ended");
|
info!(frames_sent, "browser send loop ended");
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Relay → Browser: receive from relay, decode, send PCM to WebSocket ---
|
// Relay -> Browser
|
||||||
let recv_transport = transport.clone();
|
let recv_transport = transport.clone();
|
||||||
let recv_decoder = decoder.clone();
|
let recv_decoder = decoder.clone();
|
||||||
let recv_task = tokio::spawn(async move {
|
let recv_task = tokio::spawn(async move {
|
||||||
@@ -194,19 +225,19 @@ async fn handle_ws(socket: WebSocket, state: AppState) {
|
|||||||
dec.ingest(pkt);
|
dec.ingest(pkt);
|
||||||
if !is_repair {
|
if !is_repair {
|
||||||
if let Some(_n) = dec.decode_next(&mut pcm_buf) {
|
if let Some(_n) = dec.decode_next(&mut pcm_buf) {
|
||||||
// Convert i16 PCM to bytes and send to browser
|
|
||||||
let bytes: Vec<u8> = pcm_buf
|
let bytes: Vec<u8> = pcm_buf
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|s| s.to_le_bytes())
|
.flat_map(|s| s.to_le_bytes())
|
||||||
.collect();
|
.collect();
|
||||||
if let Err(e) = ws_sender.send(Message::Binary(bytes.into())).await
|
if let Err(e) =
|
||||||
|
ws_sender.send(Message::Binary(bytes.into())).await
|
||||||
{
|
{
|
||||||
error!("ws send error: {e}");
|
error!("ws send error: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frames_recv += 1;
|
frames_recv += 1;
|
||||||
if frames_recv % 250 == 0 {
|
if frames_recv % 250 == 0 {
|
||||||
info!(frames_recv, "relay → browser");
|
info!(frames_recv, "relay -> browser");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user