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