1#![cfg_attr(docsrs, feature(doc_cfg))]
44
45extern crate self as rust_web_server;
48
49pub use rws_server::app;
50#[cfg(feature = "auth")]
51pub use rws_server::auth;
52
53#[cfg(feature = "macros")]
54pub use rws_macros::{delete, get, patch, post, put, route, Config, FromPath, FromRequest, JsonSchema, Validate};
55#[cfg(all(feature = "macros", any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql")))]
56pub use rws_macros::{Factory, Model};
57#[cfg(feature = "http2")]
58pub use rws_server::async_state;
59#[cfg(feature = "http2")]
60pub(crate) use rws_server::async_bridge;
61pub use rws_server::session;
62#[cfg(not(target_arch = "wasm32"))]
65pub(crate) use rws_server::redis_protocol;
66pub use rws_server::sse;
67pub use rws_http::compression;
68pub use rws_server::cookie;
69pub use rws_http::error;
70pub use rws_http::extract;
71#[cfg(all(test, feature = "macros"))]
72mod extract_derive_tests;
73#[cfg(test)]
74mod http_tests;
75#[cfg(all(test, feature = "macros", feature = "model-sqlite"))]
76mod test_helper_tests;
77#[cfg(test)]
78mod server_tests;
79#[cfg(test)]
80mod feature_tests;
81pub mod i18n;
82pub use rws_proxy::a2a;
83#[cfg(all(unix, any(feature = "http1", feature = "http2")))]
84pub use rws_server::systemd;
85pub use rws_server::ip_filter;
86pub mod macros;
87pub use rws_server::blocklist;
88pub use rws_server::cache;
89pub use rws_server::early_hints;
90pub use rws_server::disk_guard;
91pub use rws_proxy::traffic;
92pub use rws_server::authz;
93pub use rws_server::config_reload;
94#[cfg(not(target_arch = "wasm32"))]
95pub use rws_server::config_watch;
96pub use rws_proxy::config_check;
97#[cfg(any(feature = "auth-asymmetric", feature = "sso"))]
99pub use rws_server::rsa_key;
100pub use rws_http::server_config;
101pub use rws_server::feature;
102pub use rws_server::maintenance;
103pub use rws_server::metrics;
104pub use rws_server::mcp;
105pub use rws_server::request_log;
106pub use rws_server::request_id;
107#[cfg(not(target_arch = "wasm32"))]
108pub use rws_server::otel;
109#[cfg(feature = "acme")]
110pub use rws_acme::acme;
111pub use rws_server::middleware;
112pub use rws_server::rate_limit;
113pub use rws_server::router;
114pub use rws_server::handler;
115pub use rws_server::schema;
116pub use rws_server::state;
117pub use rws_server::test_client;
118pub use rws_server::application;
119pub use rws_http::app_manifest;
121pub use rws_http::body;
122pub use rws_http::client_hint;
123pub use rws_server::controller;
124pub use rws_base::core;
125pub use rws_http::cors;
126pub use rws_http::entry_point;
127pub use rws_base::ext;
128pub use rws_http::header;
129pub use rws_base::http;
130pub use rws_http::json;
131pub use rws_server::language;
132#[cfg(not(target_arch = "wasm32"))]
133pub use rws_server::log;
134pub use rws_base::mime_type;
135pub use rws_base::null;
136pub use rws_http::range;
137pub use rws_http::request;
138pub use rws_http::response;
139pub use rws_server::server;
140pub use rws_base::symbol;
141#[cfg(not(target_arch = "wasm32"))]
142pub use rws_server::thread_pool;
143pub use rws_base::url;
144pub use rws_server::pagination;
145#[cfg(not(target_arch = "wasm32"))]
146pub use rws_proxy::proxy;
147pub use rws_server::rewrite;
148pub use rws_server::scheduler;
149#[cfg(not(target_arch = "wasm32"))]
150pub use rws_proxy::tcp_proxy;
151#[cfg(not(target_arch = "wasm32"))]
152pub use rws_proxy::udp_proxy;
153#[cfg(not(target_arch = "wasm32"))]
154pub use rws_proxy::ws_proxy;
155#[cfg(not(target_arch = "wasm32"))]
156pub use rws_proxy::canary;
157#[cfg(not(target_arch = "wasm32"))]
158pub use rws_server::circuit_breaker;
159pub use rws_server::service_discovery;
164pub(crate) use rws_base::dns;
166pub use rws_server::config_binding;
167pub use rws_server::di;
168#[cfg(not(target_arch = "wasm32"))]
169pub use rws_proxy::proxy_config;
170#[cfg(not(target_arch = "wasm32"))]
171pub use rws_proxy::llm;
172#[cfg(not(target_arch = "wasm32"))]
173pub use rws_proxy::admin;
174#[cfg(not(target_arch = "wasm32"))]
176pub use rws_proxy::dev_mcp;
177#[cfg(not(target_arch = "wasm32"))]
180pub use rws_proxy::generator;
181#[cfg(not(target_arch = "wasm32"))]
184pub use rws_proxy::db_cli;
185#[cfg(all(feature = "http2", not(target_arch = "wasm32")))]
187pub use rws_proxy::fleet;
188pub use rws_proxy::waf;
190#[cfg(not(target_arch = "wasm32"))]
192pub use rws_proxy::cluster;
193#[cfg(not(target_arch = "wasm32"))]
195pub use rws_proxy::fastcgi;
196#[cfg(not(target_arch = "wasm32"))]
198pub mod ctl;
199#[cfg(feature = "wasm-plugins")]
200pub use rws_wasm_plugin::wasm_plugin;
201#[cfg(not(target_arch = "wasm32"))]
202pub use rws_proxy::ingress;
203#[cfg(feature = "tera")]
204pub mod template;
205pub use rws_server::validate;
206pub use rws_http::virtual_host;
207#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
208pub use rws_server::model;
209#[cfg(not(target_arch = "wasm32"))]
210pub use rws_server::websocket;
211pub use rws_server::http_client;
215#[cfg(feature = "crypto")]
216pub use rws_server::crypto;
217#[cfg(feature = "csrf")]
218pub use rws_server::csrf;
219#[cfg(feature = "sso")]
220pub use rws_server::sso;
221#[cfg(all(feature = "mailer", not(target_arch = "wasm32")))]
225pub mod mailer;
226#[cfg(feature = "jobs")]
227pub mod jobs;
228#[cfg(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs"))]
231pub use rws_storage::storage;
232#[cfg(all(feature = "secrets", not(any(feature = "storage-local", feature = "storage-s3", feature = "storage-azure", feature = "storage-gcs"))))]
234pub use rws_server::storage;
235#[cfg(feature = "openapi")]
236pub use rws_server::openapi;
237#[cfg(feature = "tower")]
238pub use rws_tower::tower;
239#[cfg(feature = "gateway-api")]
240pub use rws_gateway_api::gateway_api;
241#[cfg(feature = "webhook")]
242pub mod webhook;
243#[cfg(feature = "secrets")]
244pub use rws_server::secrets;
245#[cfg(not(target_arch = "wasm32"))]
248pub use rws_server::timeout;
249pub mod prelude;
250
251#[cfg(feature = "http2")]
252#[doc(hidden)]
253pub use rws_server::tls;
254
255#[cfg(feature = "http2")]
256#[doc(hidden)]
257pub use rws_server::h2_server;
258
259#[cfg(feature = "http3")]
260#[doc(hidden)]
261pub use rws_server::h3_handler;
262
263#[cfg(test)]
270mod stack_depth_tests;
271
272#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
275pub use rws_server::crypto_provider;
276pub use rws_server::FIPS;
277#[cfg(all(any(feature = "http-client", feature = "http2"), not(target_arch = "wasm32")))]
278pub(crate) use rws_server::tls_client_builder;
279
280#[cfg(test)]
281pub mod test_env {
282 use std::sync::{Mutex, OnceLock};
283 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
284 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
285 LOCK.get_or_init(|| Mutex::new(()))
286 .lock()
287 .unwrap_or_else(|e| e.into_inner())
288 }
289
290 pub struct TempPath(std::path::PathBuf);
294
295 impl TempPath {
296 pub fn new(name: &str) -> Self {
297 TempPath(std::env::temp_dir().join(format!("rws_test_{}_{}", std::process::id(), name)))
298 }
299
300 pub fn new_in_cwd(name: &str) -> Self {
303 TempPath(std::env::current_dir().expect("working directory").join(format!("rws_test_{}_{}", std::process::id(), name)))
304 }
305
306 pub fn as_str(&self) -> &str {
307 self.0.to_str().expect("temp dir path is UTF-8")
308 }
309 }
310
311 impl std::ops::Deref for TempPath {
312 type Target = std::path::Path;
313 fn deref(&self) -> &std::path::Path {
314 &self.0
315 }
316 }
317
318 impl Drop for TempPath {
319 fn drop(&mut self) {
320 let is_link = std::fs::symlink_metadata(&self.0).is_ok_and(|m| m.file_type().is_symlink());
323 if is_link {
324 let _ = std::fs::remove_file(&self.0);
325 } else if self.0.is_dir() {
326 let _ = std::fs::remove_dir_all(&self.0);
327 } else {
328 let _ = std::fs::remove_file(&self.0);
329 }
330 }
331 }
332}
333
334#[cfg(fuzzing)]
338#[doc(hidden)]
339pub mod fuzz_api {
340 #[cfg(all(feature = "http2", not(target_arch = "wasm32")))]
344 pub fn fleet_signature(data: &[u8]) {
345 let _ = crate::fleet::parse_signature(data);
346 let (body, sig) = data.split_at(data.len() / 2);
347 assert!(crate::fleet::verify(body, sig, &[7u8; 32]).is_err(), "a fuzzed signature verified");
348 }
349
350 pub fn base64_decode(data: &[u8]) {
353 use crate::core::base64::{Base64, Decoder};
354 for d in [Decoder::STANDARD, Decoder::URL_SAFE, Decoder::EITHER.skip_whitespace(), Decoder::EITHER.skip_invalid()] {
355 if let Ok(bytes) = d.decode(data) {
356 let again = Decoder::STANDARD.decode(Base64::encode(&bytes).unwrap()).unwrap();
357 assert_eq!(bytes, again, "base64 round trip changed the bytes");
358 }
359 }
360 }
361
362 #[cfg(feature = "pkcs11")]
364 pub fn pkcs11_uri(data: &[u8]) {
365 if let Ok(text) = std::str::from_utf8(data) {
366 let _ = crate::tls::pkcs11::Pkcs11Uri::parse(text);
367 let _ = crate::entry_point::shown("RWS_CONFIG_TLS_KEY_FILE", text);
368 }
369 }
370
371 pub fn proxy_normalize_target(data: &[u8]) {
374 let Ok(target) = std::str::from_utf8(data) else { return };
375 let once = crate::proxy_config::normalize_request_target(target).into_owned();
376 let twice = crate::proxy_config::normalize_request_target(&once).into_owned();
377 assert_eq!(once, twice, "normalization is not idempotent for {:?}", target);
378 }
379
380 pub fn static_path_decode(data: &[u8]) {
383 let Ok(path) = std::str::from_utf8(data) else { return };
384 if let Ok(fs_path) = crate::app::controller::static_resource::static_fs_path(path) {
385 let relative = &fs_path[crate::app::controller::static_resource::working_directory().len()..];
386 assert!(!relative.split(['/', '\\']).any(|segment| segment == ".."), "{:?} -> {:?}", path, fs_path);
387 }
388 }
389
390 pub fn openapi_to_mcp(data: &[u8]) {
393 let Ok(spec) = std::str::from_utf8(data) else { return };
394 let options = crate::mcp::openapi_tools::ToolOptions::default();
395 let Ok(found) = crate::mcp::openapi_tools::tools(spec, &options) else { return };
396 for tool in found {
397 assert!(crate::service_discovery::json_lite::parse(&tool.input_schema).is_ok());
398 assert!(tool.name.len() <= 64 && tool.name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'), "{}", tool.name);
399 let _ = tool.request(r#"{"id":"1","body":{"a":[1,2]},"q":["x","y"]}"#);
400 }
401 }
402
403 pub fn a2a_request(data: &[u8]) {
406 use crate::application::Application;
407 thread_local! {
408 static AGENT: crate::a2a::A2aServer = crate::a2a::A2aServer::new(crate::a2a::AgentCard::new("fuzz", "fuzz", "1"))
409 .on_message(|ctx, message| match message.text().len() % 3 {
410 0 => crate::a2a::A2aReply::Message(crate::a2a::Message::agent_text(message.text())),
411 1 => crate::a2a::A2aReply::Task(ctx.input_required("more?")),
412 _ => crate::a2a::A2aReply::Task(ctx.complete(vec![crate::a2a::Artifact::text("a", message.text())])),
413 });
414 }
415 let request = crate::request::Request {
416 method: "POST".to_string(),
417 request_uri: "/a2a".to_string(),
418 http_version: "HTTP/1.1".to_string(),
419 headers: vec![crate::header::Header::new("A2A-Version", "1.0".to_string())],
420 body: data.to_vec().into(),
421 };
422 let connection = crate::server::ConnectionInfo::default();
423 let response = AGENT.with(|agent| agent.execute(&request, &connection)).expect("A2A never errs");
424 if response.status_code == 200 {
425 let body = String::from_utf8(response.content_range_list[0].body.clone()).expect("UTF-8");
426 assert!(crate::service_discovery::json_lite::parse(&body).is_ok(), "not JSON: {body}");
427 }
428 }
429
430 pub fn accept_language(data: &[u8]) {
434 let Ok(value) = std::str::from_utf8(data) else { return };
435 let preferences = crate::i18n::AcceptLanguage::parse(value);
436 assert!(preferences.0.len() <= crate::i18n::MAX_LANGUAGE_RANGES);
437 let available = ["en", "en-GB", "uk", "pt-BR"];
438 if let Some(chosen) = preferences.negotiate(&available) {
439 assert!(available.contains(&chosen));
440 }
441 }
442
443 pub fn i18n_catalog(data: &[u8]) {
446 let Ok(text) = std::str::from_utf8(data) else { return };
447 if let Ok(catalogs) = crate::i18n::Catalogs::new("en").add("en", text) {
448 let t = catalogs.for_language("en");
449 for line in text.lines() {
450 let key = line.split('=').next().unwrap_or("").trim();
451 let _ = t.t_with(key, &[("name", "x")]);
452 let _ = t.plural(key.trim_end_matches(".one").trim_end_matches(".other"), 3);
453 }
454 }
455 }
456
457 pub fn accept_encoding(data: &[u8]) {
461 let Ok(value) = std::str::from_utf8(data) else { return };
462 let request = crate::request::Request {
463 method: "GET".to_string(),
464 request_uri: "/".to_string(),
465 http_version: "HTTP/1.1".to_string(),
466 headers: vec![crate::header::Header::new(crate::header::Header::_ACCEPT_ENCODING, value.to_string())],
467 body: Default::default(),
468 };
469 for preference in [crate::compression::DYNAMIC_PREFERENCE, crate::compression::STATIC_PREFERENCE] {
470 if let Some(chosen) = crate::compression::negotiate(&request, preference) {
471 assert!(preference.contains(&chosen));
472 let refused = value.split(',').any(|c| {
474 let mut parts = c.split(';');
475 parts.next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())
476 && parts
477 .map(str::trim)
478 .find(|p| p.as_bytes().get(..2).is_some_and(|b| b.eq_ignore_ascii_case(b"q=")))
479 .is_some_and(|q| q[2..].trim().parse::<f32>() == Ok(0.0))
480 });
481 let listed_once = value.split(',').filter(|c| c.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case(chosen.token())).count() == 1;
482 assert!(!(refused && listed_once), "{value:?} refused {chosen:?}");
483 }
484 }
485 }
486
487 #[cfg(feature = "wasm-plugins")]
490 pub fn wasm_plugin_output(data: &[u8]) {
491 let _ = crate::wasm_plugin::parse_request_output(data);
492 let _ = crate::wasm_plugin::parse_response_output(data);
493 }
494
495 pub fn cache_entry(data: &[u8]) {
498 if let Some((_, _, _, offset)) = crate::cache::disk::parse_entry_header(data) {
499 let _ = crate::response::Response::parse_upstream(&data[offset..], crate::response::REGENERATED_OR_HOP_BY_HOP);
500 }
501 }
502
503 pub fn traffic_classify(data: &[u8]) {
506 let text = String::from_utf8_lossy(data);
507 let (ua, path) = text.split_once('\n').unwrap_or((&text, "/"));
508 let ip = std::net::IpAddr::from([data.first().copied().unwrap_or(1), 2, 3, 4]);
509 let class = crate::traffic::classify(ip, Some(ua), path);
510 let _ = (class.group(), class.kind.label());
511 }
512
513 pub fn authz_parse(data: &[u8]) {
516 let text = String::from_utf8_lossy(data);
517 let _ = crate::authz::Tuple::parse(&text);
518 let _ = crate::authz::Subject::parse(&text);
519 if let Ok(terms) = crate::authz::rebac::parse_expression(&text) {
520 let _ = terms.len();
521 let _ = crate::authz::Schema::new().relation("t", "r", &text).and_then(|s| s.validate());
522 }
523 }
524
525 pub fn available_dictionary(data: &[u8]) {
528 use crate::compression::dictionary::{decode, parse_available_dictionary, DictionaryEncoding};
529 let _ = parse_available_dictionary(&String::from_utf8_lossy(data));
530 for encoding in [DictionaryEncoding::Dcz, DictionaryEncoding::Dcb] {
531 let _ = decode(encoding, data, b"dictionary", 1 << 16);
532 }
533 }
534
535 pub fn llm_usage(data: &[u8]) {
539 for content_type in ["application/json", "text/event-stream"] {
540 let mut parser = crate::llm::UsageParser::new(content_type);
541 for piece in data.chunks(7) {
542 parser.feed(piece);
543 }
544 let _ = parser.finish();
545 }
546 let mut decoder = crate::llm::usage::ChunkDecoder::default();
547 let mut out = Vec::new();
548 let _ = decoder.decode(data, &mut out);
549 }
550
551 pub fn mcp_gateway_reply(data: &[u8]) {
554 for content_type in ["application/json", "text/event-stream"] {
555 if let Ok(result) = crate::mcp::gateway::parse_reply(content_type, data, 1) {
556 let _ = crate::mcp::gateway::fuzzing::tools(&result);
557 }
558 }
559 }
560
561 #[cfg(not(target_arch = "wasm32"))]
564 pub fn websocket_frame(data: &[u8]) {
565 let mut cursor = std::io::Cursor::new(data);
566 while let Ok(frame) = crate::websocket::WebSocket::read_frame_limited(&mut cursor, 4096) {
567 let len = match &frame {
568 crate::websocket::Frame::Text(t) => t.len(),
569 crate::websocket::Frame::Binary(b) | crate::websocket::Frame::Ping(b) | crate::websocket::Frame::Pong(b) => b.len(),
570 crate::websocket::Frame::Continuation { data, .. } => data.len(),
571 crate::websocket::Frame::Close(_, reason) => reason.len() + 2,
572 };
573 assert!(len <= 4096 + 2);
574 }
575 }
576
577 #[cfg(not(target_arch = "wasm32"))]
579 pub fn dns_srv_response(data: &[u8]) {
580 let id = data.get(..2).map_or(0, |b| u16::from_be_bytes([b[0], b[1]]));
581 let _ = crate::service_discovery::dns_srv::parse_response(data, id);
582 }
583
584 pub fn x509_subject_alt_names(data: &[u8]) {
589 let _ = crate::core::x509::subject_alt_names(data);
590 }
591
592 pub fn dns_message(data: &[u8]) {
593 if let Ok(message) = crate::dns::parse(data) {
594 for record in message.answers.iter().chain(&message.authority).chain(&message.additional) {
595 let _ = crate::dns::txt_value(&data[record.rdata.clone()]);
596 }
597 }
598 #[cfg(feature = "acme")]
599 let _ = crate::acme::dns01::check_update_reply(data, 0);
600 }
601
602 #[cfg(not(target_arch = "wasm32"))]
604 pub fn redis_reply(data: &[u8]) {
605 let _ = crate::redis_protocol::read_reply(&mut std::io::Cursor::new(data));
606 }
607
608 pub fn cookie_header(data: &[u8]) {
610 let Ok(value) = std::str::from_utf8(data) else { return };
611 let jar = crate::cookie::CookieJar::parse(value);
612 assert!(jar.cookies.iter().all(|c| !c.name.is_empty()));
613 }
614
615 #[cfg(not(target_arch = "wasm32"))]
618 pub fn otel_baggage(data: &[u8]) {
619 let Ok(value) = std::str::from_utf8(data) else { return };
620 let baggage = crate::otel::Baggage::parse(value);
621 assert!(baggage.len() <= crate::otel::baggage::MAX_MEMBERS);
622 let again = crate::otel::Baggage::parse(&baggage.to_header());
623 for (key, value) in baggage.iter() {
624 if let Some(round_tripped) = again.get(key) {
625 assert_eq!(value, round_tripped);
626 }
627 }
628 }
629
630 #[cfg(feature = "acme")]
634 pub fn acme_renewal(data: &[u8]) {
635 let _ = crate::acme::fuzzing::cert_info_from_der(data);
636 if let Ok(text) = std::str::from_utf8(data) {
637 let _ = crate::acme::fuzzing::parse_suggested_window(text);
638 let _ = crate::acme::fuzzing::parse_rfc3339(text);
639 let _ = crate::acme::fuzzing::json_object_keys(text, "profiles");
640 }
641 }
642
643 #[cfg(feature = "sso-saml")]
645 pub fn saml_xml_parse(xml: &str) {
646 let _ = crate::sso::saml::xml_parse_for_fuzzing(xml);
647 }
648
649 pub fn http1_framing(data: &[u8]) {
653 use crate::server::framing::{self, BodyFraming, ChunkedBody, HeadScan};
654 let mut pending = data.to_vec();
655 framing::skip_leading_empty_lines(&mut pending);
656 let HeadScan::Complete(head_len) = framing::scan_head(&pending) else { return };
657 assert!(head_len <= pending.len() && head_len <= framing::MAX_HEAD_SIZE);
658 let Ok(request) = crate::request::Request::parse(&pending[..head_len]) else { return };
659 for header in &request.headers {
660 let _ = crate::http::date::parse(&header.value);
661 }
662 let _ = framing::validate_head(&pending[..head_len], &request);
663 let _ = framing::keep_alive(&request);
664 let body = &pending[head_len..];
665 if let Ok(BodyFraming::Chunked) = framing::body_framing(&request) {
666 let split = data.first().map_or(0, |&b| b as usize).min(body.len());
667 let mut decoder = ChunkedBody::new(1 << 20);
668 let mut buffer = body[..split].to_vec();
669 for next in [&body[split..], &[][..]] {
670 match decoder.feed(&buffer) {
671 Ok((consumed, _)) => {
672 assert!(consumed <= buffer.len());
673 buffer.drain(..consumed);
674 }
675 Err(_) => return,
676 }
677 buffer.extend_from_slice(next);
678 }
679 assert!(decoder.body.len() <= 1 << 20);
680 }
681 }
682}
683
684
685#[cfg(all(doctest, feature = "http3"))]
687#[doc = include_str!("../README.md")]
688pub struct ReadmeDoctests;