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