From d7f4af4d4735ef1d362e4b33590f1ba25ff586a5 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sat, 21 Mar 2026 23:37:22 +0900 Subject: [PATCH 1/7] feat: add TLS configuration to config.kdl --- config.sample.kdl | 7 ++ src/config.rs | 245 +++++++++++++++++++++++------------- src/error/error_registry.rs | 7 +- src/http.rs | 10 +- src/main.rs | 9 +- src/router.rs | 2 +- src/server.rs | 5 +- test/main_test.rs | 4 + 8 files changed, 190 insertions(+), 99 deletions(-) diff --git a/config.sample.kdl b/config.sample.kdl index 05c7ef6..1fba83c 100644 --- a/config.sample.kdl +++ b/config.sample.kdl @@ -1,6 +1,10 @@ server { listen 80 server-name "127.0.0.1" + tls { + cert "" + key "" + } location "/" { root "/var/www/html" index "index.html" "index.htm" @@ -26,7 +30,10 @@ server { code 500 "500.html" } } +} +performance { + crypto-provider "" connection-buffer-size 4096 file-read-buffer-size 8192 max-header-count 64 diff --git a/src/config.rs b/src/config.rs index 8786338..a362c66 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,24 +6,37 @@ use std::str::FromStr; pub struct Config { #[knus(child)] pub server: ServerConfig, + #[knus(child)] + pub performance: PerformanceConfig, } -#[derive(knus::Decode, Clone, Debug, PartialEq)] +#[derive(knus::Decode, Clone, Debug, Default, PartialEq)] pub struct ServerConfig { #[knus(child, unwrap(argument))] pub listen: u16, #[knus(child, unwrap(argument))] pub server_name: String, + #[knus(child)] + pub tls: Option, #[knus(children(name = "location"))] pub locations: Vec, #[knus(children(name = "error-page"))] pub error_pages: Vec, +} + +#[derive(knus::Decode, Clone, Debug, PartialEq)] +pub struct TLSConfig { #[knus(child, unwrap(argument))] - pub connection_buffer_size: usize, - #[knus(child, unwrap(argument))] - pub file_read_buffer_size: usize, + pub cert: PathBuf, #[knus(child, unwrap(argument))] - pub max_header_count: usize, + pub key: PathBuf, +} +impl TLSConfig { + pub fn validate(&self) -> miette::Result<()> { + check_path_safety(&self.cert, "cert")?; + check_path_safety(&self.key, "key")?; + Ok(()) + } } #[derive(knus::Decode, Clone, Debug, Default, PartialEq)] @@ -61,54 +74,15 @@ impl FromStr for LocationConfigType { } impl LocationConfig { pub fn validate(&self) -> miette::Result<()> { - self.check_path_safety(&self.path, "path")?; - self.check_path_safety(&self.root, "root")?; + check_path_safety(&self.path, "path")?; + check_path_safety(&self.root, "root")?; for (i, filename) in self.index.iter().enumerate() { - if !self.is_pure_filename(filename) { + if !is_pure_filename(filename) { return Err(miette!("config Error: index {},{}", i, filename)); } } Ok(()) } - fn check_path_safety(&self, target: &Path, field_name: &str) -> miette::Result<()> { - let mut depth = 0; - - for component in target.components() { - match component { - Component::Normal(_) => depth += 1, - Component::ParentDir => { - depth -= 1; - if depth < 0 { - return Err(miette!( - "config Error: ParentDir '{}', {:?}", - field_name, - target - )); - } - } - Component::CurDir => {} - Component::RootDir => {} - Component::Prefix(_) => { - return Err(miette!( - "config Error: Prefix '{}', {:?}", - field_name, - target - )); - } - } - } - Ok(()) - } - fn is_pure_filename(&self, filename: &str) -> bool { - let path = Path::new(filename); - let mut components = path.components(); - - match components.next() { - Some(Component::Normal(_)) => {} - _ => return false, - } - components.next().is_none() - } } #[derive(knus::Decode, Clone, Debug, PartialEq)] @@ -135,6 +109,23 @@ pub struct ErrorCodeEntry { pub file: PathBuf, } +#[derive(knus::Decode, Clone, Debug, Default, PartialEq)] +pub struct PerformanceConfig { + #[knus(child, default=CryptoProvider::Ring, unwrap(argument))] + pub crypto_provider: CryptoProvider, + #[knus(child, unwrap(argument))] + pub connection_buffer_size: usize, + #[knus(child, unwrap(argument))] + pub file_read_buffer_size: usize, + #[knus(child, unwrap(argument))] + pub max_header_count: usize, +} +#[derive(knus::DecodeScalar, Debug, Clone, Default, PartialEq)] +pub enum CryptoProvider { + #[default] + Ring, + AwsLcRs, +} impl Config { pub fn validate(&self) -> miette::Result<()> { for loc in &self.server.locations { @@ -150,6 +141,46 @@ pub fn parse_config(config_path: &str) -> miette::Result { Ok(config) } +fn check_path_safety(target: &Path, field_name: &str) -> miette::Result<()> { + let mut depth = 0; + + for component in target.components() { + match component { + Component::Normal(_) => depth += 1, + Component::ParentDir => { + depth -= 1; + if depth < 0 { + return Err(miette!( + "config Error: ParentDir '{}', {:?}", + field_name, + target + )); + } + } + Component::CurDir => {} + Component::RootDir => {} + Component::Prefix(_) => { + return Err(miette!( + "config Error: Prefix '{}', {:?}", + field_name, + target + )); + } + } + } + Ok(()) +} +fn is_pure_filename(filename: &str) -> bool { + let path = Path::new(filename); + let mut components = path.components(); + + match components.next() { + Some(Component::Normal(_)) => {} + _ => return false, + } + components.next().is_none() +} + #[cfg(test)] mod tests { use super::*; @@ -157,50 +188,19 @@ mod tests { #[test] fn test_is_pure_filename() { - let config = LocationConfig { - path: PathBuf::new(), - root: PathBuf::new(), - index: vec![], - ty: None, - ..Default::default() - }; - assert!(config.is_pure_filename("file.txt")); - assert!(config.is_pure_filename("file")); - assert!(!config.is_pure_filename("/path/to/file")); - assert!(!config.is_pure_filename("../file")); - assert!(!config.is_pure_filename("")); + assert!(is_pure_filename("file.txt")); + assert!(is_pure_filename("file")); + assert!(!is_pure_filename("/path/to/file")); + assert!(!is_pure_filename("../file")); + assert!(!is_pure_filename("")); } #[test] fn test_check_path_safety() { - let config = LocationConfig { - path: PathBuf::new(), - root: PathBuf::new(), - index: vec![], - ty: None, - ..Default::default() - }; - - assert!( - config - .check_path_safety(Path::new("/safe/path"), "path") - .is_ok() - ); - assert!( - config - .check_path_safety(Path::new("/safe/../path"), "path") - .is_ok() - ); - assert!( - config - .check_path_safety(Path::new("../unsafe/path"), "path") - .is_err() - ); - assert!( - config - .check_path_safety(Path::new("/unsafe/../../path"), "path") - .is_err() - ); + assert!(check_path_safety(Path::new("/safe/path"), "path").is_ok()); + assert!(check_path_safety(Path::new("/safe/../path"), "path").is_ok()); + assert!(check_path_safety(Path::new("../unsafe/path"), "path").is_err()); + assert!(check_path_safety(Path::new("/unsafe/../../path"), "path").is_err()); } #[test] @@ -241,6 +241,69 @@ mod tests { let result = parse_config(config_path.to_str().unwrap()); assert!(result.is_err()); } + #[test] + fn test_tls_config_validate() { + let valid_tls = TLSConfig { + cert: PathBuf::from("/etc/tls/cert.pem"), + key: PathBuf::from("/etc/tls/key.pem"), + }; + assert!(valid_tls.validate().is_ok()); + + let invalid_tls = TLSConfig { + cert: PathBuf::from("../cert.pem"), + key: PathBuf::from("/etc/tls/key.pem"), + }; + assert!(invalid_tls.validate().is_err()); + } + + #[test] + fn test_performance_config_defaults() { + let config_str = r#" + server { + listen 80 + server-name "localhost" + } + performance { + connection-buffer-size 1024 + file-read-buffer-size 2048 + max-header-count 32 + } + "#; + let config = knus::parse::("test.kdl", config_str).unwrap(); + assert_eq!(config.performance.crypto_provider, CryptoProvider::Ring); + assert_eq!(config.performance.connection_buffer_size, 1024); + } + + #[test] + fn test_parse_with_tls_and_performance() { + let config_str = r#" + server { + listen 443 + server-name "example.com" + tls { + cert "/path/to/cert" + key "/path/to/key" + } + } + performance { + crypto-provider "aws-lc-rs" + connection-buffer-size 8192 + file-read-buffer-size 16384 + max-header-count 128 + } + "#; + let config = knus::parse::("test.kdl", config_str).unwrap(); + + let tls = config.server.tls.as_ref().unwrap(); + assert_eq!(tls.cert, PathBuf::from("/path/to/cert")); + assert_eq!(tls.key, PathBuf::from("/path/to/key")); + + assert_eq!(config.performance.crypto_provider, CryptoProvider::AwsLcRs); + assert_eq!(config.performance.connection_buffer_size, 8192); + assert_eq!(config.performance.file_read_buffer_size, 16384); + assert_eq!(config.performance.max_header_count, 128); + } + #[test] fn test_parse() { let config = r#" @@ -266,6 +329,8 @@ mod tests { code 404 "forbidden.html" } } + } + performance { connection-buffer-size 4096 file-read-buffer-size 8192 max-header-count 64 @@ -282,6 +347,7 @@ mod tests { server: ServerConfig { listen: 80, server_name: "localhost".to_string(), + tls: None, locations: vec![ LocationConfig { path: Path::new("/").to_path_buf(), @@ -322,10 +388,13 @@ mod tests { ] } },], + }, + performance: PerformanceConfig { connection_buffer_size: 4096, file_read_buffer_size: 8192, max_header_count: 64, - } + ..Default::default() + }, } ) } diff --git a/src/error/error_registry.rs b/src/error/error_registry.rs index 1bde158..92f83a5 100644 --- a/src/error/error_registry.rs +++ b/src/error/error_registry.rs @@ -88,7 +88,7 @@ mod tests { use crate::{ config::{ Config, ErrorCodeEntry, ErrorFiles, ErrorPage, LocationConfig, LocationConfigType, - ServerConfig, + PerformanceConfig, ServerConfig, }, error::*, }; @@ -134,10 +134,13 @@ mod tests { ..Default::default() }, ], - error_pages: vec![], + ..Default::default() + }, + performance: PerformanceConfig { connection_buffer_size: 4096, file_read_buffer_size: 8192, max_header_count: 64, + ..Default::default() }, }; diff --git a/src/http.rs b/src/http.rs index b3d9308..54b06ee 100644 --- a/src/http.rs +++ b/src/http.rs @@ -8,7 +8,7 @@ pub async fn handle_request( stream: &mut T, context: &ServerContext, ) -> Result<(), DamasError> { - let mut buffer = Vec::with_capacity(context.config.server.connection_buffer_size); + let mut buffer = Vec::with_capacity(context.config.performance.connection_buffer_size); loop { let (bytes_read, buf) = buf_try!(@try stream.append(buffer).await); @@ -18,8 +18,8 @@ pub async fn handle_request( return Ok(()); } - let mut headers = - vec![httparse::EMPTY_HEADER; context.config.server.max_header_count].into_boxed_slice(); + let mut headers = vec![httparse::EMPTY_HEADER; context.config.performance.max_header_count] + .into_boxed_slice(); let mut request = httparse::Request::new(&mut headers); match request.parse(&buffer) { @@ -41,8 +41,8 @@ pub async fn handle_request( } } - let mut headers = - vec![httparse::EMPTY_HEADER; context.config.server.max_header_count].into_boxed_slice(); + let mut headers = vec![httparse::EMPTY_HEADER; context.config.performance.max_header_count] + .into_boxed_slice(); let mut request = httparse::Request::new(&mut headers); match request.parse(&buffer) { Ok(_status) => { diff --git a/src/main.rs b/src/main.rs index aeee33b..19882b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,10 +17,15 @@ async fn main() -> anyhow::Result<()> { locations = c.server.locations.len(), "🔮 parsed config.kdl and initialized server components" ); + if c.server.tls.is_some() { + tracing::debug!( + crypto_provider = ?c.performance.crypto_provider, + ) + }; tracing::debug!( - buffer_size = c.server.file_read_buffer_size, + buffer_size = c.performance.file_read_buffer_size, error_pages = c.server.error_pages.len(), - max_headers = c.server.max_header_count, + max_headers = c.performance.max_header_count, "detailed configuration loaded" ); c diff --git a/src/router.rs b/src/router.rs index 05ecb47..dedca8e 100644 --- a/src/router.rs +++ b/src/router.rs @@ -127,7 +127,7 @@ impl RouterHandler { buf_try!(@try stream.write_all(headers).await); let mut pos = 0; let mut file_buffer: Vec = - Vec::with_capacity(context.config.server.file_read_buffer_size); + Vec::with_capacity(context.config.performance.file_read_buffer_size); while pos < file_size { let (read_bytes, returned_file_buffer) = diff --git a/src/server.rs b/src/server.rs index 82c90c8..4496133 100644 --- a/src/server.rs +++ b/src/server.rs @@ -83,7 +83,6 @@ impl Server { } } } - async fn handle_connection(mut stream: T, context: ServerContext) { if let Err(e) = handle_request(&mut stream, &context) .instrument(Span::current()) @@ -154,9 +153,13 @@ mod tests { ], }, }], + ..Default::default() + }, + performance: PerformanceConfig { connection_buffer_size: 4096, file_read_buffer_size: 8192, max_header_count: 64, + ..Default::default() }, }; diff --git a/test/main_test.rs b/test/main_test.rs index f1b8089..35d142d 100644 --- a/test/main_test.rs +++ b/test/main_test.rs @@ -126,9 +126,13 @@ where ], }, }], + ..Default::default() + }, + performance: PerformanceConfig { connection_buffer_size: 4096, file_read_buffer_size: 8192, max_header_count: 64, + ..Default::default() }, }; modifier(&mut config); From b1d5cfddb6e84174d49b9c40b956aeb5724c260c Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sat, 21 Mar 2026 23:51:02 +0900 Subject: [PATCH 2/7] feat: implement TLS support using compio-tls --- Cargo.lock | 376 ++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 +- src/cert.rs | 21 +++ src/lib.rs | 1 + src/server.rs | 35 ++++- 5 files changed, 422 insertions(+), 15 deletions(-) create mode 100644 src/cert.rs diff --git a/Cargo.lock b/Cargo.lock index 2c3e15c..35e6dc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,9 +105,18 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -121,6 +130,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -156,6 +175,8 @@ dependencies = [ "compio-macros", "compio-net", "compio-runtime", + "compio-tls", + "compio-ws", ] [[package]] @@ -285,6 +306,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "compio-tls" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84cd9ca48815f384f1a30400848beebcd8c7ead2f57bfe28ebc5560babea88ec" +dependencies = [ + "compio-buf", + "compio-io", + "futures-rustls", + "futures-util", + "rustls", +] + +[[package]] +name = "compio-ws" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7281a15e8f638697415f9838030e41a92c8a8954ddccfc46556a413c16dd9a" +dependencies = [ + "compio-buf", + "compio-io", + "compio-log", + "compio-net", + "compio-tls", + "tungstenite", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -305,6 +353,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -338,6 +395,16 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "damas" version = "0.1.0" @@ -353,6 +420,8 @@ dependencies = [ "minijinja", "moka", "once_cell", + "rustls", + "rustls-pki-types", "tempfile", "thiserror", "tracing", @@ -360,6 +429,22 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.15.0" @@ -409,6 +494,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flume" version = "0.11.1" @@ -483,6 +574,17 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -512,6 +614,27 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -918,6 +1041,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -983,6 +1115,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1009,6 +1170,20 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -1028,6 +1203,40 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1094,6 +1303,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1103,6 +1323,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.12" @@ -1134,6 +1360,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "3.0.2" @@ -1325,6 +1557,31 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -1355,12 +1612,24 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "uuid" version = "1.21.0" @@ -1384,6 +1653,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -1515,13 +1790,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1533,6 +1817,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1540,58 +1840,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -1715,6 +2063,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 7bed49d..4565ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] anyhow = "1.0.100" bytes = "1.11.1" -compio = { version = "0.17.0" , features = ["macros", "runtime"]} +compio = { version = "0.17.0", features = ["macros", "runtime", "rustls", "tls"] } futures = "0.3.32" http = "1.4.0" httparse = "1.10.1" @@ -15,6 +15,8 @@ miette = { version = "7.6.0", features = ["fancy"] } minijinja = "2.15.1" moka = { version = "0.12.13" , features = ["future"]} once_cell = "1.21.3" +rustls = { version ="0.23.37", default-features = false } +rustls-pki-types = { version = "1.14.0" , default-features = false } thiserror = "2.0.18" tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } diff --git a/src/cert.rs b/src/cert.rs new file mode 100644 index 0000000..7ff35b8 --- /dev/null +++ b/src/cert.rs @@ -0,0 +1,21 @@ +use crate::error::DamasError; +use rustls::ServerConfig; +use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}; +use std::path::Path; +use std::sync::Arc; + +pub fn validate_server_config( + cert_path: &Path, + key_path: &Path, +) -> Result, DamasError> { + let cert_der = CertificateDer::from_pem_file(cert_path) + .map_err(|e| DamasError::ConfigError(e.to_string()))?; + let key_der = PrivateKeyDer::from_pem_file(key_path) + .map_err(|e| DamasError::ConfigError(e.to_string()))?; + Ok(Arc::new( + ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![cert_der], key_der) + .map_err(|e| DamasError::ConfigError(e.to_string()))?, + )) +} diff --git a/src/lib.rs b/src/lib.rs index ca89a76..0289db8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use crate::index::IndexCache; use crate::router::RouterNode; use std::sync::Arc; +pub mod cert; pub mod config; pub mod error; pub mod http; diff --git a/src/server.rs b/src/server.rs index 4496133..3271fa0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,5 @@ use crate::ServerContext; +use crate::cert::validate_server_config; use crate::config::Config; use crate::error::ErrorRegistry; use crate::http::handle_request; @@ -8,6 +9,7 @@ use crate::router::RouterNode; use compio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use compio::net::TcpListener; use compio::runtime::spawn; +use compio::tls::TlsAcceptor; use minijinja::Environment; use once_cell::sync::Lazy; use tracing::{Instrument, Span, info_span}; @@ -25,6 +27,7 @@ pub struct Server { router: RouterNode, config: Config, error_registry: ErrorRegistry, + acceptor: Option, } impl Server { @@ -34,11 +37,21 @@ impl Server { let error_registry = ErrorRegistry::new(&JINJA_ENV, 100); error_registry.init_with_config(&config).await; tracing::info!("initialized error registry"); + let acceptor = config.server.tls.as_ref().and_then(|ssl| { + match validate_server_config(&ssl.cert, &ssl.key) { + Ok(server_config) => Some(TlsAcceptor::from(server_config)), + Err(e) => { + tracing::info!("Error validating TLS config: {}", e); + None + } + } + }); Ok(Self { router, config, error_registry, + acceptor, }) } @@ -47,6 +60,7 @@ impl Server { router, config, error_registry, + acceptor, } = self; let index_cache = IndexCache::new(&JINJA_ENV, 100); let context = ServerContext::new(config, router, error_registry, index_cache); @@ -63,7 +77,7 @@ impl Server { loop { match listener.accept().await { - Ok((stream, address)) => { + Ok((tcp_stream, address)) => { tracing::info!("Accepted connection from {}", address); let ctx = context.clone(); let span = info_span!( @@ -73,8 +87,23 @@ impl Server { path = tracing::field::Empty, status = tracing::field::Empty ); - spawn(async move { handle_connection(stream, ctx).instrument(span).await }) - .detach(); + let acceptor = acceptor.clone(); + spawn(async move { + match acceptor { + Some(acceptor) => match acceptor.accept(tcp_stream).await { + Ok(tls_stream) => { + handle_connection(tls_stream, ctx).instrument(span).await; + } + Err(e) => { + tracing::error!("TLS accept error: {}", e); + } + }, + None => { + handle_connection(tcp_stream, ctx).instrument(span).await; + } + }; + }) + .detach(); } Err(err) => { tracing::error!("Error accepting connection: {}", err); From 671914a82d9041f07011ed556aa2a16f71085288 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sun, 5 Apr 2026 13:09:43 +0900 Subject: [PATCH 3/7] build: add TLS dependencies and update build environment --- .gitignore | 10 ++++++ .pre-commit-config.yaml | 2 +- Cargo.lock | 68 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 20 ++++++++++-- rust-toolchain.toml | 2 +- 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 174c1d9..038bb67 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,13 @@ config.kdl #test cobertura.xml + +# nix/direnv +.envrc +.direnv/ +flake.lock +flake.nix + + +# mac +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf51ffa..2e24ae0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,4 +43,4 @@ repos: language: system types: [rust] pass_filenames: false - always_run: true + always_run: false diff --git a/Cargo.lock b/Cargo.lock index 35e6dc3..eb8336b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,28 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -137,6 +159,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -161,6 +185,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "compio" version = "0.17.0" @@ -421,6 +454,7 @@ dependencies = [ "moka", "once_cell", "rustls", + "rustls-pemfile", "rustls-pki-types", "tempfile", "thiserror", @@ -445,6 +479,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -515,6 +555,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -771,6 +817,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1209,14 +1265,25 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "aws-lc-rs", "log", "once_cell", + "ring", "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.14.0" @@ -1232,6 +1299,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", diff --git a/Cargo.toml b/Cargo.toml index 4565ec5..47356f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] anyhow = "1.0.100" bytes = "1.11.1" -compio = { version = "0.17.0", features = ["macros", "runtime", "rustls", "tls"] } +compio = { version = "0.17.0", features = ["macros", "runtime", "rustls", "tls", "time"] } futures = "0.3.32" http = "1.4.0" httparse = "1.10.1" @@ -16,11 +16,12 @@ minijinja = "2.15.1" moka = { version = "0.12.13" , features = ["future"]} once_cell = "1.21.3" rustls = { version ="0.23.37", default-features = false } -rustls-pki-types = { version = "1.14.0" , default-features = false } +rustls-pki-types = "1.14.0" thiserror = "2.0.18" tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } urlencoding = "2.1.3" +rustls-pemfile = "2.2.0" [dev-dependencies] tempfile = "3.10.1" @@ -30,12 +31,25 @@ name = "main_test" path = "test/main_test.rs" harness = true +[[test]] +name = "tls_test" +path = "test/tls_test.rs" +harness = true + [package.metadata.tarpaulin] ignore-tests = true -exclude-files = ["tests/*"] +exclude-files = ["test/*"] [profile.release] strip = true lto = true codegen-units = 1 panic = "abort" + +[features] +default = ["ring"] +ring = ["rustls/ring"] +aws_lc_rs = ["rustls/aws_lc_rs"] + +[package.metadata.cargo-shear] +ignored = ["rustls-ring", "aws_lc_rs"] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f611ff3..c262dfe 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] channel = "1.90.0" -components = ["rustfmt", "clippy"] +components = ["rustfmt", "clippy", "rustfmt", "rust-src", "rust-analyzer"] targets = [ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", From 271ebff8a0891c227010eb7c4ed20a1380fe6d81 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sun, 5 Apr 2026 13:14:29 +0900 Subject: [PATCH 4/7] feat: implement TLS configuration and integrate server-side acceptor --- config.sample.kdl | 4 +- src/cert.rs | 106 ++++++++++++++--- src/config.rs | 295 +++++++++++++++++++++++++++++++++++++++------- src/server.rs | 29 +++-- 4 files changed, 367 insertions(+), 67 deletions(-) diff --git a/config.sample.kdl b/config.sample.kdl index 1fba83c..ea807ba 100644 --- a/config.sample.kdl +++ b/config.sample.kdl @@ -2,8 +2,8 @@ server { listen 80 server-name "127.0.0.1" tls { - cert "" - key "" + cert "/var/www/cert.pem" + key "/var/www/key.pem" } location "/" { root "/var/www/html" diff --git a/src/cert.rs b/src/cert.rs index 7ff35b8..df8a5d1 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -1,21 +1,97 @@ -use crate::error::DamasError; -use rustls::ServerConfig; -use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}; -use std::path::Path; +use crate::config::{TLSCertificate, TLSPrivateKey}; +use rustls::{ServerConfig, crypto::CryptoProvider}; + use std::sync::Arc; -pub fn validate_server_config( - cert_path: &Path, - key_path: &Path, -) -> Result, DamasError> { - let cert_der = CertificateDer::from_pem_file(cert_path) - .map_err(|e| DamasError::ConfigError(e.to_string()))?; - let key_der = PrivateKeyDer::from_pem_file(key_path) - .map_err(|e| DamasError::ConfigError(e.to_string()))?; +pub fn build_rustls_server_config( + provider: CryptoProvider, + cert: TLSCertificate, + key: TLSPrivateKey, +) -> Result, anyhow::Error> { Ok(Arc::new( - ServerConfig::builder() + ServerConfig::builder_with_provider(provider.into()) + .with_safe_default_protocol_versions() + .map_err(|e| anyhow::anyhow!("Protocol versions error: {}", e))? .with_no_client_auth() - .with_single_cert(vec![cert_der], key_der) - .map_err(|e| DamasError::ConfigError(e.to_string()))?, + .with_single_cert(vec![cert.into()], key.into()) + .map_err(|e| anyhow::anyhow!("Certificate error: {}", e))?, )) } + +#[cfg(test)] +mod tests { + use super::*; + use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}; + + const MOCK_CERT: &[u8] = b"-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUV5AL+XoFlAxN9oHMIFQ3F/2syb0wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMyMjEwMDg1M1oXDTM2MDMx +OTEwMDg1M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8A9EgfgRoxfvCp7gafm5im8cUUqjfpY2tiSY5FBKXoMN +CC8FdbDQ04PEsEC4pPo9kCUA1uW4zoMVYtBXJKtXWTWpLYALmC0aK3obHFwLUEY6 +F7cpIo62T+9TWdbNB+wTWuD58tIDDy9UW3CrCHiBUm+3cwoUa91IiA5W/mM3VO3h +211hP9DVRmn3r5nDRIUNNzeibnlAKWD28vWYtXsBH0rAjEDBBrKrytdCqllomTqL +oYjGxPaTxohvN1CkyHr6C3HkpoUE7NT5WkB2rMW1dQFuJZndTEYpFjyccICLobaO +RXYswmik87Ot/Yue9VTfznpsWvz3eTNMXrTOSHyeowIDAQABo1MwUTAdBgNVHQ4E +FgQUhWv+MszcHBAVIyzn3s7Ext4dXEMwHwYDVR0jBBgwFoAUhWv+MszcHBAVIyzn +3s7Ext4dXEMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAf5Dn +/6YfrjNUW7KhwFkHrzPmt96Rwn+wAyHZxgpyXjE58r+SVn50vTtAsdHl1pEIXoDI +A+O5BblCBcczSK5j0QS6GhhVQmq/qlgbD1seQVCdYjdhWrDwabiT7qNlMrG/Ou78 +uEIPs2YEO/9J4gLwDYfuEBbzm6YsafplRBk89ONnculUCcerK3TH7uwTj0tEMFze +MU5BnuTlkLIh/NfWWYMk6aQbyRXyGkZNrJxua6XZBOz3zphtPPmFcEh2SJYMKg2G +cLsgeG07wafYLYeQxXzTq5I+EYVLnqC9ekoYB1Ty5qoBM4RjLkYiSLFtahEZoP1D +ovn/Dd/ah7w0GjOE5Q== +-----END CERTIFICATE----- +"; + const MOCK_KEY: &[u8] = b"-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwD0SB+BGjF+8K +nuBp+bmKbxxRSqN+lja2JJjkUEpegw0ILwV1sNDTg8SwQLik+j2QJQDW5bjOgxVi +0Fckq1dZNaktgAuYLRorehscXAtQRjoXtykijrZP71NZ1s0H7BNa4Pny0gMPL1Rb +cKsIeIFSb7dzChRr3UiIDlb+YzdU7eHbXWE/0NVGafevmcNEhQ03N6JueUApYPby +9Zi1ewEfSsCMQMEGsqvK10KqWWiZOouhiMbE9pPGiG83UKTIevoLceSmhQTs1Pla +QHasxbV1AW4lmd1MRikWPJxwgIuhto5FdizCaKTzs639i571VN/Oemxa/Pd5M0xe +tM5IfJ6jAgMBAAECggEAPm9qHPd89tMhu7xol4d4pzWQwt/Lt/+viR3pme/796rT +993q6JotJeXugPzESTxASL4nAr1KnINhS4ruLz5VAIHBV3EnEtQgK1Cdvnl+A8nQ +EBz2GOPPLOkM35/LQZU3z3oV5/6RByEDKqkaAqD82YjuyH/FoewykhhQreb2HCMl +i2sS/aAszdhuFssH19z7YyYMNgRwzD0YIb3KOzwqI5t2pm3wTTJ7QzkZWFziadTN +UJ0Typ7uWLdcTuBF3TuGTm15AeLbzpY53d9oz29jrKVD0YBtnccf3OKrJwMAfxAA +U/Td7j0N4rQVDvn1uWmlHZi1FUAyOJSNBgD1CWxSEQKBgQD+6g2tEsXLRqHPFLun +er0PIU6M4Hk1+Vxcb1Qatzm8Wz4AJJ6Mr4MFUV2HNw+7k6BQ/XgKROYYYZPnepdH +jcOimukZ/Q4xFS+q4vj8+GfXHIkCIgZ86ncfQODpcrIBYlrFWNEt6i7MWOf7ttd0 +85Roj+2wmNAKpN1FA6sPaSsBCwKBgQDxFQRzv6ghzdcHShAwBT6MnVS0lzJyUdAA +UN4YMY3cOLV4z38jk8a8NieVnEGhBqKyl4iY8IvdOAXFkTCqHpCKCh9ldUZhb3kV +ZFndDtqCl/T31Hjhjtb9KD2OK2MpPQXqPMDXsDckDJDLkUIVcK1Wres1Sx16ekUn +O5ajjASHyQKBgQCD56jb/fLLlOj1tszDhQd/ZMS4sQ8HltjsG89xY45EoRIcENba +BZfOkKPM6/kAHwu93OrYpX5K73MRPKY7KGgrI+2qvP8y9ruLuZcNj5xr+yAKMoEY +8lphmbjIE8l4XeSKacMT9zHwG7Eu1xX2NnR9Brz/vJMqbtTweU1y1ACksQKBgF/6 +ORKHy7zhgOjDAJzNibBbdnyK8Sd4ELH/f9vr5ok0/nJBUWFtlKIbgTjbw3kC9kTZ +dSVGJriEdC/KdLBViL+b9hHjVYi242Kz197c6fsx2fHMYe+SeV7B5XezKEAjrjYp +x7BW1C0C36Zbhw6YFDo89TX7WJoJEXzkCT3FIYyZAoGBANCyOt5f5lYetrZOT2XP +e+w1mvgzD38vSC1bKVRt2YeWB/SWoEIyXJ28rE3IVW2UYPt53b+oPIP1lkHujLfV +HmM/bfOCTDfgtxnm77Mu43c8WLuaFiGvpH1o988Iuu2u1SOtJRW7cMiClxF7ywF5 +xJmPLcfZPY5yCuRAv0hCxafa +-----END PRIVATE KEY----- +"; + + fn mock_cert() -> (TLSCertificate, TLSPrivateKey) { + let cert_der = + CertificateDer::from_pem_slice(MOCK_CERT).expect("Failed to parse MOCK_CERT as PEM"); + + let key_der = + PrivateKeyDer::from_pem_slice(MOCK_KEY).expect("Failed to parse MOCK_KEY as PEM"); + + (TLSCertificate(cert_der), TLSPrivateKey(key_der)) + } + + #[test] + fn test_server_config_from_temp_files() { + let (cert, key) = mock_cert(); + let result = + build_rustls_server_config(rustls::crypto::ring::default_provider(), cert, key); + assert!( + result.is_ok(), + "Failed to build server config: {:?}", + result.err() + ); + } +} diff --git a/src/config.rs b/src/config.rs index a362c66..d1e9edd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,24 @@ +use anyhow::{Context, anyhow}; +use knus::ast::{Literal, TypeName}; +use knus::decode::Kind; +use knus::errors::{DecodeError, ExpectedType}; +use knus::span::Spanned; +use knus::traits::ErrorSpan; use miette::{IntoDiagnostic, miette}; +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use std::path::{Component, Path, PathBuf}; use std::str::FromStr; -#[derive(knus::Decode, Clone, Debug, PartialEq)] +#[derive(knus::Decode, Debug, PartialEq)] pub struct Config { #[knus(child)] pub server: ServerConfig, - #[knus(child)] + #[knus(child, default)] pub performance: PerformanceConfig, } -#[derive(knus::Decode, Clone, Debug, Default, PartialEq)] +#[derive(knus::Decode, Debug, Default, PartialEq)] pub struct ServerConfig { #[knus(child, unwrap(argument))] pub listen: u16, @@ -24,18 +32,150 @@ pub struct ServerConfig { pub error_pages: Vec, } -#[derive(knus::Decode, Clone, Debug, PartialEq)] +#[derive(knus::Decode, Debug, PartialEq)] pub struct TLSConfig { #[knus(child, unwrap(argument))] - pub cert: PathBuf, + pub cert: TLSCertificate, #[knus(child, unwrap(argument))] - pub key: PathBuf, + pub key: TLSPrivateKey, } -impl TLSConfig { - pub fn validate(&self) -> miette::Result<()> { - check_path_safety(&self.cert, "cert")?; - check_path_safety(&self.key, "key")?; - Ok(()) + +#[derive(Clone, Debug, PartialEq)] +pub struct TLSCertificate(pub CertificateDer<'static>); + +impl FromStr for TLSCertificate { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + Self::try_from(PathBuf::from(s)) + } +} + +impl TryFrom for TLSCertificate { + type Error = anyhow::Error; + + fn try_from(path: PathBuf) -> Result { + let data = + std::fs::read(&path).with_context(|| format!("couldn't find cert file: {:?}", path))?; + + let mut reader = &data[..]; + let cert = rustls_pemfile::certs(&mut reader) + .next() + .ok_or_else(|| anyhow!("couldn't find adequate cert file: {:?}", path))??; + + Ok(TLSCertificate(cert)) + } +} + +impl From for CertificateDer<'static> { + fn from(value: TLSCertificate) -> Self { + value.0 + } +} + +impl knus::DecodeScalar for TLSCertificate { + fn raw_decode( + val: &Spanned, + _: &mut knus::decode::Context, + ) -> Result> { + match &**val { + Literal::String(s) => { + let path = Path::new(&**s); + check_path_safety(path, "cert").map_err(|e| DecodeError::Conversion { + span: val.span().clone(), + source: e.into(), + })?; + CertificateDer::from_pem_file(path) + .map(TLSCertificate) + .map_err(|e| DecodeError::Conversion { + span: val.span().clone(), + source: Box::new(e), + }) + } + _ => Err(DecodeError::scalar_kind(Kind::String, val)), + } + } + fn type_check(type_name: &Option>, ctx: &mut knus::decode::Context) { + if let Some(typ) = type_name { + ctx.emit_error(DecodeError::TypeName { + span: typ.span().clone(), + found: Some((**typ).clone()), + expected: ExpectedType::no_type(), + rust_type: "TLSCertificate", + }); + } + } +} + +#[derive(Debug, PartialEq)] +pub struct TLSPrivateKey(pub PrivateKeyDer<'static>); + +impl TryFrom for TLSPrivateKey { + type Error = anyhow::Error; + + fn try_from(path: PathBuf) -> Result { + let data = + std::fs::read(&path).with_context(|| format!("couldn't find key file: {:?}", path))?; + + let mut reader = &data[..]; + let key = rustls_pemfile::private_key(&mut reader) + .map_err(|e| anyhow!("PEM parsing failed: {}", e))? + .ok_or_else(|| anyhow!("key file is empty: {:?}", path))?; + + Ok(TLSPrivateKey(key)) + } +} + +impl FromStr for TLSPrivateKey { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + Self::try_from(PathBuf::from(s)) + } +} + +impl From for PrivateKeyDer<'static> { + fn from(value: TLSPrivateKey) -> Self { + value.0 + } +} + +impl Clone for TLSPrivateKey { + fn clone(&self) -> Self { + TLSPrivateKey(self.0.clone_key()) + } +} + +impl knus::DecodeScalar for TLSPrivateKey { + fn raw_decode( + val: &Spanned, + _: &mut knus::decode::Context, + ) -> Result> { + match &**val { + Literal::String(s) => { + let path = Path::new(&**s); + check_path_safety(path, "key").map_err(|e| DecodeError::Conversion { + span: val.span().clone(), + source: e.into(), + })?; + PrivateKeyDer::from_pem_file(path) + .map(TLSPrivateKey) + .map_err(|e| DecodeError::Conversion { + span: val.span().clone(), + source: e.into(), + }) + } + _ => Err(DecodeError::scalar_kind(Kind::String, val)), + } + } + + fn type_check(type_name: &Option>, ctx: &mut knus::decode::Context) { + if let Some(typ) = type_name { + ctx.emit_error(DecodeError::TypeName { + span: typ.span().clone(), + found: Some((**typ).clone()), + expected: ExpectedType::no_type(), + rust_type: "TLSPrivateKey", + }); + } } } @@ -111,21 +251,62 @@ pub struct ErrorCodeEntry { #[derive(knus::Decode, Clone, Debug, Default, PartialEq)] pub struct PerformanceConfig { - #[knus(child, default=CryptoProvider::Ring, unwrap(argument))] - pub crypto_provider: CryptoProvider, - #[knus(child, unwrap(argument))] + #[knus(child, default, unwrap(argument))] + pub crypto_provider: CryptoType, + #[knus(child, default = 4096, unwrap(argument))] pub connection_buffer_size: usize, - #[knus(child, unwrap(argument))] + #[knus(child, default = 8019, unwrap(argument))] pub file_read_buffer_size: usize, - #[knus(child, unwrap(argument))] + #[knus(child, default = 64, unwrap(argument))] pub max_header_count: usize, } -#[derive(knus::DecodeScalar, Debug, Clone, Default, PartialEq)] -pub enum CryptoProvider { - #[default] +#[derive(knus::DecodeScalar, Debug, Clone, PartialEq)] +pub enum CryptoType { + #[knus(rename = "ring")] Ring, + #[knus(rename = "aws_lc_rs")] AwsLcRs, } + +impl Default for CryptoType { + fn default() -> CryptoType { + #[cfg(feature = "aws_lc_rs")] + return CryptoType::AwsLcRs; + + #[cfg(all(not(feature = "aws_lc_rs"), feature = "ring"))] + return CryptoType::Ring; + + #[cfg(all(not(feature = "aws_lc_rs"), not(feature = "ring")))] + compile_error!("At least one of 'ring' or 'aws_lc_rs' features must be enabled!"); + } +} + +impl From for rustls::crypto::CryptoProvider { + fn from(ty: CryptoType) -> Self { + match ty { + CryptoType::Ring => { + #[cfg(feature = "ring")] + { + rustls::crypto::ring::default_provider() + } + #[cfg(not(feature = "ring"))] + { + panic!("'ring' feature is not enabled. Check your Cargo.toml"); + } + } + CryptoType::AwsLcRs => { + #[cfg(feature = "aws_lc_rs")] + { + rustls::crypto::aws_lc_rs::default_provider() + } + #[cfg(not(feature = "aws_lc_rs"))] + { + panic!("'aws-lc-rs' feature is not enabled. Check your Cargo.toml"); + } + } + } + } +} impl Config { pub fn validate(&self) -> miette::Result<()> { for loc in &self.server.locations { @@ -141,7 +322,7 @@ pub fn parse_config(config_path: &str) -> miette::Result { Ok(config) } -fn check_path_safety(target: &Path, field_name: &str) -> miette::Result<()> { +fn check_path_safety(target: &Path, field_name: &str) -> Result<(), miette::Error> { let mut depth = 0; for component in target.components() { @@ -184,8 +365,23 @@ fn is_pure_filename(filename: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use std::fs; use tempfile; + const INVALID_CERT: &[u8] = b"-----BEGIN CERTIFICATE-----\nfoobar\n-----END CERTIFICATE-----"; + const INVALID_KEY: &[u8] = b"-----BEGIN PRIVATE KEY-----\nfoobar=\n-----END PRIVATE KEY-----"; + + fn mock_cert() -> (TLSCertificate, TLSPrivateKey) { + let mock_cert_path = Path::new("test/fixtures/tls/cert.pem"); + let mock_cert = fs::read_to_string(mock_cert_path).unwrap(); + let mock_key_path = Path::new("test/fixtures/tls/key.pem"); + let mock_key = fs::read_to_string(mock_key_path).unwrap(); + ( + TLSCertificate(CertificateDer::from_pem_slice(mock_cert.as_bytes()).unwrap()), + TLSPrivateKey(PrivateKeyDer::from_pem_slice(mock_key.as_bytes()).unwrap()), + ) + } + #[test] fn test_is_pure_filename() { assert!(is_pure_filename("file.txt")); @@ -241,20 +437,6 @@ mod tests { let result = parse_config(config_path.to_str().unwrap()); assert!(result.is_err()); } - #[test] - fn test_tls_config_validate() { - let valid_tls = TLSConfig { - cert: PathBuf::from("/etc/tls/cert.pem"), - key: PathBuf::from("/etc/tls/key.pem"), - }; - assert!(valid_tls.validate().is_ok()); - - let invalid_tls = TLSConfig { - cert: PathBuf::from("../cert.pem"), - key: PathBuf::from("/etc/tls/key.pem"), - }; - assert!(invalid_tls.validate().is_err()); - } #[test] fn test_performance_config_defaults() { @@ -270,7 +452,7 @@ mod tests { } "#; let config = knus::parse::("test.kdl", config_str).unwrap(); - assert_eq!(config.performance.crypto_provider, CryptoProvider::Ring); + assert_eq!(config.performance.crypto_provider, CryptoType::Ring); assert_eq!(config.performance.connection_buffer_size, 1024); } @@ -281,8 +463,8 @@ mod tests { listen 443 server-name "example.com" tls { - cert "/path/to/cert" - key "/path/to/key" + cert "test/fixtures/tls/cert.pem" + key "test/fixtures/tls/key.pem" } } performance { @@ -295,15 +477,48 @@ mod tests { let config = knus::parse::("test.kdl", config_str).unwrap(); let tls = config.server.tls.as_ref().unwrap(); - assert_eq!(tls.cert, PathBuf::from("/path/to/cert")); - assert_eq!(tls.key, PathBuf::from("/path/to/key")); + let (cert, key) = mock_cert(); + assert_eq!(tls.cert, cert); + assert_eq!(tls.key, key); - assert_eq!(config.performance.crypto_provider, CryptoProvider::AwsLcRs); + assert_eq!(config.performance.crypto_provider, CryptoType::AwsLcRs); assert_eq!(config.performance.connection_buffer_size, 8192); assert_eq!(config.performance.file_read_buffer_size, 16384); assert_eq!(config.performance.max_header_count, 128); } + #[test] + fn test_parse_with_invalid_tls() { + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join("config.kdl"); + let invalid_cert = dir.path().join("invalid_cert.pem"); + std::fs::write(&invalid_cert, INVALID_CERT).unwrap(); + let invalid_key = dir.path().join("invalid_key.pem"); + std::fs::write(&invalid_key, INVALID_KEY).unwrap(); + let invalid_config = format!( + r#" + server {{ + listen 443 + server-name "example.com" + tls {{ + cert "{}" + key "{}" + }} + }} + "#, + invalid_cert.display(), + invalid_key.display() + ); + std::fs::write(&config_path, invalid_config).unwrap(); + let result = parse_config(config_path.to_str().unwrap()); + assert!( + result.is_err(), + "parsing with invalid tls certificate should fail" + ); + let err_msg = format!("{:?}", result.err().unwrap()); + assert!(err_msg.contains("base64") || err_msg.contains("decode")); + } + #[test] fn test_parse() { let config = r#" diff --git a/src/server.rs b/src/server.rs index 3271fa0..09ec05a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,5 @@ use crate::ServerContext; -use crate::cert::validate_server_config; +use crate::cert::build_rustls_server_config; use crate::config::Config; use crate::error::ErrorRegistry; use crate::http::handle_request; @@ -37,15 +37,24 @@ impl Server { let error_registry = ErrorRegistry::new(&JINJA_ENV, 100); error_registry.init_with_config(&config).await; tracing::info!("initialized error registry"); - let acceptor = config.server.tls.as_ref().and_then(|ssl| { - match validate_server_config(&ssl.cert, &ssl.key) { - Ok(server_config) => Some(TlsAcceptor::from(server_config)), - Err(e) => { - tracing::info!("Error validating TLS config: {}", e); - None - } - } - }); + let provider = &config.performance.crypto_provider; + let acceptor = if let Some(ssl) = &config.server.tls { + let rustls_config = build_rustls_server_config( + provider.clone().into(), + ssl.cert.clone(), + ssl.key.clone(), + ) + .map_err(|e| { + tracing::error!("Invalid TLS configuration: {}", e); + e + })?; + + Some(TlsAcceptor::from(rustls_config)) + } else { + None + }; + + tracing::info!("initialized with crypto provider: {:?}", provider); Ok(Self { router, From 7325360ee45cc7c87b6e5fb6a276a6d24e1e87f8 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sun, 5 Apr 2026 13:14:29 +0900 Subject: [PATCH 5/7] feat: implement TLS configuration and integrate server-side acceptor --- rust-toolchain.toml | 2 +- test/fixtures/tls/cert.pem | 19 +++ test/fixtures/tls/invalid_pem.pem | 3 + test/fixtures/tls/key.pem | 28 ++++ test/tls_test.rs | 261 ++++++++++++++++++++++++++++++ 5 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/tls/cert.pem create mode 100644 test/fixtures/tls/invalid_pem.pem create mode 100644 test/fixtures/tls/key.pem create mode 100644 test/tls_test.rs diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c262dfe..47ed48e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] channel = "1.90.0" -components = ["rustfmt", "clippy", "rustfmt", "rust-src", "rust-analyzer"] +components = ["rustfmt", "clippy", "rust-src", "rust-analyzer"] targets = [ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", diff --git a/test/fixtures/tls/cert.pem b/test/fixtures/tls/cert.pem new file mode 100644 index 0000000..00acfa7 --- /dev/null +++ b/test/fixtures/tls/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUV5AL+XoFlAxN9oHMIFQ3F/2syb0wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMyMjEwMDg1M1oXDTM2MDMx +OTEwMDg1M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8A9EgfgRoxfvCp7gafm5im8cUUqjfpY2tiSY5FBKXoMN +CC8FdbDQ04PEsEC4pPo9kCUA1uW4zoMVYtBXJKtXWTWpLYALmC0aK3obHFwLUEY6 +F7cpIo62T+9TWdbNB+wTWuD58tIDDy9UW3CrCHiBUm+3cwoUa91IiA5W/mM3VO3h +211hP9DVRmn3r5nDRIUNNzeibnlAKWD28vWYtXsBH0rAjEDBBrKrytdCqllomTqL +oYjGxPaTxohvN1CkyHr6C3HkpoUE7NT5WkB2rMW1dQFuJZndTEYpFjyccICLobaO +RXYswmik87Ot/Yue9VTfznpsWvz3eTNMXrTOSHyeowIDAQABo1MwUTAdBgNVHQ4E +FgQUhWv+MszcHBAVIyzn3s7Ext4dXEMwHwYDVR0jBBgwFoAUhWv+MszcHBAVIyzn +3s7Ext4dXEMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAf5Dn +/6YfrjNUW7KhwFkHrzPmt96Rwn+wAyHZxgpyXjE58r+SVn50vTtAsdHl1pEIXoDI +A+O5BblCBcczSK5j0QS6GhhVQmq/qlgbD1seQVCdYjdhWrDwabiT7qNlMrG/Ou78 +uEIPs2YEO/9J4gLwDYfuEBbzm6YsafplRBk89ONnculUCcerK3TH7uwTj0tEMFze +MU5BnuTlkLIh/NfWWYMk6aQbyRXyGkZNrJxua6XZBOz3zphtPPmFcEh2SJYMKg2G +cLsgeG07wafYLYeQxXzTq5I+EYVLnqC9ekoYB1Ty5qoBM4RjLkYiSLFtahEZoP1D +ovn/Dd/ah7w0GjOE5Q== +-----END CERTIFICATE----- diff --git a/test/fixtures/tls/invalid_pem.pem b/test/fixtures/tls/invalid_pem.pem new file mode 100644 index 0000000..6806a82 --- /dev/null +++ b/test/fixtures/tls/invalid_pem.pem @@ -0,0 +1,3 @@ +-----BEGIN POOP----- +foo-bar +-----END POOP----- diff --git a/test/fixtures/tls/key.pem b/test/fixtures/tls/key.pem new file mode 100644 index 0000000..7624ea9 --- /dev/null +++ b/test/fixtures/tls/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwD0SB+BGjF+8K +nuBp+bmKbxxRSqN+lja2JJjkUEpegw0ILwV1sNDTg8SwQLik+j2QJQDW5bjOgxVi +0Fckq1dZNaktgAuYLRorehscXAtQRjoXtykijrZP71NZ1s0H7BNa4Pny0gMPL1Rb +cKsIeIFSb7dzChRr3UiIDlb+YzdU7eHbXWE/0NVGafevmcNEhQ03N6JueUApYPby +9Zi1ewEfSsCMQMEGsqvK10KqWWiZOouhiMbE9pPGiG83UKTIevoLceSmhQTs1Pla +QHasxbV1AW4lmd1MRikWPJxwgIuhto5FdizCaKTzs639i571VN/Oemxa/Pd5M0xe +tM5IfJ6jAgMBAAECggEAPm9qHPd89tMhu7xol4d4pzWQwt/Lt/+viR3pme/796rT +993q6JotJeXugPzESTxASL4nAr1KnINhS4ruLz5VAIHBV3EnEtQgK1Cdvnl+A8nQ +EBz2GOPPLOkM35/LQZU3z3oV5/6RByEDKqkaAqD82YjuyH/FoewykhhQreb2HCMl +i2sS/aAszdhuFssH19z7YyYMNgRwzD0YIb3KOzwqI5t2pm3wTTJ7QzkZWFziadTN +UJ0Typ7uWLdcTuBF3TuGTm15AeLbzpY53d9oz29jrKVD0YBtnccf3OKrJwMAfxAA +U/Td7j0N4rQVDvn1uWmlHZi1FUAyOJSNBgD1CWxSEQKBgQD+6g2tEsXLRqHPFLun +er0PIU6M4Hk1+Vxcb1Qatzm8Wz4AJJ6Mr4MFUV2HNw+7k6BQ/XgKROYYYZPnepdH +jcOimukZ/Q4xFS+q4vj8+GfXHIkCIgZ86ncfQODpcrIBYlrFWNEt6i7MWOf7ttd0 +85Roj+2wmNAKpN1FA6sPaSsBCwKBgQDxFQRzv6ghzdcHShAwBT6MnVS0lzJyUdAA +UN4YMY3cOLV4z38jk8a8NieVnEGhBqKyl4iY8IvdOAXFkTCqHpCKCh9ldUZhb3kV +ZFndDtqCl/T31Hjhjtb9KD2OK2MpPQXqPMDXsDckDJDLkUIVcK1Wres1Sx16ekUn +O5ajjASHyQKBgQCD56jb/fLLlOj1tszDhQd/ZMS4sQ8HltjsG89xY45EoRIcENba +BZfOkKPM6/kAHwu93OrYpX5K73MRPKY7KGgrI+2qvP8y9ruLuZcNj5xr+yAKMoEY +8lphmbjIE8l4XeSKacMT9zHwG7Eu1xX2NnR9Brz/vJMqbtTweU1y1ACksQKBgF/6 +ORKHy7zhgOjDAJzNibBbdnyK8Sd4ELH/f9vr5ok0/nJBUWFtlKIbgTjbw3kC9kTZ +dSVGJriEdC/KdLBViL+b9hHjVYi242Kz197c6fsx2fHMYe+SeV7B5XezKEAjrjYp +x7BW1C0C36Zbhw6YFDo89TX7WJoJEXzkCT3FIYyZAoGBANCyOt5f5lYetrZOT2XP +e+w1mvgzD38vSC1bKVRt2YeWB/SWoEIyXJ28rE3IVW2UYPt53b+oPIP1lkHujLfV +HmM/bfOCTDfgtxnm77Mu43c8WLuaFiGvpH1o988Iuu2u1SOtJRW7cMiClxF7ywF5 +xJmPLcfZPY5yCuRAv0hCxafa +-----END PRIVATE KEY----- diff --git a/test/tls_test.rs b/test/tls_test.rs new file mode 100644 index 0000000..9381a64 --- /dev/null +++ b/test/tls_test.rs @@ -0,0 +1,261 @@ +use bytes::Bytes; +use compio::BufResult; +use compio::buf::{IoBuf, IoBufMut}; +use compio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use compio::time::timeout; +use damas::cert::build_rustls_server_config; +use damas::config::{TLSCertificate, TLSPrivateKey}; +use futures::SinkExt; +use futures::channel::mpsc::{self, Receiver, Sender}; +use futures::future::join; +use rustls::SignatureScheme; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::{ClientConfig, DigitallySignedStruct}; +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::{CertificateDer, ServerName, UnixTime}; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +struct MemoryStream { + tx: Sender, + rx: Receiver, + read_buffer: Option, +} + +impl AsyncRead for MemoryStream { + async fn read(&mut self, mut buf: B) -> BufResult { + tracing::debug!(target: "MemoryStream", capacity = buf.buf_capacity(), "read requested"); + if self.read_buffer.as_ref().is_none_or(|b| b.is_empty()) { + tracing::trace!(target: "MemoryStream", "buffer empty,waiting for data"); + match self.rx.recv().await { + Ok(data) => { + tracing::debug!(target: "MemoryStream", len = data.len(), "received bytes from rx"); + self.read_buffer = Some(data) + } + _ => { + tracing::warn!(target: "MemoryStream", "rx channel closed during read"); + return BufResult(Ok(0), buf); + } + } + } + + let rb = self.read_buffer.as_mut().unwrap(); + let consumed = std::cmp::min(rb.len(), buf.buf_capacity()); + let chunk = rb.split_to(consumed); + + unsafe { + std::ptr::copy_nonoverlapping(chunk.as_ptr(), buf.as_buf_mut_ptr(), consumed); + buf.set_buf_init(consumed); + } + + if rb.is_empty() { + self.read_buffer = None; + } + tracing::info!(target: "MemoryStream", bytes = consumed, "read successful"); + BufResult(Ok(consumed), buf) + } +} + +impl AsyncWrite for MemoryStream { + async fn write(&mut self, buf: T) -> BufResult { + let len = buf.as_slice().len(); + tracing::debug!(target: "MemoryStream", len, "write requested"); + + let data = bytes::Bytes::copy_from_slice(buf.as_slice()); + + if let Err(e) = self.tx.try_send(data) { + tracing::warn!(target: "MemoryStream", "broken pipe"); + if e.is_full() { + tracing::debug!(target: "MemoryStream", "channel full, awaiting capacity"); + if self.tx.send(e.into_inner()).await.is_err() { + tracing::error!(target: "MemoryStream", "failed to send data: channel closed"); + return BufResult( + Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "closed", + )), + buf, + ); + } + } else { + tracing::error!(target: "MemoryStream", "write failed: broken pipe"); + return BufResult( + Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "closed", + )), + buf, + ); + } + }; + tracing::info!(target: "MemoryStream", bytes = len, "Channel transfer complete"); + BufResult(Ok(len), buf) + } + async fn flush(&mut self) -> Result<(), std::io::Error> { + Ok(()) + } + async fn shutdown(&mut self) -> Result<(), std::io::Error> { + self.tx.close_channel(); + Ok(()) + } +} + +fn create_duplex_pair(buffer_size: usize) -> (MemoryStream, MemoryStream) { + let (server_tx, client_rx) = mpsc::channel::(buffer_size); + let (client_tx, server_rx) = mpsc::channel::(buffer_size); + ( + MemoryStream { + tx: server_tx, + rx: server_rx, + read_buffer: None, + }, + MemoryStream { + tx: client_tx, + rx: client_rx, + read_buffer: None, + }, + ) +} +#[derive(Debug)] +struct NoVerifier; + +impl ServerCertVerifier for NoVerifier { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } +} + +fn make_dangerous_client_config() -> Result, anyhow::Error> { + let verifier: Arc = Arc::new(NoVerifier); + + let config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(verifier) + .with_no_client_auth(); + + Ok(Arc::new(config)) +} + +fn load_cert(path: &str) -> TLSCertificate { + let mut base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + base.push(path); + + let cert = rustls_pki_types::CertificateDer::from_pem_file(&base) + .unwrap_or_else(|_| panic!("fail to load cert.pem: {:?}", base)); + TLSCertificate(cert) +} + +fn load_key(path: &str) -> TLSPrivateKey { + let mut base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + base.push(path); + + let key = rustls_pki_types::PrivateKeyDer::from_pem_file(&base) + .unwrap_or_else(|_| panic!("fail to load key.pem: {:?}", base)); + TLSPrivateKey(key) +} + +#[compio::test] +async fn test_tls_handshake_runtime() -> Result<(), anyhow::Error> { + rustls::crypto::ring::default_provider() + .install_default() + .ok(); + + let client_config = make_dangerous_client_config()?; + let provider = rustls::crypto::ring::default_provider(); + let cert: TLSCertificate = load_cert("test/fixtures/tls/cert.pem"); + let key: TLSPrivateKey = load_key("test/fixtures/tls/key.pem"); + let server_config = build_rustls_server_config(provider, cert, key)?; + + //inmemory stream + let (server_io, client_io) = create_duplex_pair(65536); + + // server side handshake + let server_task = compio::runtime::spawn(async move { + let _span = tracing::info_span!("server").entered(); + tracing::info!("handshake start"); + let acceptor = compio::tls::TlsAcceptor::from(server_config); + let mut stream = acceptor + .accept(server_io) + .await + .expect("Server handshake failed"); + tracing::info!("handshake done"); + + let buf = [0u8; 4]; + let BufResult(_, buf) = stream.read(buf).await; + assert_eq!(&buf, b"ping"); + + let BufResult(res, _) = stream.write_all(b"pong").await; + assert!(res.is_ok(), "Writing to TLS stream should succeed"); + let _ = stream.shutdown().await; + Ok::<(), anyhow::Error>(()) + }); + + // client side handshake + let client_task = compio::runtime::spawn(async move { + let _span = tracing::info_span!("client").entered(); + tracing::info!("handshake start"); + let connector = compio::tls::TlsConnector::from(client_config); + let domain = "localhost"; + let mut client_stream = connector + .connect(domain, client_io) + .await + .expect("Client handshake failed"); + tracing::info!("handshake done"); + + let BufResult(res, _) = client_stream.write_all(b"ping").await; + assert!(res.is_ok(), "Writing to TLS stream should succeed"); + client_stream.flush().await.expect("flush failed"); + + let client_buf = [0u8; 4]; + let BufResult(_, buf) = client_stream.read(client_buf).await; + assert_eq!(&buf, b"pong"); + + let _ = client_stream.shutdown().await; + Ok::<(), anyhow::Error>(()) + }); + + let result = timeout(Duration::from_secs(10), join(server_task, client_task)).await; + + match result { + Ok((server_res, client_res)) => { + server_res.map_err(|e| anyhow::anyhow!("Server task panicked: {:?}", e))??; + client_res.map_err(|e| anyhow::anyhow!("Client task panicked: {:?}", e))??; + } + Err(_) => { + panic!("test timeout"); + } + } + Ok(()) +} From e78fedc48c5f04494da455331b262683bd6f7a74 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Sun, 5 Apr 2026 13:14:52 +0900 Subject: [PATCH 6/7] test: add TLS handshake integration tests and fixtures --- test/fixtures/tls/invalid_cert.pem | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/fixtures/tls/invalid_cert.pem diff --git a/test/fixtures/tls/invalid_cert.pem b/test/fixtures/tls/invalid_cert.pem new file mode 100644 index 0000000..bc29da2 --- /dev/null +++ b/test/fixtures/tls/invalid_cert.pem @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE------ +Zm9vLWJhcgo=6 +-----END CERTIFICATE------ From 935b9b7eb772a8bdea4ab372ee47426f322ca9e0 Mon Sep 17 00:00:00 2001 From: kyokuping Date: Wed, 15 Apr 2026 00:51:43 +0900 Subject: [PATCH 7/7] feat: implement graceful TLS shutdown RFC 8446 --- src/http.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/http.rs b/src/http.rs index 54b06ee..b629e8e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -83,6 +83,11 @@ pub async fn handle_request( matched_handler .handle_request(stream, context, path_str, remaining_path) .await?; + + stream.shutdown().await.map_err(|e| { + tracing::error!("TLS shutdown error: {:?}", e); + e + })?; } Ok(()) }