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 ip_filter;
64pub mod macros;
65pub mod blocklist;
66pub mod cache;
67pub mod early_hints;
68pub mod disk_guard;
69pub mod traffic;
70pub mod authz;
71pub mod config_reload;
72#[cfg(not(target_arch = "wasm32"))]
73pub mod config_watch;
74pub mod config_check;
75#[cfg(any(feature = "auth-asymmetric", feature = "sso"))]
77pub mod rsa_key;
78pub mod server_config;
79pub mod feature;
80pub mod maintenance;
81pub mod metrics;
82pub mod mcp;
83pub mod request_log;
84pub mod request_id;
85#[cfg(not(target_arch = "wasm32"))]
86pub mod otel;
87#[cfg(feature = "acme")]
88pub mod acme;
89pub mod middleware;
90pub mod rate_limit;
91pub mod router;
92pub mod schema;
93pub mod state;
94pub mod test_client;
95pub mod application;
96pub mod body;
97pub mod client_hint;
98pub mod controller;
99pub mod core;
100pub mod cors;
101pub mod entry_point;
102pub mod ext;
103pub mod header;
104pub mod http;
105pub mod json;
106pub mod language;
107#[cfg(not(target_arch = "wasm32"))]
108pub mod log;
109pub mod mime_type;
110pub mod null;
111pub mod range;
112pub mod request;
113pub mod response;
114pub mod server;
115pub mod symbol;
116#[cfg(not(target_arch = "wasm32"))]
117pub mod thread_pool;
118pub mod url;
119pub mod pagination;
120#[cfg(not(target_arch = "wasm32"))]
121pub mod proxy;
122pub mod rewrite;
123pub mod scheduler;
124#[cfg(not(target_arch = "wasm32"))]
125pub mod tcp_proxy;
126#[cfg(not(target_arch = "wasm32"))]
127pub mod udp_proxy;
128#[cfg(not(target_arch = "wasm32"))]
129pub mod ws_proxy;
130#[cfg(not(target_arch = "wasm32"))]
131pub mod canary;
132#[cfg(not(target_arch = "wasm32"))]
133pub mod circuit_breaker;
134pub mod service_discovery;
139pub(crate) mod dns;
141pub mod config_binding;
142pub mod di;
143#[cfg(not(target_arch = "wasm32"))]
144pub mod proxy_config;
145#[cfg(not(target_arch = "wasm32"))]
146pub mod llm;
147#[cfg(not(target_arch = "wasm32"))]
148pub mod admin;
149#[cfg(feature = "wasm-plugins")]
150pub mod wasm_plugin;
151#[cfg(not(target_arch = "wasm32"))]
152pub mod ingress;
153#[cfg(feature = "tera")]
154pub mod template;
155pub mod validate;
156pub mod virtual_host;
157#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
158pub mod model;
159#[cfg(not(target_arch = "wasm32"))]
160pub mod websocket;
161pub mod http_client;
165#[cfg(feature = "crypto")]
166pub mod crypto;
167#[cfg(feature = "csrf")]
168pub mod csrf;
169#[cfg(feature = "sso")]
170pub mod sso;
171#[cfg(all(feature = "mailer", not(target_arch = "wasm32")))]
175pub mod mailer;
176#[cfg(feature = "jobs")]
177pub mod jobs;
178#[cfg(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs", feature = "secrets"))]
181pub mod storage;
182#[cfg(feature = "openapi")]
183pub mod openapi;
184#[cfg(feature = "tower")]
185pub mod tower;
186#[cfg(feature = "gateway-api")]
187pub mod gateway_api;
188#[cfg(feature = "webhook")]
189pub mod webhook;
190#[cfg(feature = "secrets")]
191pub mod secrets;
192#[cfg(not(target_arch = "wasm32"))]
195pub mod timeout;
196pub mod prelude;
197
198#[cfg(feature = "http2")]
199#[doc(hidden)]
200pub mod tls;
201
202#[cfg(feature = "http2")]
203#[doc(hidden)]
204pub mod h2_server;
205
206#[cfg(feature = "http3")]
207#[doc(hidden)]
208pub mod h3_handler;
209
210#[cfg(test)]
217mod stack_depth_tests;
218
219#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
226pub(crate) fn tls_client_builder() -> rustls::ConfigBuilder<rustls::ClientConfig, rustls::WantsVerifier> {
227 rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
228 .with_safe_default_protocol_versions()
229 .expect("aws-lc-rs supports rustls's default protocol versions")
230}
231
232#[cfg(test)]
233pub mod test_env {
234 use std::sync::{Mutex, OnceLock};
235 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
236 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
237 LOCK.get_or_init(|| Mutex::new(()))
238 .lock()
239 .unwrap_or_else(|e| e.into_inner())
240 }
241
242 pub struct TempPath(std::path::PathBuf);
246
247 impl TempPath {
248 pub fn new(name: &str) -> Self {
249 TempPath(std::env::temp_dir().join(format!("rws_test_{}_{}", std::process::id(), name)))
250 }
251
252 pub fn new_in_cwd(name: &str) -> Self {
255 TempPath(std::env::current_dir().expect("working directory").join(format!("rws_test_{}_{}", std::process::id(), name)))
256 }
257
258 pub fn as_str(&self) -> &str {
259 self.0.to_str().expect("temp dir path is UTF-8")
260 }
261 }
262
263 impl std::ops::Deref for TempPath {
264 type Target = std::path::Path;
265 fn deref(&self) -> &std::path::Path {
266 &self.0
267 }
268 }
269
270 impl Drop for TempPath {
271 fn drop(&mut self) {
272 let is_link = std::fs::symlink_metadata(&self.0).is_ok_and(|m| m.file_type().is_symlink());
275 if is_link {
276 let _ = std::fs::remove_file(&self.0);
277 } else if self.0.is_dir() {
278 let _ = std::fs::remove_dir_all(&self.0);
279 } else {
280 let _ = std::fs::remove_file(&self.0);
281 }
282 }
283 }
284}
285
286#[cfg(fuzzing)]
290#[doc(hidden)]
291pub mod fuzz_api {
292 pub fn proxy_normalize_target(data: &[u8]) {
295 let Ok(target) = std::str::from_utf8(data) else { return };
296 let once = crate::proxy_config::normalize_request_target(target).into_owned();
297 let twice = crate::proxy_config::normalize_request_target(&once).into_owned();
298 assert_eq!(once, twice, "normalization is not idempotent for {:?}", target);
299 }
300
301 pub fn static_path_decode(data: &[u8]) {
304 let Ok(path) = std::str::from_utf8(data) else { return };
305 if let Ok(fs_path) = crate::app::controller::static_resource::static_fs_path(path) {
306 let relative = &fs_path[crate::app::controller::static_resource::working_directory().len()..];
307 assert!(!relative.split(['/', '\\']).any(|segment| segment == ".."), "{:?} -> {:?}", path, fs_path);
308 }
309 }
310
311 pub fn accept_language(data: &[u8]) {
315 let Ok(value) = std::str::from_utf8(data) else { return };
316 let preferences = crate::i18n::AcceptLanguage::parse(value);
317 assert!(preferences.0.len() <= crate::i18n::MAX_LANGUAGE_RANGES);
318 let available = ["en", "en-GB", "uk", "pt-BR"];
319 if let Some(chosen) = preferences.negotiate(&available) {
320 assert!(available.contains(&chosen));
321 }
322 }
323
324 pub fn i18n_catalog(data: &[u8]) {
327 let Ok(text) = std::str::from_utf8(data) else { return };
328 if let Ok(catalogs) = crate::i18n::Catalogs::new("en").add("en", text) {
329 let t = catalogs.for_language("en");
330 for line in text.lines() {
331 let key = line.split('=').next().unwrap_or("").trim();
332 let _ = t.t_with(key, &[("name", "x")]);
333 let _ = t.plural(key.trim_end_matches(".one").trim_end_matches(".other"), 3);
334 }
335 }
336 }
337
338 pub fn accept_encoding(data: &[u8]) {
342 let Ok(value) = std::str::from_utf8(data) else { return };
343 let request = crate::request::Request {
344 method: "GET".to_string(),
345 request_uri: "/".to_string(),
346 http_version: "HTTP/1.1".to_string(),
347 headers: vec![crate::header::Header::new(crate::header::Header::_ACCEPT_ENCODING, value.to_string())],
348 body: Default::default(),
349 };
350 for preference in [crate::compression::DYNAMIC_PREFERENCE, crate::compression::STATIC_PREFERENCE] {
351 if let Some(chosen) = crate::compression::negotiate(&request, preference) {
352 assert!(preference.contains(&chosen));
353 let refused = value.split(',').any(|c| {
355 let mut parts = c.split(';');
356 parts.next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())
357 && parts
358 .map(str::trim)
359 .find(|p| p.as_bytes().get(..2).is_some_and(|b| b.eq_ignore_ascii_case(b"q=")))
360 .is_some_and(|q| q[2..].trim().parse::<f32>() == Ok(0.0))
361 });
362 let listed_once = value.split(',').filter(|c| c.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())).count() == 1;
363 assert!(!(refused && listed_once), "{value:?} refused {chosen:?}");
364 }
365 }
366 }
367
368 #[cfg(feature = "wasm-plugins")]
371 pub fn wasm_plugin_output(data: &[u8]) {
372 let _ = crate::wasm_plugin::parse_request_output(data);
373 let _ = crate::wasm_plugin::parse_response_output(data);
374 }
375
376 pub fn cache_entry(data: &[u8]) {
379 if let Some((_, _, _, offset)) = crate::cache::disk::parse_entry_header(data) {
380 let _ = crate::response::Response::parse_upstream(&data[offset..], crate::proxy::REGENERATED_OR_HOP_BY_HOP);
381 }
382 }
383
384 pub fn traffic_classify(data: &[u8]) {
387 let text = String::from_utf8_lossy(data);
388 let (ua, path) = text.split_once('\n').unwrap_or((&text, "/"));
389 let ip = std::net::IpAddr::from([data.first().copied().unwrap_or(1), 2, 3, 4]);
390 let class = crate::traffic::classify(ip, Some(ua), path);
391 let _ = (class.group(), class.kind.label());
392 }
393
394 pub fn authz_parse(data: &[u8]) {
397 let text = String::from_utf8_lossy(data);
398 let _ = crate::authz::Tuple::parse(&text);
399 let _ = crate::authz::Subject::parse(&text);
400 if let Ok(terms) = crate::authz::rebac::parse_expression(&text) {
401 let _ = terms.len();
402 let _ = crate::authz::Schema::new().relation("t", "r", &text).and_then(|s| s.validate());
403 }
404 }
405
406 pub fn available_dictionary(data: &[u8]) {
409 use crate::compression::dictionary::{decode, parse_available_dictionary, DictionaryEncoding};
410 let _ = parse_available_dictionary(&String::from_utf8_lossy(data));
411 for encoding in [DictionaryEncoding::Dcz, DictionaryEncoding::Dcb] {
412 let _ = decode(encoding, data, b"dictionary", 1 << 16);
413 }
414 }
415
416 pub fn llm_usage(data: &[u8]) {
420 for content_type in ["application/json", "text/event-stream"] {
421 let mut parser = crate::llm::UsageParser::new(content_type);
422 for piece in data.chunks(7) {
423 parser.feed(piece);
424 }
425 let _ = parser.finish();
426 }
427 let mut decoder = crate::llm::usage::ChunkDecoder::default();
428 let mut out = Vec::new();
429 let _ = decoder.decode(data, &mut out);
430 }
431
432 pub fn mcp_gateway_reply(data: &[u8]) {
435 for content_type in ["application/json", "text/event-stream"] {
436 if let Ok(result) = crate::mcp::gateway::parse_reply(content_type, data, 1) {
437 let _ = crate::mcp::gateway::fuzzing::tools(&result);
438 }
439 }
440 }
441
442 #[cfg(not(target_arch = "wasm32"))]
445 pub fn websocket_frame(data: &[u8]) {
446 let mut cursor = std::io::Cursor::new(data);
447 while let Ok(frame) = crate::websocket::WebSocket::read_frame_limited(&mut cursor, 4096) {
448 let len = match &frame {
449 crate::websocket::Frame::Text(t) => t.len(),
450 crate::websocket::Frame::Binary(b) | crate::websocket::Frame::Ping(b) | crate::websocket::Frame::Pong(b) => b.len(),
451 crate::websocket::Frame::Continuation { data, .. } => data.len(),
452 crate::websocket::Frame::Close(_, reason) => reason.len() + 2,
453 };
454 assert!(len <= 4096 + 2);
455 }
456 }
457
458 #[cfg(not(target_arch = "wasm32"))]
460 pub fn dns_srv_response(data: &[u8]) {
461 let id = data.get(..2).map_or(0, |b| u16::from_be_bytes([b[0], b[1]]));
462 let _ = crate::service_discovery::dns_srv::parse_response(data, id);
463 }
464
465 pub fn x509_subject_alt_names(data: &[u8]) {
470 let _ = crate::core::x509::subject_alt_names(data);
471 }
472
473 pub fn dns_message(data: &[u8]) {
474 if let Ok(message) = crate::dns::parse(data) {
475 for record in message.answers.iter().chain(&message.authority).chain(&message.additional) {
476 let _ = crate::dns::txt_value(&data[record.rdata.clone()]);
477 }
478 }
479 #[cfg(feature = "acme")]
480 let _ = crate::acme::dns01::check_update_reply(data, 0);
481 }
482
483 #[cfg(not(target_arch = "wasm32"))]
485 pub fn redis_reply(data: &[u8]) {
486 let _ = crate::redis_protocol::read_reply(&mut std::io::Cursor::new(data));
487 }
488
489 pub fn cookie_header(data: &[u8]) {
491 let Ok(value) = std::str::from_utf8(data) else { return };
492 let jar = crate::cookie::CookieJar::parse(value);
493 assert!(jar.cookies.iter().all(|c| !c.name.is_empty()));
494 }
495
496 #[cfg(not(target_arch = "wasm32"))]
499 pub fn otel_baggage(data: &[u8]) {
500 let Ok(value) = std::str::from_utf8(data) else { return };
501 let baggage = crate::otel::Baggage::parse(value);
502 assert!(baggage.len() <= crate::otel::baggage::MAX_MEMBERS);
503 let again = crate::otel::Baggage::parse(&baggage.to_header());
504 for (key, value) in baggage.iter() {
505 if let Some(round_tripped) = again.get(key) {
506 assert_eq!(value, round_tripped);
507 }
508 }
509 }
510
511 #[cfg(feature = "acme")]
515 pub fn acme_renewal(data: &[u8]) {
516 let _ = crate::acme::fuzzing::cert_info_from_der(data);
517 if let Ok(text) = std::str::from_utf8(data) {
518 let _ = crate::acme::fuzzing::parse_suggested_window(text);
519 let _ = crate::acme::fuzzing::parse_rfc3339(text);
520 let _ = crate::acme::fuzzing::json_object_keys(text, "profiles");
521 }
522 }
523
524 #[cfg(feature = "sso-saml")]
526 pub fn saml_xml_parse(xml: &str) {
527 let _ = crate::sso::saml::xml_parse_for_fuzzing(xml);
528 }
529
530 pub fn http1_framing(data: &[u8]) {
534 use crate::server::framing::{self, BodyFraming, ChunkedBody, HeadScan};
535 let mut pending = data.to_vec();
536 framing::skip_leading_empty_lines(&mut pending);
537 let HeadScan::Complete(head_len) = framing::scan_head(&pending) else { return };
538 assert!(head_len <= pending.len() && head_len <= framing::MAX_HEAD_SIZE);
539 let Ok(request) = crate::request::Request::parse(&pending[..head_len]) else { return };
540 for header in &request.headers {
541 let _ = crate::http::date::parse(&header.value);
542 }
543 let _ = framing::validate_head(&pending[..head_len], &request);
544 let _ = framing::keep_alive(&request);
545 let body = &pending[head_len..];
546 if let Ok(BodyFraming::Chunked) = framing::body_framing(&request) {
547 let split = data.first().map_or(0, |&b| b as usize).min(body.len());
548 let mut decoder = ChunkedBody::new(1 << 20);
549 let mut buffer = body[..split].to_vec();
550 for next in [&body[split..], &[][..]] {
551 match decoder.feed(&buffer) {
552 Ok((consumed, _)) => {
553 assert!(consumed <= buffer.len());
554 buffer.drain(..consumed);
555 }
556 Err(_) => return,
557 }
558 buffer.extend_from_slice(next);
559 }
560 assert!(decoder.body.len() <= 1 << 20);
561 }
562 }
563}
564
565
566#[cfg(all(doctest, feature = "http3"))]
568#[doc = include_str!("../README.md")]
569pub struct ReadmeDoctests;