diff --git a/Cargo.lock b/Cargo.lock index 6c04d51..79a2168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "argon2" version = "0.6.0" @@ -704,6 +713,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-server" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9" +dependencies = [ + "arc-swap", + "bytes", + "fs-err", + "http 1.5.0", + "http-body 1.1.0", + "hyper 1.11.0", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -1753,6 +1784,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1992,6 +2033,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.5.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -2191,7 +2251,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2215,6 +2275,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.19", "http 1.5.0", "http-body 1.1.0", "httparse", @@ -2957,6 +3018,7 @@ dependencies = [ "async-trait", "axum", "axum-extra", + "axum-server", "bitflags", "chrono", "config", @@ -2967,6 +3029,7 @@ dependencies = [ "log", "migration", "parking_lot", + "rcgen 0.13.2", "rustrtc", "sea-orm", "serde", @@ -3482,6 +3545,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem 3.0.6", + "ring", + "rustls-pki-types", + "time", + "yasna 0.5.2", +] + [[package]] name = "rcgen" version = "0.14.10" @@ -3493,7 +3569,7 @@ dependencies = [ "rustls-pki-types", "time", "x509-parser", - "yasna", + "yasna 0.6.0", ] [[package]] @@ -3703,6 +3779,29 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.15.1" @@ -3712,6 +3811,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustrtc" version = "0.3.138" @@ -3735,7 +3846,7 @@ dependencies = [ "p256", "parking_lot", "rand 0.10.2", - "rcgen", + "rcgen 0.14.10", "serde", "serde_json", "sha1 0.11.0", @@ -4635,6 +4746,16 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.19" @@ -5446,6 +5567,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yasna" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 1ebbcbd..c78a5d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,5 @@ form_urlencoded = "1.2.2" time = "0.3.55" sha2 = "0.11.0" rustrtc = "0.3.138" +axum-server = { version = "0.7", features = ["tls-rustls"] } +rcgen = "0.13" diff --git a/config.toml b/config.toml index 140263e..bb3ef81 100644 --- a/config.toml +++ b/config.toml @@ -8,6 +8,12 @@ tcp_port = 8080 # Voice/Video port udp_port = 8080 +# Uncomment for native self-signed HTTPS/WSS. Keep both files between restarts. +# [network.tls] +# cert_path = "certs/server.pem" +# key_path = "certs/server-key.pem" +# names = ["localhost", "127.0.0.1"] # include the host clients actually use + [database] # DSN for database # SQLite diff --git a/src/config.rs b/src/config.rs index 1af1bd7..f3f4ee1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,6 +51,12 @@ tcp_port = 8080 # WebRTC ICE/Media UDP multiplexing port udp_port = 8080 +# Optional native HTTPS (omit this section to keep plain HTTP behind a proxy). +# [network.tls] +# cert_path = "certs/server.pem" +# key_path = "certs/server-key.pem" +# names = ["localhost", "127.0.0.1"] + [database] # DSN for database # SQLite @@ -103,6 +109,15 @@ pub struct NetworkConfig { pub hostv6: Option, pub tcp_port: u16, pub udp_port: u16, + #[serde(default)] + pub tls: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TlsConfig { + pub cert_path: std::path::PathBuf, + pub key_path: std::path::PathBuf, + pub names: Vec, } #[derive(Clone, Deserialize)] diff --git a/src/http/mod.rs b/src/http/mod.rs index 437bfcd..64eaacb 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -7,6 +7,7 @@ pub mod metrics; pub mod middleware; pub mod permissions; pub mod server; +mod tls; pub mod validation; pub use permissions::{RequireChannelPermission, RequireServerPermission}; diff --git a/src/http/server.rs b/src/http/server.rs index c2e6b30..8a90a51 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -15,12 +15,13 @@ use tower_http::catch_panic::CatchPanicLayer; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; -use crate::config::NetworkConfig; +use crate::config::{NetworkConfig, TlsConfig}; use crate::core::AppState; use crate::routes; use super::metrics::HttpMetrics; use super::middleware; +use super::tls; // ── Erreurs ─────────────────────────────────────────────────────────────────── @@ -66,6 +67,7 @@ pub enum HttpServerError { /// ``` pub struct HttpServer { bind_addr: SocketAddr, + tls: Option, app_state: AppState, metrics: Arc, shutdown_rx: broadcast::Receiver<()>, @@ -87,6 +89,7 @@ impl HttpServer { ( Self { bind_addr, + tls: network_config.tls.clone(), app_state, metrics, shutdown_rx, @@ -149,6 +152,22 @@ impl HttpServer { // Outermost : intercepte les panics et retourne une 500 propre .layer(CatchPanicLayer::new()); + if let Some(tls_config) = &self.tls { + let rustls = tls::load_or_create(tls_config).await?; + let handle = axum_server::Handle::new(); + let shutdown = handle.clone(); + tokio::spawn(async move { + let _ = self.shutdown_rx.recv().await; + shutdown.graceful_shutdown(Some(std::time::Duration::from_secs(10))); + }); + tracing::info!(addr = %self.bind_addr, "HTTPS server listening"); + axum_server::bind_rustls(self.bind_addr, rustls) + .handle(handle) + .serve(app.into_make_service()) + .await?; + return Ok(()); + } + let listener = TcpListener::bind(self.bind_addr) .await diff --git a/src/http/tls.rs b/src/http/tls.rs new file mode 100644 index 0000000..6104ec5 --- /dev/null +++ b/src/http/tls.rs @@ -0,0 +1,63 @@ +use crate::config::TlsConfig; +use axum_server::tls_rustls::RustlsConfig; +use std::io; + +pub async fn load_or_create(config: &TlsConfig) -> io::Result { + if config.names.is_empty() || config.names.iter().any(|name| name.is_empty()) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "TLS requires at least one non-empty name")); + } + if config.cert_path == config.key_path { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "certificate and key paths must differ")); + } + + let cert_exists = config.cert_path.exists(); + let key_exists = config.key_path.exists(); + if cert_exists != key_exists { + return Err(io::Error::new(io::ErrorKind::InvalidData, "only one TLS file exists; restore the missing file or remove both to regenerate")); + } + if !cert_exists { + let generated = rcgen::generate_simple_self_signed(config.names.clone()) + .map_err(io::Error::other)?; + for path in [&config.cert_path, &config.key_path] { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + } + std::fs::write(&config.cert_path, generated.cert.pem())?; + let key = generated.key_pair.serialize_pem(); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + use std::io::Write; + let mut file = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(&config.key_path)?; + file.write_all(key.as_bytes())?; + } + #[cfg(not(unix))] + std::fs::write(&config.key_path, key)?; + tracing::info!(cert = %config.cert_path.display(), "Generated self-signed TLS certificate"); + } + RustlsConfig::from_pem_file(&config.cert_path, &config.key_path).await +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn certificate_is_reused_and_partial_pair_is_rejected() { + let dir = std::env::temp_dir().join(format!("oxspeak-tls-{}", uuid::Uuid::new_v4())); + let config = TlsConfig { + cert_path: dir.join("cert.pem"), key_path: dir.join("key.pem"), + names: vec!["localhost".into()], + }; + load_or_create(&config).await.unwrap(); + let cert = std::fs::read(&config.cert_path).unwrap(); + let key = std::fs::read(&config.key_path).unwrap(); + load_or_create(&config).await.unwrap(); + assert_eq!(cert, std::fs::read(&config.cert_path).unwrap()); + assert_eq!(key, std::fs::read(&config.key_path).unwrap()); + std::fs::remove_file(&config.key_path).unwrap(); + assert!(load_or_create(&config).await.is_err()); + std::fs::remove_dir_all(dir).unwrap(); + } +} \ No newline at end of file diff --git a/src/voice/service.rs b/src/voice/service.rs index 2130f3f..5536992 100644 --- a/src/voice/service.rs +++ b/src/voice/service.rs @@ -341,6 +341,7 @@ mod tests { hostv6: None, tcp_port: 8080, udp_port: 9000, + tls: None, } }