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