1extern crate self as rust_web_server;
39
40pub mod app;
41#[cfg(feature = "auth")]
42pub mod auth;
43
44#[cfg(feature = "macros")]
45pub use rws_macros::{delete, get, patch, post, put, route, Config, FromRequest, JsonSchema, Validate};
46#[cfg(all(feature = "macros", any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql")))]
47pub use rws_macros::Model;
48#[cfg(feature = "http2")]
49pub mod async_state;
50#[cfg(feature = "http2")]
51pub(crate) mod async_bridge;
52pub mod session;
53#[cfg(not(target_arch = "wasm32"))]
56pub(crate) mod redis_protocol;
57pub mod sse;
58pub mod compression;
59pub mod cookie;
60pub mod error;
61pub mod extract;
62pub mod i18n;
63pub mod a2a;
64#[cfg(all(unix, any(feature = "http1", feature = "http2")))]
65pub mod systemd;
66pub mod ip_filter;
67pub mod macros;
68pub mod blocklist;
69pub mod cache;
70pub mod early_hints;
71pub mod disk_guard;
72pub mod traffic;
73pub mod authz;
74pub mod config_reload;
75#[cfg(not(target_arch = "wasm32"))]
76pub mod config_watch;
77pub mod config_check;
78#[cfg(any(feature = "auth-asymmetric", feature = "sso"))]
80pub mod rsa_key;
81pub mod server_config;
82pub mod feature;
83pub mod maintenance;
84pub mod metrics;
85pub mod mcp;
86pub mod request_log;
87pub mod request_id;
88#[cfg(not(target_arch = "wasm32"))]
89pub mod otel;
90#[cfg(feature = "acme")]
91pub mod acme;
92pub mod middleware;
93pub mod rate_limit;
94pub mod router;
95pub mod schema;
96pub mod state;
97pub mod test_client;
98pub mod application;
99pub mod body;
100pub mod client_hint;
101pub mod controller;
102pub mod core;
103pub mod cors;
104pub mod entry_point;
105pub mod ext;
106pub mod header;
107pub mod http;
108pub mod json;
109pub mod language;
110#[cfg(not(target_arch = "wasm32"))]
111pub mod log;
112pub mod mime_type;
113pub mod null;
114pub mod range;
115pub mod request;
116pub mod response;
117pub mod server;
118pub mod symbol;
119#[cfg(not(target_arch = "wasm32"))]
120pub mod thread_pool;
121pub mod url;
122pub mod pagination;
123#[cfg(not(target_arch = "wasm32"))]
124pub mod proxy;
125pub mod rewrite;
126pub mod scheduler;
127#[cfg(not(target_arch = "wasm32"))]
128pub mod tcp_proxy;
129#[cfg(not(target_arch = "wasm32"))]
130pub mod udp_proxy;
131#[cfg(not(target_arch = "wasm32"))]
132pub mod ws_proxy;
133#[cfg(not(target_arch = "wasm32"))]
134pub mod canary;
135#[cfg(not(target_arch = "wasm32"))]
136pub mod circuit_breaker;
137pub mod service_discovery;
142pub(crate) mod dns;
144pub mod config_binding;
145pub mod di;
146#[cfg(not(target_arch = "wasm32"))]
147pub mod proxy_config;
148#[cfg(not(target_arch = "wasm32"))]
149pub mod llm;
150#[cfg(not(target_arch = "wasm32"))]
151pub mod admin;
152#[cfg(all(feature = "http2", not(target_arch = "wasm32")))]
154pub mod fleet;
155pub mod waf;
157#[cfg(not(target_arch = "wasm32"))]
159pub mod cluster;
160#[cfg(not(target_arch = "wasm32"))]
162pub mod fastcgi;
163#[cfg(not(target_arch = "wasm32"))]
165pub mod ctl;
166#[cfg(feature = "wasm-plugins")]
167pub mod wasm_plugin;
168#[cfg(not(target_arch = "wasm32"))]
169pub mod ingress;
170#[cfg(feature = "tera")]
171pub mod template;
172pub mod validate;
173pub mod virtual_host;
174#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
175pub mod model;
176#[cfg(not(target_arch = "wasm32"))]
177pub mod websocket;
178pub mod http_client;
182#[cfg(feature = "crypto")]
183pub mod crypto;
184#[cfg(feature = "csrf")]
185pub mod csrf;
186#[cfg(feature = "sso")]
187pub mod sso;
188#[cfg(all(feature = "mailer", not(target_arch = "wasm32")))]
192pub mod mailer;
193#[cfg(feature = "jobs")]
194pub mod jobs;
195#[cfg(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs", feature = "secrets"))]
198pub mod storage;
199#[cfg(feature = "openapi")]
200pub mod openapi;
201#[cfg(feature = "tower")]
202pub mod tower;
203#[cfg(feature = "gateway-api")]
204pub mod gateway_api;
205#[cfg(feature = "webhook")]
206pub mod webhook;
207#[cfg(feature = "secrets")]
208pub mod secrets;
209#[cfg(not(target_arch = "wasm32"))]
212pub mod timeout;
213pub mod prelude;
214
215#[cfg(feature = "http2")]
216#[doc(hidden)]
217pub mod tls;
218
219#[cfg(feature = "http2")]
220#[doc(hidden)]
221pub mod h2_server;
222
223#[cfg(feature = "http3")]
224#[doc(hidden)]
225pub mod h3_handler;
226
227#[cfg(test)]
234mod stack_depth_tests;
235
236#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
240pub fn crypto_provider() -> rustls::crypto::CryptoProvider {
241 #[cfg(feature = "fips")]
242 return rustls::crypto::default_fips_provider();
243 #[cfg(not(feature = "fips"))]
244 rustls::crypto::aws_lc_rs::default_provider()
245}
246
247pub const FIPS: bool = cfg!(feature = "fips");
249
250#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
257pub(crate) fn tls_client_builder() -> rustls::ConfigBuilder<rustls::ClientConfig, rustls::WantsVerifier> {
258 rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(crate::crypto_provider()))
259 .with_safe_default_protocol_versions()
260 .expect("aws-lc-rs supports rustls's default protocol versions")
261}
262
263#[cfg(test)]
264pub mod test_env {
265 use std::sync::{Mutex, OnceLock};
266 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
267 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
268 LOCK.get_or_init(|| Mutex::new(()))
269 .lock()
270 .unwrap_or_else(|e| e.into_inner())
271 }
272
273 pub struct TempPath(std::path::PathBuf);
277
278 impl TempPath {
279 pub fn new(name: &str) -> Self {
280 TempPath(std::env::temp_dir().join(format!("rws_test_{}_{}", std::process::id(), name)))
281 }
282
283 pub fn new_in_cwd(name: &str) -> Self {
286 TempPath(std::env::current_dir().expect("working directory").join(format!("rws_test_{}_{}", std::process::id(), name)))
287 }
288
289 pub fn as_str(&self) -> &str {
290 self.0.to_str().expect("temp dir path is UTF-8")
291 }
292 }
293
294 impl std::ops::Deref for TempPath {
295 type Target = std::path::Path;
296 fn deref(&self) -> &std::path::Path {
297 &self.0
298 }
299 }
300
301 impl Drop for TempPath {
302 fn drop(&mut self) {
303 let is_link = std::fs::symlink_metadata(&self.0).is_ok_and(|m| m.file_type().is_symlink());
306 if is_link {
307 let _ = std::fs::remove_file(&self.0);
308 } else if self.0.is_dir() {
309 let _ = std::fs::remove_dir_all(&self.0);
310 } else {
311 let _ = std::fs::remove_file(&self.0);
312 }
313 }
314 }
315}
316
317#[cfg(fuzzing)]
321#[doc(hidden)]
322pub mod fuzz_api {
323 #[cfg(all(feature = "http2", not(target_arch = "wasm32")))]
327 pub fn fleet_signature(data: &[u8]) {
328 let _ = crate::fleet::parse_signature(data);
329 let (body, sig) = data.split_at(data.len() / 2);
330 assert!(crate::fleet::verify(body, sig, &[7u8; 32]).is_err(), "a fuzzed signature verified");
331 }
332
333 #[cfg(feature = "pkcs11")]
335 pub fn pkcs11_uri(data: &[u8]) {
336 if let Ok(text) = std::str::from_utf8(data) {
337 let _ = crate::tls::pkcs11::Pkcs11Uri::parse(text);
338 let _ = crate::entry_point::shown("RWS_CONFIG_TLS_KEY_FILE", text);
339 }
340 }
341
342 pub fn proxy_normalize_target(data: &[u8]) {
345 let Ok(target) = std::str::from_utf8(data) else { return };
346 let once = crate::proxy_config::normalize_request_target(target).into_owned();
347 let twice = crate::proxy_config::normalize_request_target(&once).into_owned();
348 assert_eq!(once, twice, "normalization is not idempotent for {:?}", target);
349 }
350
351 pub fn static_path_decode(data: &[u8]) {
354 let Ok(path) = std::str::from_utf8(data) else { return };
355 if let Ok(fs_path) = crate::app::controller::static_resource::static_fs_path(path) {
356 let relative = &fs_path[crate::app::controller::static_resource::working_directory().len()..];
357 assert!(!relative.split(['/', '\\']).any(|segment| segment == ".."), "{:?} -> {:?}", path, fs_path);
358 }
359 }
360
361 pub fn openapi_to_mcp(data: &[u8]) {
364 let Ok(spec) = std::str::from_utf8(data) else { return };
365 let options = crate::mcp::openapi_tools::ToolOptions::default();
366 let Ok(found) = crate::mcp::openapi_tools::tools(spec, &options) else { return };
367 for tool in found {
368 assert!(crate::service_discovery::json_lite::parse(&tool.input_schema).is_ok());
369 assert!(tool.name.len() <= 64 && tool.name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'), "{}", tool.name);
370 let _ = tool.request(r#"{"id":"1","body":{"a":[1,2]},"q":["x","y"]}"#);
371 }
372 }
373
374 pub fn a2a_request(data: &[u8]) {
377 use crate::application::Application;
378 thread_local! {
379 static AGENT: crate::a2a::A2aServer = crate::a2a::A2aServer::new(crate::a2a::AgentCard::new("fuzz", "fuzz", "1"))
380 .on_message(|ctx, message| match message.text().len() % 3 {
381 0 => crate::a2a::A2aReply::Message(crate::a2a::Message::agent_text(message.text())),
382 1 => crate::a2a::A2aReply::Task(ctx.input_required("more?")),
383 _ => crate::a2a::A2aReply::Task(ctx.complete(vec![crate::a2a::Artifact::text("a", message.text())])),
384 });
385 }
386 let request = crate::request::Request {
387 method: "POST".to_string(),
388 request_uri: "/a2a".to_string(),
389 http_version: "HTTP/1.1".to_string(),
390 headers: vec![crate::header::Header::new("A2A-Version", "1.0".to_string())],
391 body: data.to_vec().into(),
392 };
393 let connection = crate::server::ConnectionInfo::default();
394 let response = AGENT.with(|agent| agent.execute(&request, &connection)).expect("A2A never errs");
395 if response.status_code == 200 {
396 let body = String::from_utf8(response.content_range_list[0].body.clone()).expect("UTF-8");
397 assert!(crate::service_discovery::json_lite::parse(&body).is_ok(), "not JSON: {body}");
398 }
399 }
400
401 pub fn accept_language(data: &[u8]) {
405 let Ok(value) = std::str::from_utf8(data) else { return };
406 let preferences = crate::i18n::AcceptLanguage::parse(value);
407 assert!(preferences.0.len() <= crate::i18n::MAX_LANGUAGE_RANGES);
408 let available = ["en", "en-GB", "uk", "pt-BR"];
409 if let Some(chosen) = preferences.negotiate(&available) {
410 assert!(available.contains(&chosen));
411 }
412 }
413
414 pub fn i18n_catalog(data: &[u8]) {
417 let Ok(text) = std::str::from_utf8(data) else { return };
418 if let Ok(catalogs) = crate::i18n::Catalogs::new("en").add("en", text) {
419 let t = catalogs.for_language("en");
420 for line in text.lines() {
421 let key = line.split('=').next().unwrap_or("").trim();
422 let _ = t.t_with(key, &[("name", "x")]);
423 let _ = t.plural(key.trim_end_matches(".one").trim_end_matches(".other"), 3);
424 }
425 }
426 }
427
428 pub fn accept_encoding(data: &[u8]) {
432 let Ok(value) = std::str::from_utf8(data) else { return };
433 let request = crate::request::Request {
434 method: "GET".to_string(),
435 request_uri: "/".to_string(),
436 http_version: "HTTP/1.1".to_string(),
437 headers: vec![crate::header::Header::new(crate::header::Header::_ACCEPT_ENCODING, value.to_string())],
438 body: Default::default(),
439 };
440 for preference in [crate::compression::DYNAMIC_PREFERENCE, crate::compression::STATIC_PREFERENCE] {
441 if let Some(chosen) = crate::compression::negotiate(&request, preference) {
442 assert!(preference.contains(&chosen));
443 let refused = value.split(',').any(|c| {
445 let mut parts = c.split(';');
446 parts.next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())
447 && parts
448 .map(str::trim)
449 .find(|p| p.as_bytes().get(..2).is_some_and(|b| b.eq_ignore_ascii_case(b"q=")))
450 .is_some_and(|q| q[2..].trim().parse::<f32>() == Ok(0.0))
451 });
452 let listed_once = value.split(',').filter(|c| c.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())).count() == 1;
453 assert!(!(refused && listed_once), "{value:?} refused {chosen:?}");
454 }
455 }
456 }
457
458 #[cfg(feature = "wasm-plugins")]
461 pub fn wasm_plugin_output(data: &[u8]) {
462 let _ = crate::wasm_plugin::parse_request_output(data);
463 let _ = crate::wasm_plugin::parse_response_output(data);
464 }
465
466 pub fn cache_entry(data: &[u8]) {
469 if let Some((_, _, _, offset)) = crate::cache::disk::parse_entry_header(data) {
470 let _ = crate::response::Response::parse_upstream(&data[offset..], crate::proxy::REGENERATED_OR_HOP_BY_HOP);
471 }
472 }
473
474 pub fn traffic_classify(data: &[u8]) {
477 let text = String::from_utf8_lossy(data);
478 let (ua, path) = text.split_once('\n').unwrap_or((&text, "/"));
479 let ip = std::net::IpAddr::from([data.first().copied().unwrap_or(1), 2, 3, 4]);
480 let class = crate::traffic::classify(ip, Some(ua), path);
481 let _ = (class.group(), class.kind.label());
482 }
483
484 pub fn authz_parse(data: &[u8]) {
487 let text = String::from_utf8_lossy(data);
488 let _ = crate::authz::Tuple::parse(&text);
489 let _ = crate::authz::Subject::parse(&text);
490 if let Ok(terms) = crate::authz::rebac::parse_expression(&text) {
491 let _ = terms.len();
492 let _ = crate::authz::Schema::new().relation("t", "r", &text).and_then(|s| s.validate());
493 }
494 }
495
496 pub fn available_dictionary(data: &[u8]) {
499 use crate::compression::dictionary::{decode, parse_available_dictionary, DictionaryEncoding};
500 let _ = parse_available_dictionary(&String::from_utf8_lossy(data));
501 for encoding in [DictionaryEncoding::Dcz, DictionaryEncoding::Dcb] {
502 let _ = decode(encoding, data, b"dictionary", 1 << 16);
503 }
504 }
505
506 pub fn llm_usage(data: &[u8]) {
510 for content_type in ["application/json", "text/event-stream"] {
511 let mut parser = crate::llm::UsageParser::new(content_type);
512 for piece in data.chunks(7) {
513 parser.feed(piece);
514 }
515 let _ = parser.finish();
516 }
517 let mut decoder = crate::llm::usage::ChunkDecoder::default();
518 let mut out = Vec::new();
519 let _ = decoder.decode(data, &mut out);
520 }
521
522 pub fn mcp_gateway_reply(data: &[u8]) {
525 for content_type in ["application/json", "text/event-stream"] {
526 if let Ok(result) = crate::mcp::gateway::parse_reply(content_type, data, 1) {
527 let _ = crate::mcp::gateway::fuzzing::tools(&result);
528 }
529 }
530 }
531
532 #[cfg(not(target_arch = "wasm32"))]
535 pub fn websocket_frame(data: &[u8]) {
536 let mut cursor = std::io::Cursor::new(data);
537 while let Ok(frame) = crate::websocket::WebSocket::read_frame_limited(&mut cursor, 4096) {
538 let len = match &frame {
539 crate::websocket::Frame::Text(t) => t.len(),
540 crate::websocket::Frame::Binary(b) | crate::websocket::Frame::Ping(b) | crate::websocket::Frame::Pong(b) => b.len(),
541 crate::websocket::Frame::Continuation { data, .. } => data.len(),
542 crate::websocket::Frame::Close(_, reason) => reason.len() + 2,
543 };
544 assert!(len <= 4096 + 2);
545 }
546 }
547
548 #[cfg(not(target_arch = "wasm32"))]
550 pub fn dns_srv_response(data: &[u8]) {
551 let id = data.get(..2).map_or(0, |b| u16::from_be_bytes([b[0], b[1]]));
552 let _ = crate::service_discovery::dns_srv::parse_response(data, id);
553 }
554
555 pub fn x509_subject_alt_names(data: &[u8]) {
560 let _ = crate::core::x509::subject_alt_names(data);
561 }
562
563 pub fn dns_message(data: &[u8]) {
564 if let Ok(message) = crate::dns::parse(data) {
565 for record in message.answers.iter().chain(&message.authority).chain(&message.additional) {
566 let _ = crate::dns::txt_value(&data[record.rdata.clone()]);
567 }
568 }
569 #[cfg(feature = "acme")]
570 let _ = crate::acme::dns01::check_update_reply(data, 0);
571 }
572
573 #[cfg(not(target_arch = "wasm32"))]
575 pub fn redis_reply(data: &[u8]) {
576 let _ = crate::redis_protocol::read_reply(&mut std::io::Cursor::new(data));
577 }
578
579 pub fn cookie_header(data: &[u8]) {
581 let Ok(value) = std::str::from_utf8(data) else { return };
582 let jar = crate::cookie::CookieJar::parse(value);
583 assert!(jar.cookies.iter().all(|c| !c.name.is_empty()));
584 }
585
586 #[cfg(not(target_arch = "wasm32"))]
589 pub fn otel_baggage(data: &[u8]) {
590 let Ok(value) = std::str::from_utf8(data) else { return };
591 let baggage = crate::otel::Baggage::parse(value);
592 assert!(baggage.len() <= crate::otel::baggage::MAX_MEMBERS);
593 let again = crate::otel::Baggage::parse(&baggage.to_header());
594 for (key, value) in baggage.iter() {
595 if let Some(round_tripped) = again.get(key) {
596 assert_eq!(value, round_tripped);
597 }
598 }
599 }
600
601 #[cfg(feature = "acme")]
605 pub fn acme_renewal(data: &[u8]) {
606 let _ = crate::acme::fuzzing::cert_info_from_der(data);
607 if let Ok(text) = std::str::from_utf8(data) {
608 let _ = crate::acme::fuzzing::parse_suggested_window(text);
609 let _ = crate::acme::fuzzing::parse_rfc3339(text);
610 let _ = crate::acme::fuzzing::json_object_keys(text, "profiles");
611 }
612 }
613
614 #[cfg(feature = "sso-saml")]
616 pub fn saml_xml_parse(xml: &str) {
617 let _ = crate::sso::saml::xml_parse_for_fuzzing(xml);
618 }
619
620 pub fn http1_framing(data: &[u8]) {
624 use crate::server::framing::{self, BodyFraming, ChunkedBody, HeadScan};
625 let mut pending = data.to_vec();
626 framing::skip_leading_empty_lines(&mut pending);
627 let HeadScan::Complete(head_len) = framing::scan_head(&pending) else { return };
628 assert!(head_len <= pending.len() && head_len <= framing::MAX_HEAD_SIZE);
629 let Ok(request) = crate::request::Request::parse(&pending[..head_len]) else { return };
630 for header in &request.headers {
631 let _ = crate::http::date::parse(&header.value);
632 }
633 let _ = framing::validate_head(&pending[..head_len], &request);
634 let _ = framing::keep_alive(&request);
635 let body = &pending[head_len..];
636 if let Ok(BodyFraming::Chunked) = framing::body_framing(&request) {
637 let split = data.first().map_or(0, |&b| b as usize).min(body.len());
638 let mut decoder = ChunkedBody::new(1 << 20);
639 let mut buffer = body[..split].to_vec();
640 for next in [&body[split..], &[][..]] {
641 match decoder.feed(&buffer) {
642 Ok((consumed, _)) => {
643 assert!(consumed <= buffer.len());
644 buffer.drain(..consumed);
645 }
646 Err(_) => return,
647 }
648 buffer.extend_from_slice(next);
649 }
650 assert!(decoder.body.len() <= 1 << 20);
651 }
652 }
653}
654
655
656#[cfg(all(doctest, feature = "http3"))]
658#[doc = include_str!("../README.md")]
659pub struct ReadmeDoctests;