diff --git a/crates/rutster/src/config.rs b/crates/rutster/src/config.rs index 765f626..99a8e59 100644 --- a/crates/rutster/src/config.rs +++ b/crates/rutster/src/config.rs @@ -8,6 +8,7 @@ //! Closes 2026-07-04 scalability review "HTTP bind hardcoded" (minor). use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; use rutster_media::MediaAddressConfig; use rutster_trunk::provider::TwilioCredentials; @@ -222,6 +223,59 @@ pub fn twilio_credentials( })) } +/// `RUTSTER_TLS_CERT` โ€” filesystem path to the TLS certificate (PEM) used by +/// the axum HTTPS surface (deploy slice C ยง5.4). +/// +/// `None` means the operator did not enable HTTPS; `main.rs` will serve plain +/// HTTP. `Some(path)` is validated to point at an existing regular file so a +/// typo fails-fast at startup instead of failing mid-request. +/// +/// This is a pure function over `Option` (not a direct `std::env::var` +/// call) so unit tests never mutate process env and never touch the real +/// filesystem unless the test itself creates a tempfile. +pub fn tls_cert_path(raw: Option) -> Result, String> { + match raw { + None => Ok(None), + Some(s) => { + let p = PathBuf::from(s); + if !p.is_file() { + return Err(format!( + "RUTSTER_TLS_CERT {:?}: file does not exist (or is not a regular file)", + p.display() + )); + } + Ok(Some(p)) + } + } +} + +/// `RUTSTER_TLS_KEY` โ€” filesystem path to the TLS private key (PEM) paired +/// with [`tls_cert_path`]. +/// +/// `None` means HTTPS is disabled; `main.rs` will serve plain HTTP. When both +/// `RUTSTER_TLS_CERT` and `RUTSTER_TLS_KEY` are `Some`, `main.rs` feeds the +/// pair to `tokio_rustls` (or equivalent) to terminate TLS on the signaling +/// surface. The mutual presence check lives in `main.rs`, not here โ€” this +/// parser only validates its own path. +/// +/// Like the other parsers in this module, the function is pure over +/// `Option` so tests stay deterministic and env-free. +pub fn tls_key_path(raw: Option) -> Result, String> { + match raw { + None => Ok(None), + Some(s) => { + let p = PathBuf::from(s); + if !p.is_file() { + return Err(format!( + "RUTSTER_TLS_KEY {:?}: file does not exist (or is not a regular file)", + p.display() + )); + } + Ok(Some(p)) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -430,4 +484,35 @@ mod tests { assert!(err.contains("RUTSTER_TRUSTED_PROXIES")); assert!(err.contains("not-a-cidr")); } + + #[test] + fn tls_cert_path_none_when_unset() { + assert!(tls_cert_path(None).unwrap().is_none()); + } + + #[test] + fn tls_cert_path_some_when_file_exists() { + let path = std::env::temp_dir().join("rutster_tls_cert_path_test.txt"); + std::fs::write(&path, b"test cert").unwrap(); + let result = tls_cert_path(Some(path.to_str().unwrap().to_string())).unwrap(); + assert_eq!(result, Some(path.clone())); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn tls_cert_path_rejects_missing_file_with_var_name_in_error() { + let err = tls_cert_path(Some("/nonexistent/cert.pem".into())).unwrap_err(); + assert!(err.contains("RUTSTER_TLS_CERT")); + } + + #[test] + fn tls_key_path_none_when_unset() { + assert!(tls_key_path(None).unwrap().is_none()); + } + + #[test] + fn tls_key_path_rejects_missing_file_with_var_name_in_error() { + let err = tls_key_path(Some("/nonexistent/key.pem".into())).unwrap_err(); + assert!(err.contains("RUTSTER_TLS_KEY")); + } }