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