1use std::collections::HashSet;
54use std::future::Future;
55use std::ops::ControlFlow as StdControlFlow;
56use std::pin::Pin;
57use std::sync::Arc;
58use std::time::Instant;
59
60use crate::context::RequestContext;
61use crate::dependency::DependencyOverrides;
62use crate::logging::{LogConfig, RequestLogger};
63use crate::request::{Body, Request};
64use crate::response::Response;
65
66pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
68
69#[derive(Debug)]
74pub enum ControlFlow {
75 Continue,
77 Break(Response),
82}
83
84impl ControlFlow {
85 #[must_use]
87 pub fn is_continue(&self) -> bool {
88 matches!(self, Self::Continue)
89 }
90
91 #[must_use]
93 pub fn is_break(&self) -> bool {
94 matches!(self, Self::Break(_))
95 }
96}
97
98impl From<ControlFlow> for StdControlFlow<Response, ()> {
99 fn from(cf: ControlFlow) -> Self {
100 match cf {
101 ControlFlow::Continue => StdControlFlow::Continue(()),
102 ControlFlow::Break(r) => StdControlFlow::Break(r),
103 }
104 }
105}
106
107pub trait Middleware: Send + Sync {
148 fn before<'a>(
164 &'a self,
165 _ctx: &'a RequestContext,
166 _req: &'a mut Request,
167 ) -> BoxFuture<'a, ControlFlow> {
168 Box::pin(async { ControlFlow::Continue })
169 }
170
171 fn after<'a>(
187 &'a self,
188 _ctx: &'a RequestContext,
189 _req: &'a Request,
190 response: Response,
191 ) -> BoxFuture<'a, Response> {
192 Box::pin(async move { response })
193 }
194
195 fn name(&self) -> &'static str {
199 std::any::type_name::<Self>()
200 }
201}
202
203pub trait Handler: Send + Sync {
208 fn call<'a>(&'a self, ctx: &'a RequestContext, req: &'a mut Request)
210 -> BoxFuture<'a, Response>;
211
212 fn dependency_overrides(&self) -> Option<Arc<DependencyOverrides>> {
216 None
217 }
218}
219
220impl<F, Fut> Handler for F
225where
226 F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync,
227 Fut: Future<Output = Response> + Send + 'static,
228{
229 fn call<'a>(
230 &'a self,
231 ctx: &'a RequestContext,
232 req: &'a mut Request,
233 ) -> BoxFuture<'a, Response> {
234 let fut = self(ctx, req);
235 Box::pin(fut)
236 }
237}
238
239impl<H: Handler + ?Sized> Handler for Arc<H> {
244 fn call<'a>(
245 &'a self,
246 ctx: &'a RequestContext,
247 req: &'a mut Request,
248 ) -> BoxFuture<'a, Response> {
249 (**self).call(ctx, req)
250 }
251
252 fn dependency_overrides(&self) -> Option<Arc<DependencyOverrides>> {
253 (**self).dependency_overrides()
254 }
255}
256
257#[derive(Default)]
275pub struct MiddlewareStack {
276 middleware: Vec<Arc<dyn Middleware>>,
277}
278
279impl MiddlewareStack {
280 #[must_use]
282 pub fn new() -> Self {
283 Self {
284 middleware: Vec::new(),
285 }
286 }
287
288 #[must_use]
290 pub fn with_capacity(capacity: usize) -> Self {
291 Self {
292 middleware: Vec::with_capacity(capacity),
293 }
294 }
295
296 pub fn push<M: Middleware + 'static>(&mut self, middleware: M) {
300 self.middleware.push(Arc::new(middleware));
301 }
302
303 pub fn push_arc(&mut self, middleware: Arc<dyn Middleware>) {
307 self.middleware.push(middleware);
308 }
309
310 #[must_use]
312 pub fn len(&self) -> usize {
313 self.middleware.len()
314 }
315
316 #[must_use]
318 pub fn is_empty(&self) -> bool {
319 self.middleware.is_empty()
320 }
321
322 pub async fn execute<H: Handler>(
340 &self,
341 handler: &H,
342 ctx: &RequestContext,
343 req: &mut Request,
344 ) -> Response {
345 let mut ran_before_count = 0;
347
348 for mw in &self.middleware {
350 let _ = ctx.checkpoint();
351 match mw.before(ctx, req).await {
352 ControlFlow::Continue => {
353 ran_before_count += 1;
354 }
355 ControlFlow::Break(response) => {
356 return self
358 .run_after_hooks(ctx, req, response, ran_before_count)
359 .await;
360 }
361 }
362 }
363
364 let _ = ctx.checkpoint();
366 let response = handler.call(ctx, req).await;
367
368 self.run_after_hooks(ctx, req, response, ran_before_count)
370 .await
371 }
372
373 async fn run_after_hooks(
375 &self,
376 ctx: &RequestContext,
377 req: &Request,
378 mut response: Response,
379 count: usize,
380 ) -> Response {
381 for mw in self.middleware[..count].iter().rev() {
383 let _ = ctx.checkpoint();
384 response = mw.after(ctx, req, response).await;
385 }
386 response
387 }
388}
389
390pub struct Layer<M> {
401 middleware: M,
402}
403
404impl<M: Middleware + Clone> Layer<M> {
405 pub fn new(middleware: M) -> Self {
407 Self { middleware }
408 }
409
410 pub fn wrap<H: Handler>(&self, handler: H) -> Layered<M, H> {
412 Layered {
413 middleware: self.middleware.clone(),
414 inner: handler,
415 }
416 }
417}
418
419pub struct Layered<M, H> {
421 middleware: M,
422 inner: H,
423}
424
425impl<M: Middleware, H: Handler> Handler for Layered<M, H> {
426 fn call<'a>(
427 &'a self,
428 ctx: &'a RequestContext,
429 req: &'a mut Request,
430 ) -> BoxFuture<'a, Response> {
431 Box::pin(async move {
432 let _ = ctx.checkpoint();
434 match self.middleware.before(ctx, req).await {
435 ControlFlow::Continue => {
436 let _ = ctx.checkpoint();
438 let response = self.inner.call(ctx, req).await;
439 let _ = ctx.checkpoint();
441 self.middleware.after(ctx, req, response).await
442 }
443 ControlFlow::Break(response) => {
444 let _ = ctx.checkpoint();
446 self.middleware.after(ctx, req, response).await
447 }
448 }
449 })
450 }
451}
452
453#[derive(Debug, Clone, Copy, Default)]
461pub struct NoopMiddleware;
462
463impl Middleware for NoopMiddleware {
464 fn name(&self) -> &'static str {
465 "Noop"
466 }
467}
468
469#[derive(Debug, Clone)]
479pub struct AddResponseHeader {
480 name: String,
481 value: Vec<u8>,
482}
483
484impl AddResponseHeader {
485 pub fn new(name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
487 Self {
488 name: name.into(),
489 value: value.into(),
490 }
491 }
492}
493
494impl Middleware for AddResponseHeader {
495 fn after<'a>(
496 &'a self,
497 _ctx: &'a RequestContext,
498 _req: &'a Request,
499 response: Response,
500 ) -> BoxFuture<'a, Response> {
501 let name = self.name.clone();
502 let value = self.value.clone();
503 Box::pin(async move { response.header(name, value) })
504 }
505
506 fn name(&self) -> &'static str {
507 "AddResponseHeader"
508 }
509}
510
511#[derive(Debug, Clone)]
523pub struct RequireHeader {
524 name: String,
525}
526
527impl RequireHeader {
528 pub fn new(name: impl Into<String>) -> Self {
530 Self { name: name.into() }
531 }
532}
533
534impl Middleware for RequireHeader {
535 fn before<'a>(
536 &'a self,
537 _ctx: &'a RequestContext,
538 req: &'a mut Request,
539 ) -> BoxFuture<'a, ControlFlow> {
540 let has_header = req.headers().get(&self.name).is_some();
541 let name = self.name.clone();
542 Box::pin(async move {
543 if has_header {
544 ControlFlow::Continue
545 } else {
546 let body = format!("Missing required header: {name}");
547 ControlFlow::Break(
548 Response::with_status(crate::response::StatusCode::BAD_REQUEST)
549 .header("content-type", b"text/plain".to_vec())
550 .body(crate::response::ResponseBody::Bytes(body.into_bytes())),
551 )
552 }
553 })
554 }
555
556 fn name(&self) -> &'static str {
557 "RequireHeader"
558 }
559}
560
561#[derive(Debug, Clone)]
574pub struct PathPrefixFilter {
575 prefix: String,
576}
577
578impl PathPrefixFilter {
579 pub fn new(prefix: impl Into<String>) -> Self {
581 Self {
582 prefix: prefix.into(),
583 }
584 }
585}
586
587impl Middleware for PathPrefixFilter {
588 fn before<'a>(
589 &'a self,
590 _ctx: &'a RequestContext,
591 req: &'a mut Request,
592 ) -> BoxFuture<'a, ControlFlow> {
593 let path_matches = req.path().starts_with(&self.prefix);
594 Box::pin(async move {
595 if path_matches {
596 ControlFlow::Continue
597 } else {
598 ControlFlow::Break(Response::with_status(
599 crate::response::StatusCode::NOT_FOUND,
600 ))
601 }
602 })
603 }
604
605 fn name(&self) -> &'static str {
606 "PathPrefixFilter"
607 }
608}
609
610#[derive(Debug, Clone)]
614pub struct ConditionalStatus<F>
615where
616 F: Fn(&Request) -> bool + Send + Sync,
617{
618 condition: F,
619 status_if_true: crate::response::StatusCode,
620 status_if_false: crate::response::StatusCode,
621}
622
623impl<F> ConditionalStatus<F>
624where
625 F: Fn(&Request) -> bool + Send + Sync,
626{
627 pub fn new(
632 condition: F,
633 status_if_true: crate::response::StatusCode,
634 status_if_false: crate::response::StatusCode,
635 ) -> Self {
636 Self {
637 condition,
638 status_if_true,
639 status_if_false,
640 }
641 }
642}
643
644impl<F> Middleware for ConditionalStatus<F>
645where
646 F: Fn(&Request) -> bool + Send + Sync,
647{
648 fn after<'a>(
649 &'a self,
650 _ctx: &'a RequestContext,
651 req: &'a Request,
652 response: Response,
653 ) -> BoxFuture<'a, Response> {
654 let matches = (self.condition)(req);
655 let status = if matches {
656 self.status_if_true
657 } else {
658 self.status_if_false
659 };
660 Box::pin(async move { Response::with_status(status).body(response.body_ref().into()) })
661 }
662
663 fn name(&self) -> &'static str {
664 "ConditionalStatus"
665 }
666}
667
668#[derive(Debug, Clone)]
674pub enum OriginPattern {
675 Any,
677 Exact(String),
679 Wildcard(String),
681 Regex(String),
683}
684
685impl OriginPattern {
686 fn matches(&self, origin: &str) -> bool {
687 match self {
688 Self::Any => true,
689 Self::Exact(value) => value == origin,
690 Self::Wildcard(pattern) => wildcard_match(pattern, origin),
691 Self::Regex(pattern) => regex_match(pattern, origin),
692 }
693 }
694}
695
696#[derive(Debug, Clone)]
745pub struct CorsConfig {
746 allow_any_origin: bool,
747 allow_credentials: bool,
748 allowed_methods: Vec<crate::request::Method>,
749 allowed_headers: Vec<String>,
750 expose_headers: Vec<String>,
751 max_age: Option<u32>,
752 origins: Vec<OriginPattern>,
753}
754
755impl Default for CorsConfig {
756 fn default() -> Self {
757 Self {
758 allow_any_origin: false,
759 allow_credentials: false,
760 allowed_methods: vec![
761 crate::request::Method::Get,
762 crate::request::Method::Post,
763 crate::request::Method::Put,
764 crate::request::Method::Patch,
765 crate::request::Method::Delete,
766 crate::request::Method::Options,
767 crate::request::Method::Head,
768 ],
769 allowed_headers: Vec::new(),
770 expose_headers: Vec::new(),
771 max_age: None,
772 origins: Vec::new(),
773 }
774 }
775}
776
777#[derive(Debug, Clone)]
779pub struct Cors {
780 config: CorsConfig,
781}
782
783impl Cors {
784 #[must_use]
786 pub fn new() -> Self {
787 Self {
788 config: CorsConfig::default(),
789 }
790 }
791
792 #[must_use]
794 pub fn config(mut self, config: CorsConfig) -> Self {
795 self.config = config;
796 self
797 }
798
799 #[must_use]
801 pub fn allow_any_origin(mut self) -> Self {
802 self.config.allow_any_origin = true;
803 self
804 }
805
806 #[must_use]
808 pub fn allow_origin(mut self, origin: impl Into<String>) -> Self {
809 self.config
810 .origins
811 .push(OriginPattern::Exact(origin.into()));
812 self
813 }
814
815 #[must_use]
817 pub fn allow_origin_wildcard(mut self, pattern: impl Into<String>) -> Self {
818 self.config
819 .origins
820 .push(OriginPattern::Wildcard(pattern.into()));
821 self
822 }
823
824 #[must_use]
826 pub fn allow_origin_regex(mut self, pattern: impl Into<String>) -> Self {
827 self.config
828 .origins
829 .push(OriginPattern::Regex(pattern.into()));
830 self
831 }
832
833 #[must_use]
835 pub fn allow_credentials(mut self, allow: bool) -> Self {
836 self.config.allow_credentials = allow;
837 self
838 }
839
840 #[must_use]
842 pub fn allow_methods<I>(mut self, methods: I) -> Self
843 where
844 I: IntoIterator<Item = crate::request::Method>,
845 {
846 self.config.allowed_methods = methods.into_iter().collect();
847 self
848 }
849
850 #[must_use]
852 pub fn allow_headers<I, S>(mut self, headers: I) -> Self
853 where
854 I: IntoIterator<Item = S>,
855 S: Into<String>,
856 {
857 self.config.allowed_headers = headers.into_iter().map(Into::into).collect();
858 self
859 }
860
861 #[must_use]
863 pub fn expose_headers<I, S>(mut self, headers: I) -> Self
864 where
865 I: IntoIterator<Item = S>,
866 S: Into<String>,
867 {
868 self.config.expose_headers = headers.into_iter().map(Into::into).collect();
869 self
870 }
871
872 #[must_use]
874 pub fn max_age(mut self, seconds: u32) -> Self {
875 self.config.max_age = Some(seconds);
876 self
877 }
878
879 fn is_origin_allowed(&self, origin: &str) -> bool {
880 if self.config.allow_any_origin {
881 return true;
882 }
883 self.config
884 .origins
885 .iter()
886 .any(|pattern| pattern.matches(origin))
887 }
888
889 fn allow_origin_value(&self, origin: &str) -> Option<String> {
890 if !self.is_origin_allowed(origin) {
891 return None;
892 }
893 if self.config.allow_any_origin && !self.config.allow_credentials {
894 Some("*".to_string())
895 } else {
896 Some(origin.to_string())
897 }
898 }
899
900 fn allow_methods_value(&self) -> String {
901 self.config
902 .allowed_methods
903 .iter()
904 .map(|method| method.as_str())
905 .collect::<Vec<_>>()
906 .join(", ")
907 }
908
909 fn allow_headers_value(&self, request: &Request) -> Option<String> {
910 if self.config.allowed_headers.is_empty() {
911 return None;
916 }
917
918 if self.config.allowed_headers.iter().any(|h| h == "*") {
923 if self.config.allow_credentials {
924 return request
927 .headers()
928 .get("access-control-request-headers")
929 .and_then(|value| std::str::from_utf8(value).ok())
930 .map(ToString::to_string);
931 }
932 return Some("*".to_string());
933 }
934
935 Some(self.config.allowed_headers.join(", "))
936 }
937
938 fn apply_common_headers(&self, mut response: Response, origin: &str) -> Response {
939 if let Some(allow_origin) = self.allow_origin_value(origin) {
940 let is_wildcard = allow_origin == "*";
941 response = response.header("access-control-allow-origin", allow_origin.into_bytes());
942 if !is_wildcard {
943 response = response.header("vary", b"Origin".to_vec());
944 }
945 if self.config.allow_credentials {
946 response = response.header("access-control-allow-credentials", b"true".to_vec());
947 }
948 if !self.config.expose_headers.is_empty() {
949 response = response.header(
950 "access-control-expose-headers",
951 self.config.expose_headers.join(", ").into_bytes(),
952 );
953 }
954 }
955 response
956 }
957}
958
959impl Default for Cors {
960 fn default() -> Self {
961 Self::new()
962 }
963}
964
965#[derive(Debug, Clone)]
966struct CorsOrigin(String);
967
968impl Middleware for Cors {
969 fn before<'a>(
970 &'a self,
971 _ctx: &'a RequestContext,
972 req: &'a mut Request,
973 ) -> BoxFuture<'a, ControlFlow> {
974 let origin = req
975 .headers()
976 .get("origin")
977 .and_then(|value| std::str::from_utf8(value).ok())
978 .map(ToString::to_string);
979
980 let Some(origin) = origin else {
981 return Box::pin(async { ControlFlow::Continue });
982 };
983
984 if !self.is_origin_allowed(&origin) {
985 let is_preflight = req.method() == crate::request::Method::Options
986 && req.headers().get("access-control-request-method").is_some();
987 if is_preflight {
988 return Box::pin(async {
989 ControlFlow::Break(Response::with_status(
990 crate::response::StatusCode::FORBIDDEN,
991 ))
992 });
993 }
994 return Box::pin(async { ControlFlow::Continue });
995 }
996
997 let is_preflight = req.method() == crate::request::Method::Options
998 && req.headers().get("access-control-request-method").is_some();
999
1000 if is_preflight {
1001 let mut response = Response::no_content();
1002 response = self.apply_common_headers(response, &origin);
1003 response = response.header(
1004 "access-control-allow-methods",
1005 self.allow_methods_value().into_bytes(),
1006 );
1007
1008 if let Some(value) = self.allow_headers_value(req) {
1009 response = response.header("access-control-allow-headers", value.into_bytes());
1010 }
1011
1012 if let Some(max_age) = self.config.max_age {
1013 response =
1014 response.header("access-control-max-age", max_age.to_string().into_bytes());
1015 }
1016
1017 return Box::pin(async move { ControlFlow::Break(response) });
1018 }
1019
1020 req.insert_extension(CorsOrigin(origin));
1021 Box::pin(async { ControlFlow::Continue })
1022 }
1023
1024 fn after<'a>(
1025 &'a self,
1026 _ctx: &'a RequestContext,
1027 req: &'a Request,
1028 response: Response,
1029 ) -> BoxFuture<'a, Response> {
1030 let origin = req.get_extension::<CorsOrigin>().map(|v| v.0.clone());
1031 Box::pin(async move {
1032 if let Some(origin) = origin {
1033 return self.apply_common_headers(response, &origin);
1034 }
1035 response
1036 })
1037 }
1038
1039 fn name(&self) -> &'static str {
1040 "Cors"
1041 }
1042}
1043
1044fn wildcard_match(pattern: &str, value: &str) -> bool {
1045 let mut pat_chars = pattern.chars().peekable();
1047 let mut val_chars = value.chars().peekable();
1048 let mut star = None;
1049 let mut match_after_star = None;
1050
1051 while let Some(p) = pat_chars.next() {
1052 match p {
1053 '*' => {
1054 star = Some(pat_chars.clone());
1055 match_after_star = Some(val_chars.clone());
1056 }
1057 _ => {
1058 if let Some(v) = val_chars.next() {
1059 if p != v {
1060 if let (Some(pat_backup), Some(val_backup)) =
1061 (star.clone(), match_after_star.clone())
1062 {
1063 pat_chars = pat_backup;
1064 val_chars = val_backup;
1065 val_chars.next();
1066 match_after_star = Some(val_chars.clone());
1067 continue;
1068 }
1069 return false;
1070 }
1071 } else {
1072 return false;
1073 }
1074 }
1075 }
1076 }
1077
1078 if pat_chars.peek().is_none() && val_chars.peek().is_none() {
1080 return true;
1081 }
1082
1083 if let Some(pat_backup) = star {
1084 if val_chars.peek().is_none() {
1085 let trailing = pat_backup;
1086 for ch in trailing {
1087 if ch != '*' {
1088 return false;
1089 }
1090 }
1091 return true;
1092 }
1093 }
1094
1095 val_chars.peek().is_none()
1096}
1097
1098fn regex_match(pattern: &str, value: &str) -> bool {
1099 let pat = pattern.as_bytes();
1101 let text = value.as_bytes();
1102
1103 if pat.first() == Some(&b'^') {
1104 return regex_match_here(&pat[1..], text);
1105 }
1106
1107 let mut i = 0;
1108 loop {
1109 if regex_match_here(pat, &text[i..]) {
1110 return true;
1111 }
1112 if i == text.len() {
1113 break;
1114 }
1115 i += 1;
1116 }
1117 false
1118}
1119
1120fn regex_match_here(pattern: &[u8], text: &[u8]) -> bool {
1121 if pattern.is_empty() {
1122 return true;
1123 }
1124 if pattern == b"$" {
1125 return text.is_empty();
1126 }
1127 if pattern.len() >= 2 && pattern[1] == b'*' {
1128 return regex_match_star(pattern[0], &pattern[2..], text);
1129 }
1130 if !text.is_empty() && (pattern[0] == b'.' || pattern[0] == text[0]) {
1131 return regex_match_here(&pattern[1..], &text[1..]);
1132 }
1133 false
1134}
1135
1136fn regex_match_star(ch: u8, pattern: &[u8], text: &[u8]) -> bool {
1137 let mut i = 0;
1138 loop {
1139 if regex_match_here(pattern, &text[i..]) {
1140 return true;
1141 }
1142 if i == text.len() {
1143 return false;
1144 }
1145 if ch != b'.' && text[i] != ch {
1146 return false;
1147 }
1148 i += 1;
1149 }
1150}
1151
1152#[derive(Debug, Clone)]
1158pub struct RequestResponseLogger {
1159 log_config: LogConfig,
1160 redact_headers: HashSet<String>,
1161 log_request_headers: bool,
1162 log_response_headers: bool,
1163 log_body: bool,
1164 max_body_bytes: usize,
1165}
1166
1167impl Default for RequestResponseLogger {
1168 fn default() -> Self {
1169 Self {
1170 log_config: LogConfig::production(),
1171 redact_headers: default_redacted_headers(),
1172 log_request_headers: true,
1173 log_response_headers: true,
1174 log_body: false,
1175 max_body_bytes: 1024,
1176 }
1177 }
1178}
1179
1180impl RequestResponseLogger {
1181 #[must_use]
1183 pub fn new() -> Self {
1184 Self::default()
1185 }
1186
1187 #[must_use]
1189 pub fn log_config(mut self, config: LogConfig) -> Self {
1190 self.log_config = config;
1191 self
1192 }
1193
1194 #[must_use]
1196 pub fn log_request_headers(mut self, enabled: bool) -> Self {
1197 self.log_request_headers = enabled;
1198 self
1199 }
1200
1201 #[must_use]
1203 pub fn log_response_headers(mut self, enabled: bool) -> Self {
1204 self.log_response_headers = enabled;
1205 self
1206 }
1207
1208 #[must_use]
1210 pub fn log_body(mut self, enabled: bool) -> Self {
1211 self.log_body = enabled;
1212 self
1213 }
1214
1215 #[must_use]
1217 pub fn max_body_bytes(mut self, max: usize) -> Self {
1218 self.max_body_bytes = max;
1219 self
1220 }
1221
1222 #[must_use]
1224 pub fn redact_header(mut self, name: impl Into<String>) -> Self {
1225 self.redact_headers.insert(name.into().to_ascii_lowercase());
1226 self
1227 }
1228}
1229
1230#[derive(Debug, Clone)]
1231struct RequestStart(Instant);
1232
1233impl Middleware for RequestResponseLogger {
1234 fn before<'a>(
1235 &'a self,
1236 ctx: &'a RequestContext,
1237 req: &'a mut Request,
1238 ) -> BoxFuture<'a, ControlFlow> {
1239 let logger = RequestLogger::new(ctx, self.log_config.clone());
1240 req.insert_extension(RequestStart(Instant::now()));
1241
1242 let method = req.method();
1243 let path = req.path();
1244 let query = req.query();
1245 let body_bytes = body_len(req.body());
1246
1247 logger.info_with_fields("request", |entry| {
1248 let mut entry = entry
1249 .field("method", method)
1250 .field("path", path)
1251 .field("body_bytes", body_bytes);
1252
1253 if let Some(q) = query {
1254 entry = entry.field("query", q);
1255 }
1256
1257 if self.log_request_headers {
1258 let headers = format_headers(req.headers().iter(), &self.redact_headers);
1259 entry = entry.field("headers", headers);
1260 }
1261
1262 if self.log_body {
1263 if let Some(body) = preview_body(req.body(), self.max_body_bytes) {
1264 entry = entry.field("body", body);
1265 }
1266 }
1267
1268 entry
1269 });
1270
1271 Box::pin(async { ControlFlow::Continue })
1272 }
1273
1274 fn after<'a>(
1275 &'a self,
1276 ctx: &'a RequestContext,
1277 req: &'a Request,
1278 response: Response,
1279 ) -> BoxFuture<'a, Response> {
1280 let logger = RequestLogger::new(ctx, self.log_config.clone());
1281 let duration = req
1282 .get_extension::<RequestStart>()
1283 .map(|start| start.0.elapsed())
1284 .unwrap_or_default();
1285
1286 let status = response.status();
1287 let body_bytes = response.body_ref().len();
1288
1289 logger.info_with_fields("response", |entry| {
1290 let mut entry = entry
1291 .field("status", status.as_u16())
1292 .field("duration_us", duration.as_micros())
1293 .field("body_bytes", body_bytes);
1294
1295 if self.log_response_headers {
1296 let headers = format_response_headers(response.headers(), &self.redact_headers);
1297 entry = entry.field("headers", headers);
1298 }
1299
1300 if self.log_body {
1301 if let Some(body) = preview_response_body(response.body_ref(), self.max_body_bytes)
1302 {
1303 entry = entry.field("body", body);
1304 }
1305 }
1306
1307 entry
1308 });
1309
1310 Box::pin(async move { response })
1311 }
1312
1313 fn name(&self) -> &'static str {
1314 "RequestResponseLogger"
1315 }
1316}
1317
1318fn default_redacted_headers() -> HashSet<String> {
1319 [
1320 "authorization",
1321 "proxy-authorization",
1322 "cookie",
1323 "set-cookie",
1324 ]
1325 .iter()
1326 .map(ToString::to_string)
1327 .collect()
1328}
1329
1330fn body_len(body: &Body) -> usize {
1331 match body {
1332 Body::Empty => 0,
1333 Body::Bytes(bytes) => bytes.len(),
1334 Body::Stream { content_length, .. } => content_length.unwrap_or(0),
1335 }
1336}
1337
1338fn preview_body(body: &Body, max_bytes: usize) -> Option<String> {
1339 if max_bytes == 0 {
1340 return None;
1341 }
1342 match body {
1343 Body::Empty => None,
1344 Body::Bytes(bytes) => {
1345 if bytes.is_empty() {
1346 None
1347 } else {
1348 Some(format_bytes(bytes, max_bytes))
1349 }
1350 }
1351 Body::Stream { .. } => None,
1352 }
1353}
1354
1355fn preview_response_body(body: &crate::response::ResponseBody, max_bytes: usize) -> Option<String> {
1356 if max_bytes == 0 {
1357 return None;
1358 }
1359 match body {
1360 crate::response::ResponseBody::Empty => None,
1361 crate::response::ResponseBody::Bytes(bytes) => {
1362 if bytes.is_empty() {
1363 None
1364 } else {
1365 Some(format_bytes(bytes, max_bytes))
1366 }
1367 }
1368 crate::response::ResponseBody::Stream(_) => None,
1369 }
1370}
1371
1372fn format_headers<'a>(
1373 headers: impl Iterator<Item = (&'a str, &'a [u8])>,
1374 redacted: &HashSet<String>,
1375) -> String {
1376 let mut out = String::new();
1377 for (idx, (name, value)) in headers.enumerate() {
1378 if idx > 0 {
1379 out.push_str(", ");
1380 }
1381 out.push_str(name);
1382 out.push('=');
1383
1384 let lowered = name.to_ascii_lowercase();
1385 if redacted.contains(&lowered) {
1386 out.push_str("<redacted>");
1387 continue;
1388 }
1389
1390 match std::str::from_utf8(value) {
1391 Ok(text) => out.push_str(text),
1392 Err(_) => out.push_str("<binary>"),
1393 }
1394 }
1395 out
1396}
1397
1398fn format_response_headers(headers: &[(String, Vec<u8>)], redacted: &HashSet<String>) -> String {
1399 format_headers(
1400 headers
1401 .iter()
1402 .map(|(name, value)| (name.as_str(), value.as_slice())),
1403 redacted,
1404 )
1405}
1406
1407fn format_bytes(bytes: &[u8], max_bytes: usize) -> String {
1408 let limit = max_bytes.min(bytes.len());
1409 match std::str::from_utf8(&bytes[..limit]) {
1410 Ok(text) => {
1411 let mut output = text.to_string();
1412 if bytes.len() > max_bytes {
1413 output.push_str("...");
1414 }
1415 output
1416 }
1417 Err(_) => format!("<{} bytes binary>", bytes.len()),
1418 }
1419}
1420
1421impl From<&crate::response::ResponseBody> for crate::response::ResponseBody {
1423 fn from(body: &crate::response::ResponseBody) -> Self {
1424 match body {
1425 crate::response::ResponseBody::Empty => crate::response::ResponseBody::Empty,
1426 crate::response::ResponseBody::Bytes(b) => {
1427 crate::response::ResponseBody::Bytes(b.clone())
1428 }
1429 crate::response::ResponseBody::Stream(_) => crate::response::ResponseBody::Empty,
1430 }
1431 }
1432}
1433
1434#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1443pub struct RequestId(pub String);
1444
1445impl RequestId {
1446 #[must_use]
1448 pub fn new(id: impl Into<String>) -> Self {
1449 Self(id.into())
1450 }
1451
1452 #[must_use]
1454 pub fn as_str(&self) -> &str {
1455 &self.0
1456 }
1457
1458 #[must_use]
1463 pub fn generate() -> Self {
1464 use std::sync::atomic::{AtomicU64, Ordering};
1465 use std::time::{SystemTime, UNIX_EPOCH};
1466
1467 static COUNTER: AtomicU64 = AtomicU64::new(0);
1468
1469 let timestamp = SystemTime::now()
1470 .duration_since(UNIX_EPOCH)
1471 .map(|d| d.as_micros() as u64)
1472 .unwrap_or(0);
1473 let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
1474
1475 Self(format!("{:x}-{:x}", timestamp, counter))
1477 }
1478}
1479
1480impl std::fmt::Display for RequestId {
1481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1482 write!(f, "{}", self.0)
1483 }
1484}
1485
1486impl From<String> for RequestId {
1487 fn from(s: String) -> Self {
1488 Self(s)
1489 }
1490}
1491
1492impl From<&str> for RequestId {
1493 fn from(s: &str) -> Self {
1494 Self(s.to_string())
1495 }
1496}
1497
1498#[derive(Debug, Clone)]
1500pub struct RequestIdConfig {
1501 pub header_name: String,
1503 pub accept_from_client: bool,
1505 pub add_to_response: bool,
1507 pub max_client_id_length: usize,
1509}
1510
1511impl Default for RequestIdConfig {
1512 fn default() -> Self {
1513 Self {
1514 header_name: "x-request-id".to_string(),
1515 accept_from_client: true,
1516 add_to_response: true,
1517 max_client_id_length: 128,
1518 }
1519 }
1520}
1521
1522impl RequestIdConfig {
1523 #[must_use]
1525 pub fn new() -> Self {
1526 Self::default()
1527 }
1528
1529 #[must_use]
1531 pub fn header_name(mut self, name: impl Into<String>) -> Self {
1532 self.header_name = name.into();
1533 self
1534 }
1535
1536 #[must_use]
1538 pub fn accept_from_client(mut self, accept: bool) -> Self {
1539 self.accept_from_client = accept;
1540 self
1541 }
1542
1543 #[must_use]
1545 pub fn add_to_response(mut self, add: bool) -> Self {
1546 self.add_to_response = add;
1547 self
1548 }
1549
1550 #[must_use]
1552 pub fn max_client_id_length(mut self, max: usize) -> Self {
1553 self.max_client_id_length = max;
1554 self
1555 }
1556}
1557
1558#[derive(Debug, Clone)]
1583pub struct RequestIdMiddleware {
1584 config: RequestIdConfig,
1585}
1586
1587impl Default for RequestIdMiddleware {
1588 fn default() -> Self {
1589 Self::new()
1590 }
1591}
1592
1593impl RequestIdMiddleware {
1594 #[must_use]
1596 pub fn new() -> Self {
1597 Self {
1598 config: RequestIdConfig::default(),
1599 }
1600 }
1601
1602 #[must_use]
1604 pub fn with_config(config: RequestIdConfig) -> Self {
1605 Self { config }
1606 }
1607
1608 fn get_or_generate_id(&self, req: &Request) -> RequestId {
1610 if self.config.accept_from_client {
1611 if let Some(header_value) = req.headers().get(&self.config.header_name) {
1612 if let Ok(client_id) = std::str::from_utf8(header_value) {
1613 if !client_id.is_empty()
1615 && client_id.len() <= self.config.max_client_id_length
1616 && is_valid_request_id(client_id)
1617 {
1618 return RequestId::new(client_id);
1619 }
1620 }
1621 }
1622 }
1623 RequestId::generate()
1624 }
1625}
1626
1627fn is_valid_request_id(id: &str) -> bool {
1629 !id.is_empty()
1630 && id
1631 .chars()
1632 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
1633}
1634
1635impl Middleware for RequestIdMiddleware {
1636 fn before<'a>(
1637 &'a self,
1638 _ctx: &'a RequestContext,
1639 req: &'a mut Request,
1640 ) -> BoxFuture<'a, ControlFlow> {
1641 let request_id = self.get_or_generate_id(req);
1642 req.insert_extension(request_id);
1643 Box::pin(async { ControlFlow::Continue })
1644 }
1645
1646 fn after<'a>(
1647 &'a self,
1648 _ctx: &'a RequestContext,
1649 req: &'a Request,
1650 response: Response,
1651 ) -> BoxFuture<'a, Response> {
1652 if !self.config.add_to_response {
1653 return Box::pin(async move { response });
1654 }
1655
1656 let request_id = req.get_extension::<RequestId>().cloned();
1657 let header_name = self.config.header_name.clone();
1658
1659 Box::pin(async move {
1660 if let Some(id) = request_id {
1661 response.header(header_name, id.0.into_bytes())
1662 } else {
1663 response
1664 }
1665 })
1666 }
1667
1668 fn name(&self) -> &'static str {
1669 "RequestId"
1670 }
1671}
1672
1673#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1681pub enum XFrameOptions {
1682 Deny,
1684 SameOrigin,
1686}
1687
1688impl XFrameOptions {
1689 fn as_bytes(self) -> &'static [u8] {
1690 match self {
1691 Self::Deny => b"DENY",
1692 Self::SameOrigin => b"SAMEORIGIN",
1693 }
1694 }
1695}
1696
1697#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1701pub enum ReferrerPolicy {
1702 NoReferrer,
1704 NoReferrerWhenDowngrade,
1706 Origin,
1708 OriginWhenCrossOrigin,
1710 SameOrigin,
1712 StrictOrigin,
1714 StrictOriginWhenCrossOrigin,
1716 UnsafeUrl,
1718}
1719
1720impl ReferrerPolicy {
1721 fn as_bytes(self) -> &'static [u8] {
1722 match self {
1723 Self::NoReferrer => b"no-referrer",
1724 Self::NoReferrerWhenDowngrade => b"no-referrer-when-downgrade",
1725 Self::Origin => b"origin",
1726 Self::OriginWhenCrossOrigin => b"origin-when-cross-origin",
1727 Self::SameOrigin => b"same-origin",
1728 Self::StrictOrigin => b"strict-origin",
1729 Self::StrictOriginWhenCrossOrigin => b"strict-origin-when-cross-origin",
1730 Self::UnsafeUrl => b"unsafe-url",
1731 }
1732 }
1733}
1734
1735#[derive(Debug, Clone)]
1759pub struct SecurityHeadersConfig {
1760 pub x_content_type_options: Option<&'static str>,
1763 pub x_frame_options: Option<XFrameOptions>,
1766 pub x_xss_protection: Option<&'static str>,
1772 pub content_security_policy: Option<String>,
1775 pub hsts: Option<(u64, bool, bool)>,
1779 pub referrer_policy: Option<ReferrerPolicy>,
1782 pub permissions_policy: Option<String>,
1785}
1786
1787impl Default for SecurityHeadersConfig {
1788 fn default() -> Self {
1789 Self {
1790 x_content_type_options: Some("nosniff"),
1791 x_frame_options: Some(XFrameOptions::Deny),
1792 x_xss_protection: Some("0"),
1793 content_security_policy: None,
1794 hsts: None,
1795 referrer_policy: Some(ReferrerPolicy::StrictOriginWhenCrossOrigin),
1796 permissions_policy: None,
1797 }
1798 }
1799}
1800
1801impl SecurityHeadersConfig {
1802 #[must_use]
1804 pub fn new() -> Self {
1805 Self::default()
1806 }
1807
1808 #[must_use]
1810 pub fn none() -> Self {
1811 Self {
1812 x_content_type_options: None,
1813 x_frame_options: None,
1814 x_xss_protection: None,
1815 content_security_policy: None,
1816 hsts: None,
1817 referrer_policy: None,
1818 permissions_policy: None,
1819 }
1820 }
1821
1822 #[must_use]
1829 pub fn strict() -> Self {
1830 Self {
1831 x_content_type_options: Some("nosniff"),
1832 x_frame_options: Some(XFrameOptions::Deny),
1833 x_xss_protection: Some("0"),
1834 content_security_policy: Some("default-src 'self'".to_string()),
1835 hsts: Some((31536000, true, false)), referrer_policy: Some(ReferrerPolicy::NoReferrer),
1837 permissions_policy: Some("geolocation=(), camera=(), microphone=()".to_string()),
1838 }
1839 }
1840
1841 #[must_use]
1843 pub fn x_content_type_options(mut self, value: Option<&'static str>) -> Self {
1844 self.x_content_type_options = value;
1845 self
1846 }
1847
1848 #[must_use]
1850 pub fn x_frame_options(mut self, value: Option<XFrameOptions>) -> Self {
1851 self.x_frame_options = value;
1852 self
1853 }
1854
1855 #[must_use]
1857 pub fn x_xss_protection(mut self, value: Option<&'static str>) -> Self {
1858 self.x_xss_protection = value;
1859 self
1860 }
1861
1862 #[must_use]
1864 pub fn content_security_policy(mut self, value: impl Into<String>) -> Self {
1865 self.content_security_policy = Some(value.into());
1866 self
1867 }
1868
1869 #[must_use]
1871 pub fn no_content_security_policy(mut self) -> Self {
1872 self.content_security_policy = None;
1873 self
1874 }
1875
1876 #[must_use]
1889 pub fn hsts(mut self, max_age: u64, include_sub_domains: bool, preload: bool) -> Self {
1890 self.hsts = Some((max_age, include_sub_domains, preload));
1891 self
1892 }
1893
1894 #[must_use]
1896 pub fn no_hsts(mut self) -> Self {
1897 self.hsts = None;
1898 self
1899 }
1900
1901 #[must_use]
1903 pub fn referrer_policy(mut self, value: Option<ReferrerPolicy>) -> Self {
1904 self.referrer_policy = value;
1905 self
1906 }
1907
1908 #[must_use]
1910 pub fn permissions_policy(mut self, value: impl Into<String>) -> Self {
1911 self.permissions_policy = Some(value.into());
1912 self
1913 }
1914
1915 #[must_use]
1917 pub fn no_permissions_policy(mut self) -> Self {
1918 self.permissions_policy = None;
1919 self
1920 }
1921
1922 fn build_hsts_value(&self) -> Option<String> {
1924 self.hsts.map(|(max_age, include_sub, preload)| {
1925 let mut value = format!("max-age={}", max_age);
1926 if include_sub {
1927 value.push_str("; includeSubDomains");
1928 }
1929 if preload {
1930 value.push_str("; preload");
1931 }
1932 value
1933 })
1934 }
1935}
1936
1937#[derive(Debug, Clone)]
1968pub struct SecurityHeaders {
1969 config: SecurityHeadersConfig,
1970}
1971
1972impl Default for SecurityHeaders {
1973 fn default() -> Self {
1974 Self::new()
1975 }
1976}
1977
1978impl SecurityHeaders {
1979 #[must_use]
1981 pub fn new() -> Self {
1982 Self {
1983 config: SecurityHeadersConfig::default(),
1984 }
1985 }
1986
1987 #[must_use]
1989 pub fn with_config(config: SecurityHeadersConfig) -> Self {
1990 Self { config }
1991 }
1992
1993 #[must_use]
1995 pub fn strict() -> Self {
1996 Self {
1997 config: SecurityHeadersConfig::strict(),
1998 }
1999 }
2000}
2001
2002impl Middleware for SecurityHeaders {
2003 fn after<'a>(
2004 &'a self,
2005 _ctx: &'a RequestContext,
2006 _req: &'a Request,
2007 response: Response,
2008 ) -> BoxFuture<'a, Response> {
2009 let config = self.config.clone();
2010 Box::pin(async move {
2011 let mut resp = response;
2012
2013 if let Some(value) = config.x_content_type_options {
2015 resp = resp.header("X-Content-Type-Options", value.as_bytes().to_vec());
2016 }
2017
2018 if let Some(value) = config.x_frame_options {
2020 resp = resp.header("X-Frame-Options", value.as_bytes().to_vec());
2021 }
2022
2023 if let Some(value) = config.x_xss_protection {
2025 resp = resp.header("X-XSS-Protection", value.as_bytes().to_vec());
2026 }
2027
2028 if let Some(ref value) = config.content_security_policy {
2030 resp = resp.header("Content-Security-Policy", value.as_bytes().to_vec());
2031 }
2032
2033 if let Some(ref hsts_value) = config.build_hsts_value() {
2035 resp = resp.header("Strict-Transport-Security", hsts_value.as_bytes().to_vec());
2036 }
2037
2038 if let Some(value) = config.referrer_policy {
2040 resp = resp.header("Referrer-Policy", value.as_bytes().to_vec());
2041 }
2042
2043 if let Some(ref value) = config.permissions_policy {
2045 resp = resp.header("Permissions-Policy", value.as_bytes().to_vec());
2046 }
2047
2048 resp
2049 })
2050 }
2051
2052 fn name(&self) -> &'static str {
2053 "SecurityHeaders"
2054 }
2055}
2056
2057#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2066pub struct CsrfToken(pub String);
2067
2068impl CsrfToken {
2069 #[must_use]
2071 pub fn new(token: impl Into<String>) -> Self {
2072 Self(token.into())
2073 }
2074
2075 #[must_use]
2077 pub fn as_str(&self) -> &str {
2078 &self.0
2079 }
2080
2081 #[must_use]
2091 pub fn generate() -> Self {
2092 let mut bytes = [0u8; 32];
2093 if let Err(err) = getrandom::fill(&mut bytes) {
2094 panic!(
2095 "FATAL: OS cryptographically secure random source is unavailable ({err}). \
2096 CSRF token generation requires a CSPRNG. Cannot safely generate CSRF tokens \
2097 without cryptographic entropy."
2098 );
2099 }
2100 Self(Self::bytes_to_hex(&bytes))
2101 }
2102
2103 fn bytes_to_hex(bytes: &[u8]) -> String {
2104 use std::fmt::Write;
2105 let mut s = String::with_capacity(bytes.len() * 2);
2106 for b in bytes {
2107 let _ = write!(s, "{b:02x}");
2108 }
2109 s
2110 }
2111}
2112
2113impl std::fmt::Display for CsrfToken {
2114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2115 f.write_str(&self.0)
2116 }
2117}
2118
2119impl From<&str> for CsrfToken {
2120 fn from(s: &str) -> Self {
2121 Self(s.to_string())
2122 }
2123}
2124
2125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2127pub enum CsrfMode {
2128 #[default]
2131 DoubleSubmit,
2132 HeaderOnly,
2134}
2135
2136#[derive(Debug, Clone)]
2138pub struct CsrfConfig {
2139 pub cookie_name: String,
2141 pub header_name: String,
2143 pub mode: CsrfMode,
2145 pub rotate_token: bool,
2147 pub production: bool,
2149 pub error_message: Option<String>,
2151}
2152
2153impl Default for CsrfConfig {
2154 fn default() -> Self {
2155 Self {
2156 cookie_name: "csrf_token".to_string(),
2157 header_name: "x-csrf-token".to_string(),
2158 mode: CsrfMode::DoubleSubmit,
2159 rotate_token: false,
2160 production: true,
2161 error_message: None,
2162 }
2163 }
2164}
2165
2166impl CsrfConfig {
2167 #[must_use]
2169 pub fn new() -> Self {
2170 Self::default()
2171 }
2172
2173 #[must_use]
2175 pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
2176 self.cookie_name = name.into();
2177 self
2178 }
2179
2180 #[must_use]
2182 pub fn header_name(mut self, name: impl Into<String>) -> Self {
2183 self.header_name = name.into();
2184 self
2185 }
2186
2187 #[must_use]
2189 pub fn mode(mut self, mode: CsrfMode) -> Self {
2190 self.mode = mode;
2191 self
2192 }
2193
2194 #[must_use]
2196 pub fn rotate_token(mut self, rotate: bool) -> Self {
2197 self.rotate_token = rotate;
2198 self
2199 }
2200
2201 #[must_use]
2203 pub fn production(mut self, production: bool) -> Self {
2204 self.production = production;
2205 self
2206 }
2207
2208 #[must_use]
2210 pub fn error_message(mut self, message: impl Into<String>) -> Self {
2211 self.error_message = Some(message.into());
2212 self
2213 }
2214}
2215
2216#[derive(Debug, Clone)]
2246pub struct CsrfMiddleware {
2247 config: CsrfConfig,
2248}
2249
2250impl Default for CsrfMiddleware {
2251 fn default() -> Self {
2252 Self::new()
2253 }
2254}
2255
2256impl CsrfMiddleware {
2257 #[must_use]
2259 pub fn new() -> Self {
2260 Self {
2261 config: CsrfConfig::default(),
2262 }
2263 }
2264
2265 #[must_use]
2267 pub fn with_config(config: CsrfConfig) -> Self {
2268 Self { config }
2269 }
2270
2271 fn is_safe_method(method: crate::request::Method) -> bool {
2273 matches!(
2274 method,
2275 crate::request::Method::Get
2276 | crate::request::Method::Head
2277 | crate::request::Method::Options
2278 | crate::request::Method::Trace
2279 )
2280 }
2281
2282 fn get_cookie_token(&self, req: &Request) -> Option<String> {
2284 let cookie_header = req.headers().get("cookie")?;
2285 let cookie_str = std::str::from_utf8(cookie_header).ok()?;
2286
2287 for part in cookie_str.split(';') {
2289 let part = part.trim();
2290 if let Some((name, value)) = part.split_once('=') {
2291 if name.trim() == self.config.cookie_name {
2292 return Some(value.trim().to_string());
2293 }
2294 }
2295 }
2296 None
2297 }
2298
2299 fn get_header_token(&self, req: &Request) -> Option<String> {
2301 let header_value = req.headers().get(&self.config.header_name)?;
2302 std::str::from_utf8(header_value)
2303 .ok()
2304 .map(|s| s.trim().to_string())
2305 }
2306
2307 fn validate_token(&self, req: &Request) -> Result<Option<CsrfToken>, Response> {
2309 let header_token = self.get_header_token(req);
2310
2311 match self.config.mode {
2312 CsrfMode::DoubleSubmit => {
2313 let cookie_token = self.get_cookie_token(req);
2314
2315 match (header_token, cookie_token) {
2316 (Some(header), Some(cookie))
2317 if !header.is_empty()
2318 && crate::password::constant_time_eq(
2319 header.as_bytes(),
2320 cookie.as_bytes(),
2321 ) =>
2322 {
2323 Ok(Some(CsrfToken::new(header)))
2324 }
2325 (None, _) | (_, None) => Err(self.csrf_error_response("CSRF token missing")),
2326 _ => Err(self.csrf_error_response("CSRF token mismatch")),
2327 }
2328 }
2329 CsrfMode::HeaderOnly => match header_token {
2330 Some(token) if !token.is_empty() => Ok(Some(CsrfToken::new(token))),
2331 _ => Err(self.csrf_error_response("CSRF token missing in header")),
2332 },
2333 }
2334 }
2335
2336 fn csrf_error_response(&self, default_message: &str) -> Response {
2338 let message = self
2339 .config
2340 .error_message
2341 .as_deref()
2342 .unwrap_or(default_message);
2343
2344 let detail = serde_json::json!({
2347 "detail": [{
2348 "type": "csrf_error",
2349 "loc": ["header", self.config.header_name],
2350 "msg": message,
2351 }]
2352 });
2353 let body = detail.to_string();
2354
2355 Response::with_status(crate::response::StatusCode::FORBIDDEN)
2356 .header("content-type", b"application/json".to_vec())
2357 .body(crate::response::ResponseBody::Bytes(body.into_bytes()))
2358 }
2359
2360 fn make_set_cookie_header_value(cookie_name: &str, token: &str, production: bool) -> Vec<u8> {
2362 let mut cookie = format!("{}={}; Path=/; SameSite=Strict", cookie_name, token);
2363
2364 if production {
2365 cookie.push_str("; Secure");
2366 }
2367
2368 cookie.into_bytes()
2371 }
2372}
2373
2374impl Middleware for CsrfMiddleware {
2375 fn before<'a>(
2376 &'a self,
2377 _ctx: &'a RequestContext,
2378 req: &'a mut Request,
2379 ) -> BoxFuture<'a, ControlFlow> {
2380 Box::pin(async move {
2381 if Self::is_safe_method(req.method()) {
2382 let existing_token = self.get_cookie_token(req);
2384 let token = existing_token
2385 .map(CsrfToken::new)
2386 .unwrap_or_else(CsrfToken::generate);
2387 req.insert_extension(token);
2388 ControlFlow::Continue
2389 } else {
2390 match self.validate_token(req) {
2392 Ok(Some(token)) => {
2393 req.insert_extension(token);
2394 ControlFlow::Continue
2395 }
2396 Ok(None) => ControlFlow::Continue,
2397 Err(response) => ControlFlow::Break(response),
2398 }
2399 }
2400 })
2401 }
2402
2403 fn after<'a>(
2404 &'a self,
2405 _ctx: &'a RequestContext,
2406 req: &'a Request,
2407 response: Response,
2408 ) -> BoxFuture<'a, Response> {
2409 let config = self.config.clone();
2410 let is_safe = Self::is_safe_method(req.method());
2411 let existing_cookie_token = self.get_cookie_token(req);
2412 let token = req.get_extension::<CsrfToken>().cloned();
2413
2414 Box::pin(async move {
2415 if is_safe {
2419 let should_set_cookie = existing_cookie_token.is_none() || config.rotate_token;
2420
2421 if should_set_cookie {
2422 if let Some(token) = token {
2423 let cookie_value = Self::make_set_cookie_header_value(
2424 &config.cookie_name,
2425 token.as_str(),
2426 config.production,
2427 );
2428 return response.header("set-cookie", cookie_value);
2429 }
2430 }
2431 }
2432 response
2433 })
2434 }
2435
2436 fn name(&self) -> &'static str {
2437 "CSRF"
2438 }
2439}
2440
2441#[cfg(feature = "compression")]
2464#[derive(Debug, Clone)]
2465pub struct CompressionConfig {
2466 pub min_size: usize,
2470 pub level: u32,
2473 pub skip_content_types: Vec<&'static str>,
2476}
2477
2478#[cfg(feature = "compression")]
2479impl Default for CompressionConfig {
2480 fn default() -> Self {
2481 Self {
2482 min_size: 1024,
2483 level: 6,
2484 skip_content_types: vec![
2485 "image/jpeg",
2487 "image/png",
2488 "image/gif",
2489 "image/webp",
2490 "image/avif",
2491 "video/",
2493 "audio/",
2494 "application/zip",
2496 "application/gzip",
2497 "application/x-gzip",
2498 "application/x-bzip2",
2499 "application/x-xz",
2500 "application/x-7z-compressed",
2501 "application/x-rar-compressed",
2502 "application/pdf",
2504 "application/woff",
2505 "application/woff2",
2506 "font/woff",
2507 "font/woff2",
2508 ],
2509 }
2510 }
2511}
2512
2513#[cfg(feature = "compression")]
2514impl CompressionConfig {
2515 #[must_use]
2517 pub fn new() -> Self {
2518 Self::default()
2519 }
2520
2521 #[must_use]
2526 pub fn min_size(mut self, size: usize) -> Self {
2527 self.min_size = size;
2528 self
2529 }
2530
2531 #[must_use]
2539 pub fn level(mut self, level: u32) -> Self {
2540 self.level = level.clamp(1, 9);
2541 self
2542 }
2543
2544 #[must_use]
2548 pub fn skip_content_type(mut self, content_type: &'static str) -> Self {
2549 self.skip_content_types.push(content_type);
2550 self
2551 }
2552
2553 fn should_skip_content_type(&self, content_type: &str) -> bool {
2555 let ct_lower = content_type.to_ascii_lowercase();
2556 for skip in &self.skip_content_types {
2557 if skip.ends_with('/') {
2558 if ct_lower.starts_with(*skip) {
2560 return true;
2561 }
2562 } else {
2563 if ct_lower == *skip || ct_lower.starts_with(&format!("{skip};")) {
2565 return true;
2566 }
2567 }
2568 }
2569 false
2570 }
2571}
2572
2573#[cfg(feature = "compression")]
2606#[derive(Debug, Clone)]
2607pub struct CompressionMiddleware {
2608 config: CompressionConfig,
2609}
2610
2611#[cfg(feature = "compression")]
2612impl Default for CompressionMiddleware {
2613 fn default() -> Self {
2614 Self::new()
2615 }
2616}
2617
2618#[cfg(feature = "compression")]
2619impl CompressionMiddleware {
2620 #[must_use]
2622 pub fn new() -> Self {
2623 Self {
2624 config: CompressionConfig::default(),
2625 }
2626 }
2627
2628 #[must_use]
2630 pub fn with_config(config: CompressionConfig) -> Self {
2631 Self { config }
2632 }
2633
2634 fn accepts_gzip(req: &Request) -> bool {
2636 if let Some(accept_encoding) = req.headers().get("accept-encoding") {
2637 if let Ok(value) = std::str::from_utf8(accept_encoding) {
2638 for part in value.split(',') {
2641 let encoding = part.trim().split(';').next().unwrap_or("").trim();
2642 if encoding.eq_ignore_ascii_case("gzip") {
2643 return true;
2644 }
2645 if encoding == "*" {
2647 return true;
2648 }
2649 }
2650 }
2651 }
2652 false
2653 }
2654
2655 fn get_content_type(headers: &[(String, Vec<u8>)]) -> Option<String> {
2657 for (name, value) in headers {
2658 if name.eq_ignore_ascii_case("content-type") {
2659 return std::str::from_utf8(value).ok().map(String::from);
2660 }
2661 }
2662 None
2663 }
2664
2665 fn has_content_encoding(headers: &[(String, Vec<u8>)]) -> bool {
2667 headers
2668 .iter()
2669 .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"))
2670 }
2671
2672 fn compress_gzip(data: &[u8], level: u32) -> Result<Vec<u8>, std::io::Error> {
2674 use flate2::Compression;
2675 use flate2::write::GzEncoder;
2676 use std::io::Write;
2677
2678 let mut encoder = GzEncoder::new(Vec::new(), Compression::new(level));
2679 encoder.write_all(data)?;
2680 encoder.finish()
2681 }
2682}
2683
2684#[cfg(feature = "compression")]
2685impl Middleware for CompressionMiddleware {
2686 fn after<'a>(
2687 &'a self,
2688 _ctx: &'a RequestContext,
2689 req: &'a Request,
2690 response: Response,
2691 ) -> BoxFuture<'a, Response> {
2692 let config = self.config.clone();
2693
2694 Box::pin(async move {
2695 if !Self::accepts_gzip(req) {
2697 return response;
2698 }
2699
2700 let (status, headers, body) = response.into_parts();
2702
2703 if Self::has_content_encoding(&headers) {
2705 return Response::with_status(status)
2706 .body(body)
2707 .rebuild_with_headers(headers);
2708 }
2709
2710 let body_bytes = match body {
2712 crate::response::ResponseBody::Bytes(bytes) => bytes,
2713 other => {
2714 return Response::with_status(status)
2716 .body(other)
2717 .rebuild_with_headers(headers);
2718 }
2719 };
2720
2721 if body_bytes.len() < config.min_size {
2723 return Response::with_status(status)
2724 .body(crate::response::ResponseBody::Bytes(body_bytes))
2725 .rebuild_with_headers(headers);
2726 }
2727
2728 if let Some(content_type) = Self::get_content_type(&headers) {
2730 if config.should_skip_content_type(&content_type) {
2731 return Response::with_status(status)
2732 .body(crate::response::ResponseBody::Bytes(body_bytes))
2733 .rebuild_with_headers(headers);
2734 }
2735 }
2736
2737 match Self::compress_gzip(&body_bytes, config.level) {
2739 Ok(compressed) => {
2740 if compressed.len() >= body_bytes.len() {
2742 return Response::with_status(status)
2743 .body(crate::response::ResponseBody::Bytes(body_bytes))
2744 .rebuild_with_headers(headers);
2745 }
2746
2747 let mut resp = Response::with_status(status)
2749 .body(crate::response::ResponseBody::Bytes(compressed));
2750
2751 for (name, value) in headers {
2753 if !name.eq_ignore_ascii_case("content-length") {
2754 resp = resp.header(name, value);
2755 }
2756 }
2757
2758 resp = resp.header("Content-Encoding", b"gzip".to_vec());
2760 resp = resp.header("Vary", b"Accept-Encoding".to_vec());
2761
2762 resp
2763 }
2764 Err(_) => {
2765 Response::with_status(status)
2767 .body(crate::response::ResponseBody::Bytes(body_bytes))
2768 .rebuild_with_headers(headers)
2769 }
2770 }
2771 })
2772 }
2773
2774 fn name(&self) -> &'static str {
2775 "Compression"
2776 }
2777}
2778
2779use parking_lot::Mutex;
2784use std::collections::HashMap as StdHashMap;
2785use std::time::Duration;
2786
2787#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2789pub enum RateLimitAlgorithm {
2790 TokenBucket,
2792 FixedWindow,
2794 SlidingWindow,
2796}
2797
2798#[derive(Debug, Clone)]
2800pub struct RateLimitResult {
2801 pub allowed: bool,
2803 pub limit: u64,
2805 pub remaining: u64,
2807 pub reset_after_secs: u64,
2809}
2810
2811pub trait KeyExtractor: Send + Sync {
2816 fn extract_key(&self, req: &Request) -> Option<String>;
2820}
2821
2822#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2840pub struct RemoteAddr(pub std::net::IpAddr);
2841
2842impl std::fmt::Display for RemoteAddr {
2843 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2844 write!(f, "{}", self.0)
2845 }
2846}
2847
2848#[derive(Debug, Clone)]
2869pub struct ConnectedIpKeyExtractor;
2870
2871impl KeyExtractor for ConnectedIpKeyExtractor {
2872 fn extract_key(&self, req: &Request) -> Option<String> {
2873 req.get_extension::<RemoteAddr>().map(ToString::to_string)
2874 }
2875}
2876
2877#[derive(Debug, Clone)]
2906pub struct IpKeyExtractor;
2907
2908impl KeyExtractor for IpKeyExtractor {
2909 fn extract_key(&self, req: &Request) -> Option<String> {
2910 if let Some(forwarded) = req.headers().get("x-forwarded-for") {
2912 if let Ok(s) = std::str::from_utf8(forwarded) {
2913 if let Some(ip) = s.split(',').next() {
2915 return Some(ip.trim().to_string());
2916 }
2917 }
2918 }
2919 if let Some(real_ip) = req.headers().get("x-real-ip") {
2920 if let Ok(s) = std::str::from_utf8(real_ip) {
2921 return Some(s.trim().to_string());
2922 }
2923 }
2924 Some("unknown".to_string())
2925 }
2926}
2927
2928#[derive(Debug, Clone)]
2959pub struct TrustedProxyIpKeyExtractor {
2960 trusted_cidrs: Vec<(std::net::IpAddr, u8)>,
2962}
2963
2964impl TrustedProxyIpKeyExtractor {
2965 #[must_use]
2967 pub fn new() -> Self {
2968 Self {
2969 trusted_cidrs: Vec::new(),
2970 }
2971 }
2972
2973 #[must_use]
2979 pub fn trust_cidr(mut self, cidr: &str) -> Self {
2980 let (ip, prefix) = parse_cidr(cidr).expect("invalid CIDR notation");
2981 self.trusted_cidrs.push((ip, prefix));
2982 self
2983 }
2984
2985 #[must_use]
2987 pub fn trust_loopback(mut self) -> Self {
2988 self.trusted_cidrs.push((
2989 std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 0)),
2990 8,
2991 ));
2992 self.trusted_cidrs
2993 .push((std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST), 128));
2994 self
2995 }
2996
2997 fn is_trusted(&self, ip: std::net::IpAddr) -> bool {
2999 self.trusted_cidrs
3000 .iter()
3001 .any(|(cidr_ip, prefix)| ip_in_cidr(ip, *cidr_ip, *prefix))
3002 }
3003
3004 fn extract_from_header(&self, req: &Request) -> Option<String> {
3006 if let Some(forwarded) = req.headers().get("x-forwarded-for") {
3007 if let Ok(s) = std::str::from_utf8(forwarded) {
3008 if let Some(ip) = s.split(',').next() {
3009 return Some(ip.trim().to_string());
3010 }
3011 }
3012 }
3013 if let Some(real_ip) = req.headers().get("x-real-ip") {
3014 if let Ok(s) = std::str::from_utf8(real_ip) {
3015 return Some(s.trim().to_string());
3016 }
3017 }
3018 None
3019 }
3020}
3021
3022impl Default for TrustedProxyIpKeyExtractor {
3023 fn default() -> Self {
3024 Self::new()
3025 }
3026}
3027
3028impl KeyExtractor for TrustedProxyIpKeyExtractor {
3029 fn extract_key(&self, req: &Request) -> Option<String> {
3030 let remote = req.get_extension::<RemoteAddr>()?;
3031
3032 if self.is_trusted(remote.0) {
3033 self.extract_from_header(req)
3035 .or_else(|| Some(remote.to_string()))
3036 } else {
3037 Some(remote.to_string())
3039 }
3040 }
3041}
3042
3043fn parse_cidr(cidr: &str) -> Option<(std::net::IpAddr, u8)> {
3045 let (ip_str, prefix_str) = cidr.split_once('/')?;
3046 let ip: std::net::IpAddr = ip_str.parse().ok()?;
3047 let prefix: u8 = prefix_str.parse().ok()?;
3048
3049 let max_prefix = match ip {
3051 std::net::IpAddr::V4(_) => 32,
3052 std::net::IpAddr::V6(_) => 128,
3053 };
3054 if prefix > max_prefix {
3055 return None;
3056 }
3057
3058 Some((ip, prefix))
3059}
3060
3061fn ip_in_cidr(ip: std::net::IpAddr, cidr_ip: std::net::IpAddr, prefix: u8) -> bool {
3063 match (ip, cidr_ip) {
3064 (std::net::IpAddr::V4(ip), std::net::IpAddr::V4(cidr)) => {
3065 if prefix == 0 {
3066 return true;
3067 }
3068 let ip_bits = u32::from(ip);
3069 let cidr_bits = u32::from(cidr);
3070 let mask = !0u32 << (32 - prefix);
3071 (ip_bits & mask) == (cidr_bits & mask)
3072 }
3073 (std::net::IpAddr::V6(ip), std::net::IpAddr::V6(cidr)) => {
3074 if prefix == 0 {
3075 return true;
3076 }
3077 let ip_bits = u128::from(ip);
3078 let cidr_bits = u128::from(cidr);
3079 let mask = !0u128 << (128 - prefix);
3080 (ip_bits & mask) == (cidr_bits & mask)
3081 }
3082 _ => false, }
3084}
3085
3086#[derive(Debug, Clone)]
3088pub struct HeaderKeyExtractor {
3089 header_name: String,
3090}
3091
3092impl HeaderKeyExtractor {
3093 #[must_use]
3095 pub fn new(header_name: impl Into<String>) -> Self {
3096 Self {
3097 header_name: header_name.into(),
3098 }
3099 }
3100}
3101
3102impl KeyExtractor for HeaderKeyExtractor {
3103 fn extract_key(&self, req: &Request) -> Option<String> {
3104 req.headers()
3105 .get(&self.header_name)
3106 .and_then(|v| std::str::from_utf8(v).ok())
3107 .map(str::to_string)
3108 }
3109}
3110
3111#[derive(Debug, Clone)]
3113pub struct PathKeyExtractor;
3114
3115impl KeyExtractor for PathKeyExtractor {
3116 fn extract_key(&self, req: &Request) -> Option<String> {
3117 Some(req.path().to_string())
3118 }
3119}
3120
3121pub struct CompositeKeyExtractor {
3126 extractors: Vec<Box<dyn KeyExtractor>>,
3127}
3128
3129impl CompositeKeyExtractor {
3130 #[must_use]
3132 pub fn new(extractors: Vec<Box<dyn KeyExtractor>>) -> Self {
3133 Self { extractors }
3134 }
3135}
3136
3137impl KeyExtractor for CompositeKeyExtractor {
3138 fn extract_key(&self, req: &Request) -> Option<String> {
3139 let parts: Vec<String> = self
3140 .extractors
3141 .iter()
3142 .filter_map(|e| e.extract_key(req))
3143 .collect();
3144 if parts.is_empty() {
3145 None
3146 } else {
3147 Some(parts.join(":"))
3148 }
3149 }
3150}
3151
3152#[derive(Debug, Clone)]
3154struct TokenBucketState {
3155 tokens: f64,
3156 last_refill: Instant,
3157 last_seen: Instant,
3158 stale_after: Duration,
3159}
3160
3161#[derive(Debug, Clone)]
3163struct FixedWindowState {
3164 count: u64,
3165 window_start: Instant,
3166 last_seen: Instant,
3167 stale_after: Duration,
3168}
3169
3170#[derive(Debug, Clone)]
3172struct SlidingWindowState {
3173 current_count: u64,
3174 previous_count: u64,
3175 current_window_start: Instant,
3176 last_seen: Instant,
3177 stale_after: Duration,
3178}
3179
3180pub const DEFAULT_RATE_LIMIT_MAX_KEYS: usize = 65_536;
3182
3183const RATE_LIMIT_SWEEP_INTERVAL: Duration = Duration::from_secs(1);
3184
3185struct BoundedRateLimitMap<T> {
3186 entries: StdHashMap<String, T>,
3187 last_sweep: Instant,
3188}
3189
3190impl<T> BoundedRateLimitMap<T> {
3191 fn new(now: Instant) -> Self {
3192 Self {
3193 entries: StdHashMap::new(),
3194 last_sweep: now,
3195 }
3196 }
3197
3198 fn ensure_key<F, S>(
3199 &mut self,
3200 key: &str,
3201 max_keys: usize,
3202 now: Instant,
3203 mut is_stale: S,
3204 create: F,
3205 ) -> bool
3206 where
3207 F: FnOnce() -> T,
3208 S: FnMut(&T) -> bool,
3209 {
3210 if self.entries.contains_key(key) {
3211 return true;
3212 }
3213
3214 if self.entries.len() >= max_keys
3215 && now.saturating_duration_since(self.last_sweep) >= RATE_LIMIT_SWEEP_INTERVAL
3216 {
3217 self.entries.retain(|_, state| !is_stale(state));
3218 self.last_sweep = now;
3219 }
3220
3221 if self.entries.len() >= max_keys {
3222 return false;
3223 }
3224
3225 self.entries.entry(key.to_string()).or_insert_with(create);
3226 true
3227 }
3228}
3229
3230pub struct InMemoryRateLimitStore {
3239 max_keys: usize,
3240 token_buckets: Mutex<BoundedRateLimitMap<TokenBucketState>>,
3241 fixed_windows: Mutex<BoundedRateLimitMap<FixedWindowState>>,
3242 sliding_windows: Mutex<BoundedRateLimitMap<SlidingWindowState>>,
3243}
3244
3245impl InMemoryRateLimitStore {
3246 #[must_use]
3248 pub fn new() -> Self {
3249 Self::with_max_keys(DEFAULT_RATE_LIMIT_MAX_KEYS)
3250 }
3251
3252 #[must_use]
3256 pub fn with_max_keys(max_keys: usize) -> Self {
3257 let now = Instant::now();
3258 Self {
3259 max_keys,
3260 token_buckets: Mutex::new(BoundedRateLimitMap::new(now)),
3261 fixed_windows: Mutex::new(BoundedRateLimitMap::new(now)),
3262 sliding_windows: Mutex::new(BoundedRateLimitMap::new(now)),
3263 }
3264 }
3265
3266 fn cleanup_window(window: Duration) -> Duration {
3267 if window.is_zero() {
3268 RATE_LIMIT_SWEEP_INTERVAL
3269 } else {
3270 window
3271 }
3272 }
3273
3274 fn saturated_result(max_requests: u64, window: Duration) -> RateLimitResult {
3275 let retry_window = Self::cleanup_window(window);
3276 let reset_after_secs = retry_window
3277 .as_secs()
3278 .saturating_add(u64::from(retry_window.subsec_nanos() > 0))
3279 .max(1);
3280 RateLimitResult {
3281 allowed: false,
3282 limit: max_requests,
3283 remaining: 0,
3284 reset_after_secs,
3285 }
3286 }
3287
3288 #[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
3289 fn check_token_bucket(
3290 &self,
3291 key: &str,
3292 max_tokens: u64,
3293 refill_rate: f64,
3294 window: Duration,
3295 now: Instant,
3296 ) -> RateLimitResult {
3297 let mut buckets = self.token_buckets.lock();
3298 let cleanup_window = Self::cleanup_window(window);
3299
3300 if !buckets.ensure_key(
3301 key,
3302 self.max_keys,
3303 now,
3304 |state| now.saturating_duration_since(state.last_seen) >= state.stale_after,
3305 || TokenBucketState {
3306 tokens: max_tokens as f64,
3307 last_refill: now,
3308 last_seen: now,
3309 stale_after: cleanup_window,
3310 },
3311 ) {
3312 return Self::saturated_result(max_tokens, window);
3313 }
3314 let Some(state) = buckets.entries.get_mut(key) else {
3315 return Self::saturated_result(max_tokens, window);
3316 };
3317
3318 let elapsed = now.duration_since(state.last_refill);
3320 let refill = elapsed.as_secs_f64() * refill_rate;
3321 state.tokens = (state.tokens + refill).min(max_tokens as f64);
3322 state.last_refill = now;
3323 state.last_seen = now;
3324 state.stale_after = cleanup_window;
3325
3326 if state.tokens >= 1.0 {
3327 state.tokens -= 1.0;
3328 RateLimitResult {
3329 allowed: true,
3330 limit: max_tokens,
3331 remaining: state.tokens as u64,
3332 reset_after_secs: if state.tokens < max_tokens as f64 {
3333 ((max_tokens as f64 - state.tokens) / refill_rate).ceil() as u64
3334 } else {
3335 window.as_secs()
3336 },
3337 }
3338 } else {
3339 let wait_secs = ((1.0 - state.tokens) / refill_rate).ceil() as u64;
3340 RateLimitResult {
3341 allowed: false,
3342 limit: max_tokens,
3343 remaining: 0,
3344 reset_after_secs: wait_secs,
3345 }
3346 }
3347 }
3348
3349 fn check_fixed_window(
3350 &self,
3351 key: &str,
3352 max_requests: u64,
3353 window: Duration,
3354 now: Instant,
3355 ) -> RateLimitResult {
3356 let mut windows = self.fixed_windows.lock();
3357 let cleanup_window = Self::cleanup_window(window);
3358
3359 if !windows.ensure_key(
3360 key,
3361 self.max_keys,
3362 now,
3363 |state| now.saturating_duration_since(state.last_seen) >= state.stale_after,
3364 || FixedWindowState {
3365 count: 0,
3366 window_start: now,
3367 last_seen: now,
3368 stale_after: cleanup_window,
3369 },
3370 ) {
3371 return Self::saturated_result(max_requests, window);
3372 }
3373 let Some(state) = windows.entries.get_mut(key) else {
3374 return Self::saturated_result(max_requests, window);
3375 };
3376
3377 let elapsed = now.duration_since(state.window_start);
3379 if elapsed >= window {
3380 state.count = 0;
3381 state.window_start = now;
3382 }
3383 state.last_seen = now;
3384 state.stale_after = cleanup_window;
3385
3386 let remaining_time = window
3387 .checked_sub(now.duration_since(state.window_start))
3388 .unwrap_or(Duration::ZERO);
3389
3390 if state.count < max_requests {
3391 state.count += 1;
3392 RateLimitResult {
3393 allowed: true,
3394 limit: max_requests,
3395 remaining: max_requests - state.count,
3396 reset_after_secs: remaining_time.as_secs(),
3397 }
3398 } else {
3399 RateLimitResult {
3400 allowed: false,
3401 limit: max_requests,
3402 remaining: 0,
3403 reset_after_secs: remaining_time.as_secs(),
3404 }
3405 }
3406 }
3407
3408 #[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
3409 fn check_sliding_window(
3410 &self,
3411 key: &str,
3412 max_requests: u64,
3413 window: Duration,
3414 now: Instant,
3415 ) -> RateLimitResult {
3416 let mut windows = self.sliding_windows.lock();
3417 let cleanup_window = Self::cleanup_window(window);
3418 let stale_after = cleanup_window.saturating_add(cleanup_window);
3419
3420 if !windows.ensure_key(
3421 key,
3422 self.max_keys,
3423 now,
3424 |state| now.saturating_duration_since(state.last_seen) >= state.stale_after,
3425 || SlidingWindowState {
3426 current_count: 0,
3427 previous_count: 0,
3428 current_window_start: now,
3429 last_seen: now,
3430 stale_after,
3431 },
3432 ) {
3433 return Self::saturated_result(max_requests, window);
3434 }
3435 let Some(state) = windows.entries.get_mut(key) else {
3436 return Self::saturated_result(max_requests, window);
3437 };
3438
3439 let elapsed = now.duration_since(state.current_window_start);
3441 if elapsed >= stale_after {
3442 state.previous_count = 0;
3444 state.current_count = 0;
3445 state.current_window_start = now;
3446 } else if elapsed >= window {
3447 state.previous_count = state.current_count;
3448 state.current_count = 0;
3449 state.current_window_start = state
3450 .current_window_start
3451 .checked_add(window)
3452 .unwrap_or(now);
3453 }
3454 state.last_seen = now;
3455 state.stale_after = stale_after;
3456
3457 let window_elapsed = now.duration_since(state.current_window_start);
3460 let window_fraction = window_elapsed.as_secs_f64() / window.as_secs_f64();
3461 let previous_weight = 1.0 - window_fraction;
3462 let weighted_count =
3463 (state.previous_count as f64 * previous_weight) + state.current_count as f64;
3464
3465 let remaining_time = window.checked_sub(window_elapsed).unwrap_or(Duration::ZERO);
3466
3467 if weighted_count < max_requests as f64 {
3468 state.current_count += 1;
3469 let new_weighted =
3470 (state.previous_count as f64 * previous_weight) + state.current_count as f64;
3471 let remaining = (max_requests as f64 - new_weighted).max(0.0) as u64;
3472 RateLimitResult {
3473 allowed: true,
3474 limit: max_requests,
3475 remaining,
3476 reset_after_secs: remaining_time.as_secs(),
3477 }
3478 } else {
3479 RateLimitResult {
3480 allowed: false,
3481 limit: max_requests,
3482 remaining: 0,
3483 reset_after_secs: remaining_time.as_secs(),
3484 }
3485 }
3486 }
3487
3488 #[allow(clippy::cast_precision_loss)]
3489 fn check_at(
3490 &self,
3491 key: &str,
3492 algorithm: RateLimitAlgorithm,
3493 max_requests: u64,
3494 window: Duration,
3495 now: Instant,
3496 ) -> RateLimitResult {
3497 if window.is_zero() {
3498 return Self::saturated_result(max_requests, window);
3499 }
3500
3501 match algorithm {
3502 RateLimitAlgorithm::TokenBucket => {
3503 let refill_rate = max_requests as f64 / window.as_secs_f64();
3504 self.check_token_bucket(key, max_requests, refill_rate, window, now)
3505 }
3506 RateLimitAlgorithm::FixedWindow => {
3507 self.check_fixed_window(key, max_requests, window, now)
3508 }
3509 RateLimitAlgorithm::SlidingWindow => {
3510 self.check_sliding_window(key, max_requests, window, now)
3511 }
3512 }
3513 }
3514
3515 pub fn check(
3517 &self,
3518 key: &str,
3519 algorithm: RateLimitAlgorithm,
3520 max_requests: u64,
3521 window: Duration,
3522 ) -> RateLimitResult {
3523 self.check_at(key, algorithm, max_requests, window, Instant::now())
3524 }
3525}
3526
3527impl Default for InMemoryRateLimitStore {
3528 fn default() -> Self {
3529 Self::new()
3530 }
3531}
3532
3533#[derive(Clone)]
3568pub struct RateLimitConfig {
3569 pub max_requests: u64,
3571 pub window: Duration,
3573 pub algorithm: RateLimitAlgorithm,
3575 pub max_keys: usize,
3577 pub include_headers: bool,
3579 pub retry_message: String,
3581}
3582
3583impl Default for RateLimitConfig {
3584 fn default() -> Self {
3585 Self {
3586 max_requests: 100,
3587 window: Duration::from_secs(60),
3588 algorithm: RateLimitAlgorithm::TokenBucket,
3589 max_keys: DEFAULT_RATE_LIMIT_MAX_KEYS,
3590 include_headers: true,
3591 retry_message: "Rate limit exceeded. Please retry later.".to_string(),
3592 }
3593 }
3594}
3595
3596pub struct RateLimitBuilder {
3598 config: RateLimitConfig,
3599 key_extractor: Option<Box<dyn KeyExtractor>>,
3600}
3601
3602impl RateLimitBuilder {
3603 #[must_use]
3605 pub fn new() -> Self {
3606 Self {
3607 config: RateLimitConfig::default(),
3608 key_extractor: None,
3609 }
3610 }
3611
3612 #[must_use]
3614 pub fn requests(mut self, max: u64) -> Self {
3615 self.config.max_requests = max;
3616 self
3617 }
3618
3619 #[must_use]
3621 pub fn per(mut self, window: Duration) -> Self {
3622 self.config.window = window;
3623 self
3624 }
3625
3626 #[must_use]
3628 pub fn per_second(self, secs: u64) -> Self {
3629 self.per(Duration::from_secs(secs))
3630 }
3631
3632 #[must_use]
3634 pub fn per_minute(self, minutes: u64) -> Self {
3635 self.per(Duration::from_secs(minutes * 60))
3636 }
3637
3638 #[must_use]
3640 pub fn per_hour(self, hours: u64) -> Self {
3641 self.per(Duration::from_secs(hours * 3600))
3642 }
3643
3644 #[must_use]
3646 pub fn algorithm(mut self, algo: RateLimitAlgorithm) -> Self {
3647 self.config.algorithm = algo;
3648 self
3649 }
3650
3651 #[must_use]
3655 pub fn max_keys(mut self, max_keys: usize) -> Self {
3656 self.config.max_keys = max_keys;
3657 self
3658 }
3659
3660 #[must_use]
3662 pub fn key_extractor(mut self, extractor: impl KeyExtractor + 'static) -> Self {
3663 self.key_extractor = Some(Box::new(extractor));
3664 self
3665 }
3666
3667 #[must_use]
3669 pub fn include_headers(mut self, include: bool) -> Self {
3670 self.config.include_headers = include;
3671 self
3672 }
3673
3674 #[must_use]
3676 pub fn retry_message(mut self, msg: impl Into<String>) -> Self {
3677 self.config.retry_message = msg.into();
3678 self
3679 }
3680
3681 #[must_use]
3683 pub fn build(self) -> RateLimitMiddleware {
3684 let key_extractor = self
3685 .key_extractor
3686 .unwrap_or_else(|| Box::new(IpKeyExtractor));
3687 let store = Arc::new(InMemoryRateLimitStore::with_max_keys(self.config.max_keys));
3688 RateLimitMiddleware {
3689 config: self.config,
3690 store,
3691 key_extractor: Arc::from(key_extractor),
3692 }
3693 }
3694}
3695
3696impl Default for RateLimitBuilder {
3697 fn default() -> Self {
3698 Self::new()
3699 }
3700}
3701
3702#[derive(Debug, Clone)]
3704struct RateLimitInfo {
3705 result: RateLimitResult,
3706}
3707
3708pub struct RateLimitMiddleware {
3731 config: RateLimitConfig,
3732 store: Arc<InMemoryRateLimitStore>,
3733 key_extractor: Arc<dyn KeyExtractor>,
3734}
3735
3736impl RateLimitMiddleware {
3737 #[must_use]
3739 pub fn new() -> Self {
3740 Self::builder().build()
3741 }
3742
3743 #[must_use]
3745 pub fn builder() -> RateLimitBuilder {
3746 RateLimitBuilder::new()
3747 }
3748
3749 fn too_many_requests_body(&self, result: &RateLimitResult) -> Vec<u8> {
3751 format!(
3752 r#"{{"detail":"{}","retry_after_secs":{}}}"#,
3753 self.config.retry_message, result.reset_after_secs
3754 )
3755 .into_bytes()
3756 }
3757
3758 fn add_headers(&self, response: Response, result: &RateLimitResult) -> Response {
3760 response
3761 .header("X-RateLimit-Limit", result.limit.to_string().into_bytes())
3762 .header(
3763 "X-RateLimit-Remaining",
3764 result.remaining.to_string().into_bytes(),
3765 )
3766 .header(
3767 "X-RateLimit-Reset",
3768 result.reset_after_secs.to_string().into_bytes(),
3769 )
3770 }
3771}
3772
3773impl Default for RateLimitMiddleware {
3774 fn default() -> Self {
3775 Self::new()
3776 }
3777}
3778
3779impl Middleware for RateLimitMiddleware {
3780 fn before<'a>(
3781 &'a self,
3782 _ctx: &'a RequestContext,
3783 req: &'a mut Request,
3784 ) -> BoxFuture<'a, ControlFlow> {
3785 Box::pin(async move {
3786 let Some(key) = self.key_extractor.extract_key(req) else {
3788 return ControlFlow::Continue;
3790 };
3791
3792 let result = self.store.check(
3794 &key,
3795 self.config.algorithm,
3796 self.config.max_requests,
3797 self.config.window,
3798 );
3799
3800 if result.allowed {
3801 req.insert_extension(RateLimitInfo { result });
3803 ControlFlow::Continue
3804 } else {
3805 let body = self.too_many_requests_body(&result);
3807 let mut response =
3808 Response::with_status(crate::response::StatusCode::TOO_MANY_REQUESTS)
3809 .header("Content-Type", b"application/json".to_vec())
3810 .header(
3811 "Retry-After",
3812 result.reset_after_secs.to_string().into_bytes(),
3813 )
3814 .body(crate::response::ResponseBody::Bytes(body));
3815
3816 if self.config.include_headers {
3817 response = self.add_headers(response, &result);
3818 }
3819
3820 ControlFlow::Break(response)
3821 }
3822 })
3823 }
3824
3825 fn after<'a>(
3826 &'a self,
3827 _ctx: &'a RequestContext,
3828 req: &'a Request,
3829 response: Response,
3830 ) -> BoxFuture<'a, Response> {
3831 Box::pin(async move {
3832 if !self.config.include_headers {
3833 return response;
3834 }
3835
3836 if let Some(info) = req.get_extension::<RateLimitInfo>() {
3838 self.add_headers(response, &info.result)
3839 } else {
3840 response
3841 }
3842 })
3843 }
3844
3845 fn name(&self) -> &'static str {
3846 "RateLimit"
3847 }
3848}
3849
3850#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3862pub enum InspectionVerbosity {
3863 Minimal,
3867
3868 Normal,
3872
3873 Verbose,
3878}
3879
3880pub struct RequestInspectionMiddleware {
3919 log_config: LogConfig,
3920 verbosity: InspectionVerbosity,
3921 redact_headers: HashSet<String>,
3922 slow_threshold_ms: u64,
3923 max_body_preview: usize,
3924}
3925
3926impl Default for RequestInspectionMiddleware {
3927 fn default() -> Self {
3928 Self {
3929 log_config: LogConfig::development(),
3930 verbosity: InspectionVerbosity::Normal,
3931 redact_headers: default_redacted_headers(),
3932 slow_threshold_ms: 1000,
3933 max_body_preview: 2048,
3934 }
3935 }
3936}
3937
3938impl RequestInspectionMiddleware {
3939 #[must_use]
3941 pub fn new() -> Self {
3942 Self::default()
3943 }
3944
3945 #[must_use]
3947 pub fn log_config(mut self, config: LogConfig) -> Self {
3948 self.log_config = config;
3949 self
3950 }
3951
3952 #[must_use]
3954 pub fn verbosity(mut self, level: InspectionVerbosity) -> Self {
3955 self.verbosity = level;
3956 self
3957 }
3958
3959 #[must_use]
3961 pub fn slow_threshold_ms(mut self, ms: u64) -> Self {
3962 self.slow_threshold_ms = ms;
3963 self
3964 }
3965
3966 #[must_use]
3968 pub fn max_body_preview(mut self, max: usize) -> Self {
3969 self.max_body_preview = max;
3970 self
3971 }
3972
3973 #[must_use]
3975 pub fn redact_header(mut self, name: impl Into<String>) -> Self {
3976 self.redact_headers.insert(name.into().to_ascii_lowercase());
3977 self
3978 }
3979
3980 fn format_body_preview(&self, bytes: &[u8], content_type: Option<&[u8]>) -> Option<String> {
3982 if bytes.is_empty() || self.max_body_preview == 0 {
3983 return None;
3984 }
3985
3986 let is_json = content_type
3987 .and_then(|ct| std::str::from_utf8(ct).ok())
3988 .is_some_and(|ct| ct.contains("application/json"));
3989
3990 let limit = self.max_body_preview.min(bytes.len());
3991 let truncated = bytes.len() > self.max_body_preview;
3992
3993 match std::str::from_utf8(&bytes[..limit]) {
3994 Ok(text) => {
3995 if is_json {
3996 if let Some(pretty) = try_pretty_json(text) {
3998 let mut output = pretty;
3999 if truncated {
4000 output.push_str("\n ... (truncated)");
4001 }
4002 return Some(output);
4003 }
4004 }
4005 let mut output = text.to_string();
4006 if truncated {
4007 output.push_str("...");
4008 }
4009 Some(output)
4010 }
4011 Err(_) => Some(format!("<{} bytes binary>", bytes.len())),
4012 }
4013 }
4014
4015 fn format_response_preview(
4017 &self,
4018 body: &crate::response::ResponseBody,
4019 content_type: Option<&[u8]>,
4020 ) -> Option<String> {
4021 match body {
4022 crate::response::ResponseBody::Empty => None,
4023 crate::response::ResponseBody::Bytes(bytes) => {
4024 self.format_body_preview(bytes, content_type)
4025 }
4026 crate::response::ResponseBody::Stream(_) => Some("<streaming body>".to_string()),
4027 }
4028 }
4029
4030 fn format_inspection_headers<'a>(
4032 &self,
4033 headers: impl Iterator<Item = (&'a str, &'a [u8])>,
4034 ) -> String {
4035 let mut out = String::new();
4036 for (name, value) in headers {
4037 out.push_str("\n ");
4038 out.push_str(name);
4039 out.push_str(": ");
4040
4041 let lowered = name.to_ascii_lowercase();
4042 if self.redact_headers.contains(&lowered) {
4043 out.push_str("[REDACTED]");
4044 } else {
4045 match std::str::from_utf8(value) {
4046 Ok(text) => out.push_str(text),
4047 Err(_) => out.push_str("<binary>"),
4048 }
4049 }
4050 }
4051 out
4052 }
4053
4054 fn format_response_inspection_headers(&self, headers: &[(String, Vec<u8>)]) -> String {
4056 self.format_inspection_headers(
4057 headers
4058 .iter()
4059 .map(|(name, value)| (name.as_str(), value.as_slice())),
4060 )
4061 }
4062}
4063
4064#[derive(Debug, Clone)]
4066struct InspectionStart(Instant);
4067
4068impl Middleware for RequestInspectionMiddleware {
4069 fn before<'a>(
4070 &'a self,
4071 ctx: &'a RequestContext,
4072 req: &'a mut Request,
4073 ) -> BoxFuture<'a, ControlFlow> {
4074 let logger = RequestLogger::new(ctx, self.log_config.clone());
4075 req.insert_extension(InspectionStart(Instant::now()));
4076
4077 let method = req.method();
4078 let path = req.path();
4079 let query = req.query();
4080
4081 let mut request_line = format!("--> {method} {path}");
4083 if let Some(q) = query {
4084 request_line.push('?');
4085 request_line.push_str(q);
4086 }
4087
4088 let body_size = body_len(req.body());
4089 if body_size > 0 {
4090 request_line.push_str(&format!(" ({body_size} bytes)"));
4091 }
4092
4093 match self.verbosity {
4094 InspectionVerbosity::Minimal => {
4095 logger.info(request_line);
4096 }
4097 InspectionVerbosity::Normal => {
4098 let headers = self.format_inspection_headers(req.headers().iter());
4099 logger.info(format!("{request_line}{headers}"));
4100 }
4101 InspectionVerbosity::Verbose => {
4102 let headers = self.format_inspection_headers(req.headers().iter());
4103 let content_type = req.headers().get("content-type");
4104 let body_preview = match req.body() {
4105 Body::Empty => None,
4106 Body::Bytes(bytes) => self.format_body_preview(bytes, content_type),
4107 Body::Stream { .. } => None,
4108 };
4109
4110 let mut output = format!("{request_line}{headers}");
4111 if let Some(body) = body_preview {
4112 output.push_str("\n ");
4113 output.push_str(&body.replace('\n', "\n "));
4115 }
4116 logger.info(output);
4117 }
4118 }
4119
4120 Box::pin(async { ControlFlow::Continue })
4121 }
4122
4123 fn after<'a>(
4124 &'a self,
4125 ctx: &'a RequestContext,
4126 req: &'a Request,
4127 response: Response,
4128 ) -> BoxFuture<'a, Response> {
4129 let logger = RequestLogger::new(ctx, self.log_config.clone());
4130 let duration = req
4131 .get_extension::<InspectionStart>()
4132 .map(|start| start.0.elapsed())
4133 .unwrap_or_default();
4134
4135 let status = response.status();
4136 let duration_ms = duration.as_millis();
4137
4138 let mut response_line = format!(
4140 "<-- {} {} ({duration_ms}ms)",
4141 status.as_u16(),
4142 status.canonical_reason(),
4143 );
4144
4145 if duration_ms >= u128::from(self.slow_threshold_ms) {
4147 response_line.push_str(" [SLOW]");
4148 }
4149
4150 match self.verbosity {
4151 InspectionVerbosity::Minimal => {
4152 if duration_ms >= u128::from(self.slow_threshold_ms) {
4153 logger.warn(response_line);
4154 } else {
4155 logger.info(response_line);
4156 }
4157 }
4158 InspectionVerbosity::Normal => {
4159 let headers = self.format_response_inspection_headers(response.headers());
4160 let output = format!("{response_line}{headers}");
4161 if duration_ms >= u128::from(self.slow_threshold_ms) {
4162 logger.warn(output);
4163 } else {
4164 logger.info(output);
4165 }
4166 }
4167 InspectionVerbosity::Verbose => {
4168 let headers = self.format_response_inspection_headers(response.headers());
4169
4170 let resp_content_type: Option<&[u8]> = response
4172 .headers()
4173 .iter()
4174 .find(|(name, _)| name.eq_ignore_ascii_case("content-type"))
4175 .map(|(_, value)| value.as_slice());
4176
4177 let body_preview =
4178 self.format_response_preview(response.body_ref(), resp_content_type);
4179
4180 let mut output = format!("{response_line}{headers}");
4181 if let Some(body) = body_preview {
4182 output.push_str("\n ");
4183 output.push_str(&body.replace('\n', "\n "));
4184 }
4185
4186 if duration_ms >= u128::from(self.slow_threshold_ms) {
4187 logger.warn(output);
4188 } else {
4189 logger.info(output);
4190 }
4191 }
4192 }
4193
4194 Box::pin(async move { response })
4195 }
4196
4197 fn name(&self) -> &'static str {
4198 "RequestInspection"
4199 }
4200}
4201
4202fn try_pretty_json(input: &str) -> Option<String> {
4207 let trimmed = input.trim();
4208 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
4209 return None;
4210 }
4211
4212 let mut output = String::with_capacity(trimmed.len() * 2);
4214 if json_pretty_format(trimmed, &mut output).is_ok() {
4215 Some(output)
4216 } else {
4217 None
4218 }
4219}
4220
4221fn json_pretty_format(input: &str, output: &mut String) -> Result<(), ()> {
4226 let bytes = input.as_bytes();
4227 let mut pos = 0;
4228 let mut indent: usize = 0;
4229 let mut in_string = false;
4230 let mut escape_next = false;
4231
4232 while pos < bytes.len() {
4233 let ch = bytes[pos] as char;
4234
4235 if escape_next {
4236 output.push(ch);
4237 escape_next = false;
4238 pos += 1;
4239 continue;
4240 }
4241
4242 if in_string {
4243 output.push(ch);
4244 if ch == '\\' {
4245 escape_next = true;
4246 } else if ch == '"' {
4247 in_string = false;
4248 }
4249 pos += 1;
4250 continue;
4251 }
4252
4253 match ch {
4254 '"' => {
4255 in_string = true;
4256 output.push('"');
4257 }
4258 '{' | '[' => {
4259 output.push(ch);
4260 let peek = skip_whitespace(bytes, pos + 1);
4262 let closing = if ch == '{' { '}' } else { ']' };
4263 if peek < bytes.len() && bytes[peek] as char == closing {
4264 output.push(closing);
4265 pos = peek + 1;
4266 continue;
4267 }
4268 indent += 1;
4269 output.push('\n');
4270 push_indent(output, indent);
4271 }
4272 '}' | ']' => {
4273 indent = indent.saturating_sub(1);
4274 output.push('\n');
4275 push_indent(output, indent);
4276 output.push(ch);
4277 }
4278 ':' => {
4279 output.push_str(": ");
4280 }
4281 ',' => {
4282 output.push(',');
4283 output.push('\n');
4284 push_indent(output, indent);
4285 }
4286 c if c.is_ascii_whitespace() => {
4287 }
4289 _ => {
4290 output.push(ch);
4291 }
4292 }
4293
4294 pos += 1;
4295 }
4296
4297 if in_string || indent != 0 {
4298 return Err(());
4299 }
4300
4301 Ok(())
4302}
4303
4304fn skip_whitespace(bytes: &[u8], start: usize) -> usize {
4305 let mut i = start;
4306 while i < bytes.len() && (bytes[i] as char).is_ascii_whitespace() {
4307 i += 1;
4308 }
4309 i
4310}
4311
4312fn push_indent(output: &mut String, level: usize) {
4313 for _ in 0..level {
4314 output.push_str(" ");
4315 }
4316}
4317
4318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4328pub enum ETagMode {
4329 Auto,
4332 Manual,
4335 Disabled,
4337}
4338
4339impl Default for ETagMode {
4340 fn default() -> Self {
4341 Self::Auto
4342 }
4343}
4344
4345#[derive(Debug, Clone)]
4347pub struct ETagConfig {
4348 pub mode: ETagMode,
4350 pub weak: bool,
4353 pub min_size: usize,
4356}
4357
4358impl Default for ETagConfig {
4359 fn default() -> Self {
4360 Self {
4361 mode: ETagMode::Auto,
4362 weak: false,
4363 min_size: 0,
4364 }
4365 }
4366}
4367
4368impl ETagConfig {
4369 #[must_use]
4371 pub fn new() -> Self {
4372 Self::default()
4373 }
4374
4375 #[must_use]
4377 pub fn mode(mut self, mode: ETagMode) -> Self {
4378 self.mode = mode;
4379 self
4380 }
4381
4382 #[must_use]
4384 pub fn weak(mut self, weak: bool) -> Self {
4385 self.weak = weak;
4386 self
4387 }
4388
4389 #[must_use]
4391 pub fn min_size(mut self, size: usize) -> Self {
4392 self.min_size = size;
4393 self
4394 }
4395}
4396
4397pub struct ETagMiddleware {
4438 config: ETagConfig,
4439}
4440
4441impl Default for ETagMiddleware {
4442 fn default() -> Self {
4443 Self::new()
4444 }
4445}
4446
4447impl ETagMiddleware {
4448 #[must_use]
4450 pub fn new() -> Self {
4451 Self {
4452 config: ETagConfig::default(),
4453 }
4454 }
4455
4456 #[must_use]
4458 pub fn with_config(config: ETagConfig) -> Self {
4459 Self { config }
4460 }
4461
4462 fn generate_etag(data: &[u8], weak: bool) -> String {
4469 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
4471 const FNV_PRIME: u64 = 0x100000001b3;
4472
4473 let mut hash = FNV_OFFSET_BASIS;
4474 for &byte in data {
4475 hash ^= u64::from(byte);
4476 hash = hash.wrapping_mul(FNV_PRIME);
4477 }
4478
4479 if weak {
4481 format!("W/\"{:016x}\"", hash)
4482 } else {
4483 format!("\"{:016x}\"", hash)
4484 }
4485 }
4486
4487 fn parse_if_none_match(value: &str) -> Vec<String> {
4495 let trimmed = value.trim();
4496
4497 if trimmed == "*" {
4499 return vec!["*".to_string()];
4500 }
4501
4502 let mut etags = Vec::new();
4503 let mut current = String::new();
4504 let mut in_quote = false;
4505 let mut prev_char = '\0';
4506
4507 for ch in trimmed.chars() {
4508 match ch {
4509 '"' if prev_char != '\\' => {
4510 current.push(ch);
4511 if in_quote {
4512 let etag = current.trim().to_string();
4514 if !etag.is_empty() {
4515 etags.push(etag);
4516 }
4517 current.clear();
4518 }
4519 in_quote = !in_quote;
4520 }
4521 ',' if !in_quote => {
4522 current.clear();
4524 }
4525 _ => {
4526 current.push(ch);
4527 }
4528 }
4529 prev_char = ch;
4530 }
4531
4532 etags
4533 }
4534
4535 fn etags_match_weak(etag1: &str, etag2: &str) -> bool {
4543 let e1 = Self::strip_weak_prefix(etag1);
4545 let e2 = Self::strip_weak_prefix(etag2);
4546 e1 == e2
4547 }
4548
4549 fn strip_weak_prefix(s: &str) -> &str {
4551 if s.starts_with("W/") || s.starts_with("w/") {
4552 &s[2..]
4553 } else {
4554 s
4555 }
4556 }
4557
4558 fn is_cacheable_method(method: crate::request::Method) -> bool {
4560 matches!(
4561 method,
4562 crate::request::Method::Get | crate::request::Method::Head
4563 )
4564 }
4565
4566 fn get_existing_etag(headers: &[(String, Vec<u8>)]) -> Option<String> {
4568 for (name, value) in headers {
4569 if name.eq_ignore_ascii_case("etag") {
4570 return std::str::from_utf8(value).ok().map(String::from);
4571 }
4572 }
4573 None
4574 }
4575}
4576
4577impl Middleware for ETagMiddleware {
4578 fn after<'a>(
4579 &'a self,
4580 _ctx: &'a RequestContext,
4581 req: &'a Request,
4582 response: Response,
4583 ) -> BoxFuture<'a, Response> {
4584 let config = self.config.clone();
4585
4586 Box::pin(async move {
4587 if config.mode == ETagMode::Disabled {
4589 return response;
4590 }
4591
4592 if !Self::is_cacheable_method(req.method()) {
4594 return response;
4595 }
4596
4597 let (status, headers, body) = response.into_parts();
4599
4600 let existing_etag = Self::get_existing_etag(&headers);
4602
4603 let body_bytes = match &body {
4605 crate::response::ResponseBody::Bytes(bytes) => Some(bytes.clone()),
4606 crate::response::ResponseBody::Empty => Some(Vec::new()),
4607 crate::response::ResponseBody::Stream(_) => None,
4608 };
4609
4610 let etag = if let Some(existing) = existing_etag {
4612 Some(existing)
4613 } else if config.mode == ETagMode::Auto {
4614 if let Some(ref bytes) = body_bytes {
4615 if bytes.len() >= config.min_size {
4616 Some(Self::generate_etag(bytes, config.weak))
4617 } else {
4618 None
4619 }
4620 } else {
4621 None
4622 }
4623 } else {
4624 None
4625 };
4626
4627 if let Some(ref etag_value) = etag {
4629 if let Some(if_none_match) = req.headers().get("if-none-match") {
4630 if let Ok(value) = std::str::from_utf8(if_none_match) {
4631 let client_etags = Self::parse_if_none_match(value);
4632
4633 let matches = client_etags.iter().any(|client_etag| {
4635 client_etag == "*" || Self::etags_match_weak(client_etag, etag_value)
4636 });
4637
4638 if matches {
4639 return Response::with_status(
4641 crate::response::StatusCode::NOT_MODIFIED,
4642 )
4643 .header("etag", etag_value.as_bytes().to_vec());
4644 }
4645 }
4646 }
4647 }
4648
4649 let mut new_response = Response::with_status(status)
4651 .body(body)
4652 .rebuild_with_headers(headers);
4653
4654 if let Some(etag_value) = etag {
4655 new_response = new_response.header("etag", etag_value.into_bytes());
4656 }
4657
4658 new_response
4659 })
4660 }
4661
4662 fn name(&self) -> &'static str {
4663 "ETagMiddleware"
4664 }
4665}
4666
4667#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4676pub enum CacheDirective {
4677 Public,
4679 Private,
4681 NoStore,
4683 NoCache,
4685 NoTransform,
4687 MustRevalidate,
4689 ProxyRevalidate,
4691 StaleIfError,
4693 StaleWhileRevalidate,
4695 SMaxAge,
4697 OnlyIfCached,
4699 Immutable,
4701}
4702
4703impl CacheDirective {
4704 fn as_str(self) -> &'static str {
4706 match self {
4707 Self::Public => "public",
4708 Self::Private => "private",
4709 Self::NoStore => "no-store",
4710 Self::NoCache => "no-cache",
4711 Self::NoTransform => "no-transform",
4712 Self::MustRevalidate => "must-revalidate",
4713 Self::ProxyRevalidate => "proxy-revalidate",
4714 Self::StaleIfError => "stale-if-error",
4715 Self::StaleWhileRevalidate => "stale-while-revalidate",
4716 Self::SMaxAge => "s-maxage",
4717 Self::OnlyIfCached => "only-if-cached",
4718 Self::Immutable => "immutable",
4719 }
4720 }
4721}
4722
4723#[derive(Debug, Clone, Default)]
4753pub struct CacheControlBuilder {
4754 directives: Vec<CacheDirective>,
4755 max_age: Option<u32>,
4756 s_maxage: Option<u32>,
4757 stale_while_revalidate: Option<u32>,
4758 stale_if_error: Option<u32>,
4759}
4760
4761impl CacheControlBuilder {
4762 #[must_use]
4764 pub fn new() -> Self {
4765 Self::default()
4766 }
4767
4768 #[must_use]
4770 pub fn public(mut self) -> Self {
4771 self.directives.push(CacheDirective::Public);
4772 self
4773 }
4774
4775 #[must_use]
4777 pub fn private(mut self) -> Self {
4778 self.directives.push(CacheDirective::Private);
4779 self
4780 }
4781
4782 #[must_use]
4784 pub fn no_store(mut self) -> Self {
4785 self.directives.push(CacheDirective::NoStore);
4786 self
4787 }
4788
4789 #[must_use]
4791 pub fn no_cache(mut self) -> Self {
4792 self.directives.push(CacheDirective::NoCache);
4793 self
4794 }
4795
4796 #[must_use]
4798 pub fn no_transform(mut self) -> Self {
4799 self.directives.push(CacheDirective::NoTransform);
4800 self
4801 }
4802
4803 #[must_use]
4805 pub fn must_revalidate(mut self) -> Self {
4806 self.directives.push(CacheDirective::MustRevalidate);
4807 self
4808 }
4809
4810 #[must_use]
4812 pub fn proxy_revalidate(mut self) -> Self {
4813 self.directives.push(CacheDirective::ProxyRevalidate);
4814 self
4815 }
4816
4817 #[must_use]
4819 pub fn immutable(mut self) -> Self {
4820 self.directives.push(CacheDirective::Immutable);
4821 self
4822 }
4823
4824 #[must_use]
4826 pub fn max_age_secs(mut self, seconds: u32) -> Self {
4827 self.max_age = Some(seconds);
4828 self
4829 }
4830
4831 #[must_use]
4833 pub fn max_age(self, duration: std::time::Duration) -> Self {
4834 self.max_age_secs(duration.as_secs() as u32)
4835 }
4836
4837 #[must_use]
4839 pub fn s_maxage_secs(mut self, seconds: u32) -> Self {
4840 self.s_maxage = Some(seconds);
4841 self
4842 }
4843
4844 #[must_use]
4846 pub fn s_maxage(self, duration: std::time::Duration) -> Self {
4847 self.s_maxage_secs(duration.as_secs() as u32)
4848 }
4849
4850 #[must_use]
4852 pub fn stale_while_revalidate_secs(mut self, seconds: u32) -> Self {
4853 self.stale_while_revalidate = Some(seconds);
4854 self
4855 }
4856
4857 #[must_use]
4859 pub fn stale_if_error_secs(mut self, seconds: u32) -> Self {
4860 self.stale_if_error = Some(seconds);
4861 self
4862 }
4863
4864 #[must_use]
4866 pub fn build(&self) -> String {
4867 let mut parts = Vec::new();
4868
4869 for directive in &self.directives {
4871 parts.push(directive.as_str().to_string());
4872 }
4873
4874 if let Some(age) = self.max_age {
4876 parts.push(format!("max-age={age}"));
4877 }
4878
4879 if let Some(age) = self.s_maxage {
4881 parts.push(format!("s-maxage={age}"));
4882 }
4883
4884 if let Some(seconds) = self.stale_while_revalidate {
4886 parts.push(format!("stale-while-revalidate={seconds}"));
4887 }
4888
4889 if let Some(seconds) = self.stale_if_error {
4891 parts.push(format!("stale-if-error={seconds}"));
4892 }
4893
4894 parts.join(", ")
4895 }
4896
4897 #[must_use]
4899 pub fn is_no_cache(&self) -> bool {
4900 self.directives.contains(&CacheDirective::NoStore)
4901 || self.directives.contains(&CacheDirective::NoCache)
4902 }
4903}
4904
4905#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4907pub enum CachePreset {
4908 NoCache,
4910 PrivateNoCache,
4912 PublicOneHour,
4914 Immutable,
4916 CdnFriendly,
4918 StaticAssets,
4920}
4921
4922impl CachePreset {
4923 #[must_use]
4925 pub fn to_header_value(&self) -> String {
4926 match self {
4927 Self::NoCache => "no-store, no-cache, must-revalidate".to_string(),
4928 Self::PrivateNoCache => "private, max-age=0, must-revalidate".to_string(),
4929 Self::PublicOneHour => "public, max-age=3600".to_string(),
4930 Self::Immutable => "public, max-age=31536000, immutable".to_string(),
4931 Self::CdnFriendly => "public, max-age=60, s-maxage=3600".to_string(),
4932 Self::StaticAssets => "public, max-age=86400".to_string(),
4933 }
4934 }
4935
4936 #[must_use]
4938 pub fn to_builder(&self) -> CacheControlBuilder {
4939 match self {
4940 Self::NoCache => CacheControlBuilder::new()
4941 .no_store()
4942 .no_cache()
4943 .must_revalidate(),
4944 Self::PrivateNoCache => CacheControlBuilder::new()
4945 .private()
4946 .max_age_secs(0)
4947 .must_revalidate(),
4948 Self::PublicOneHour => CacheControlBuilder::new().public().max_age_secs(3600),
4949 Self::Immutable => CacheControlBuilder::new()
4950 .public()
4951 .max_age_secs(31536000)
4952 .immutable(),
4953 Self::CdnFriendly => CacheControlBuilder::new()
4954 .public()
4955 .max_age_secs(60)
4956 .s_maxage_secs(3600),
4957 Self::StaticAssets => CacheControlBuilder::new().public().max_age_secs(86400),
4958 }
4959 }
4960}
4961
4962#[derive(Debug, Clone)]
4964pub struct CacheControlConfig {
4965 pub cache_control: String,
4967 pub vary: Vec<String>,
4969 pub set_expires: bool,
4971 pub preserve_existing: bool,
4973 pub methods: Vec<crate::request::Method>,
4975 pub path_patterns: Vec<String>,
4977 pub cacheable_statuses: Vec<u16>,
4979}
4980
4981impl Default for CacheControlConfig {
4982 fn default() -> Self {
4983 Self {
4984 cache_control: CachePreset::NoCache.to_header_value(),
4985 vary: Vec::new(),
4986 set_expires: false,
4987 preserve_existing: true,
4988 methods: vec![crate::request::Method::Get, crate::request::Method::Head],
4989 path_patterns: Vec::new(),
4990 cacheable_statuses: (200..300).collect(),
4991 }
4992 }
4993}
4994
4995impl CacheControlConfig {
4996 #[must_use]
4998 pub fn new() -> Self {
4999 Self::default()
5000 }
5001
5002 #[must_use]
5004 pub fn from_preset(preset: CachePreset) -> Self {
5005 Self {
5006 cache_control: preset.to_header_value(),
5007 ..Self::default()
5008 }
5009 }
5010
5011 #[must_use]
5013 pub fn from_builder(builder: CacheControlBuilder) -> Self {
5014 Self {
5015 cache_control: builder.build(),
5016 ..Self::default()
5017 }
5018 }
5019
5020 #[must_use]
5022 pub fn cache_control(mut self, value: impl Into<String>) -> Self {
5023 self.cache_control = value.into();
5024 self
5025 }
5026
5027 #[must_use]
5029 pub fn vary(mut self, header: impl Into<String>) -> Self {
5030 self.vary.push(header.into());
5031 self
5032 }
5033
5034 #[must_use]
5036 pub fn vary_headers(mut self, headers: Vec<String>) -> Self {
5037 self.vary.extend(headers);
5038 self
5039 }
5040
5041 #[must_use]
5043 pub fn with_expires(mut self, enable: bool) -> Self {
5044 self.set_expires = enable;
5045 self
5046 }
5047
5048 #[must_use]
5050 pub fn preserve_existing(mut self, preserve: bool) -> Self {
5051 self.preserve_existing = preserve;
5052 self
5053 }
5054
5055 #[must_use]
5057 pub fn methods(mut self, methods: Vec<crate::request::Method>) -> Self {
5058 self.methods = methods;
5059 self
5060 }
5061
5062 #[must_use]
5064 pub fn path_patterns(mut self, patterns: Vec<String>) -> Self {
5065 self.path_patterns = patterns;
5066 self
5067 }
5068
5069 #[must_use]
5071 pub fn cacheable_statuses(mut self, statuses: Vec<u16>) -> Self {
5072 self.cacheable_statuses = statuses;
5073 self
5074 }
5075}
5076
5077pub struct CacheControlMiddleware {
5124 config: CacheControlConfig,
5125}
5126
5127impl Default for CacheControlMiddleware {
5128 fn default() -> Self {
5129 Self::new()
5130 }
5131}
5132
5133impl CacheControlMiddleware {
5134 #[must_use]
5138 pub fn new() -> Self {
5139 Self {
5140 config: CacheControlConfig::default(),
5141 }
5142 }
5143
5144 #[must_use]
5146 pub fn with_preset(preset: CachePreset) -> Self {
5147 Self {
5148 config: CacheControlConfig::from_preset(preset),
5149 }
5150 }
5151
5152 #[must_use]
5154 pub fn with_config(config: CacheControlConfig) -> Self {
5155 Self { config }
5156 }
5157
5158 fn is_cacheable_method(&self, method: crate::request::Method) -> bool {
5160 self.config.methods.contains(&method)
5161 }
5162
5163 fn is_cacheable_status(&self, status: u16) -> bool {
5165 self.config.cacheable_statuses.contains(&status)
5166 }
5167
5168 fn matches_path(&self, path: &str) -> bool {
5170 if self.config.path_patterns.is_empty() {
5171 return true; }
5173
5174 for pattern in &self.config.path_patterns {
5175 if path_matches_pattern(path, pattern) {
5176 return true;
5177 }
5178 }
5179 false
5180 }
5181
5182 fn has_cache_control(headers: &[(String, Vec<u8>)]) -> bool {
5184 headers
5185 .iter()
5186 .any(|(name, _)| name.eq_ignore_ascii_case("cache-control"))
5187 }
5188
5189 fn calculate_expires(cache_control: &str) -> Option<String> {
5191 for directive in cache_control.split(',') {
5193 let directive = directive.trim();
5194 if directive.starts_with("max-age=") {
5195 if let Ok(seconds) = directive[8..].parse::<u64>() {
5196 let now = std::time::SystemTime::now();
5198 if let Some(expires) = now.checked_add(std::time::Duration::from_secs(seconds))
5199 {
5200 return Some(format_http_date(expires));
5201 }
5202 }
5203 }
5204 }
5205 None
5206 }
5207}
5208
5209fn path_matches_pattern(path: &str, pattern: &str) -> bool {
5211 if pattern == "*" {
5212 return true;
5213 }
5214
5215 if pattern.contains('*') {
5216 let parts: Vec<&str> = pattern.split('*').collect();
5218 if parts.len() == 2 {
5219 let (prefix, suffix) = (parts[0], parts[1]);
5220 return path.starts_with(prefix) && path.ends_with(suffix);
5221 }
5222 let fixed_parts: Vec<&str> = pattern.split('*').filter(|s| !s.is_empty()).collect();
5224 let mut remaining = path;
5225 for part in fixed_parts {
5226 if let Some(pos) = remaining.find(part) {
5227 remaining = &remaining[pos + part.len()..];
5228 } else {
5229 return false;
5230 }
5231 }
5232 true
5233 } else {
5234 path == pattern
5235 }
5236}
5237
5238fn format_http_date(time: std::time::SystemTime) -> String {
5240 match time.duration_since(std::time::UNIX_EPOCH) {
5242 Ok(duration) => {
5243 let secs = duration.as_secs();
5245 let days = secs / 86400;
5247 let remaining_secs = secs % 86400;
5248 let hours = remaining_secs / 3600;
5249 let minutes = (remaining_secs % 3600) / 60;
5250 let seconds = remaining_secs % 60;
5251
5252 let day_of_week = ((days + 4) % 7) as usize;
5254 let day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
5255
5256 let (year, month, day) = days_to_date(days);
5258 let month_names = [
5259 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
5260 ];
5261
5262 format!(
5263 "{}, {:02} {} {} {:02}:{:02}:{:02} GMT",
5264 day_names[day_of_week],
5265 day,
5266 month_names[(month - 1) as usize],
5267 year,
5268 hours,
5269 minutes,
5270 seconds
5271 )
5272 }
5273 Err(_) => "Thu, 01 Jan 1970 00:00:00 GMT".to_string(),
5274 }
5275}
5276
5277fn days_to_date(days: u64) -> (u64, u64, u64) {
5279 let mut remaining_days = days;
5281 let mut year = 1970u64;
5282
5283 loop {
5284 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
5285 if remaining_days < days_in_year {
5286 break;
5287 }
5288 remaining_days -= days_in_year;
5289 year += 1;
5290 }
5291
5292 let leap = is_leap_year(year);
5293 let month_days: [u64; 12] = if leap {
5294 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5295 } else {
5296 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5297 };
5298
5299 let mut month = 1u64;
5300 for &days_in_month in &month_days {
5301 if remaining_days < days_in_month {
5302 break;
5303 }
5304 remaining_days -= days_in_month;
5305 month += 1;
5306 }
5307
5308 (year, month, remaining_days + 1)
5309}
5310
5311fn is_leap_year(year: u64) -> bool {
5313 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
5314}
5315
5316impl Middleware for CacheControlMiddleware {
5317 fn after<'a>(
5318 &'a self,
5319 _ctx: &'a RequestContext,
5320 req: &'a Request,
5321 response: Response,
5322 ) -> BoxFuture<'a, Response> {
5323 let config = self.config.clone();
5324
5325 Box::pin(async move {
5326 if !self.is_cacheable_method(req.method()) {
5328 return response;
5329 }
5330
5331 if !self.is_cacheable_status(response.status().as_u16()) {
5332 return response;
5333 }
5334
5335 if !self.matches_path(req.path()) {
5336 return response;
5337 }
5338
5339 let (status, mut headers, body) = response.into_parts();
5341
5342 if config.preserve_existing && Self::has_cache_control(&headers) {
5344 let mut resp = Response::with_status(status);
5346 for (name, value) in headers {
5347 resp = resp.header(name, value);
5348 }
5349 return resp.body(body);
5350 }
5351
5352 headers.push((
5354 "Cache-Control".to_string(),
5355 config.cache_control.as_bytes().to_vec(),
5356 ));
5357
5358 if !config.vary.is_empty() {
5360 let vary_value = config.vary.join(", ");
5361 headers.push(("Vary".to_string(), vary_value.into_bytes()));
5362 }
5363
5364 if config.set_expires {
5366 if let Some(expires) = Self::calculate_expires(&config.cache_control) {
5367 headers.push(("Expires".to_string(), expires.into_bytes()));
5368 }
5369 }
5370
5371 let mut resp = Response::with_status(status);
5373 for (name, value) in headers {
5374 resp = resp.header(name, value);
5375 }
5376 resp.body(body)
5377 })
5378 }
5379
5380 fn name(&self) -> &'static str {
5381 "CacheControlMiddleware"
5382 }
5383}
5384
5385#[derive(Debug, Clone)]
5420pub struct TraceRejectionMiddleware {
5421 log_attempts: bool,
5423}
5424
5425impl Default for TraceRejectionMiddleware {
5426 fn default() -> Self {
5427 Self::new()
5428 }
5429}
5430
5431impl TraceRejectionMiddleware {
5432 #[must_use]
5436 pub fn new() -> Self {
5437 Self { log_attempts: true }
5438 }
5439
5440 #[must_use]
5445 pub fn log_attempts(mut self, log: bool) -> Self {
5446 self.log_attempts = log;
5447 self
5448 }
5449
5450 fn rejection_response(path: &str) -> Response {
5452 let body = format!(
5453 r#"{{"detail":"HTTP TRACE method is not allowed","path":"{}"}}"#,
5454 path.replace('"', "\\\"")
5455 );
5456 Response::with_status(crate::response::StatusCode::METHOD_NOT_ALLOWED)
5457 .header("Content-Type", b"application/json".to_vec())
5458 .header(
5459 "Allow",
5460 b"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD".to_vec(),
5461 )
5462 .body(crate::response::ResponseBody::Bytes(body.into_bytes()))
5463 }
5464}
5465
5466impl Middleware for TraceRejectionMiddleware {
5467 fn before<'a>(
5468 &'a self,
5469 _ctx: &'a RequestContext,
5470 req: &'a mut Request,
5471 ) -> BoxFuture<'a, ControlFlow> {
5472 Box::pin(async move {
5473 if req.method() == crate::request::Method::Trace {
5474 if self.log_attempts {
5475 let path = req.path();
5477 let remote_ip = req
5478 .headers()
5479 .get("X-Forwarded-For")
5480 .or_else(|| req.headers().get("X-Real-IP"))
5481 .map(|v| String::from_utf8_lossy(v).to_string())
5482 .unwrap_or_else(|| "unknown".to_string());
5483
5484 eprintln!(
5485 "[SECURITY] TRACE request blocked: path={}, remote_ip={}",
5486 path, remote_ip
5487 );
5488 }
5489
5490 return ControlFlow::Break(Self::rejection_response(req.path()));
5491 }
5492
5493 ControlFlow::Continue
5494 })
5495 }
5496
5497 fn name(&self) -> &'static str {
5498 "TraceRejection"
5499 }
5500}
5501
5502#[derive(Debug, Clone)]
5512#[allow(clippy::struct_excessive_bools)]
5513pub struct HttpsRedirectConfig {
5514 pub redirect_enabled: bool,
5516 pub permanent_redirect: bool,
5518 pub hsts_max_age_secs: u64,
5520 pub hsts_include_subdomains: bool,
5522 pub hsts_preload: bool,
5524 pub exclude_paths: Vec<String>,
5526 pub https_port: u16,
5528}
5529
5530impl Default for HttpsRedirectConfig {
5531 fn default() -> Self {
5532 Self {
5533 redirect_enabled: true,
5534 permanent_redirect: true, hsts_max_age_secs: 31_536_000, hsts_include_subdomains: false,
5537 hsts_preload: false,
5538 exclude_paths: Vec::new(),
5539 https_port: 443,
5540 }
5541 }
5542}
5543
5544#[derive(Debug, Clone)]
5584pub struct HttpsRedirectMiddleware {
5585 config: HttpsRedirectConfig,
5586}
5587
5588impl Default for HttpsRedirectMiddleware {
5589 fn default() -> Self {
5590 Self::new()
5591 }
5592}
5593
5594impl HttpsRedirectMiddleware {
5595 #[must_use]
5597 pub fn new() -> Self {
5598 Self {
5599 config: HttpsRedirectConfig::default(),
5600 }
5601 }
5602
5603 #[must_use]
5605 pub fn redirect_enabled(mut self, enabled: bool) -> Self {
5606 self.config.redirect_enabled = enabled;
5607 self
5608 }
5609
5610 #[must_use]
5614 pub fn permanent_redirect(mut self, permanent: bool) -> Self {
5615 self.config.permanent_redirect = permanent;
5616 self
5617 }
5618
5619 #[must_use]
5624 pub fn hsts_max_age_secs(mut self, secs: u64) -> Self {
5625 self.config.hsts_max_age_secs = secs;
5626 self
5627 }
5628
5629 #[must_use]
5631 pub fn include_subdomains(mut self, include: bool) -> Self {
5632 self.config.hsts_include_subdomains = include;
5633 self
5634 }
5635
5636 #[must_use]
5641 pub fn preload(mut self, preload: bool) -> Self {
5642 self.config.hsts_preload = preload;
5643 self
5644 }
5645
5646 #[must_use]
5651 pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
5652 self.config.exclude_paths.push(path.into());
5653 self
5654 }
5655
5656 #[must_use]
5658 pub fn exclude_paths(mut self, paths: Vec<String>) -> Self {
5659 self.config.exclude_paths = paths;
5660 self
5661 }
5662
5663 #[must_use]
5665 pub fn https_port(mut self, port: u16) -> Self {
5666 self.config.https_port = port;
5667 self
5668 }
5669
5670 fn is_secure(&self, req: &Request) -> bool {
5675 fn trim_ascii(mut bytes: &[u8]) -> &[u8] {
5676 while matches!(bytes.first(), Some(b' ' | b'\t')) {
5677 bytes = &bytes[1..];
5678 }
5679 while matches!(bytes.last(), Some(b' ' | b'\t')) {
5680 bytes = &bytes[..bytes.len() - 1];
5681 }
5682 bytes
5683 }
5684
5685 if let Some(info) = req.get_extension::<crate::request::ConnectionInfo>() {
5686 if info.is_tls {
5687 return true;
5688 }
5689 }
5690
5691 if let Some(forwarded) = req.headers().get("Forwarded") {
5693 if let Ok(s) = std::str::from_utf8(forwarded) {
5694 for entry in s.split(',') {
5695 for param in entry.split(';') {
5696 let param = param.trim();
5697 if let Some((k, v)) = param.split_once('=') {
5698 if k.trim().eq_ignore_ascii_case("proto") {
5699 let proto = v.trim().trim_matches('"');
5700 if proto.eq_ignore_ascii_case("https") {
5701 return true;
5702 }
5703 }
5704 }
5705 }
5706 }
5707 }
5708 }
5709
5710 if let Some(proto) = req.headers().get("X-Forwarded-Proto") {
5712 let first = proto.split(|&b| b == b',').next().unwrap_or(proto);
5713 return trim_ascii(first).eq_ignore_ascii_case(b"https");
5714 }
5715
5716 if let Some(ssl) = req.headers().get("X-Forwarded-Ssl") {
5718 return ssl.eq_ignore_ascii_case(b"on");
5719 }
5720
5721 if let Some(https) = req.headers().get("Front-End-Https") {
5723 return https.eq_ignore_ascii_case(b"on");
5724 }
5725
5726 false
5727 }
5728
5729 fn is_excluded(&self, path: &str) -> bool {
5731 self.config
5732 .exclude_paths
5733 .iter()
5734 .any(|p| path.starts_with(p))
5735 }
5736
5737 fn build_hsts_header(&self) -> Option<Vec<u8>> {
5739 if self.config.hsts_max_age_secs == 0 {
5740 return None;
5741 }
5742
5743 let mut value = format!("max-age={}", self.config.hsts_max_age_secs);
5744
5745 if self.config.hsts_include_subdomains {
5746 value.push_str("; includeSubDomains");
5747 }
5748
5749 if self.config.hsts_preload {
5750 value.push_str("; preload");
5751 }
5752
5753 Some(value.into_bytes())
5754 }
5755
5756 fn build_redirect_url(&self, req: &Request) -> String {
5758 let host = req
5759 .headers()
5760 .get("Host")
5761 .map(|h| String::from_utf8_lossy(h).to_string())
5762 .unwrap_or_else(|| "localhost".to_string());
5763
5764 let host_without_port = host.split(':').next().unwrap_or(&host);
5766
5767 let path = req.path();
5768 let query = req.query();
5769
5770 if self.config.https_port == 443 {
5771 match query {
5772 Some(q) => format!("https://{}{}?{}", host_without_port, path, q),
5773 None => format!("https://{}{}", host_without_port, path),
5774 }
5775 } else {
5776 match query {
5777 Some(q) => format!(
5778 "https://{}:{}{}?{}",
5779 host_without_port, self.config.https_port, path, q
5780 ),
5781 None => format!(
5782 "https://{}:{}{}",
5783 host_without_port, self.config.https_port, path
5784 ),
5785 }
5786 }
5787 }
5788}
5789
5790impl Middleware for HttpsRedirectMiddleware {
5791 fn before<'a>(
5792 &'a self,
5793 _ctx: &'a RequestContext,
5794 req: &'a mut Request,
5795 ) -> BoxFuture<'a, ControlFlow> {
5796 Box::pin(async move {
5797 if !self.config.redirect_enabled {
5799 return ControlFlow::Continue;
5800 }
5801
5802 if self.is_secure(req) {
5804 return ControlFlow::Continue;
5805 }
5806
5807 if self.is_excluded(req.path()) {
5809 return ControlFlow::Continue;
5810 }
5811
5812 let redirect_url = self.build_redirect_url(req);
5814
5815 let status = if self.config.permanent_redirect {
5817 crate::response::StatusCode::MOVED_PERMANENTLY
5818 } else {
5819 crate::response::StatusCode::TEMPORARY_REDIRECT
5820 };
5821
5822 let response = Response::with_status(status)
5824 .header("Location", redirect_url.into_bytes())
5825 .header("Content-Type", b"text/plain".to_vec())
5826 .body(crate::response::ResponseBody::Bytes(
5827 b"Redirecting to HTTPS...".to_vec(),
5828 ));
5829
5830 ControlFlow::Break(response)
5831 })
5832 }
5833
5834 fn after<'a>(
5835 &'a self,
5836 _ctx: &'a RequestContext,
5837 req: &'a Request,
5838 response: Response,
5839 ) -> BoxFuture<'a, Response> {
5840 Box::pin(async move {
5841 if !self.is_secure(req) {
5843 return response;
5844 }
5845
5846 if let Some(hsts_value) = self.build_hsts_header() {
5848 response.header("Strict-Transport-Security", hsts_value)
5849 } else {
5850 response
5851 }
5852 })
5853 }
5854
5855 fn name(&self) -> &'static str {
5856 "HttpsRedirect"
5857 }
5858}
5859
5860pub trait ResponseInterceptor: Send + Sync {
5899 fn intercept<'a>(
5910 &'a self,
5911 ctx: &'a ResponseInterceptorContext<'a>,
5912 response: Response,
5913 ) -> BoxFuture<'a, Response>;
5914
5915 fn name(&self) -> &'static str {
5917 std::any::type_name::<Self>()
5918 }
5919}
5920
5921#[derive(Debug)]
5926pub struct ResponseInterceptorContext<'a> {
5927 pub request: &'a Request,
5929 pub start_time: Instant,
5931 pub request_ctx: &'a RequestContext,
5933}
5934
5935impl<'a> ResponseInterceptorContext<'a> {
5936 pub fn new(request: &'a Request, request_ctx: &'a RequestContext, start_time: Instant) -> Self {
5938 Self {
5939 request,
5940 start_time,
5941 request_ctx,
5942 }
5943 }
5944
5945 pub fn elapsed(&self) -> std::time::Duration {
5947 self.start_time.elapsed()
5948 }
5949
5950 pub fn elapsed_ms(&self) -> u128 {
5952 self.start_time.elapsed().as_millis()
5953 }
5954}
5955
5956#[derive(Default)]
5971pub struct ResponseInterceptorStack {
5972 interceptors: Vec<Arc<dyn ResponseInterceptor>>,
5973}
5974
5975impl ResponseInterceptorStack {
5976 #[must_use]
5978 pub fn new() -> Self {
5979 Self {
5980 interceptors: Vec::new(),
5981 }
5982 }
5983
5984 #[must_use]
5986 pub fn with_capacity(capacity: usize) -> Self {
5987 Self {
5988 interceptors: Vec::with_capacity(capacity),
5989 }
5990 }
5991
5992 pub fn push<I: ResponseInterceptor + 'static>(&mut self, interceptor: I) {
5994 self.interceptors.push(Arc::new(interceptor));
5995 }
5996
5997 pub fn push_arc(&mut self, interceptor: Arc<dyn ResponseInterceptor>) {
5999 self.interceptors.push(interceptor);
6000 }
6001
6002 #[must_use]
6004 pub fn len(&self) -> usize {
6005 self.interceptors.len()
6006 }
6007
6008 #[must_use]
6010 pub fn is_empty(&self) -> bool {
6011 self.interceptors.is_empty()
6012 }
6013
6014 pub async fn process(
6016 &self,
6017 ctx: &ResponseInterceptorContext<'_>,
6018 mut response: Response,
6019 ) -> Response {
6020 for interceptor in &self.interceptors {
6021 let _ = ctx.request_ctx.checkpoint();
6022 response = interceptor.intercept(ctx, response).await;
6023 }
6024 response
6025 }
6026}
6027
6028#[derive(Debug, Clone)]
6045pub struct TimingInterceptor {
6046 header_name: String,
6048 include_server_timing: bool,
6050 server_timing_name: String,
6052}
6053
6054impl Default for TimingInterceptor {
6055 fn default() -> Self {
6056 Self::new()
6057 }
6058}
6059
6060impl TimingInterceptor {
6061 #[must_use]
6063 pub fn new() -> Self {
6064 Self {
6065 header_name: "X-Response-Time".to_string(),
6066 include_server_timing: false,
6067 server_timing_name: "total".to_string(),
6068 }
6069 }
6070
6071 #[must_use]
6073 pub fn with_server_timing(mut self, metric_name: impl Into<String>) -> Self {
6074 self.include_server_timing = true;
6075 self.server_timing_name = metric_name.into();
6076 self
6077 }
6078
6079 #[must_use]
6081 pub fn header_name(mut self, name: impl Into<String>) -> Self {
6082 self.header_name = name.into();
6083 self
6084 }
6085}
6086
6087impl ResponseInterceptor for TimingInterceptor {
6088 fn intercept<'a>(
6089 &'a self,
6090 ctx: &'a ResponseInterceptorContext<'a>,
6091 response: Response,
6092 ) -> BoxFuture<'a, Response> {
6093 Box::pin(async move {
6094 let elapsed_ms = ctx.elapsed_ms();
6095 let timing_value = format!("{}ms", elapsed_ms);
6096
6097 let response = response.header(&self.header_name, timing_value.clone().into_bytes());
6098
6099 if self.include_server_timing {
6100 let server_timing = format!("{};dur={}", self.server_timing_name, elapsed_ms);
6102 response.header("Server-Timing", server_timing.into_bytes())
6103 } else {
6104 response
6105 }
6106 })
6107 }
6108
6109 fn name(&self) -> &'static str {
6110 "TimingInterceptor"
6111 }
6112}
6113
6114#[derive(Debug, Clone)]
6138#[allow(clippy::struct_excessive_bools)]
6139pub struct DebugInfoInterceptor {
6140 include_path: bool,
6142 include_method: bool,
6144 include_request_id: bool,
6146 include_timing: bool,
6148 header_prefix: String,
6150}
6151
6152impl Default for DebugInfoInterceptor {
6153 fn default() -> Self {
6154 Self::new()
6155 }
6156}
6157
6158impl DebugInfoInterceptor {
6159 #[must_use]
6161 pub fn new() -> Self {
6162 Self {
6163 include_path: true,
6164 include_method: true,
6165 include_request_id: true,
6166 include_timing: true,
6167 header_prefix: "X-Debug-".to_string(),
6168 }
6169 }
6170
6171 #[must_use]
6173 pub fn include_path(mut self, include: bool) -> Self {
6174 self.include_path = include;
6175 self
6176 }
6177
6178 #[must_use]
6180 pub fn include_method(mut self, include: bool) -> Self {
6181 self.include_method = include;
6182 self
6183 }
6184
6185 #[must_use]
6187 pub fn include_request_id(mut self, include: bool) -> Self {
6188 self.include_request_id = include;
6189 self
6190 }
6191
6192 #[must_use]
6194 pub fn include_timing(mut self, include: bool) -> Self {
6195 self.include_timing = include;
6196 self
6197 }
6198
6199 #[must_use]
6201 pub fn header_prefix(mut self, prefix: impl Into<String>) -> Self {
6202 self.header_prefix = prefix.into();
6203 self
6204 }
6205}
6206
6207impl ResponseInterceptor for DebugInfoInterceptor {
6208 fn intercept<'a>(
6209 &'a self,
6210 ctx: &'a ResponseInterceptorContext<'a>,
6211 response: Response,
6212 ) -> BoxFuture<'a, Response> {
6213 Box::pin(async move {
6214 let mut resp = response;
6215
6216 if self.include_path {
6217 let header_name = format!("{}Path", self.header_prefix);
6218 resp = resp.header(header_name, ctx.request.path().as_bytes().to_vec());
6219 }
6220
6221 if self.include_method {
6222 let header_name = format!("{}Method", self.header_prefix);
6223 resp = resp.header(
6224 header_name,
6225 ctx.request.method().as_str().as_bytes().to_vec(),
6226 );
6227 }
6228
6229 if self.include_request_id {
6230 if let Some(request_id) = ctx.request.get_extension::<RequestId>() {
6231 let header_name = format!("{}Request-Id", self.header_prefix);
6232 resp = resp.header(header_name, request_id.0.as_bytes().to_vec());
6233 }
6234 }
6235
6236 if self.include_timing {
6237 let header_name = format!("{}Handler-Time", self.header_prefix);
6238 let timing = format!("{}ms", ctx.elapsed_ms());
6239 resp = resp.header(header_name, timing.into_bytes());
6240 }
6241
6242 resp
6243 })
6244 }
6245
6246 fn name(&self) -> &'static str {
6247 "DebugInfoInterceptor"
6248 }
6249}
6250
6251pub struct ResponseBodyTransform<F>
6272where
6273 F: Fn(Vec<u8>) -> Vec<u8> + Send + Sync,
6274{
6275 transform_fn: F,
6276 content_type_filter: Option<String>,
6278}
6279
6280impl<F> ResponseBodyTransform<F>
6281where
6282 F: Fn(Vec<u8>) -> Vec<u8> + Send + Sync,
6283{
6284 pub fn new(transform_fn: F) -> Self {
6286 Self {
6287 transform_fn,
6288 content_type_filter: None,
6289 }
6290 }
6291
6292 #[must_use]
6294 pub fn for_content_type(mut self, content_type: impl Into<String>) -> Self {
6295 self.content_type_filter = Some(content_type.into());
6296 self
6297 }
6298
6299 fn should_transform(&self, response: &Response) -> bool {
6300 match &self.content_type_filter {
6301 Some(filter) => response
6302 .headers()
6303 .iter()
6304 .find(|(name, _)| name.eq_ignore_ascii_case("content-type"))
6305 .and_then(|(_, ct)| std::str::from_utf8(ct).ok())
6306 .map(|ct| ct.starts_with(filter))
6307 .unwrap_or(false),
6308 None => true,
6309 }
6310 }
6311}
6312
6313impl<F> ResponseInterceptor for ResponseBodyTransform<F>
6314where
6315 F: Fn(Vec<u8>) -> Vec<u8> + Send + Sync,
6316{
6317 fn intercept<'a>(
6318 &'a self,
6319 _ctx: &'a ResponseInterceptorContext<'a>,
6320 response: Response,
6321 ) -> BoxFuture<'a, Response> {
6322 Box::pin(async move {
6323 if !self.should_transform(&response) {
6324 return response;
6325 }
6326
6327 let body_bytes = match response.body_ref() {
6329 crate::response::ResponseBody::Empty => Vec::new(),
6330 crate::response::ResponseBody::Bytes(b) => b.clone(),
6331 crate::response::ResponseBody::Stream(_) => {
6332 return response;
6334 }
6335 };
6336
6337 let transformed = (self.transform_fn)(body_bytes);
6339
6340 response.body(crate::response::ResponseBody::Bytes(transformed))
6342 })
6343 }
6344
6345 fn name(&self) -> &'static str {
6346 "ResponseBodyTransform"
6347 }
6348}
6349
6350#[derive(Debug, Clone, Default)]
6367pub struct HeaderTransformInterceptor {
6368 add_headers: Vec<(String, Vec<u8>)>,
6370 remove_headers: Vec<String>,
6372 rename_headers: Vec<(String, String)>,
6374}
6375
6376impl HeaderTransformInterceptor {
6377 #[must_use]
6379 pub fn new() -> Self {
6380 Self::default()
6381 }
6382
6383 #[must_use]
6385 pub fn add(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
6386 self.add_headers.push((name.into(), value.into()));
6387 self
6388 }
6389
6390 #[must_use]
6392 pub fn remove(mut self, name: impl Into<String>) -> Self {
6393 self.remove_headers.push(name.into());
6394 self
6395 }
6396
6397 #[must_use]
6399 pub fn rename(mut self, old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
6400 self.rename_headers.push((old_name.into(), new_name.into()));
6401 self
6402 }
6403}
6404
6405impl ResponseInterceptor for HeaderTransformInterceptor {
6406 fn intercept<'a>(
6407 &'a self,
6408 _ctx: &'a ResponseInterceptorContext<'a>,
6409 response: Response,
6410 ) -> BoxFuture<'a, Response> {
6411 let add_headers = self.add_headers.clone();
6412 let remove_headers = self.remove_headers.clone();
6413 let rename_headers = self.rename_headers.clone();
6414
6415 Box::pin(async move {
6416 let mut resp = response;
6417
6418 for (old_name, new_name) in &rename_headers {
6420 let values: Vec<Vec<u8>> = resp
6421 .headers()
6422 .iter()
6423 .filter(|(name, _)| name.eq_ignore_ascii_case(old_name))
6424 .map(|(_, v)| v.clone())
6425 .collect();
6426
6427 if !values.is_empty() {
6428 resp = resp.remove_header(old_name);
6429 for v in values {
6430 resp = resp.header(new_name, v);
6431 }
6432 }
6433 }
6434
6435 for (name, value) in add_headers {
6437 resp = resp.header(name, value);
6438 }
6439
6440 for name in &remove_headers {
6442 resp = resp.remove_header(name);
6443 }
6444
6445 resp
6446 })
6447 }
6448
6449 fn name(&self) -> &'static str {
6450 "HeaderTransformInterceptor"
6451 }
6452}
6453
6454pub struct ConditionalInterceptor<I, F>
6470where
6471 I: ResponseInterceptor,
6472 F: Fn(&ResponseInterceptorContext, &Response) -> bool + Send + Sync,
6473{
6474 inner: I,
6475 condition: F,
6476}
6477
6478impl<I, F> ConditionalInterceptor<I, F>
6479where
6480 I: ResponseInterceptor,
6481 F: Fn(&ResponseInterceptorContext, &Response) -> bool + Send + Sync,
6482{
6483 pub fn new(inner: I, condition: F) -> Self {
6485 Self { inner, condition }
6486 }
6487}
6488
6489impl<I, F> ResponseInterceptor for ConditionalInterceptor<I, F>
6490where
6491 I: ResponseInterceptor,
6492 F: Fn(&ResponseInterceptorContext, &Response) -> bool + Send + Sync,
6493{
6494 fn intercept<'a>(
6495 &'a self,
6496 ctx: &'a ResponseInterceptorContext<'a>,
6497 response: Response,
6498 ) -> BoxFuture<'a, Response> {
6499 Box::pin(async move {
6500 if (self.condition)(ctx, &response) {
6501 self.inner.intercept(ctx, response).await
6502 } else {
6503 response
6504 }
6505 })
6506 }
6507
6508 fn name(&self) -> &'static str {
6509 "ConditionalInterceptor"
6510 }
6511}
6512
6513#[derive(Debug, Clone)]
6532pub struct ErrorResponseTransformer {
6533 status_codes: HashSet<u16>,
6535 replacement_body: Option<Vec<u8>>,
6537 add_error_id: bool,
6539}
6540
6541impl Default for ErrorResponseTransformer {
6542 fn default() -> Self {
6543 Self::new()
6544 }
6545}
6546
6547impl ErrorResponseTransformer {
6548 #[must_use]
6550 pub fn new() -> Self {
6551 Self {
6552 status_codes: HashSet::new(),
6553 replacement_body: None,
6554 add_error_id: false,
6555 }
6556 }
6557
6558 #[must_use]
6560 pub fn hide_details_for_status(mut self, status: crate::response::StatusCode) -> Self {
6561 self.status_codes.insert(status.as_u16());
6562 self
6563 }
6564
6565 #[must_use]
6567 pub fn with_replacement_body(mut self, body: impl Into<Vec<u8>>) -> Self {
6568 self.replacement_body = Some(body.into());
6569 self
6570 }
6571
6572 #[must_use]
6574 pub fn add_error_id(mut self, enable: bool) -> Self {
6575 self.add_error_id = enable;
6576 self
6577 }
6578}
6579
6580impl ResponseInterceptor for ErrorResponseTransformer {
6581 fn intercept<'a>(
6582 &'a self,
6583 ctx: &'a ResponseInterceptorContext<'a>,
6584 response: Response,
6585 ) -> BoxFuture<'a, Response> {
6586 Box::pin(async move {
6587 let status_code = response.status().as_u16();
6588
6589 if !self.status_codes.contains(&status_code) {
6590 return response;
6591 }
6592
6593 let mut resp = response;
6594
6595 if let Some(ref replacement) = self.replacement_body {
6597 resp = resp.body(crate::response::ResponseBody::Bytes(replacement.clone()));
6598 }
6599
6600 if self.add_error_id {
6602 let error_id = ctx
6604 .request
6605 .get_extension::<RequestId>()
6606 .map(|r| r.0.clone())
6607 .unwrap_or_else(|| format!("err-{}", ctx.elapsed_ms()));
6608 resp = resp.header("X-Error-Id", error_id.into_bytes());
6609 }
6610
6611 resp
6612 })
6613 }
6614
6615 fn name(&self) -> &'static str {
6616 "ErrorResponseTransformer"
6617 }
6618}
6619
6620pub struct ResponseInterceptorMiddleware<I>
6636where
6637 I: ResponseInterceptor,
6638{
6639 interceptor: I,
6640}
6641
6642impl<I> ResponseInterceptorMiddleware<I>
6643where
6644 I: ResponseInterceptor,
6645{
6646 pub fn new(interceptor: I) -> Self {
6648 Self { interceptor }
6649 }
6650}
6651
6652impl<I> Middleware for ResponseInterceptorMiddleware<I>
6653where
6654 I: ResponseInterceptor,
6655{
6656 fn before<'a>(
6657 &'a self,
6658 _ctx: &'a RequestContext,
6659 req: &'a mut Request,
6660 ) -> BoxFuture<'a, ControlFlow> {
6661 req.insert_extension(InterceptorStartTime(Instant::now()));
6663 Box::pin(async { ControlFlow::Continue })
6664 }
6665
6666 fn after<'a>(
6667 &'a self,
6668 ctx: &'a RequestContext,
6669 req: &'a Request,
6670 response: Response,
6671 ) -> BoxFuture<'a, Response> {
6672 Box::pin(async move {
6673 let start_time = req
6675 .get_extension::<InterceptorStartTime>()
6676 .map(|t| t.0)
6677 .unwrap_or_else(Instant::now);
6678
6679 let interceptor_ctx = ResponseInterceptorContext::new(req, ctx, start_time);
6680 self.interceptor.intercept(&interceptor_ctx, response).await
6681 })
6682 }
6683
6684 fn name(&self) -> &'static str {
6685 self.interceptor.name()
6686 }
6687}
6688
6689#[derive(Debug, Clone, Copy)]
6691struct InterceptorStartTime(Instant);
6692
6693#[derive(Debug, Clone)]
6725pub struct ServerTimingEntry {
6726 name: String,
6728 duration_ms: f64,
6730 description: Option<String>,
6732}
6733
6734impl ServerTimingEntry {
6735 #[must_use]
6737 pub fn new(name: impl Into<String>, duration_ms: f64) -> Self {
6738 Self {
6739 name: name.into(),
6740 duration_ms,
6741 description: None,
6742 }
6743 }
6744
6745 #[must_use]
6747 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
6748 self.description = Some(desc.into());
6749 self
6750 }
6751
6752 #[must_use]
6754 pub fn to_header_value(&self) -> String {
6755 match &self.description {
6756 Some(desc) => format!(
6757 "{};dur={:.3};desc=\"{}\"",
6758 self.name, self.duration_ms, desc
6759 ),
6760 None => format!("{};dur={:.3}", self.name, self.duration_ms),
6761 }
6762 }
6763}
6764
6765#[derive(Debug, Clone, Default)]
6781pub struct ServerTimingBuilder {
6782 entries: Vec<ServerTimingEntry>,
6783}
6784
6785impl ServerTimingBuilder {
6786 #[must_use]
6788 pub fn new() -> Self {
6789 Self::default()
6790 }
6791
6792 #[must_use]
6794 pub fn add(mut self, name: impl Into<String>, duration_ms: f64) -> Self {
6795 self.entries.push(ServerTimingEntry::new(name, duration_ms));
6796 self
6797 }
6798
6799 #[must_use]
6801 pub fn add_with_desc(
6802 mut self,
6803 name: impl Into<String>,
6804 duration_ms: f64,
6805 description: impl Into<String>,
6806 ) -> Self {
6807 self.entries
6808 .push(ServerTimingEntry::new(name, duration_ms).with_description(description));
6809 self
6810 }
6811
6812 #[must_use]
6814 pub fn add_entry(mut self, entry: ServerTimingEntry) -> Self {
6815 self.entries.push(entry);
6816 self
6817 }
6818
6819 #[must_use]
6821 pub fn build(&self) -> String {
6822 self.entries
6823 .iter()
6824 .map(ServerTimingEntry::to_header_value)
6825 .collect::<Vec<_>>()
6826 .join(", ")
6827 }
6828
6829 #[must_use]
6831 pub fn is_empty(&self) -> bool {
6832 self.entries.is_empty()
6833 }
6834
6835 #[must_use]
6837 pub fn len(&self) -> usize {
6838 self.entries.len()
6839 }
6840}
6841
6842#[derive(Debug, Clone)]
6858pub struct TimingMetrics {
6859 pub start_time: Instant,
6861 pub first_byte_time: Option<Instant>,
6863 pub custom_metrics: Vec<(String, f64, Option<String>)>,
6865}
6866
6867impl TimingMetrics {
6868 #[must_use]
6870 pub fn new() -> Self {
6871 Self {
6872 start_time: Instant::now(),
6873 first_byte_time: None,
6874 custom_metrics: Vec::new(),
6875 }
6876 }
6877
6878 #[must_use]
6880 pub fn with_start_time(start_time: Instant) -> Self {
6881 Self {
6882 start_time,
6883 first_byte_time: None,
6884 custom_metrics: Vec::new(),
6885 }
6886 }
6887
6888 pub fn mark_first_byte(&mut self) {
6890 self.first_byte_time = Some(Instant::now());
6891 }
6892
6893 pub fn add_metric(&mut self, name: impl Into<String>, duration_ms: f64) {
6895 self.custom_metrics.push((name.into(), duration_ms, None));
6896 }
6897
6898 pub fn add_metric_with_desc(
6900 &mut self,
6901 name: impl Into<String>,
6902 duration_ms: f64,
6903 desc: impl Into<String>,
6904 ) {
6905 self.custom_metrics
6906 .push((name.into(), duration_ms, Some(desc.into())));
6907 }
6908
6909 #[must_use]
6911 pub fn total_ms(&self) -> f64 {
6912 self.start_time.elapsed().as_secs_f64() * 1000.0
6913 }
6914
6915 #[must_use]
6917 pub fn ttfb_ms(&self) -> Option<f64> {
6918 self.first_byte_time
6919 .map(|t| t.duration_since(self.start_time).as_secs_f64() * 1000.0)
6920 }
6921
6922 #[must_use]
6924 pub fn to_server_timing(&self) -> ServerTimingBuilder {
6925 let mut builder = ServerTimingBuilder::new().add_with_desc(
6926 "total",
6927 self.total_ms(),
6928 "Total request time",
6929 );
6930
6931 if let Some(ttfb) = self.ttfb_ms() {
6932 builder = builder.add_with_desc("ttfb", ttfb, "Time to first byte");
6933 }
6934
6935 for (name, duration, desc) in &self.custom_metrics {
6936 match desc {
6937 Some(d) => builder = builder.add_with_desc(name, *duration, d),
6938 None => builder = builder.add(name, *duration),
6939 }
6940 }
6941
6942 builder
6943 }
6944}
6945
6946impl Default for TimingMetrics {
6947 fn default() -> Self {
6948 Self::new()
6949 }
6950}
6951
6952#[derive(Debug, Clone)]
6954#[allow(clippy::struct_excessive_bools)]
6955pub struct TimingMetricsConfig {
6956 pub add_server_timing_header: bool,
6958 pub add_response_time_header: bool,
6960 pub response_time_header_name: String,
6962 pub include_custom_metrics: bool,
6964 pub include_ttfb: bool,
6966}
6967
6968impl Default for TimingMetricsConfig {
6969 fn default() -> Self {
6970 Self {
6971 add_server_timing_header: true,
6972 add_response_time_header: true,
6973 response_time_header_name: "X-Response-Time".to_string(),
6974 include_custom_metrics: true,
6975 include_ttfb: true,
6976 }
6977 }
6978}
6979
6980impl TimingMetricsConfig {
6981 #[must_use]
6983 pub fn new() -> Self {
6984 Self::default()
6985 }
6986
6987 #[must_use]
6989 pub fn server_timing(mut self, enabled: bool) -> Self {
6990 self.add_server_timing_header = enabled;
6991 self
6992 }
6993
6994 #[must_use]
6996 pub fn response_time(mut self, enabled: bool) -> Self {
6997 self.add_response_time_header = enabled;
6998 self
6999 }
7000
7001 #[must_use]
7003 pub fn response_time_header(mut self, name: impl Into<String>) -> Self {
7004 self.response_time_header_name = name.into();
7005 self
7006 }
7007
7008 #[must_use]
7010 pub fn custom_metrics(mut self, enabled: bool) -> Self {
7011 self.include_custom_metrics = enabled;
7012 self
7013 }
7014
7015 #[must_use]
7017 pub fn ttfb(mut self, enabled: bool) -> Self {
7018 self.include_ttfb = enabled;
7019 self
7020 }
7021
7022 #[must_use]
7024 pub fn production() -> Self {
7025 Self {
7026 add_server_timing_header: false,
7027 add_response_time_header: true,
7028 response_time_header_name: "X-Response-Time".to_string(),
7029 include_custom_metrics: false,
7030 include_ttfb: false,
7031 }
7032 }
7033
7034 #[must_use]
7036 pub fn development() -> Self {
7037 Self::default()
7038 }
7039}
7040
7041#[derive(Debug, Clone)]
7060pub struct TimingMetricsMiddleware {
7061 config: TimingMetricsConfig,
7062}
7063
7064impl TimingMetricsMiddleware {
7065 #[must_use]
7067 pub fn new() -> Self {
7068 Self {
7069 config: TimingMetricsConfig::default(),
7070 }
7071 }
7072
7073 #[must_use]
7075 pub fn with_config(config: TimingMetricsConfig) -> Self {
7076 Self { config }
7077 }
7078
7079 #[must_use]
7081 pub fn production() -> Self {
7082 Self {
7083 config: TimingMetricsConfig::production(),
7084 }
7085 }
7086
7087 #[must_use]
7089 pub fn development() -> Self {
7090 Self {
7091 config: TimingMetricsConfig::development(),
7092 }
7093 }
7094}
7095
7096impl Default for TimingMetricsMiddleware {
7097 fn default() -> Self {
7098 Self::new()
7099 }
7100}
7101
7102impl Middleware for TimingMetricsMiddleware {
7103 fn before<'a>(
7104 &'a self,
7105 _ctx: &'a RequestContext,
7106 req: &'a mut Request,
7107 ) -> BoxFuture<'a, ControlFlow> {
7108 req.insert_extension(TimingMetrics::new());
7110 Box::pin(async { ControlFlow::Continue })
7111 }
7112
7113 fn after<'a>(
7114 &'a self,
7115 _ctx: &'a RequestContext,
7116 req: &'a Request,
7117 response: Response,
7118 ) -> BoxFuture<'a, Response> {
7119 let config = self.config.clone();
7120
7121 Box::pin(async move {
7122 let mut resp = response;
7123
7124 let metrics = req.get_extension::<TimingMetrics>();
7126
7127 match metrics {
7128 Some(metrics) => {
7129 if config.add_response_time_header {
7131 let timing = format!("{:.3}ms", metrics.total_ms());
7132 resp = resp.header(&config.response_time_header_name, timing.into_bytes());
7133 }
7134
7135 if config.add_server_timing_header {
7137 let mut builder = ServerTimingBuilder::new().add_with_desc(
7138 "total",
7139 metrics.total_ms(),
7140 "Total request time",
7141 );
7142
7143 if config.include_ttfb {
7145 if let Some(ttfb) = metrics.ttfb_ms() {
7146 builder = builder.add_with_desc("ttfb", ttfb, "Time to first byte");
7147 }
7148 }
7149
7150 if config.include_custom_metrics {
7152 for (name, duration, desc) in &metrics.custom_metrics {
7153 match desc {
7154 Some(d) => builder = builder.add_with_desc(name, *duration, d),
7155 None => builder = builder.add(name, *duration),
7156 }
7157 }
7158 }
7159
7160 let header_value = builder.build();
7161 resp = resp.header("Server-Timing", header_value.into_bytes());
7162 }
7163 }
7164 None => {
7165 if config.add_response_time_header {
7168 resp = resp.header(&config.response_time_header_name, b"0.000ms".to_vec());
7169 }
7170 }
7171 }
7172
7173 resp
7174 })
7175 }
7176
7177 fn name(&self) -> &'static str {
7178 "TimingMetrics"
7179 }
7180}
7181
7182#[derive(Debug, Clone)]
7186pub struct TimingHistogramBucket {
7187 pub le: f64,
7189 pub count: u64,
7191}
7192
7193#[derive(Debug, Clone)]
7212pub struct TimingHistogram {
7213 bucket_bounds: Vec<f64>,
7215 bucket_counts: Vec<u64>,
7217 sum: f64,
7219 count: u64,
7221}
7222
7223impl TimingHistogram {
7224 #[must_use]
7228 pub fn with_buckets(bucket_bounds: Vec<f64>) -> Self {
7229 let bucket_counts = vec![0; bucket_bounds.len()];
7230 Self {
7231 bucket_bounds,
7232 bucket_counts,
7233 sum: 0.0,
7234 count: 0,
7235 }
7236 }
7237
7238 #[must_use]
7242 pub fn http_latency() -> Self {
7243 Self::with_buckets(vec![
7244 1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0, 10000.0,
7245 ])
7246 }
7247
7248 pub fn observe(&mut self, value_ms: f64) {
7250 self.sum += value_ms;
7251 self.count += 1;
7252
7253 for (i, bound) in self.bucket_bounds.iter().enumerate() {
7255 if value_ms <= *bound {
7256 self.bucket_counts[i] += 1;
7257 }
7258 }
7259 }
7260
7261 #[must_use]
7263 pub fn count(&self) -> u64 {
7264 self.count
7265 }
7266
7267 #[must_use]
7269 pub fn sum(&self) -> f64 {
7270 self.sum
7271 }
7272
7273 #[must_use]
7275 pub fn mean(&self) -> f64 {
7276 if self.count == 0 {
7277 0.0
7278 } else {
7279 #[allow(clippy::cast_precision_loss)]
7280 {
7281 self.sum / self.count as f64
7282 }
7283 }
7284 }
7285
7286 #[must_use]
7288 pub fn buckets(&self) -> Vec<TimingHistogramBucket> {
7289 self.bucket_bounds
7290 .iter()
7291 .zip(&self.bucket_counts)
7292 .map(|(&le, &count)| TimingHistogramBucket { le, count })
7293 .collect()
7294 }
7295
7296 pub fn reset(&mut self) {
7298 self.sum = 0.0;
7299 self.count = 0;
7300 for count in &mut self.bucket_counts {
7301 *count = 0;
7302 }
7303 }
7304}
7305
7306impl Default for TimingHistogram {
7307 fn default() -> Self {
7308 Self::http_latency()
7309 }
7310}
7311
7312#[cfg(test)]
7317mod timing_metrics_tests {
7318 use super::*;
7319 use crate::request::Method;
7320 use crate::response::StatusCode;
7321
7322 fn test_context() -> RequestContext {
7323 RequestContext::new(asupersync::Cx::for_testing(), 1)
7324 }
7325
7326 fn test_request() -> Request {
7327 Request::new(Method::Get, "/test")
7328 }
7329
7330 fn run_middleware_before(mw: &impl Middleware, req: &mut Request) -> ControlFlow {
7331 let ctx = test_context();
7332 futures_executor::block_on(mw.before(&ctx, req))
7333 }
7334
7335 fn run_middleware_after(mw: &impl Middleware, req: &Request, resp: Response) -> Response {
7336 let ctx = test_context();
7337 futures_executor::block_on(mw.after(&ctx, req, resp))
7338 }
7339
7340 #[test]
7341 fn server_timing_entry_basic() {
7342 let entry = ServerTimingEntry::new("db", 42.5);
7343 assert_eq!(entry.to_header_value(), "db;dur=42.500");
7344 }
7345
7346 #[test]
7347 fn server_timing_entry_with_description() {
7348 let entry = ServerTimingEntry::new("db", 42.5).with_description("Database query");
7349 assert_eq!(
7350 entry.to_header_value(),
7351 "db;dur=42.500;desc=\"Database query\""
7352 );
7353 }
7354
7355 #[test]
7356 fn server_timing_builder_single_entry() {
7357 let timing = ServerTimingBuilder::new().add("total", 150.0).build();
7358 assert_eq!(timing, "total;dur=150.000");
7359 }
7360
7361 #[test]
7362 fn server_timing_builder_multiple_entries() {
7363 let timing = ServerTimingBuilder::new()
7364 .add("total", 150.0)
7365 .add_with_desc("db", 42.0, "Database")
7366 .add("cache", 5.0)
7367 .build();
7368
7369 assert!(timing.contains("total;dur=150.000"));
7370 assert!(timing.contains("db;dur=42.000;desc=\"Database\""));
7371 assert!(timing.contains("cache;dur=5.000"));
7372 assert!(timing.contains(", ")); }
7374
7375 #[test]
7376 fn server_timing_builder_empty() {
7377 let builder = ServerTimingBuilder::new();
7378 assert!(builder.is_empty());
7379 assert_eq!(builder.len(), 0);
7380 assert_eq!(builder.build(), "");
7381 }
7382
7383 #[test]
7384 fn timing_metrics_basic() {
7385 let metrics = TimingMetrics::new();
7386 std::thread::sleep(std::time::Duration::from_millis(5));
7387
7388 let total = metrics.total_ms();
7389 assert!(total >= 5.0, "Total should be at least 5ms");
7390 assert!(metrics.ttfb_ms().is_none(), "TTFB should not be set");
7391 }
7392
7393 #[test]
7394 fn timing_metrics_custom_metrics() {
7395 let mut metrics = TimingMetrics::new();
7396 metrics.add_metric("db", 42.5);
7397 metrics.add_metric_with_desc("cache", 5.0, "Cache lookup");
7398
7399 let timing = metrics.to_server_timing();
7400 assert_eq!(timing.len(), 3); let header = timing.build();
7403 assert!(header.contains("total"));
7404 assert!(header.contains("db;dur=42.500"));
7405 assert!(header.contains("cache;dur=5.000;desc=\"Cache lookup\""));
7406 }
7407
7408 #[test]
7409 fn timing_metrics_ttfb() {
7410 let mut metrics = TimingMetrics::new();
7411 std::thread::sleep(std::time::Duration::from_millis(5));
7412 metrics.mark_first_byte();
7413
7414 let ttfb = metrics.ttfb_ms().unwrap();
7415 assert!(ttfb >= 5.0, "TTFB should be at least 5ms");
7416 }
7417
7418 #[test]
7419 fn timing_metrics_config_default() {
7420 let config = TimingMetricsConfig::default();
7421 assert!(config.add_server_timing_header);
7422 assert!(config.add_response_time_header);
7423 assert!(config.include_custom_metrics);
7424 assert!(config.include_ttfb);
7425 }
7426
7427 #[test]
7428 fn timing_metrics_config_production() {
7429 let config = TimingMetricsConfig::production();
7430 assert!(!config.add_server_timing_header);
7431 assert!(config.add_response_time_header);
7432 assert!(!config.include_custom_metrics);
7433 }
7434
7435 #[test]
7436 fn timing_middleware_adds_metrics_to_request() {
7437 let mw = TimingMetricsMiddleware::new();
7438 let mut req = test_request();
7439
7440 let result = run_middleware_before(&mw, &mut req);
7442 assert!(result.is_continue());
7443
7444 let metrics = req.get_extension::<TimingMetrics>();
7445 assert!(metrics.is_some(), "TimingMetrics should be in extensions");
7446 }
7447
7448 #[test]
7449 fn timing_middleware_adds_response_time_header() {
7450 let mw = TimingMetricsMiddleware::new();
7451 let mut req = test_request();
7452
7453 run_middleware_before(&mw, &mut req);
7455
7456 let resp = Response::with_status(StatusCode::OK);
7457 let result = run_middleware_after(&mw, &req, resp);
7458
7459 let has_timing = result
7460 .headers()
7461 .iter()
7462 .any(|(name, _)| name == "X-Response-Time");
7463 assert!(has_timing, "Should have X-Response-Time header");
7464 }
7465
7466 #[test]
7467 fn timing_middleware_adds_server_timing_header() {
7468 let mw = TimingMetricsMiddleware::new();
7469 let mut req = test_request();
7470
7471 run_middleware_before(&mw, &mut req);
7472
7473 let resp = Response::with_status(StatusCode::OK);
7474 let result = run_middleware_after(&mw, &req, resp);
7475
7476 let server_timing = result
7477 .headers()
7478 .iter()
7479 .find(|(name, _)| name == "Server-Timing")
7480 .map(|(_, v)| String::from_utf8_lossy(v).to_string());
7481
7482 assert!(server_timing.is_some(), "Should have Server-Timing header");
7483 let header = server_timing.unwrap();
7484 assert!(header.contains("total"), "Should have total timing");
7485 }
7486
7487 #[test]
7488 fn timing_middleware_production_mode() {
7489 let mw = TimingMetricsMiddleware::production();
7490 let mut req = test_request();
7491
7492 run_middleware_before(&mw, &mut req);
7493
7494 let resp = Response::with_status(StatusCode::OK);
7495 let result = run_middleware_after(&mw, &req, resp);
7496
7497 let has_response_time = result
7499 .headers()
7500 .iter()
7501 .any(|(name, _)| name == "X-Response-Time");
7502 assert!(has_response_time);
7503
7504 let has_server_timing = result
7506 .headers()
7507 .iter()
7508 .any(|(name, _)| name == "Server-Timing");
7509 assert!(!has_server_timing);
7510 }
7511
7512 #[test]
7513 #[allow(clippy::float_cmp)]
7514 fn timing_histogram_basic() {
7515 let mut histogram = TimingHistogram::http_latency();
7516 assert_eq!(histogram.count(), 0);
7517 assert_eq!(histogram.sum(), 0.0);
7518
7519 histogram.observe(42.0);
7520 histogram.observe(150.0);
7521 histogram.observe(5.0);
7522
7523 assert_eq!(histogram.count(), 3);
7524 assert_eq!(histogram.sum(), 197.0);
7525 assert!((histogram.mean() - 65.666).abs() < 0.01);
7526 }
7527
7528 #[test]
7529 fn timing_histogram_buckets() {
7530 let mut histogram = TimingHistogram::with_buckets(vec![10.0, 50.0, 100.0]);
7531
7532 histogram.observe(5.0); histogram.observe(25.0); histogram.observe(75.0); histogram.observe(150.0); let buckets = histogram.buckets();
7538 assert_eq!(buckets.len(), 3);
7539
7540 assert_eq!(buckets[0].count, 1); assert_eq!(buckets[1].count, 2); assert_eq!(buckets[2].count, 3); }
7545
7546 #[test]
7547 #[allow(clippy::float_cmp)]
7548 fn timing_histogram_reset() {
7549 let mut histogram = TimingHistogram::http_latency();
7550 histogram.observe(100.0);
7551 histogram.observe(200.0);
7552
7553 assert_eq!(histogram.count(), 2);
7554
7555 histogram.reset();
7556
7557 assert_eq!(histogram.count(), 0);
7558 assert_eq!(histogram.sum(), 0.0);
7559 }
7560}
7561
7562#[cfg(test)]
7563mod response_interceptor_tests {
7564 use super::*;
7565 use crate::request::Method;
7566 use crate::response::StatusCode;
7567
7568 fn test_context() -> RequestContext {
7569 RequestContext::new(asupersync::Cx::for_testing(), 1)
7570 }
7571
7572 fn test_request() -> Request {
7573 Request::new(Method::Get, "/test")
7574 }
7575
7576 fn run_interceptor<I: ResponseInterceptor>(
7577 interceptor: &I,
7578 req: &Request,
7579 resp: Response,
7580 ) -> Response {
7581 let ctx = test_context();
7582 let start_time = Instant::now();
7583 let interceptor_ctx = ResponseInterceptorContext::new(req, &ctx, start_time);
7584 futures_executor::block_on(interceptor.intercept(&interceptor_ctx, resp))
7585 }
7586
7587 #[test]
7588 fn timing_interceptor_adds_header() {
7589 let interceptor = TimingInterceptor::new();
7590 let req = test_request();
7591 let resp = Response::with_status(StatusCode::OK);
7592
7593 let result = run_interceptor(&interceptor, &req, resp);
7594
7595 let has_timing = result
7596 .headers()
7597 .iter()
7598 .any(|(name, _)| name == "X-Response-Time");
7599 assert!(has_timing, "Should have X-Response-Time header");
7600 }
7601
7602 #[test]
7603 fn timing_interceptor_with_server_timing() {
7604 let interceptor = TimingInterceptor::new().with_server_timing("app");
7605 let req = test_request();
7606 let resp = Response::with_status(StatusCode::OK);
7607
7608 let result = run_interceptor(&interceptor, &req, resp);
7609
7610 let has_server_timing = result
7611 .headers()
7612 .iter()
7613 .any(|(name, _)| name == "Server-Timing");
7614 assert!(has_server_timing, "Should have Server-Timing header");
7615 }
7616
7617 #[test]
7618 fn timing_interceptor_custom_header_name() {
7619 let interceptor = TimingInterceptor::new().header_name("X-Custom-Time");
7620 let req = test_request();
7621 let resp = Response::with_status(StatusCode::OK);
7622
7623 let result = run_interceptor(&interceptor, &req, resp);
7624
7625 let has_custom = result
7626 .headers()
7627 .iter()
7628 .any(|(name, _)| name == "X-Custom-Time");
7629 assert!(has_custom, "Should have X-Custom-Time header");
7630 }
7631
7632 #[test]
7633 fn debug_info_interceptor_adds_headers() {
7634 let interceptor = DebugInfoInterceptor::new();
7635 let req = test_request();
7636 let resp = Response::with_status(StatusCode::OK);
7637
7638 let result = run_interceptor(&interceptor, &req, resp);
7639
7640 let has_path = result
7641 .headers()
7642 .iter()
7643 .any(|(name, _)| name == "X-Debug-Path");
7644 let has_method = result
7645 .headers()
7646 .iter()
7647 .any(|(name, _)| name == "X-Debug-Method");
7648 let has_timing = result
7649 .headers()
7650 .iter()
7651 .any(|(name, _)| name == "X-Debug-Handler-Time");
7652
7653 assert!(has_path, "Should have X-Debug-Path header");
7654 assert!(has_method, "Should have X-Debug-Method header");
7655 assert!(has_timing, "Should have X-Debug-Handler-Time header");
7656 }
7657
7658 #[test]
7659 fn debug_info_interceptor_custom_prefix() {
7660 let interceptor = DebugInfoInterceptor::new().header_prefix("X-Trace-");
7661 let req = test_request();
7662 let resp = Response::with_status(StatusCode::OK);
7663
7664 let result = run_interceptor(&interceptor, &req, resp);
7665
7666 let has_trace_path = result
7667 .headers()
7668 .iter()
7669 .any(|(name, _)| name == "X-Trace-Path");
7670 assert!(has_trace_path, "Should have X-Trace-Path header");
7671 }
7672
7673 #[test]
7674 fn debug_info_interceptor_selective_options() {
7675 let interceptor = DebugInfoInterceptor::new()
7676 .include_path(true)
7677 .include_method(false)
7678 .include_timing(false)
7679 .include_request_id(false);
7680 let req = test_request();
7681 let resp = Response::with_status(StatusCode::OK);
7682
7683 let result = run_interceptor(&interceptor, &req, resp);
7684
7685 let has_path = result
7686 .headers()
7687 .iter()
7688 .any(|(name, _)| name == "X-Debug-Path");
7689 let has_method = result
7690 .headers()
7691 .iter()
7692 .any(|(name, _)| name == "X-Debug-Method");
7693
7694 assert!(has_path, "Should have X-Debug-Path header");
7695 assert!(!has_method, "Should NOT have X-Debug-Method header");
7696 }
7697
7698 #[test]
7699 fn header_transform_adds_headers() {
7700 let interceptor = HeaderTransformInterceptor::new()
7701 .add("X-Powered-By", b"fastapi_rust".to_vec())
7702 .add("X-Version", b"1.0".to_vec());
7703 let req = test_request();
7704 let resp = Response::with_status(StatusCode::OK);
7705
7706 let result = run_interceptor(&interceptor, &req, resp);
7707
7708 let has_powered_by = result
7709 .headers()
7710 .iter()
7711 .any(|(name, _)| name == "X-Powered-By");
7712 let has_version = result.headers().iter().any(|(name, _)| name == "X-Version");
7713
7714 assert!(has_powered_by, "Should have X-Powered-By header");
7715 assert!(has_version, "Should have X-Version header");
7716 }
7717
7718 #[test]
7719 fn response_body_transform_modifies_body() {
7720 let transformer = ResponseBodyTransform::new(|body| {
7721 let mut result = b"[".to_vec();
7722 result.extend_from_slice(&body);
7723 result.extend_from_slice(b"]");
7724 result
7725 });
7726 let req = test_request();
7727 let resp = Response::with_status(StatusCode::OK)
7728 .body(crate::response::ResponseBody::Bytes(b"hello".to_vec()));
7729
7730 let result = run_interceptor(&transformer, &req, resp);
7731
7732 match result.body_ref() {
7733 crate::response::ResponseBody::Bytes(b) => {
7734 assert_eq!(b, b"[hello]");
7735 }
7736 _ => panic!("Expected bytes body"),
7737 }
7738 }
7739
7740 #[test]
7741 fn response_body_transform_with_content_type_filter() {
7742 let transformer =
7743 ResponseBodyTransform::new(|_| b"transformed".to_vec()).for_content_type("text/plain");
7744 let req = test_request();
7745
7746 let json_resp = Response::with_status(StatusCode::OK)
7748 .header("content-type", b"application/json".to_vec())
7749 .body(crate::response::ResponseBody::Bytes(b"original".to_vec()));
7750
7751 let result = run_interceptor(&transformer, &req, json_resp);
7752
7753 match result.body_ref() {
7754 crate::response::ResponseBody::Bytes(b) => {
7755 assert_eq!(b, b"original", "JSON should not be transformed");
7756 }
7757 _ => panic!("Expected bytes body"),
7758 }
7759
7760 let text_resp = Response::with_status(StatusCode::OK)
7762 .header("content-type", b"text/plain".to_vec())
7763 .body(crate::response::ResponseBody::Bytes(b"original".to_vec()));
7764
7765 let result = run_interceptor(&transformer, &req, text_resp);
7766
7767 match result.body_ref() {
7768 crate::response::ResponseBody::Bytes(b) => {
7769 assert_eq!(b, b"transformed", "Text should be transformed");
7770 }
7771 _ => panic!("Expected bytes body"),
7772 }
7773 }
7774
7775 #[test]
7776 fn error_response_transformer_hides_details() {
7777 let transformer = ErrorResponseTransformer::new()
7778 .hide_details_for_status(StatusCode::INTERNAL_SERVER_ERROR)
7779 .with_replacement_body(b"An error occurred");
7780
7781 let req = test_request();
7782
7783 let error_resp = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR).body(
7785 crate::response::ResponseBody::Bytes(b"Sensitive error details".to_vec()),
7786 );
7787
7788 let result = run_interceptor(&transformer, &req, error_resp);
7789
7790 match result.body_ref() {
7791 crate::response::ResponseBody::Bytes(b) => {
7792 assert_eq!(b, b"An error occurred");
7793 }
7794 _ => panic!("Expected bytes body"),
7795 }
7796
7797 let ok_resp = Response::with_status(StatusCode::OK)
7799 .body(crate::response::ResponseBody::Bytes(b"Success".to_vec()));
7800
7801 let result = run_interceptor(&transformer, &req, ok_resp);
7802
7803 match result.body_ref() {
7804 crate::response::ResponseBody::Bytes(b) => {
7805 assert_eq!(b, b"Success");
7806 }
7807 _ => panic!("Expected bytes body"),
7808 }
7809 }
7810
7811 #[test]
7812 fn response_interceptor_stack_chains_interceptors() {
7813 let mut stack = ResponseInterceptorStack::new();
7814 stack.push(TimingInterceptor::new());
7815 stack.push(HeaderTransformInterceptor::new().add("X-Extra", b"value".to_vec()));
7816
7817 let req = test_request();
7818 let resp = Response::with_status(StatusCode::OK);
7819
7820 let ctx = test_context();
7821 let start_time = Instant::now();
7822 let interceptor_ctx = ResponseInterceptorContext::new(&req, &ctx, start_time);
7823 let result = futures_executor::block_on(stack.process(&interceptor_ctx, resp));
7824
7825 let has_timing = result
7826 .headers()
7827 .iter()
7828 .any(|(name, _)| name == "X-Response-Time");
7829 let has_extra = result.headers().iter().any(|(name, _)| name == "X-Extra");
7830
7831 assert!(
7832 has_timing,
7833 "Should have timing header from first interceptor"
7834 );
7835 assert!(
7836 has_extra,
7837 "Should have extra header from second interceptor"
7838 );
7839 }
7840
7841 #[test]
7842 fn response_interceptor_stack_empty_is_noop() {
7843 let stack = ResponseInterceptorStack::new();
7844 assert!(stack.is_empty());
7845 assert_eq!(stack.len(), 0);
7846
7847 let req = test_request();
7848 let resp = Response::with_status(StatusCode::OK)
7849 .body(crate::response::ResponseBody::Bytes(b"unchanged".to_vec()));
7850
7851 let ctx = test_context();
7852 let start_time = Instant::now();
7853 let interceptor_ctx = ResponseInterceptorContext::new(&req, &ctx, start_time);
7854 let result = futures_executor::block_on(stack.process(&interceptor_ctx, resp));
7855
7856 match result.body_ref() {
7857 crate::response::ResponseBody::Bytes(b) => {
7858 assert_eq!(b, b"unchanged");
7859 }
7860 _ => panic!("Expected bytes body"),
7861 }
7862 }
7863
7864 #[test]
7865 fn interceptor_context_provides_timing() {
7866 let ctx = test_context();
7867 let req = test_request();
7868 let start_time = Instant::now();
7869 std::thread::sleep(std::time::Duration::from_millis(5));
7870
7871 let interceptor_ctx = ResponseInterceptorContext::new(&req, &ctx, start_time);
7872
7873 assert!(
7874 interceptor_ctx.elapsed_ms() >= 5,
7875 "Elapsed time should be at least 5ms"
7876 );
7877 assert!(interceptor_ctx.elapsed().as_millis() >= 5);
7878 }
7879
7880 #[test]
7881 fn conditional_interceptor_applies_conditionally() {
7882 let inner = HeaderTransformInterceptor::new().add("X-Success", b"true".to_vec());
7884 let conditional =
7885 ConditionalInterceptor::new(inner, |_ctx, resp| resp.status().as_u16() == 200);
7886
7887 let req = test_request();
7888
7889 let ok_resp = Response::with_status(StatusCode::OK);
7891 let result = run_interceptor(&conditional, &req, ok_resp);
7892 let has_success = result.headers().iter().any(|(name, _)| name == "X-Success");
7893 assert!(has_success, "200 response should get X-Success header");
7894
7895 let not_found = Response::with_status(StatusCode::NOT_FOUND);
7897 let result = run_interceptor(&conditional, &req, not_found);
7898 let has_success = result.headers().iter().any(|(name, _)| name == "X-Success");
7899 assert!(!has_success, "404 response should NOT get X-Success header");
7900 }
7901}
7902
7903#[cfg(test)]
7904mod cache_control_tests {
7905 use super::*;
7906 use crate::request::Method;
7907 use crate::response::StatusCode;
7908
7909 fn test_context() -> RequestContext {
7910 RequestContext::new(asupersync::Cx::for_testing(), 1)
7911 }
7912
7913 fn run_after(mw: &CacheControlMiddleware, req: &Request, resp: Response) -> Response {
7914 let ctx = test_context();
7915 let fut = mw.after(&ctx, req, resp);
7916 futures_executor::block_on(fut)
7917 }
7918
7919 #[test]
7920 fn cache_directive_as_str_works() {
7921 assert_eq!(CacheDirective::Public.as_str(), "public");
7922 assert_eq!(CacheDirective::Private.as_str(), "private");
7923 assert_eq!(CacheDirective::NoStore.as_str(), "no-store");
7924 assert_eq!(CacheDirective::NoCache.as_str(), "no-cache");
7925 assert_eq!(CacheDirective::MustRevalidate.as_str(), "must-revalidate");
7926 assert_eq!(CacheDirective::Immutable.as_str(), "immutable");
7927 }
7928
7929 #[test]
7930 fn cache_control_builder_basic() {
7931 let cc = CacheControlBuilder::new()
7932 .public()
7933 .max_age_secs(3600)
7934 .build();
7935 assert!(cc.contains("public"));
7936 assert!(cc.contains("max-age=3600"));
7937 }
7938
7939 #[test]
7940 fn cache_control_builder_complex() {
7941 let cc = CacheControlBuilder::new()
7942 .public()
7943 .max_age_secs(60)
7944 .s_maxage_secs(3600)
7945 .stale_while_revalidate_secs(86400)
7946 .build();
7947 assert!(cc.contains("public"));
7948 assert!(cc.contains("max-age=60"));
7949 assert!(cc.contains("s-maxage=3600"));
7950 assert!(cc.contains("stale-while-revalidate=86400"));
7951 }
7952
7953 #[test]
7954 fn cache_control_builder_no_cache() {
7955 let cc = CacheControlBuilder::new()
7956 .no_store()
7957 .no_cache()
7958 .must_revalidate()
7959 .build();
7960 assert!(cc.contains("no-store"));
7961 assert!(cc.contains("no-cache"));
7962 assert!(cc.contains("must-revalidate"));
7963 }
7964
7965 #[test]
7966 fn cache_preset_no_cache() {
7967 let value = CachePreset::NoCache.to_header_value();
7968 assert!(value.contains("no-store"));
7969 assert!(value.contains("no-cache"));
7970 assert!(value.contains("must-revalidate"));
7971 }
7972
7973 #[test]
7974 fn cache_preset_immutable() {
7975 let value = CachePreset::Immutable.to_header_value();
7976 assert!(value.contains("public"));
7977 assert!(value.contains("max-age=31536000"));
7978 assert!(value.contains("immutable"));
7979 }
7980
7981 #[test]
7982 fn cache_preset_static_assets() {
7983 let value = CachePreset::StaticAssets.to_header_value();
7984 assert!(value.contains("public"));
7985 assert!(value.contains("max-age=86400"));
7986 }
7987
7988 #[test]
7989 fn middleware_adds_cache_control_header() {
7990 let mw = CacheControlMiddleware::with_preset(CachePreset::PublicOneHour);
7991 let req = Request::new(Method::Get, "/api/test");
7992 let resp = Response::with_status(StatusCode::OK);
7993
7994 let result = run_after(&mw, &req, resp);
7995 let headers = result.headers();
7996 let cc_header = headers
7997 .iter()
7998 .find(|(name, _)| name.eq_ignore_ascii_case("cache-control"));
7999 assert!(
8000 cc_header.is_some(),
8001 "Cache-Control header should be present"
8002 );
8003 let (_, value) = cc_header.unwrap();
8004 let value_str = String::from_utf8_lossy(value);
8005 assert!(value_str.contains("public"));
8006 assert!(value_str.contains("max-age=3600"));
8007 }
8008
8009 #[test]
8010 fn middleware_skips_post_requests() {
8011 let mw = CacheControlMiddleware::with_preset(CachePreset::PublicOneHour);
8012 let req = Request::new(Method::Post, "/api/test");
8013 let resp = Response::with_status(StatusCode::OK);
8014
8015 let result = run_after(&mw, &req, resp);
8016 let headers = result.headers();
8017 let cc_header = headers
8018 .iter()
8019 .find(|(name, _)| name.eq_ignore_ascii_case("cache-control"));
8020 assert!(
8021 cc_header.is_none(),
8022 "Cache-Control should not be added for POST"
8023 );
8024 }
8025
8026 #[test]
8027 fn middleware_skips_error_responses() {
8028 let mw = CacheControlMiddleware::with_preset(CachePreset::PublicOneHour);
8029 let req = Request::new(Method::Get, "/api/test");
8030 let resp = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
8031
8032 let result = run_after(&mw, &req, resp);
8033 let headers = result.headers();
8034 let cc_header = headers
8035 .iter()
8036 .find(|(name, _)| name.eq_ignore_ascii_case("cache-control"));
8037 assert!(
8038 cc_header.is_none(),
8039 "Cache-Control should not be added for error responses"
8040 );
8041 }
8042
8043 #[test]
8044 fn middleware_with_vary_header() {
8045 let mw = CacheControlMiddleware::with_config(
8046 CacheControlConfig::from_preset(CachePreset::PublicOneHour)
8047 .vary("Accept-Encoding")
8048 .vary("Accept-Language"),
8049 );
8050 let req = Request::new(Method::Get, "/api/test");
8051 let resp = Response::with_status(StatusCode::OK);
8052
8053 let result = run_after(&mw, &req, resp);
8054 let headers = result.headers();
8055 let vary_header = headers
8056 .iter()
8057 .find(|(name, _)| name.eq_ignore_ascii_case("vary"));
8058 assert!(vary_header.is_some(), "Vary header should be present");
8059 let (_, value) = vary_header.unwrap();
8060 let value_str = String::from_utf8_lossy(value);
8061 assert!(value_str.contains("Accept-Encoding"));
8062 assert!(value_str.contains("Accept-Language"));
8063 }
8064
8065 #[test]
8066 fn middleware_preserves_existing_cache_control() {
8067 let mw = CacheControlMiddleware::with_config(
8068 CacheControlConfig::from_preset(CachePreset::PublicOneHour).preserve_existing(true),
8069 );
8070 let req = Request::new(Method::Get, "/api/test");
8071 let resp =
8072 Response::with_status(StatusCode::OK).header("Cache-Control", b"max-age=60".to_vec());
8073
8074 let result = run_after(&mw, &req, resp);
8075 let headers = result.headers();
8076 let cc_headers: Vec<_> = headers
8077 .iter()
8078 .filter(|(name, _)| name.eq_ignore_ascii_case("cache-control"))
8079 .collect();
8080 assert_eq!(cc_headers.len(), 1);
8082 let (_, value) = cc_headers[0];
8083 let value_str = String::from_utf8_lossy(value);
8084 assert_eq!(value_str, "max-age=60");
8085 }
8086
8087 #[test]
8088 fn path_pattern_matching_exact() {
8089 assert!(path_matches_pattern("/api/users", "/api/users"));
8090 assert!(!path_matches_pattern("/api/users", "/api/items"));
8091 }
8092
8093 #[test]
8094 fn path_pattern_matching_wildcard() {
8095 assert!(path_matches_pattern("/api/users/123", "/api/users/*"));
8096 assert!(path_matches_pattern("/static/css/style.css", "/static/*"));
8097 assert!(path_matches_pattern("/anything", "*"));
8098 }
8099
8100 #[test]
8101 fn date_formatting_works() {
8102 let now = std::time::SystemTime::now();
8104 let formatted = format_http_date(now);
8105 assert!(formatted.ends_with(" GMT"));
8107 let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
8109 assert!(days.iter().any(|d| formatted.starts_with(d)));
8110 }
8111
8112 #[test]
8113 fn leap_year_detection() {
8114 assert!(!is_leap_year(1900)); assert!(is_leap_year(2000)); assert!(is_leap_year(2024)); assert!(!is_leap_year(2023)); }
8119}
8120
8121#[cfg(test)]
8126mod trace_rejection_tests {
8127 use super::*;
8128 use crate::request::Method;
8129 use crate::response::StatusCode;
8130
8131 fn test_context() -> RequestContext {
8132 RequestContext::new(asupersync::Cx::for_testing(), 1)
8133 }
8134
8135 fn run_before(mw: &TraceRejectionMiddleware, req: &mut Request) -> ControlFlow {
8136 let ctx = test_context();
8137 let fut = mw.before(&ctx, req);
8138 futures_executor::block_on(fut)
8139 }
8140
8141 fn find_header<'a>(headers: &'a [(String, Vec<u8>)], name: &str) -> Option<&'a [u8]> {
8142 headers
8143 .iter()
8144 .find(|(n, _)| n.eq_ignore_ascii_case(name))
8145 .map(|(_, v)| v.as_slice())
8146 }
8147
8148 #[test]
8149 fn trace_request_rejected() {
8150 let mw = TraceRejectionMiddleware::new();
8151 let mut req = Request::new(Method::Trace, "/");
8152
8153 let result = run_before(&mw, &mut req);
8154
8155 match result {
8156 ControlFlow::Break(response) => {
8157 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
8158 }
8159 ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8160 }
8161 }
8162
8163 #[test]
8164 fn trace_request_with_path() {
8165 let mw = TraceRejectionMiddleware::new();
8166 let mut req = Request::new(Method::Trace, "/api/users/123");
8167
8168 let result = run_before(&mw, &mut req);
8169
8170 match result {
8171 ControlFlow::Break(response) => {
8172 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
8173 }
8174 ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8175 }
8176 }
8177
8178 #[test]
8179 fn get_request_allowed() {
8180 let mw = TraceRejectionMiddleware::new();
8181 let mut req = Request::new(Method::Get, "/");
8182
8183 let result = run_before(&mw, &mut req);
8184
8185 match result {
8186 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("GET request should be allowed"),
8188 }
8189 }
8190
8191 #[test]
8192 fn post_request_allowed() {
8193 let mw = TraceRejectionMiddleware::new();
8194 let mut req = Request::new(Method::Post, "/api/users");
8195
8196 let result = run_before(&mw, &mut req);
8197
8198 match result {
8199 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("POST request should be allowed"),
8201 }
8202 }
8203
8204 #[test]
8205 fn put_request_allowed() {
8206 let mw = TraceRejectionMiddleware::new();
8207 let mut req = Request::new(Method::Put, "/api/users/1");
8208
8209 let result = run_before(&mw, &mut req);
8210
8211 match result {
8212 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("PUT request should be allowed"),
8214 }
8215 }
8216
8217 #[test]
8218 fn delete_request_allowed() {
8219 let mw = TraceRejectionMiddleware::new();
8220 let mut req = Request::new(Method::Delete, "/api/users/1");
8221
8222 let result = run_before(&mw, &mut req);
8223
8224 match result {
8225 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("DELETE request should be allowed"),
8227 }
8228 }
8229
8230 #[test]
8231 fn patch_request_allowed() {
8232 let mw = TraceRejectionMiddleware::new();
8233 let mut req = Request::new(Method::Patch, "/api/users/1");
8234
8235 let result = run_before(&mw, &mut req);
8236
8237 match result {
8238 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("PATCH request should be allowed"),
8240 }
8241 }
8242
8243 #[test]
8244 fn options_request_allowed() {
8245 let mw = TraceRejectionMiddleware::new();
8246 let mut req = Request::new(Method::Options, "/api/users");
8247
8248 let result = run_before(&mw, &mut req);
8249
8250 match result {
8251 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("OPTIONS request should be allowed"),
8253 }
8254 }
8255
8256 #[test]
8257 fn head_request_allowed() {
8258 let mw = TraceRejectionMiddleware::new();
8259 let mut req = Request::new(Method::Head, "/");
8260
8261 let result = run_before(&mw, &mut req);
8262
8263 match result {
8264 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("HEAD request should be allowed"),
8266 }
8267 }
8268
8269 #[test]
8270 fn response_includes_allow_header() {
8271 let mw = TraceRejectionMiddleware::new();
8272 let mut req = Request::new(Method::Trace, "/");
8273
8274 let result = run_before(&mw, &mut req);
8275
8276 match result {
8277 ControlFlow::Break(response) => {
8278 let allow_header = find_header(response.headers(), "Allow");
8279 assert!(
8280 allow_header.is_some(),
8281 "Response should include Allow header"
8282 );
8283 }
8284 ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8285 }
8286 }
8287
8288 #[test]
8289 fn response_has_json_content_type() {
8290 let mw = TraceRejectionMiddleware::new();
8291 let mut req = Request::new(Method::Trace, "/");
8292
8293 let result = run_before(&mw, &mut req);
8294
8295 match result {
8296 ControlFlow::Break(response) => {
8297 let ct_header = find_header(response.headers(), "Content-Type");
8298 assert_eq!(ct_header, Some(b"application/json".as_slice()));
8299 }
8300 ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8301 }
8302 }
8303
8304 #[test]
8305 fn default_enables_logging() {
8306 let mw = TraceRejectionMiddleware::new();
8307 assert!(mw.log_attempts);
8308 }
8309
8310 #[test]
8311 fn log_attempts_can_be_disabled() {
8312 let mw = TraceRejectionMiddleware::new().log_attempts(false);
8313 assert!(!mw.log_attempts);
8314 }
8315
8316 #[test]
8317 fn middleware_name() {
8318 let mw = TraceRejectionMiddleware::new();
8319 assert_eq!(mw.name(), "TraceRejection");
8320 }
8321
8322 #[test]
8323 fn default_impl() {
8324 let mw = TraceRejectionMiddleware::default();
8325 assert!(mw.log_attempts);
8326 }
8327}
8328
8329#[cfg(test)]
8338mod https_redirect_tests {
8339 use super::*;
8340 use crate::request::Method;
8341 use crate::response::StatusCode;
8342
8343 fn test_context() -> RequestContext {
8344 RequestContext::new(asupersync::Cx::for_testing(), 1)
8345 }
8346
8347 fn run_before(mw: &HttpsRedirectMiddleware, req: &mut Request) -> ControlFlow {
8348 let ctx = test_context();
8349 let fut = mw.before(&ctx, req);
8350 futures_executor::block_on(fut)
8351 }
8352
8353 fn run_after(mw: &HttpsRedirectMiddleware, req: &Request, resp: Response) -> Response {
8354 let ctx = test_context();
8355 let fut = mw.after(&ctx, req, resp);
8356 futures_executor::block_on(fut)
8357 }
8358
8359 fn find_header<'a>(headers: &'a [(String, Vec<u8>)], name: &str) -> Option<&'a [u8]> {
8360 headers
8361 .iter()
8362 .find(|(n, _)| n.eq_ignore_ascii_case(name))
8363 .map(|(_, v)| v.as_slice())
8364 }
8365
8366 #[test]
8367 fn http_request_redirected() {
8368 let mw = HttpsRedirectMiddleware::new();
8369 let mut req = Request::new(Method::Get, "/");
8370 req.headers_mut().insert("Host", b"example.com".to_vec());
8371
8372 let result = run_before(&mw, &mut req);
8373
8374 match result {
8375 ControlFlow::Break(response) => {
8376 assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY);
8377 let location = find_header(response.headers(), "Location");
8378 assert_eq!(location, Some(b"https://example.com/".as_slice()));
8379 }
8380 ControlFlow::Continue => panic!("HTTP request should be redirected"),
8381 }
8382 }
8383
8384 #[test]
8385 fn http_request_with_path_and_query() {
8386 let mw = HttpsRedirectMiddleware::new();
8387 let mut req = Request::new(Method::Get, "/api/users?page=1");
8388 req.headers_mut().insert("Host", b"example.com".to_vec());
8389
8390 let result = run_before(&mw, &mut req);
8391
8392 match result {
8393 ControlFlow::Break(response) => {
8394 let location = find_header(response.headers(), "Location");
8395 assert_eq!(
8396 location,
8397 Some(b"https://example.com/api/users?page=1".as_slice())
8398 );
8399 }
8400 ControlFlow::Continue => panic!("HTTP request should be redirected"),
8401 }
8402 }
8403
8404 #[test]
8405 fn https_request_not_redirected() {
8406 let mw = HttpsRedirectMiddleware::new();
8407 let mut req = Request::new(Method::Get, "/");
8408 req.headers_mut().insert("Host", b"example.com".to_vec());
8409 req.headers_mut()
8410 .insert("X-Forwarded-Proto", b"https".to_vec());
8411
8412 let result = run_before(&mw, &mut req);
8413
8414 match result {
8415 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("HTTPS request should not be redirected"),
8417 }
8418 }
8419
8420 #[test]
8421 fn x_forwarded_ssl_recognized() {
8422 let mw = HttpsRedirectMiddleware::new();
8423 let mut req = Request::new(Method::Get, "/");
8424 req.headers_mut().insert("Host", b"example.com".to_vec());
8425 req.headers_mut().insert("X-Forwarded-Ssl", b"on".to_vec());
8426
8427 let result = run_before(&mw, &mut req);
8428
8429 match result {
8430 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("Request with X-Forwarded-Ssl=on should not redirect"),
8432 }
8433 }
8434
8435 #[test]
8436 fn excluded_path_not_redirected() {
8437 let mw = HttpsRedirectMiddleware::new().exclude_path("/health");
8438 let mut req = Request::new(Method::Get, "/health");
8439 req.headers_mut().insert("Host", b"example.com".to_vec());
8440
8441 let result = run_before(&mw, &mut req);
8442
8443 match result {
8444 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("Excluded path should not be redirected"),
8446 }
8447 }
8448
8449 #[test]
8450 fn excluded_path_prefix_matches() {
8451 let mw = HttpsRedirectMiddleware::new().exclude_path("/health");
8452 let mut req = Request::new(Method::Get, "/health/live");
8453 req.headers_mut().insert("Host", b"example.com".to_vec());
8454
8455 let result = run_before(&mw, &mut req);
8456
8457 match result {
8458 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("Path with excluded prefix should not be redirected"),
8460 }
8461 }
8462
8463 #[test]
8464 fn temporary_redirect_option() {
8465 let mw = HttpsRedirectMiddleware::new().permanent_redirect(false);
8466 let mut req = Request::new(Method::Get, "/");
8467 req.headers_mut().insert("Host", b"example.com".to_vec());
8468
8469 let result = run_before(&mw, &mut req);
8470
8471 match result {
8472 ControlFlow::Break(response) => {
8473 assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT);
8474 }
8475 ControlFlow::Continue => panic!("HTTP request should be redirected"),
8476 }
8477 }
8478
8479 #[test]
8480 fn redirect_disabled() {
8481 let mw = HttpsRedirectMiddleware::new().redirect_enabled(false);
8482 let mut req = Request::new(Method::Get, "/");
8483 req.headers_mut().insert("Host", b"example.com".to_vec());
8484
8485 let result = run_before(&mw, &mut req);
8486
8487 match result {
8488 ControlFlow::Continue => {} ControlFlow::Break(_) => panic!("Redirects are disabled, should continue"),
8490 }
8491 }
8492
8493 #[test]
8494 fn hsts_header_on_https_response() {
8495 let mw = HttpsRedirectMiddleware::new();
8496 let mut req = Request::new(Method::Get, "/");
8497 req.headers_mut()
8498 .insert("X-Forwarded-Proto", b"https".to_vec());
8499
8500 let response = Response::with_status(StatusCode::OK);
8501 let result = run_after(&mw, &req, response);
8502
8503 let hsts = find_header(result.headers(), "Strict-Transport-Security");
8504 assert!(
8505 hsts.is_some(),
8506 "HSTS header should be present on HTTPS response"
8507 );
8508 let hsts_str = String::from_utf8_lossy(hsts.unwrap());
8509 assert!(hsts_str.contains("max-age=31536000"));
8510 }
8511
8512 #[test]
8513 fn hsts_header_not_on_http_response() {
8514 let mw = HttpsRedirectMiddleware::new().redirect_enabled(false);
8515 let req = Request::new(Method::Get, "/");
8516 let response = Response::with_status(StatusCode::OK);
8519 let result = run_after(&mw, &req, response);
8520
8521 let hsts = find_header(result.headers(), "Strict-Transport-Security");
8522 assert!(hsts.is_none(), "HSTS header should not be on HTTP response");
8523 }
8524
8525 #[test]
8526 fn hsts_with_include_subdomains() {
8527 let mw = HttpsRedirectMiddleware::new().include_subdomains(true);
8528 let mut req = Request::new(Method::Get, "/");
8529 req.headers_mut()
8530 .insert("X-Forwarded-Proto", b"https".to_vec());
8531
8532 let response = Response::with_status(StatusCode::OK);
8533 let result = run_after(&mw, &req, response);
8534
8535 let hsts = find_header(result.headers(), "Strict-Transport-Security");
8536 let hsts_str = String::from_utf8_lossy(hsts.unwrap());
8537 assert!(hsts_str.contains("includeSubDomains"));
8538 }
8539
8540 #[test]
8541 fn hsts_with_preload() {
8542 let mw = HttpsRedirectMiddleware::new().preload(true);
8543 let mut req = Request::new(Method::Get, "/");
8544 req.headers_mut()
8545 .insert("X-Forwarded-Proto", b"https".to_vec());
8546
8547 let response = Response::with_status(StatusCode::OK);
8548 let result = run_after(&mw, &req, response);
8549
8550 let hsts = find_header(result.headers(), "Strict-Transport-Security");
8551 let hsts_str = String::from_utf8_lossy(hsts.unwrap());
8552 assert!(hsts_str.contains("preload"));
8553 }
8554
8555 #[test]
8556 fn hsts_disabled_with_zero_max_age() {
8557 let mw = HttpsRedirectMiddleware::new().hsts_max_age_secs(0);
8558 let mut req = Request::new(Method::Get, "/");
8559 req.headers_mut()
8560 .insert("X-Forwarded-Proto", b"https".to_vec());
8561
8562 let response = Response::with_status(StatusCode::OK);
8563 let result = run_after(&mw, &req, response);
8564
8565 let hsts = find_header(result.headers(), "Strict-Transport-Security");
8566 assert!(hsts.is_none(), "HSTS should be disabled with max-age=0");
8567 }
8568
8569 #[test]
8570 fn custom_https_port() {
8571 let mw = HttpsRedirectMiddleware::new().https_port(8443);
8572 let mut req = Request::new(Method::Get, "/");
8573 req.headers_mut().insert("Host", b"example.com".to_vec());
8574
8575 let result = run_before(&mw, &mut req);
8576
8577 match result {
8578 ControlFlow::Break(response) => {
8579 let location = find_header(response.headers(), "Location");
8580 assert_eq!(location, Some(b"https://example.com:8443/".as_slice()));
8581 }
8582 ControlFlow::Continue => panic!("HTTP request should be redirected"),
8583 }
8584 }
8585
8586 #[test]
8587 fn host_with_port_stripped() {
8588 let mw = HttpsRedirectMiddleware::new();
8589 let mut req = Request::new(Method::Get, "/");
8590 req.headers_mut()
8591 .insert("Host", b"example.com:8080".to_vec());
8592
8593 let result = run_before(&mw, &mut req);
8594
8595 match result {
8596 ControlFlow::Break(response) => {
8597 let location = find_header(response.headers(), "Location");
8598 assert_eq!(location, Some(b"https://example.com/".as_slice()));
8600 }
8601 ControlFlow::Continue => panic!("HTTP request should be redirected"),
8602 }
8603 }
8604
8605 #[test]
8606 fn middleware_name() {
8607 let mw = HttpsRedirectMiddleware::new();
8608 assert_eq!(mw.name(), "HttpsRedirect");
8609 }
8610
8611 #[test]
8612 fn default_impl() {
8613 let mw = HttpsRedirectMiddleware::default();
8614 assert!(mw.config.redirect_enabled);
8615 assert!(mw.config.permanent_redirect);
8616 assert_eq!(mw.config.hsts_max_age_secs, 31_536_000);
8617 }
8618
8619 #[test]
8620 fn config_builder() {
8621 let mw = HttpsRedirectMiddleware::new()
8622 .redirect_enabled(false)
8623 .permanent_redirect(false)
8624 .hsts_max_age_secs(86400)
8625 .include_subdomains(true)
8626 .preload(true)
8627 .https_port(8443);
8628
8629 assert!(!mw.config.redirect_enabled);
8630 assert!(!mw.config.permanent_redirect);
8631 assert_eq!(mw.config.hsts_max_age_secs, 86400);
8632 assert!(mw.config.hsts_include_subdomains);
8633 assert!(mw.config.hsts_preload);
8634 assert_eq!(mw.config.https_port, 8443);
8635 }
8636
8637 #[test]
8638 fn exclude_paths_method() {
8639 let mw = HttpsRedirectMiddleware::new()
8640 .exclude_paths(vec!["/health".to_string(), "/ready".to_string()]);
8641
8642 assert_eq!(mw.config.exclude_paths.len(), 2);
8643 assert!(mw.config.exclude_paths.contains(&"/health".to_string()));
8644 assert!(mw.config.exclude_paths.contains(&"/ready".to_string()));
8645 }
8646}
8647
8648#[cfg(test)]
8657mod tests {
8658 use super::*;
8659 use crate::response::{ResponseBody, StatusCode};
8660
8661 #[allow(dead_code)]
8663 struct AddHeaderMiddleware {
8664 name: &'static str,
8665 value: &'static [u8],
8666 }
8667
8668 impl Middleware for AddHeaderMiddleware {
8669 fn after<'a>(
8670 &'a self,
8671 _ctx: &'a RequestContext,
8672 _req: &'a Request,
8673 response: Response,
8674 ) -> BoxFuture<'a, Response> {
8675 Box::pin(async move { response.header(self.name, self.value.to_vec()) })
8676 }
8677 }
8678
8679 #[allow(dead_code)]
8681 struct BlockingMiddleware;
8682
8683 impl Middleware for BlockingMiddleware {
8684 fn before<'a>(
8685 &'a self,
8686 _ctx: &'a RequestContext,
8687 _req: &'a mut Request,
8688 ) -> BoxFuture<'a, ControlFlow> {
8689 Box::pin(async {
8690 ControlFlow::Break(
8691 Response::with_status(StatusCode::FORBIDDEN)
8692 .body(ResponseBody::Bytes(b"blocked".to_vec())),
8693 )
8694 })
8695 }
8696 }
8697
8698 #[allow(dead_code)]
8700 struct TrackingMiddleware {
8701 before_count: std::sync::atomic::AtomicUsize,
8702 after_count: std::sync::atomic::AtomicUsize,
8703 }
8704
8705 #[allow(dead_code)]
8706 impl TrackingMiddleware {
8707 fn new() -> Self {
8708 Self {
8709 before_count: std::sync::atomic::AtomicUsize::new(0),
8710 after_count: std::sync::atomic::AtomicUsize::new(0),
8711 }
8712 }
8713
8714 fn before_count(&self) -> usize {
8715 self.before_count.load(std::sync::atomic::Ordering::SeqCst)
8716 }
8717
8718 fn after_count(&self) -> usize {
8719 self.after_count.load(std::sync::atomic::Ordering::SeqCst)
8720 }
8721 }
8722
8723 impl Middleware for TrackingMiddleware {
8724 fn before<'a>(
8725 &'a self,
8726 _ctx: &'a RequestContext,
8727 _req: &'a mut Request,
8728 ) -> BoxFuture<'a, ControlFlow> {
8729 self.before_count
8730 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8731 Box::pin(async { ControlFlow::Continue })
8732 }
8733
8734 fn after<'a>(
8735 &'a self,
8736 _ctx: &'a RequestContext,
8737 _req: &'a Request,
8738 response: Response,
8739 ) -> BoxFuture<'a, Response> {
8740 self.after_count
8741 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8742 Box::pin(async move { response })
8743 }
8744 }
8745
8746 #[test]
8747 fn control_flow_variants() {
8748 let cont = ControlFlow::Continue;
8749 assert!(cont.is_continue());
8750 assert!(!cont.is_break());
8751
8752 let brk = ControlFlow::Break(Response::ok());
8753 assert!(!brk.is_continue());
8754 assert!(brk.is_break());
8755 }
8756
8757 #[test]
8758 fn middleware_stack_empty() {
8759 let stack = MiddlewareStack::new();
8760 assert!(stack.is_empty());
8761 assert_eq!(stack.len(), 0);
8762 }
8763
8764 #[test]
8765 fn middleware_stack_push() {
8766 let mut stack = MiddlewareStack::new();
8767 stack.push(NoopMiddleware);
8768 stack.push(NoopMiddleware);
8769 assert_eq!(stack.len(), 2);
8770 assert!(!stack.is_empty());
8771 }
8772
8773 #[test]
8774 fn noop_middleware_name() {
8775 let mw = NoopMiddleware;
8776 assert_eq!(mw.name(), "Noop");
8777 }
8778
8779 #[test]
8780 fn logging_redacts_sensitive_headers() {
8781 let mut headers = crate::request::Headers::new();
8782 headers.insert("Authorization", b"secret".to_vec());
8783 headers.insert("X-Request-Id", b"abc123".to_vec());
8784
8785 let redacted = super::default_redacted_headers();
8786 let formatted = super::format_headers(headers.iter(), &redacted);
8787
8788 assert!(formatted.contains("authorization=<redacted>"));
8789 assert!(formatted.contains("x-request-id=abc123"));
8790 }
8791
8792 #[test]
8793 fn logging_body_truncation() {
8794 let body = b"abcdef";
8795 let preview = super::format_bytes(body, 4);
8796 assert_eq!(preview, "abcd...");
8797
8798 let preview_full = super::format_bytes(body, 10);
8799 assert_eq!(preview_full, "abcdef");
8800 }
8801
8802 fn test_context() -> RequestContext {
8803 let cx = asupersync::Cx::for_testing();
8804 RequestContext::new(cx, 1)
8805 }
8806
8807 fn header_value(response: &Response, name: &str) -> Option<String> {
8808 response
8809 .headers()
8810 .iter()
8811 .find(|(n, _)| n.eq_ignore_ascii_case(name))
8812 .and_then(|(_, v)| std::str::from_utf8(v).ok())
8813 .map(ToString::to_string)
8814 }
8815
8816 #[test]
8817 fn cors_exact_origin_allows() {
8818 let cors = Cors::new().allow_origin("https://example.com");
8819 let ctx = test_context();
8820 let mut req = Request::new(crate::request::Method::Get, "/");
8821 req.headers_mut()
8822 .insert("origin", b"https://example.com".to_vec());
8823
8824 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8825 assert!(matches!(result, ControlFlow::Continue));
8826
8827 let response = Response::ok().body(ResponseBody::Bytes(b"ok".to_vec()));
8828 let response = futures_executor::block_on(cors.after(&ctx, &req, response));
8829
8830 assert_eq!(
8831 header_value(&response, "access-control-allow-origin"),
8832 Some("https://example.com".to_string())
8833 );
8834 assert_eq!(header_value(&response, "vary"), Some("Origin".to_string()));
8835 }
8836
8837 #[test]
8838 fn cors_wildcard_origin_allows() {
8839 let cors = Cors::new().allow_origin_wildcard("https://*.example.com");
8840 let ctx = test_context();
8841 let mut req = Request::new(crate::request::Method::Get, "/");
8842 req.headers_mut()
8843 .insert("origin", b"https://api.example.com".to_vec());
8844
8845 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8846 assert!(matches!(result, ControlFlow::Continue));
8847 }
8848
8849 #[test]
8850 fn cors_regex_origin_allows() {
8851 let cors = Cors::new().allow_origin_regex(r"^https://.*\.example\.com$");
8852 let ctx = test_context();
8853 let mut req = Request::new(crate::request::Method::Get, "/");
8854 req.headers_mut()
8855 .insert("origin", b"https://svc.example.com".to_vec());
8856
8857 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8858 assert!(matches!(result, ControlFlow::Continue));
8859 }
8860
8861 #[test]
8862 fn cors_preflight_handled() {
8863 let cors = Cors::new()
8864 .allow_any_origin()
8865 .allow_headers(["x-test", "content-type"])
8866 .max_age(600);
8867 let ctx = test_context();
8868 let mut req = Request::new(crate::request::Method::Options, "/");
8869 req.headers_mut()
8870 .insert("origin", b"https://example.com".to_vec());
8871 req.headers_mut()
8872 .insert("access-control-request-method", b"POST".to_vec());
8873 req.headers_mut().insert(
8874 "access-control-request-headers",
8875 b"x-test, content-type".to_vec(),
8876 );
8877
8878 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8879 let ControlFlow::Break(response) = result else {
8880 panic!("expected preflight break");
8881 };
8882
8883 assert_eq!(response.status().as_u16(), 204);
8884 assert_eq!(
8885 header_value(&response, "access-control-allow-origin"),
8886 Some("*".to_string())
8887 );
8888 assert_eq!(
8889 header_value(&response, "access-control-allow-methods"),
8890 Some("GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD".to_string())
8891 );
8892 assert_eq!(
8893 header_value(&response, "access-control-allow-headers"),
8894 Some("x-test, content-type".to_string())
8895 );
8896 assert_eq!(
8897 header_value(&response, "access-control-max-age"),
8898 Some("600".to_string())
8899 );
8900 }
8901
8902 #[test]
8903 fn cors_credentials_echo_origin() {
8904 let cors = Cors::new().allow_any_origin().allow_credentials(true);
8905 let ctx = test_context();
8906 let mut req = Request::new(crate::request::Method::Get, "/");
8907 req.headers_mut()
8908 .insert("origin", b"https://example.com".to_vec());
8909
8910 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8911 assert!(matches!(result, ControlFlow::Continue));
8912
8913 let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
8914 assert_eq!(
8915 header_value(&response, "access-control-allow-origin"),
8916 Some("https://example.com".to_string())
8917 );
8918 assert_eq!(
8919 header_value(&response, "access-control-allow-credentials"),
8920 Some("true".to_string())
8921 );
8922 }
8923
8924 #[test]
8929 fn cors_spec_compliance_credentials_never_wildcard_origin() {
8930 let cors = Cors::new().allow_any_origin().allow_credentials(true);
8933 let ctx = test_context();
8934
8935 for origin in &[
8937 "https://example.com",
8938 "https://api.example.com",
8939 "http://localhost:3000",
8940 ] {
8941 let mut req = Request::new(crate::request::Method::Get, "/");
8942 req.headers_mut()
8943 .insert("origin", origin.as_bytes().to_vec());
8944
8945 futures_executor::block_on(cors.before(&ctx, &mut req));
8946 let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
8947
8948 let allow_origin = header_value(&response, "access-control-allow-origin");
8949 assert_eq!(
8950 allow_origin,
8951 Some((*origin).to_string()),
8952 "With credentials enabled, Access-Control-Allow-Origin must echo '{}', not '*'",
8953 origin
8954 );
8955 assert_ne!(
8956 allow_origin,
8957 Some("*".to_string()),
8958 "CORS spec violation: credentials + wildcard origin is forbidden"
8959 );
8960 }
8961 }
8962
8963 #[test]
8964 fn cors_spec_compliance_preflight_with_credentials() {
8965 let cors = Cors::new()
8967 .allow_any_origin()
8968 .allow_credentials(true)
8969 .allow_headers(["content-type", "x-custom-header"]);
8970 let ctx = test_context();
8971
8972 let mut req = Request::new(crate::request::Method::Options, "/");
8973 req.headers_mut()
8974 .insert("origin", b"https://example.com".to_vec());
8975 req.headers_mut()
8976 .insert("access-control-request-method", b"POST".to_vec());
8977 req.headers_mut()
8978 .insert("access-control-request-headers", b"content-type".to_vec());
8979
8980 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8981 let ControlFlow::Break(response) = result else {
8982 panic!("expected preflight break");
8983 };
8984
8985 let allow_origin = header_value(&response, "access-control-allow-origin");
8987 assert_eq!(allow_origin, Some("https://example.com".to_string()));
8988 assert_ne!(
8989 allow_origin,
8990 Some("*".to_string()),
8991 "CORS spec violation: preflight with credentials must not use wildcard origin"
8992 );
8993
8994 assert_eq!(
8996 header_value(&response, "access-control-allow-credentials"),
8997 Some("true".to_string())
8998 );
8999 }
9000
9001 #[test]
9002 fn cors_spec_without_credentials_allows_wildcard() {
9003 let cors = Cors::new().allow_any_origin();
9005 let ctx = test_context();
9006 let mut req = Request::new(crate::request::Method::Get, "/");
9007 req.headers_mut()
9008 .insert("origin", b"https://example.com".to_vec());
9009
9010 futures_executor::block_on(cors.before(&ctx, &mut req));
9011 let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9012
9013 assert_eq!(
9015 header_value(&response, "access-control-allow-origin"),
9016 Some("*".to_string())
9017 );
9018 assert!(header_value(&response, "access-control-allow-credentials").is_none());
9020 }
9021
9022 #[test]
9023 fn cors_disallowed_preflight_forbidden() {
9024 let cors = Cors::new().allow_origin("https://good.example");
9025 let ctx = test_context();
9026 let mut req = Request::new(crate::request::Method::Options, "/");
9027 req.headers_mut()
9028 .insert("origin", b"https://evil.example".to_vec());
9029 req.headers_mut()
9030 .insert("access-control-request-method", b"GET".to_vec());
9031
9032 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9033 let ControlFlow::Break(response) = result else {
9034 panic!("expected forbidden preflight");
9035 };
9036 assert_eq!(response.status().as_u16(), 403);
9037 }
9038
9039 #[test]
9040 fn cors_simple_request_disallowed_origin_no_headers() {
9041 let cors = Cors::new().allow_origin("https://good.example");
9043 let ctx = test_context();
9044 let mut req = Request::new(crate::request::Method::Get, "/");
9045 req.headers_mut()
9046 .insert("origin", b"https://evil.example".to_vec());
9047
9048 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9049 assert!(matches!(result, ControlFlow::Continue));
9051
9052 let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9053 assert!(header_value(&response, "access-control-allow-origin").is_none());
9055 }
9056
9057 #[test]
9058 fn cors_expose_headers_configuration() {
9059 let cors = Cors::new()
9060 .allow_any_origin()
9061 .expose_headers(["x-custom-header", "x-another-header"]);
9062 let ctx = test_context();
9063 let mut req = Request::new(crate::request::Method::Get, "/");
9064 req.headers_mut()
9065 .insert("origin", b"https://example.com".to_vec());
9066
9067 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9068 assert!(matches!(result, ControlFlow::Continue));
9069
9070 let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9071 assert_eq!(
9072 header_value(&response, "access-control-expose-headers"),
9073 Some("x-custom-header, x-another-header".to_string())
9074 );
9075 }
9076
9077 #[test]
9078 fn cors_any_origin_sets_wildcard() {
9079 let cors = Cors::new().allow_any_origin();
9080 let ctx = test_context();
9081 let mut req = Request::new(crate::request::Method::Get, "/");
9082 req.headers_mut()
9083 .insert("origin", b"https://any-site.com".to_vec());
9084
9085 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9086 assert!(matches!(result, ControlFlow::Continue));
9087
9088 let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9089 assert_eq!(
9090 header_value(&response, "access-control-allow-origin"),
9091 Some("*".to_string())
9092 );
9093 }
9094
9095 #[test]
9096 fn cors_config_allows_method_override() {
9097 let cors = Cors::new()
9099 .allow_any_origin()
9100 .allow_methods([crate::request::Method::Get, crate::request::Method::Post]);
9101 let ctx = test_context();
9102 let mut req = Request::new(crate::request::Method::Options, "/");
9103 req.headers_mut()
9104 .insert("origin", b"https://example.com".to_vec());
9105 req.headers_mut()
9106 .insert("access-control-request-method", b"POST".to_vec());
9107
9108 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9109 let ControlFlow::Break(response) = result else {
9110 panic!("expected preflight break");
9111 };
9112 assert_eq!(
9113 header_value(&response, "access-control-allow-methods"),
9114 Some("GET, POST".to_string())
9115 );
9116 }
9117
9118 #[test]
9119 fn cors_no_origin_header_skips_cors() {
9120 let cors = Cors::new().allow_any_origin();
9122 let ctx = test_context();
9123 let mut req = Request::new(crate::request::Method::Get, "/");
9124
9125 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9126 assert!(matches!(result, ControlFlow::Continue));
9127
9128 let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9129 assert!(header_value(&response, "access-control-allow-origin").is_none());
9130 }
9131
9132 #[test]
9133 fn cors_middleware_name() {
9134 let cors = Cors::new();
9135 assert_eq!(cors.name(), "Cors");
9136 }
9137
9138 #[test]
9139 fn cors_empty_allowed_headers_does_not_reflect_request_headers() {
9140 let cors = Cors::new().allow_any_origin(); let ctx = test_context();
9145 let mut req = Request::new(crate::request::Method::Options, "/api");
9146 req.headers_mut()
9147 .insert("origin", b"https://example.com".to_vec());
9148 req.headers_mut()
9149 .insert("access-control-request-method", b"GET".to_vec());
9150 req.headers_mut().insert(
9151 "access-control-request-headers",
9152 b"x-evil-custom, authorization".to_vec(),
9153 );
9154
9155 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9156 if let ControlFlow::Break(response) = result {
9157 assert_eq!(
9160 header_value(&response, "access-control-allow-headers"),
9161 None,
9162 "Empty allowed_headers must not reflect request headers"
9163 );
9164 } else {
9165 panic!("Preflight should have been handled (Break)");
9166 }
9167 }
9168
9169 #[test]
9170 fn cors_explicit_allowed_headers_returned_in_preflight() {
9171 let cors = Cors::new()
9172 .allow_any_origin()
9173 .allow_headers(["x-token", "content-type"]);
9174 let ctx = test_context();
9175 let mut req = Request::new(crate::request::Method::Options, "/api");
9176 req.headers_mut()
9177 .insert("origin", b"https://example.com".to_vec());
9178 req.headers_mut()
9179 .insert("access-control-request-method", b"POST".to_vec());
9180
9181 let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9182 if let ControlFlow::Break(response) = result {
9183 let headers_val = header_value(&response, "access-control-allow-headers");
9184 assert!(headers_val.is_some());
9185 let val = headers_val.unwrap();
9186 assert!(val.contains("x-token"));
9187 assert!(val.contains("content-type"));
9188 } else {
9189 panic!("Preflight should have been handled (Break)");
9190 }
9191 }
9192
9193 #[test]
9198 fn request_id_generates_unique_ids() {
9199 let id1 = RequestId::generate();
9200 let id2 = RequestId::generate();
9201 let id3 = RequestId::generate();
9202
9203 assert_ne!(id1, id2);
9204 assert_ne!(id2, id3);
9205 assert_ne!(id1, id3);
9206
9207 assert!(!id1.as_str().is_empty());
9209 assert!(!id2.as_str().is_empty());
9210 assert!(!id3.as_str().is_empty());
9211 }
9212
9213 #[test]
9214 fn request_id_display() {
9215 let id = RequestId::new("test-request-123");
9216 assert_eq!(format!("{}", id), "test-request-123");
9217 }
9218
9219 #[test]
9220 fn request_id_from_string() {
9221 let id: RequestId = "my-id".into();
9222 assert_eq!(id.as_str(), "my-id");
9223
9224 let id2: RequestId = String::from("my-id-2").into();
9225 assert_eq!(id2.as_str(), "my-id-2");
9226 }
9227
9228 #[test]
9229 fn request_id_config_defaults() {
9230 let config = RequestIdConfig::default();
9231 assert_eq!(config.header_name, "x-request-id");
9232 assert!(config.accept_from_client);
9233 assert!(config.add_to_response);
9234 assert_eq!(config.max_client_id_length, 128);
9235 }
9236
9237 #[test]
9238 fn request_id_config_builder() {
9239 let config = RequestIdConfig::new()
9240 .header_name("X-Trace-ID")
9241 .accept_from_client(false)
9242 .add_to_response(false)
9243 .max_client_id_length(64);
9244
9245 assert_eq!(config.header_name, "X-Trace-ID");
9246 assert!(!config.accept_from_client);
9247 assert!(!config.add_to_response);
9248 assert_eq!(config.max_client_id_length, 64);
9249 }
9250
9251 #[test]
9252 fn request_id_middleware_generates_id() {
9253 let middleware = RequestIdMiddleware::new();
9254 let ctx = test_context();
9255 let mut req = Request::new(crate::request::Method::Get, "/");
9256
9257 let result = futures_executor::block_on(middleware.before(&ctx, &mut req));
9258 assert!(matches!(result, ControlFlow::Continue));
9259
9260 let stored_id = req.get_extension::<RequestId>();
9261 assert!(stored_id.is_some());
9262 assert!(!stored_id.unwrap().as_str().is_empty());
9263 }
9264
9265 #[test]
9266 fn request_id_middleware_accepts_client_id() {
9267 let middleware = RequestIdMiddleware::new();
9268 let ctx = test_context();
9269 let mut req = Request::new(crate::request::Method::Get, "/");
9270 req.headers_mut()
9271 .insert("x-request-id", b"client-provided-id-123".to_vec());
9272
9273 futures_executor::block_on(middleware.before(&ctx, &mut req));
9274
9275 let stored_id = req.get_extension::<RequestId>().unwrap();
9276 assert_eq!(stored_id.as_str(), "client-provided-id-123");
9277 }
9278
9279 #[test]
9280 fn request_id_middleware_rejects_invalid_client_id() {
9281 let middleware = RequestIdMiddleware::new();
9282 let ctx = test_context();
9283
9284 let mut req = Request::new(crate::request::Method::Get, "/");
9286 req.headers_mut()
9287 .insert("x-request-id", b"invalid<script>id".to_vec());
9288
9289 futures_executor::block_on(middleware.before(&ctx, &mut req));
9290
9291 let stored_id = req.get_extension::<RequestId>().unwrap();
9292 assert_ne!(stored_id.as_str(), "invalid<script>id");
9294 }
9295
9296 #[test]
9297 fn request_id_middleware_rejects_too_long_client_id() {
9298 let config = RequestIdConfig::new().max_client_id_length(10);
9299 let middleware = RequestIdMiddleware::with_config(config);
9300 let ctx = test_context();
9301
9302 let mut req = Request::new(crate::request::Method::Get, "/");
9303 req.headers_mut()
9304 .insert("x-request-id", b"this-id-is-way-too-long".to_vec());
9305
9306 futures_executor::block_on(middleware.before(&ctx, &mut req));
9307
9308 let stored_id = req.get_extension::<RequestId>().unwrap();
9309 assert_ne!(stored_id.as_str(), "this-id-is-way-too-long");
9311 }
9312
9313 #[test]
9314 fn request_id_middleware_adds_to_response() {
9315 let middleware = RequestIdMiddleware::new();
9316 let ctx = test_context();
9317 let mut req = Request::new(crate::request::Method::Get, "/");
9318
9319 futures_executor::block_on(middleware.before(&ctx, &mut req));
9320 let stored_id = req.get_extension::<RequestId>().unwrap().clone();
9321
9322 let response = Response::ok();
9323 let response = futures_executor::block_on(middleware.after(&ctx, &req, response));
9324
9325 let header = header_value(&response, "x-request-id");
9326 assert_eq!(header, Some(stored_id.0));
9327 }
9328
9329 #[test]
9330 fn request_id_middleware_respects_add_to_response_false() {
9331 let config = RequestIdConfig::new().add_to_response(false);
9332 let middleware = RequestIdMiddleware::with_config(config);
9333 let ctx = test_context();
9334 let mut req = Request::new(crate::request::Method::Get, "/");
9335
9336 futures_executor::block_on(middleware.before(&ctx, &mut req));
9337
9338 let response = Response::ok();
9339 let response = futures_executor::block_on(middleware.after(&ctx, &req, response));
9340
9341 let header = header_value(&response, "x-request-id");
9342 assert!(header.is_none());
9343 }
9344
9345 #[test]
9346 fn request_id_middleware_respects_accept_from_client_false() {
9347 let config = RequestIdConfig::new().accept_from_client(false);
9348 let middleware = RequestIdMiddleware::with_config(config);
9349 let ctx = test_context();
9350 let mut req = Request::new(crate::request::Method::Get, "/");
9351 req.headers_mut()
9352 .insert("x-request-id", b"client-id".to_vec());
9353
9354 futures_executor::block_on(middleware.before(&ctx, &mut req));
9355
9356 let stored_id = req.get_extension::<RequestId>().unwrap();
9357 assert_ne!(stored_id.as_str(), "client-id");
9359 }
9360
9361 #[test]
9362 fn request_id_middleware_custom_header_name() {
9363 let config = RequestIdConfig::new().header_name("X-Trace-ID");
9364 let middleware = RequestIdMiddleware::with_config(config);
9365 let ctx = test_context();
9366 let mut req = Request::new(crate::request::Method::Get, "/");
9367 req.headers_mut()
9368 .insert("X-Trace-ID", b"trace-123".to_vec());
9369
9370 futures_executor::block_on(middleware.before(&ctx, &mut req));
9371
9372 let stored_id = req.get_extension::<RequestId>().unwrap();
9373 assert_eq!(stored_id.as_str(), "trace-123");
9374
9375 let response = Response::ok();
9376 let response = futures_executor::block_on(middleware.after(&ctx, &req, response));
9377
9378 let header = header_value(&response, "X-Trace-ID");
9379 assert_eq!(header, Some("trace-123".to_string()));
9380 }
9381
9382 #[test]
9383 fn is_valid_request_id_accepts_valid() {
9384 assert!(super::is_valid_request_id("abc123"));
9385 assert!(super::is_valid_request_id("request-id-123"));
9386 assert!(super::is_valid_request_id("request_id_123"));
9387 assert!(super::is_valid_request_id("request.id.123"));
9388 assert!(super::is_valid_request_id("ABC123"));
9389 assert!(super::is_valid_request_id("a-b_c.D"));
9390 }
9391
9392 #[test]
9393 fn is_valid_request_id_rejects_invalid() {
9394 assert!(!super::is_valid_request_id(""));
9395 assert!(!super::is_valid_request_id("id with spaces"));
9396 assert!(!super::is_valid_request_id("id<script>"));
9397 assert!(!super::is_valid_request_id("id\nwith\nnewlines"));
9398 assert!(!super::is_valid_request_id("id;with;semicolons"));
9399 assert!(!super::is_valid_request_id("id/with/slashes"));
9400 }
9401
9402 #[test]
9403 fn request_id_middleware_name() {
9404 let middleware = RequestIdMiddleware::new();
9405 assert_eq!(middleware.name(), "RequestId");
9406 }
9407
9408 struct OrderTrackingMiddleware {
9414 id: &'static str,
9415 log: Arc<std::sync::Mutex<Vec<String>>>,
9416 }
9417
9418 impl OrderTrackingMiddleware {
9419 fn new(id: &'static str, log: Arc<std::sync::Mutex<Vec<String>>>) -> Self {
9420 Self { id, log }
9421 }
9422 }
9423
9424 impl Middleware for OrderTrackingMiddleware {
9425 fn before<'a>(
9426 &'a self,
9427 _ctx: &'a RequestContext,
9428 _req: &'a mut Request,
9429 ) -> BoxFuture<'a, ControlFlow> {
9430 self.log.lock().unwrap().push(format!("{}.before", self.id));
9431 Box::pin(async { ControlFlow::Continue })
9432 }
9433
9434 fn after<'a>(
9435 &'a self,
9436 _ctx: &'a RequestContext,
9437 _req: &'a Request,
9438 response: Response,
9439 ) -> BoxFuture<'a, Response> {
9440 self.log.lock().unwrap().push(format!("{}.after", self.id));
9441 Box::pin(async move { response })
9442 }
9443 }
9444
9445 struct ConditionalBreakMiddleware {
9447 id: &'static str,
9448 should_break: bool,
9449 log: Arc<std::sync::Mutex<Vec<String>>>,
9450 }
9451
9452 impl ConditionalBreakMiddleware {
9453 fn new(
9454 id: &'static str,
9455 should_break: bool,
9456 log: Arc<std::sync::Mutex<Vec<String>>>,
9457 ) -> Self {
9458 Self {
9459 id,
9460 should_break,
9461 log,
9462 }
9463 }
9464 }
9465
9466 impl Middleware for ConditionalBreakMiddleware {
9467 fn before<'a>(
9468 &'a self,
9469 _ctx: &'a RequestContext,
9470 _req: &'a mut Request,
9471 ) -> BoxFuture<'a, ControlFlow> {
9472 self.log.lock().unwrap().push(format!("{}.before", self.id));
9473 let should_break = self.should_break;
9474 Box::pin(async move {
9475 if should_break {
9476 ControlFlow::Break(
9477 Response::with_status(StatusCode::FORBIDDEN)
9478 .body(ResponseBody::Bytes(b"blocked".to_vec())),
9479 )
9480 } else {
9481 ControlFlow::Continue
9482 }
9483 })
9484 }
9485
9486 fn after<'a>(
9487 &'a self,
9488 _ctx: &'a RequestContext,
9489 _req: &'a Request,
9490 response: Response,
9491 ) -> BoxFuture<'a, Response> {
9492 self.log.lock().unwrap().push(format!("{}.after", self.id));
9493 Box::pin(async move { response })
9494 }
9495 }
9496
9497 struct OkHandler;
9499
9500 impl Handler for OkHandler {
9501 fn call<'a>(
9502 &'a self,
9503 _ctx: &'a RequestContext,
9504 _req: &'a mut Request,
9505 ) -> BoxFuture<'a, Response> {
9506 Box::pin(async move { Response::ok().body(ResponseBody::Bytes(b"handler".to_vec())) })
9507 }
9508 }
9509
9510 struct CheckHeaderHandler;
9512
9513 impl Handler for CheckHeaderHandler {
9514 fn call<'a>(
9515 &'a self,
9516 _ctx: &'a RequestContext,
9517 req: &'a mut Request,
9518 ) -> BoxFuture<'a, Response> {
9519 let has_header = req.headers().get("X-Modified-By").is_some();
9520 Box::pin(async move {
9521 if has_header {
9522 Response::ok().body(ResponseBody::Bytes(b"header-present".to_vec()))
9523 } else {
9524 Response::with_status(StatusCode::BAD_REQUEST)
9525 }
9526 })
9527 }
9528 }
9529
9530 struct ErrorHandler;
9532
9533 impl Handler for ErrorHandler {
9534 fn call<'a>(
9535 &'a self,
9536 _ctx: &'a RequestContext,
9537 _req: &'a mut Request,
9538 ) -> BoxFuture<'a, Response> {
9539 Box::pin(async move { Response::with_status(StatusCode::INTERNAL_SERVER_ERROR) })
9540 }
9541 }
9542
9543 #[test]
9544 fn middleware_stack_executes_in_correct_order() {
9545 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9548
9549 let mut stack = MiddlewareStack::new();
9550 stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
9551 stack.push(OrderTrackingMiddleware::new("mw2", log.clone()));
9552 stack.push(OrderTrackingMiddleware::new("mw3", log.clone()));
9553
9554 let ctx = test_context();
9555 let mut req = Request::new(crate::request::Method::Get, "/");
9556
9557 futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9558
9559 let calls = log.lock().unwrap().clone();
9560 assert_eq!(
9561 calls,
9562 vec![
9563 "mw1.before",
9564 "mw2.before",
9565 "mw3.before",
9566 "mw3.after",
9567 "mw2.after",
9568 "mw1.after",
9569 ]
9570 );
9571 }
9572
9573 #[test]
9574 fn middleware_stack_short_circuit_skips_later_middleware() {
9575 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9578
9579 let mut stack = MiddlewareStack::new();
9580 stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
9581 stack.push(ConditionalBreakMiddleware::new("mw2", true, log.clone()));
9582 stack.push(OrderTrackingMiddleware::new("mw3", log.clone()));
9583
9584 let ctx = test_context();
9585 let mut req = Request::new(crate::request::Method::Get, "/");
9586
9587 let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9588
9589 assert_eq!(response.status().as_u16(), 403);
9591
9592 let calls = log.lock().unwrap().clone();
9593 assert_eq!(
9594 calls,
9595 vec![
9596 "mw1.before",
9597 "mw2.before",
9598 "mw1.after",
9601 ]
9602 );
9603 }
9604
9605 #[test]
9606 fn middleware_stack_first_middleware_breaks() {
9607 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9609
9610 let mut stack = MiddlewareStack::new();
9611 stack.push(ConditionalBreakMiddleware::new("mw1", true, log.clone()));
9612 stack.push(OrderTrackingMiddleware::new("mw2", log.clone()));
9613
9614 let ctx = test_context();
9615 let mut req = Request::new(crate::request::Method::Get, "/");
9616
9617 let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9618
9619 assert_eq!(response.status().as_u16(), 403);
9620
9621 let calls = log.lock().unwrap().clone();
9622 assert_eq!(calls, vec!["mw1.before"]);
9623 }
9625
9626 #[test]
9627 fn middleware_stack_last_middleware_breaks() {
9628 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9630
9631 let mut stack = MiddlewareStack::new();
9632 stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
9633 stack.push(OrderTrackingMiddleware::new("mw2", log.clone()));
9634 stack.push(ConditionalBreakMiddleware::new("mw3", true, log.clone()));
9635
9636 let ctx = test_context();
9637 let mut req = Request::new(crate::request::Method::Get, "/");
9638
9639 let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9640
9641 assert_eq!(response.status().as_u16(), 403);
9642
9643 let calls = log.lock().unwrap().clone();
9644 assert_eq!(
9645 calls,
9646 vec![
9647 "mw1.before",
9648 "mw2.before",
9649 "mw3.before",
9650 "mw2.after",
9652 "mw1.after",
9653 ]
9654 );
9655 }
9656
9657 #[test]
9658 fn middleware_stack_empty_executes_handler_directly() {
9659 let stack = MiddlewareStack::new();
9660 let ctx = test_context();
9661 let mut req = Request::new(crate::request::Method::Get, "/");
9662
9663 let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9664
9665 assert_eq!(response.status().as_u16(), 200);
9666 }
9667
9668 #[test]
9669 fn middleware_stack_with_capacity() {
9670 let stack = MiddlewareStack::with_capacity(10);
9671 assert!(stack.is_empty());
9672 assert_eq!(stack.len(), 0);
9673 }
9674
9675 #[test]
9676 fn middleware_stack_push_arc() {
9677 let mut stack = MiddlewareStack::new();
9678 let mw: Arc<dyn Middleware> = Arc::new(NoopMiddleware);
9679 stack.push_arc(mw);
9680 assert_eq!(stack.len(), 1);
9681 }
9682
9683 #[test]
9688 fn add_response_header_adds_header() {
9689 let mw = AddResponseHeader::new("X-Custom", b"custom-value".to_vec());
9690 let ctx = test_context();
9691 let req = Request::new(crate::request::Method::Get, "/");
9692
9693 let response = Response::ok();
9694 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9695
9696 assert_eq!(
9697 header_value(&response, "X-Custom"),
9698 Some("custom-value".to_string())
9699 );
9700 }
9701
9702 #[test]
9703 fn add_response_header_preserves_existing_headers() {
9704 let mw = AddResponseHeader::new("X-New", b"new".to_vec());
9705 let ctx = test_context();
9706 let req = Request::new(crate::request::Method::Get, "/");
9707
9708 let response = Response::ok().header("X-Existing", b"existing".to_vec());
9709 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9710
9711 assert_eq!(
9712 header_value(&response, "X-Existing"),
9713 Some("existing".to_string())
9714 );
9715 assert_eq!(header_value(&response, "X-New"), Some("new".to_string()));
9716 }
9717
9718 #[test]
9719 fn add_response_header_name() {
9720 let mw = AddResponseHeader::new("X-Test", b"test".to_vec());
9721 assert_eq!(mw.name(), "AddResponseHeader");
9722 }
9723
9724 #[test]
9729 fn require_header_allows_with_header() {
9730 let mw = RequireHeader::new("X-Api-Key");
9731 let ctx = test_context();
9732 let mut req = Request::new(crate::request::Method::Get, "/");
9733 req.headers_mut()
9734 .insert("X-Api-Key", b"secret-key".to_vec());
9735
9736 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9737 assert!(matches!(result, ControlFlow::Continue));
9738 }
9739
9740 #[test]
9741 fn require_header_blocks_without_header() {
9742 let mw = RequireHeader::new("X-Api-Key");
9743 let ctx = test_context();
9744 let mut req = Request::new(crate::request::Method::Get, "/");
9745
9746 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9747
9748 match result {
9749 ControlFlow::Break(response) => {
9750 assert_eq!(response.status().as_u16(), 400);
9751 }
9752 ControlFlow::Continue => panic!("Expected Break, got Continue"),
9753 }
9754 }
9755
9756 #[test]
9757 fn require_header_name() {
9758 let mw = RequireHeader::new("X-Test");
9759 assert_eq!(mw.name(), "RequireHeader");
9760 }
9761
9762 #[test]
9767 fn path_prefix_filter_allows_matching_path() {
9768 let mw = PathPrefixFilter::new("/api");
9769 let ctx = test_context();
9770 let mut req = Request::new(crate::request::Method::Get, "/api/users");
9771
9772 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9773 assert!(matches!(result, ControlFlow::Continue));
9774 }
9775
9776 #[test]
9777 fn path_prefix_filter_allows_exact_prefix() {
9778 let mw = PathPrefixFilter::new("/api");
9779 let ctx = test_context();
9780 let mut req = Request::new(crate::request::Method::Get, "/api");
9781
9782 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9783 assert!(matches!(result, ControlFlow::Continue));
9784 }
9785
9786 #[test]
9787 fn path_prefix_filter_blocks_non_matching_path() {
9788 let mw = PathPrefixFilter::new("/api");
9789 let ctx = test_context();
9790 let mut req = Request::new(crate::request::Method::Get, "/admin/users");
9791
9792 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9793
9794 match result {
9795 ControlFlow::Break(response) => {
9796 assert_eq!(response.status().as_u16(), 404);
9797 }
9798 ControlFlow::Continue => panic!("Expected Break, got Continue"),
9799 }
9800 }
9801
9802 #[test]
9803 fn path_prefix_filter_name() {
9804 let mw = PathPrefixFilter::new("/api");
9805 assert_eq!(mw.name(), "PathPrefixFilter");
9806 }
9807
9808 #[test]
9813 fn conditional_status_applies_true_status() {
9814 let mw = ConditionalStatus::new(
9815 |req| req.path() == "/health",
9816 StatusCode::OK,
9817 StatusCode::NOT_FOUND,
9818 );
9819 let ctx = test_context();
9820 let req = Request::new(crate::request::Method::Get, "/health");
9821 let response = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
9822
9823 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9824 assert_eq!(response.status().as_u16(), 200);
9825 }
9826
9827 #[test]
9828 fn conditional_status_applies_false_status() {
9829 let mw = ConditionalStatus::new(
9830 |req| req.path() == "/health",
9831 StatusCode::OK,
9832 StatusCode::NOT_FOUND,
9833 );
9834 let ctx = test_context();
9835 let req = Request::new(crate::request::Method::Get, "/other");
9836 let response = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
9837
9838 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9839 assert_eq!(response.status().as_u16(), 404);
9840 }
9841
9842 #[test]
9843 fn conditional_status_name() {
9844 let mw = ConditionalStatus::new(|_| true, StatusCode::OK, StatusCode::NOT_FOUND);
9845 assert_eq!(mw.name(), "ConditionalStatus");
9846 }
9847
9848 #[derive(Clone)]
9853 struct LayerTestMiddleware {
9854 prefix: String,
9855 }
9856
9857 impl LayerTestMiddleware {
9858 fn new(prefix: impl Into<String>) -> Self {
9859 Self {
9860 prefix: prefix.into(),
9861 }
9862 }
9863 }
9864
9865 impl Middleware for LayerTestMiddleware {
9866 fn after<'a>(
9867 &'a self,
9868 _ctx: &'a RequestContext,
9869 _req: &'a Request,
9870 response: Response,
9871 ) -> BoxFuture<'a, Response> {
9872 let prefix = self.prefix.clone();
9873 Box::pin(async move { response.header("X-Layer", prefix.into_bytes()) })
9874 }
9875 }
9876
9877 #[test]
9878 fn layer_wraps_handler() {
9879 let layer = Layer::new(LayerTestMiddleware::new("wrapped"));
9880 let wrapped = layer.wrap(OkHandler);
9881
9882 let ctx = test_context();
9883 let mut req = Request::new(crate::request::Method::Get, "/");
9884
9885 let response = futures_executor::block_on(wrapped.call(&ctx, &mut req));
9886
9887 assert_eq!(response.status().as_u16(), 200);
9888 assert_eq!(
9889 header_value(&response, "X-Layer"),
9890 Some("wrapped".to_string())
9891 );
9892 }
9893
9894 #[test]
9895 fn layered_handles_break() {
9896 #[derive(Clone)]
9897 struct BreakingMiddleware;
9898
9899 impl Middleware for BreakingMiddleware {
9900 fn before<'a>(
9901 &'a self,
9902 _ctx: &'a RequestContext,
9903 _req: &'a mut Request,
9904 ) -> BoxFuture<'a, ControlFlow> {
9905 Box::pin(async {
9906 ControlFlow::Break(Response::with_status(StatusCode::UNAUTHORIZED))
9907 })
9908 }
9909
9910 fn after<'a>(
9911 &'a self,
9912 _ctx: &'a RequestContext,
9913 _req: &'a Request,
9914 response: Response,
9915 ) -> BoxFuture<'a, Response> {
9916 Box::pin(async move { response.header("X-After", b"ran".to_vec()) })
9917 }
9918 }
9919
9920 let layer = Layer::new(BreakingMiddleware);
9921 let wrapped = layer.wrap(OkHandler);
9922
9923 let ctx = test_context();
9924 let mut req = Request::new(crate::request::Method::Get, "/");
9925
9926 let response = futures_executor::block_on(wrapped.call(&ctx, &mut req));
9927
9928 assert_eq!(response.status().as_u16(), 401);
9930 assert_eq!(header_value(&response, "X-After"), Some("ran".to_string()));
9932 }
9933
9934 #[test]
9939 fn request_response_logger_default() {
9940 let logger = RequestResponseLogger::default();
9941 assert!(logger.log_request_headers);
9942 assert!(logger.log_response_headers);
9943 assert!(!logger.log_body);
9944 assert_eq!(logger.max_body_bytes, 1024);
9945 }
9946
9947 #[test]
9948 fn request_response_logger_builder() {
9949 let logger = RequestResponseLogger::new()
9950 .log_request_headers(false)
9951 .log_response_headers(false)
9952 .log_body(true)
9953 .max_body_bytes(2048)
9954 .redact_header("x-secret");
9955
9956 assert!(!logger.log_request_headers);
9957 assert!(!logger.log_response_headers);
9958 assert!(logger.log_body);
9959 assert_eq!(logger.max_body_bytes, 2048);
9960 assert!(logger.redact_headers.contains("x-secret"));
9961 }
9962
9963 #[test]
9964 fn request_response_logger_name() {
9965 let logger = RequestResponseLogger::new();
9966 assert_eq!(logger.name(), "RequestResponseLogger");
9967 }
9968
9969 #[test]
9974 fn middleware_stack_modifies_request_for_handler() {
9975 struct RequestModifier;
9977
9978 impl Middleware for RequestModifier {
9979 fn before<'a>(
9980 &'a self,
9981 _ctx: &'a RequestContext,
9982 req: &'a mut Request,
9983 ) -> BoxFuture<'a, ControlFlow> {
9984 req.headers_mut()
9985 .insert("X-Modified-By", b"middleware".to_vec());
9986 Box::pin(async { ControlFlow::Continue })
9987 }
9988 }
9989
9990 let mut stack = MiddlewareStack::new();
9991 stack.push(RequestModifier);
9992
9993 let ctx = test_context();
9994 let mut req = Request::new(crate::request::Method::Get, "/");
9995
9996 let response =
9997 futures_executor::block_on(stack.execute(&CheckHeaderHandler, &ctx, &mut req));
9998
9999 assert_eq!(response.status().as_u16(), 200);
10000 }
10001
10002 #[test]
10003 fn middleware_stack_multiple_response_modifications() {
10004 let mut stack = MiddlewareStack::new();
10005 stack.push(AddResponseHeader::new("X-First", b"1".to_vec()));
10006 stack.push(AddResponseHeader::new("X-Second", b"2".to_vec()));
10007 stack.push(AddResponseHeader::new("X-Third", b"3".to_vec()));
10008
10009 let ctx = test_context();
10010 let mut req = Request::new(crate::request::Method::Get, "/");
10011
10012 let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
10013
10014 assert_eq!(header_value(&response, "X-First"), Some("1".to_string()));
10016 assert_eq!(header_value(&response, "X-Second"), Some("2".to_string()));
10017 assert_eq!(header_value(&response, "X-Third"), Some("3".to_string()));
10018 }
10019
10020 #[test]
10021 fn middleware_stack_handler_receives_response_after_break() {
10022 let mut stack = MiddlewareStack::new();
10024 stack.push(ConditionalBreakMiddleware::new(
10025 "breaker",
10026 true,
10027 Arc::new(std::sync::Mutex::new(Vec::new())),
10028 ));
10029
10030 let ctx = test_context();
10031 let mut req = Request::new(crate::request::Method::Get, "/");
10032
10033 let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
10034
10035 assert_eq!(response.status().as_u16(), 403);
10036 match response.body_ref() {
10038 ResponseBody::Bytes(b) => assert_eq!(b, b"blocked"),
10039 _ => panic!("Expected Bytes body"),
10040 }
10041 }
10042
10043 #[test]
10048 fn middleware_after_can_change_status() {
10049 struct StatusChanger;
10050
10051 impl Middleware for StatusChanger {
10052 fn after<'a>(
10053 &'a self,
10054 _ctx: &'a RequestContext,
10055 _req: &'a Request,
10056 _response: Response,
10057 ) -> BoxFuture<'a, Response> {
10058 Box::pin(async { Response::with_status(StatusCode::SERVICE_UNAVAILABLE) })
10059 }
10060 }
10061
10062 let mut stack = MiddlewareStack::new();
10063 stack.push(StatusChanger);
10064
10065 let ctx = test_context();
10066 let mut req = Request::new(crate::request::Method::Get, "/");
10067
10068 let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
10069
10070 assert_eq!(response.status().as_u16(), 503);
10072 }
10073
10074 #[test]
10075 fn middleware_after_runs_even_on_error_status() {
10076 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
10077 let mut stack = MiddlewareStack::new();
10078 stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
10079
10080 let ctx = test_context();
10081 let mut req = Request::new(crate::request::Method::Get, "/");
10082
10083 let response = futures_executor::block_on(stack.execute(&ErrorHandler, &ctx, &mut req));
10084
10085 assert_eq!(response.status().as_u16(), 500);
10086
10087 let calls = log.lock().unwrap().clone();
10088 assert_eq!(calls, vec!["mw1.before", "mw1.after"]);
10090 }
10091
10092 #[test]
10097 fn wildcard_match_simple() {
10098 assert!(super::wildcard_match("*.example.com", "api.example.com"));
10099 assert!(super::wildcard_match("*.example.com", "www.example.com"));
10100 assert!(!super::wildcard_match("*.example.com", "example.com"));
10101 }
10102
10103 #[test]
10104 fn wildcard_match_suffix_pattern() {
10105 assert!(super::wildcard_match("*.txt", "file.txt"));
10107 assert!(super::wildcard_match("*.txt", "document.txt"));
10108 assert!(!super::wildcard_match("*.txt", "file.doc"));
10109 assert!(super::wildcard_match("*-suffix", "any-suffix"));
10110 }
10111
10112 #[test]
10113 fn wildcard_match_no_wildcard() {
10114 assert!(super::wildcard_match("exact", "exact"));
10115 assert!(!super::wildcard_match("exact", "different"));
10116 }
10117
10118 #[test]
10119 fn regex_match_anchored() {
10120 assert!(super::regex_match("^hello$", "hello"));
10121 assert!(!super::regex_match("^hello$", "hello world"));
10122 assert!(!super::regex_match("^hello$", "say hello"));
10123 }
10124
10125 #[test]
10126 fn regex_match_dot_wildcard() {
10127 assert!(super::regex_match("h.llo", "hello"));
10128 assert!(super::regex_match("h.llo", "hallo"));
10129 }
10130
10131 #[test]
10132 fn regex_match_star() {
10133 assert!(super::regex_match("hel*o", "hello"));
10134 assert!(super::regex_match("hel*o", "helo"));
10135 assert!(super::regex_match("hel*o", "hellllllo"));
10136 }
10137
10138 #[test]
10143 fn middleware_default_before_continues() {
10144 struct DefaultBefore;
10145 impl Middleware for DefaultBefore {}
10146
10147 let mw = DefaultBefore;
10148 let ctx = test_context();
10149 let mut req = Request::new(crate::request::Method::Get, "/");
10150
10151 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
10152 assert!(matches!(result, ControlFlow::Continue));
10153 }
10154
10155 #[test]
10156 fn middleware_default_after_passes_through() {
10157 struct DefaultAfter;
10158 impl Middleware for DefaultAfter {}
10159
10160 let mw = DefaultAfter;
10161 let ctx = test_context();
10162 let req = Request::new(crate::request::Method::Get, "/");
10163 let response = Response::with_status(StatusCode::CREATED);
10164
10165 let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10166 assert_eq!(result.status().as_u16(), 201);
10167 }
10168
10169 #[test]
10170 fn middleware_default_name_is_type_name() {
10171 struct MyCustomMiddleware;
10172 impl Middleware for MyCustomMiddleware {}
10173
10174 let mw = MyCustomMiddleware;
10175 assert!(mw.name().contains("MyCustomMiddleware"));
10176 }
10177
10178 #[test]
10183 fn security_headers_default_config() {
10184 let config = SecurityHeadersConfig::default();
10185 assert_eq!(config.x_content_type_options, Some("nosniff"));
10186 assert_eq!(config.x_frame_options, Some(XFrameOptions::Deny));
10187 assert_eq!(config.x_xss_protection, Some("0"));
10188 assert!(config.content_security_policy.is_none());
10189 assert!(config.hsts.is_none());
10190 assert_eq!(
10191 config.referrer_policy,
10192 Some(ReferrerPolicy::StrictOriginWhenCrossOrigin)
10193 );
10194 assert!(config.permissions_policy.is_none());
10195 }
10196
10197 #[test]
10198 fn security_headers_none_config() {
10199 let config = SecurityHeadersConfig::none();
10200 assert!(config.x_content_type_options.is_none());
10201 assert!(config.x_frame_options.is_none());
10202 assert!(config.x_xss_protection.is_none());
10203 assert!(config.content_security_policy.is_none());
10204 assert!(config.hsts.is_none());
10205 assert!(config.referrer_policy.is_none());
10206 assert!(config.permissions_policy.is_none());
10207 }
10208
10209 #[test]
10210 fn security_headers_strict_config() {
10211 let config = SecurityHeadersConfig::strict();
10212 assert_eq!(config.x_content_type_options, Some("nosniff"));
10213 assert_eq!(config.x_frame_options, Some(XFrameOptions::Deny));
10214 assert_eq!(
10215 config.content_security_policy,
10216 Some("default-src 'self'".to_string())
10217 );
10218 assert_eq!(config.hsts, Some((31536000, true, false)));
10219 assert_eq!(config.referrer_policy, Some(ReferrerPolicy::NoReferrer));
10220 assert!(config.permissions_policy.is_some());
10221 }
10222
10223 #[test]
10224 fn security_headers_config_builder() {
10225 let config = SecurityHeadersConfig::new()
10226 .x_frame_options(Some(XFrameOptions::SameOrigin))
10227 .content_security_policy("default-src 'self'")
10228 .hsts(86400, false, false)
10229 .referrer_policy(Some(ReferrerPolicy::Origin));
10230
10231 assert_eq!(config.x_frame_options, Some(XFrameOptions::SameOrigin));
10232 assert_eq!(
10233 config.content_security_policy,
10234 Some("default-src 'self'".to_string())
10235 );
10236 assert_eq!(config.hsts, Some((86400, false, false)));
10237 assert_eq!(config.referrer_policy, Some(ReferrerPolicy::Origin));
10238 }
10239
10240 #[test]
10241 fn security_headers_hsts_value_format() {
10242 let config = SecurityHeadersConfig::none().hsts(3600, false, false);
10244 assert_eq!(config.build_hsts_value(), Some("max-age=3600".to_string()));
10245
10246 let config = SecurityHeadersConfig::none().hsts(3600, true, false);
10248 assert_eq!(
10249 config.build_hsts_value(),
10250 Some("max-age=3600; includeSubDomains".to_string())
10251 );
10252
10253 let config = SecurityHeadersConfig::none().hsts(3600, false, true);
10255 assert_eq!(
10256 config.build_hsts_value(),
10257 Some("max-age=3600; preload".to_string())
10258 );
10259
10260 let config = SecurityHeadersConfig::none().hsts(3600, true, true);
10262 assert_eq!(
10263 config.build_hsts_value(),
10264 Some("max-age=3600; includeSubDomains; preload".to_string())
10265 );
10266 }
10267
10268 #[test]
10269 fn security_headers_middleware_adds_default_headers() {
10270 let mw = SecurityHeaders::new();
10271 let ctx = test_context();
10272 let req = Request::new(crate::request::Method::Get, "/");
10273 let response = Response::ok();
10274
10275 let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10276
10277 assert!(header_value(&result, "X-Content-Type-Options").is_some());
10279 assert!(header_value(&result, "X-Frame-Options").is_some());
10280 assert!(header_value(&result, "X-XSS-Protection").is_some());
10281 assert!(header_value(&result, "Referrer-Policy").is_some());
10282
10283 assert!(header_value(&result, "Content-Security-Policy").is_none());
10285 assert!(header_value(&result, "Strict-Transport-Security").is_none());
10286 assert!(header_value(&result, "Permissions-Policy").is_none());
10287 }
10288
10289 #[test]
10290 fn security_headers_middleware_with_csp() {
10291 let config = SecurityHeadersConfig::new()
10292 .content_security_policy("default-src 'self'; script-src 'self' 'unsafe-inline'");
10293 let mw = SecurityHeaders::with_config(config);
10294 let ctx = test_context();
10295 let req = Request::new(crate::request::Method::Get, "/");
10296 let response = Response::ok();
10297
10298 let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10299
10300 let csp = header_value(&result, "Content-Security-Policy");
10301 assert!(csp.is_some());
10302 assert_eq!(
10303 csp.unwrap(),
10304 "default-src 'self'; script-src 'self' 'unsafe-inline'"
10305 );
10306 }
10307
10308 #[test]
10309 fn security_headers_middleware_with_hsts() {
10310 let config = SecurityHeadersConfig::new().hsts(31536000, true, false);
10311 let mw = SecurityHeaders::with_config(config);
10312 let ctx = test_context();
10313 let req = Request::new(crate::request::Method::Get, "/");
10314 let response = Response::ok();
10315
10316 let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10317
10318 let hsts = header_value(&result, "Strict-Transport-Security");
10319 assert!(hsts.is_some());
10320 assert_eq!(hsts.unwrap(), "max-age=31536000; includeSubDomains");
10321 }
10322
10323 #[test]
10324 fn security_headers_middleware_name() {
10325 let mw = SecurityHeaders::new();
10326 assert_eq!(mw.name(), "SecurityHeaders");
10327 }
10328
10329 #[test]
10330 fn x_frame_options_values() {
10331 assert_eq!(XFrameOptions::Deny.as_bytes(), b"DENY");
10332 assert_eq!(XFrameOptions::SameOrigin.as_bytes(), b"SAMEORIGIN");
10333 }
10334
10335 #[test]
10336 fn referrer_policy_values() {
10337 assert_eq!(ReferrerPolicy::NoReferrer.as_bytes(), b"no-referrer");
10338 assert_eq!(
10339 ReferrerPolicy::NoReferrerWhenDowngrade.as_bytes(),
10340 b"no-referrer-when-downgrade"
10341 );
10342 assert_eq!(ReferrerPolicy::Origin.as_bytes(), b"origin");
10343 assert_eq!(
10344 ReferrerPolicy::OriginWhenCrossOrigin.as_bytes(),
10345 b"origin-when-cross-origin"
10346 );
10347 assert_eq!(ReferrerPolicy::SameOrigin.as_bytes(), b"same-origin");
10348 assert_eq!(ReferrerPolicy::StrictOrigin.as_bytes(), b"strict-origin");
10349 assert_eq!(
10350 ReferrerPolicy::StrictOriginWhenCrossOrigin.as_bytes(),
10351 b"strict-origin-when-cross-origin"
10352 );
10353 assert_eq!(ReferrerPolicy::UnsafeUrl.as_bytes(), b"unsafe-url");
10354 }
10355
10356 #[test]
10357 fn security_headers_strict_preset() {
10358 let mw = SecurityHeaders::strict();
10359 let ctx = test_context();
10360 let req = Request::new(crate::request::Method::Get, "/");
10361 let response = Response::ok();
10362
10363 let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10364
10365 assert!(header_value(&result, "X-Content-Type-Options").is_some());
10367 assert!(header_value(&result, "X-Frame-Options").is_some());
10368 assert!(header_value(&result, "Content-Security-Policy").is_some());
10369 assert!(header_value(&result, "Strict-Transport-Security").is_some());
10370 assert!(header_value(&result, "Referrer-Policy").is_some());
10371 assert!(header_value(&result, "Permissions-Policy").is_some());
10372 }
10373
10374 #[test]
10375 fn security_headers_config_clearing_methods() {
10376 let config = SecurityHeadersConfig::strict()
10377 .no_content_security_policy()
10378 .no_hsts()
10379 .no_permissions_policy();
10380
10381 assert!(config.content_security_policy.is_none());
10382 assert!(config.hsts.is_none());
10383 assert!(config.permissions_policy.is_none());
10384 }
10385
10386 #[test]
10391 fn csrf_token_generate_produces_unique_tokens() {
10392 let token1 = CsrfToken::generate();
10393 let token2 = CsrfToken::generate();
10394 assert_ne!(token1, token2);
10395 assert!(!token1.as_str().is_empty());
10396 assert!(!token2.as_str().is_empty());
10397 }
10398
10399 #[test]
10400 fn csrf_token_display() {
10401 let token = CsrfToken::new("test-token-123");
10402 assert_eq!(format!("{}", token), "test-token-123");
10403 }
10404
10405 #[test]
10406 fn csrf_config_defaults() {
10407 let config = CsrfConfig::default();
10408 assert_eq!(config.cookie_name, "csrf_token");
10409 assert_eq!(config.header_name, "x-csrf-token");
10410 assert_eq!(config.mode, CsrfMode::DoubleSubmit);
10411 assert!(!config.rotate_token);
10412 assert!(config.production);
10413 assert!(config.error_message.is_none());
10414 }
10415
10416 #[test]
10417 fn csrf_config_builder() {
10418 let config = CsrfConfig::new()
10419 .cookie_name("XSRF-TOKEN")
10420 .header_name("X-XSRF-Token")
10421 .mode(CsrfMode::HeaderOnly)
10422 .rotate_token(true)
10423 .production(false)
10424 .error_message("Custom CSRF error");
10425
10426 assert_eq!(config.cookie_name, "XSRF-TOKEN");
10427 assert_eq!(config.header_name, "X-XSRF-Token");
10428 assert_eq!(config.mode, CsrfMode::HeaderOnly);
10429 assert!(config.rotate_token);
10430 assert!(!config.production);
10431 assert_eq!(config.error_message, Some("Custom CSRF error".to_string()));
10432 }
10433
10434 #[test]
10435 fn csrf_middleware_allows_get_without_token() {
10436 let csrf = CsrfMiddleware::new();
10437 let ctx = test_context();
10438 let mut req = Request::new(crate::request::Method::Get, "/");
10439
10440 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10441 assert!(result.is_continue());
10442 assert!(req.get_extension::<CsrfToken>().is_some());
10444 }
10445
10446 #[test]
10447 fn csrf_middleware_allows_head_without_token() {
10448 let csrf = CsrfMiddleware::new();
10449 let ctx = test_context();
10450 let mut req = Request::new(crate::request::Method::Head, "/");
10451
10452 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10453 assert!(result.is_continue());
10454 }
10455
10456 #[test]
10457 fn csrf_middleware_allows_options_without_token() {
10458 let csrf = CsrfMiddleware::new();
10459 let ctx = test_context();
10460 let mut req = Request::new(crate::request::Method::Options, "/");
10461
10462 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10463 assert!(result.is_continue());
10464 }
10465
10466 #[test]
10467 fn csrf_middleware_blocks_post_without_token() {
10468 let csrf = CsrfMiddleware::new();
10469 let ctx = test_context();
10470 let mut req = Request::new(crate::request::Method::Post, "/");
10471
10472 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10473 assert!(result.is_break());
10474
10475 if let ControlFlow::Break(response) = result {
10476 assert_eq!(response.status(), StatusCode::FORBIDDEN);
10477 }
10478 }
10479
10480 #[test]
10481 fn csrf_middleware_blocks_put_without_token() {
10482 let csrf = CsrfMiddleware::new();
10483 let ctx = test_context();
10484 let mut req = Request::new(crate::request::Method::Put, "/");
10485
10486 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10487 assert!(result.is_break());
10488 }
10489
10490 #[test]
10491 fn csrf_middleware_blocks_delete_without_token() {
10492 let csrf = CsrfMiddleware::new();
10493 let ctx = test_context();
10494 let mut req = Request::new(crate::request::Method::Delete, "/");
10495
10496 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10497 assert!(result.is_break());
10498 }
10499
10500 #[test]
10501 fn csrf_middleware_blocks_patch_without_token() {
10502 let csrf = CsrfMiddleware::new();
10503 let ctx = test_context();
10504 let mut req = Request::new(crate::request::Method::Patch, "/");
10505
10506 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10507 assert!(result.is_break());
10508 }
10509
10510 #[test]
10511 fn csrf_middleware_allows_post_with_matching_tokens() {
10512 let csrf = CsrfMiddleware::new();
10513 let ctx = test_context();
10514 let mut req = Request::new(crate::request::Method::Post, "/");
10515
10516 let token = "valid-csrf-token-12345";
10518 req.headers_mut()
10519 .insert("cookie", format!("csrf_token={}", token).into_bytes());
10520 req.headers_mut()
10521 .insert("x-csrf-token", token.as_bytes().to_vec());
10522
10523 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10524 assert!(result.is_continue());
10525
10526 let stored_token = req.get_extension::<CsrfToken>().unwrap();
10528 assert_eq!(stored_token.as_str(), token);
10529 }
10530
10531 #[test]
10532 fn csrf_middleware_blocks_post_with_mismatched_tokens() {
10533 let csrf = CsrfMiddleware::new();
10534 let ctx = test_context();
10535 let mut req = Request::new(crate::request::Method::Post, "/");
10536
10537 req.headers_mut()
10539 .insert("cookie", b"csrf_token=token-in-cookie".to_vec());
10540 req.headers_mut()
10541 .insert("x-csrf-token", b"different-token".to_vec());
10542
10543 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10544 assert!(result.is_break());
10545
10546 if let ControlFlow::Break(response) = result {
10547 assert_eq!(response.status(), StatusCode::FORBIDDEN);
10548 }
10549 }
10550
10551 #[test]
10552 fn csrf_middleware_blocks_post_with_header_only_in_double_submit_mode() {
10553 let csrf = CsrfMiddleware::new();
10554 let ctx = test_context();
10555 let mut req = Request::new(crate::request::Method::Post, "/");
10556
10557 req.headers_mut()
10559 .insert("x-csrf-token", b"some-token".to_vec());
10560
10561 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10562 assert!(result.is_break());
10563 }
10564
10565 #[test]
10566 fn csrf_middleware_blocks_post_with_cookie_only_in_double_submit_mode() {
10567 let csrf = CsrfMiddleware::new();
10568 let ctx = test_context();
10569 let mut req = Request::new(crate::request::Method::Post, "/");
10570
10571 req.headers_mut()
10573 .insert("cookie", b"csrf_token=some-token".to_vec());
10574
10575 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10576 assert!(result.is_break());
10577 }
10578
10579 #[test]
10580 fn csrf_middleware_header_only_mode_accepts_header_token() {
10581 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10582 let ctx = test_context();
10583 let mut req = Request::new(crate::request::Method::Post, "/");
10584
10585 req.headers_mut()
10586 .insert("x-csrf-token", b"valid-token".to_vec());
10587
10588 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10589 assert!(result.is_continue());
10590 }
10591
10592 #[test]
10593 fn csrf_middleware_header_only_mode_rejects_empty_header() {
10594 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10595 let ctx = test_context();
10596 let mut req = Request::new(crate::request::Method::Post, "/");
10597
10598 req.headers_mut().insert("x-csrf-token", b"".to_vec());
10599
10600 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10601 assert!(result.is_break());
10602 }
10603
10604 #[test]
10605 fn csrf_middleware_sets_cookie_on_get() {
10606 let csrf = CsrfMiddleware::new();
10607 let ctx = test_context();
10608 let mut req = Request::new(crate::request::Method::Get, "/");
10609
10610 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10612
10613 let response = Response::ok();
10615 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10616
10617 let cookie_value = header_value(&result, "set-cookie");
10619 assert!(cookie_value.is_some());
10620
10621 let cookie_value = cookie_value.unwrap();
10622 assert!(cookie_value.starts_with("csrf_token="));
10623 assert!(cookie_value.contains("SameSite=Strict"));
10624 assert!(cookie_value.contains("Secure")); }
10626
10627 #[test]
10628 fn csrf_middleware_no_secure_in_dev_mode() {
10629 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().production(false));
10630 let ctx = test_context();
10631 let mut req = Request::new(crate::request::Method::Get, "/");
10632
10633 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10634
10635 let response = Response::ok();
10636 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10637
10638 let cookie_value = header_value(&result, "set-cookie").unwrap();
10639 assert!(!cookie_value.contains("Secure")); }
10641
10642 #[test]
10643 fn csrf_middleware_does_not_set_cookie_if_already_present() {
10644 let csrf = CsrfMiddleware::new();
10645 let ctx = test_context();
10646 let mut req = Request::new(crate::request::Method::Get, "/");
10647
10648 req.headers_mut()
10650 .insert("cookie", b"csrf_token=existing-token".to_vec());
10651
10652 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10653
10654 let response = Response::ok();
10655 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10656
10657 assert!(header_value(&result, "set-cookie").is_none());
10659 }
10660
10661 #[test]
10662 fn csrf_middleware_rotates_token_when_configured() {
10663 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().rotate_token(true));
10664 let ctx = test_context();
10665 let mut req = Request::new(crate::request::Method::Get, "/");
10666
10667 req.headers_mut()
10669 .insert("cookie", b"csrf_token=old-token".to_vec());
10670
10671 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10672
10673 let response = Response::ok();
10674 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10675
10676 assert!(header_value(&result, "set-cookie").is_some());
10678 }
10679
10680 #[test]
10681 fn csrf_middleware_custom_header_name() {
10682 let csrf = CsrfMiddleware::with_config(
10683 CsrfConfig::new()
10684 .header_name("X-XSRF-Token")
10685 .cookie_name("XSRF-TOKEN"),
10686 );
10687 let ctx = test_context();
10688 let mut req = Request::new(crate::request::Method::Post, "/");
10689
10690 let token = "custom-token-value";
10691 req.headers_mut()
10692 .insert("cookie", format!("XSRF-TOKEN={}", token).into_bytes());
10693 req.headers_mut()
10694 .insert("x-xsrf-token", token.as_bytes().to_vec());
10695
10696 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10697 assert!(result.is_continue());
10698 }
10699
10700 #[test]
10701 fn csrf_middleware_error_response_is_json() {
10702 let csrf = CsrfMiddleware::new();
10703 let ctx = test_context();
10704 let mut req = Request::new(crate::request::Method::Post, "/");
10705
10706 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10707
10708 if let ControlFlow::Break(response) = result {
10709 let content_type = header_value(&response, "content-type");
10710 assert_eq!(content_type, Some("application/json".to_string()));
10711
10712 if let ResponseBody::Bytes(body) = response.body_ref() {
10714 let body_str = std::str::from_utf8(body).unwrap();
10715 assert!(body_str.contains("csrf_error"));
10716 assert!(body_str.contains("x-csrf-token"));
10717 } else {
10718 panic!("Expected Bytes body");
10719 }
10720 } else {
10721 panic!("Expected Break");
10722 }
10723 }
10724
10725 #[test]
10726 fn csrf_middleware_custom_error_message() {
10727 let csrf = CsrfMiddleware::with_config(
10728 CsrfConfig::new().error_message("Access denied: invalid security token"),
10729 );
10730 let ctx = test_context();
10731 let mut req = Request::new(crate::request::Method::Post, "/");
10732
10733 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10734
10735 if let ControlFlow::Break(response) = result {
10736 if let ResponseBody::Bytes(body) = response.body_ref() {
10737 let body_str = std::str::from_utf8(body).unwrap();
10738 assert!(body_str.contains("Access denied: invalid security token"));
10739 }
10740 }
10741 }
10742
10743 #[test]
10744 fn csrf_middleware_name() {
10745 let csrf = CsrfMiddleware::new();
10746 assert_eq!(csrf.name(), "CSRF");
10747 }
10748
10749 #[test]
10750 fn csrf_middleware_parses_cookie_with_multiple_cookies() {
10751 let csrf = CsrfMiddleware::new();
10752 let ctx = test_context();
10753 let mut req = Request::new(crate::request::Method::Post, "/");
10754
10755 let token = "the-csrf-token";
10757 req.headers_mut().insert(
10758 "cookie",
10759 format!("session=abc123; csrf_token={}; user=test", token).into_bytes(),
10760 );
10761 req.headers_mut()
10762 .insert("x-csrf-token", token.as_bytes().to_vec());
10763
10764 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10765 assert!(result.is_continue());
10766 }
10767
10768 #[test]
10769 fn csrf_middleware_handles_empty_token_value() {
10770 let csrf = CsrfMiddleware::new();
10771 let ctx = test_context();
10772 let mut req = Request::new(crate::request::Method::Post, "/");
10773
10774 req.headers_mut().insert("cookie", b"csrf_token=".to_vec());
10776 req.headers_mut().insert("x-csrf-token", b"".to_vec());
10777
10778 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10779 assert!(result.is_break()); }
10781
10782 #[test]
10785 fn csrf_token_generate_many_unique() {
10786 let mut tokens = std::collections::HashSet::new();
10788 for _ in 0..100 {
10789 let token = CsrfToken::generate();
10790 assert!(
10791 tokens.insert(token.0.clone()),
10792 "Duplicate token generated: {}",
10793 token.0
10794 );
10795 }
10796 assert_eq!(tokens.len(), 100);
10797 }
10798
10799 #[test]
10800 fn csrf_token_generate_format_is_hex() {
10801 let token = CsrfToken::generate();
10802 let s = token.as_str();
10803 assert!(
10805 s.len() >= 64,
10806 "Expected at least 64 hex characters, got {} in '{s}'",
10807 s.len()
10808 );
10809 assert!(
10810 s.chars().all(|c| c.is_ascii_hexdigit()),
10811 "Non-hex character in token: {s}"
10812 );
10813 }
10814
10815 #[test]
10816 fn csrf_token_generate_minimum_length() {
10817 let token = CsrfToken::generate();
10818 assert!(
10820 token.as_str().len() >= 64,
10821 "Token too short: {} (len={})",
10822 token.as_str(),
10823 token.as_str().len()
10824 );
10825 }
10826
10827 #[test]
10828 fn csrf_token_from_str() {
10829 let token: CsrfToken = "my-token".into();
10830 assert_eq!(token.as_str(), "my-token");
10831 assert_eq!(token.0, "my-token");
10832 }
10833
10834 #[test]
10835 fn csrf_token_clone_eq() {
10836 let t1 = CsrfToken::new("abc");
10837 let t2 = t1.clone();
10838 assert_eq!(t1, t2);
10839 assert_eq!(t1.as_str(), t2.as_str());
10840 }
10841
10842 #[test]
10843 fn csrf_middleware_allows_trace_without_token() {
10844 let csrf = CsrfMiddleware::new();
10845 let ctx = test_context();
10846 let mut req = Request::new(crate::request::Method::Trace, "/");
10847
10848 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10849 assert!(result.is_continue());
10850 assert!(req.get_extension::<CsrfToken>().is_some());
10852 }
10853
10854 #[test]
10855 fn csrf_safe_method_generates_token_into_extension() {
10856 let csrf = CsrfMiddleware::new();
10857 let ctx = test_context();
10858
10859 for method in [
10860 crate::request::Method::Get,
10861 crate::request::Method::Head,
10862 crate::request::Method::Options,
10863 crate::request::Method::Trace,
10864 ] {
10865 let mut req = Request::new(method, "/test");
10866 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10867 assert!(result.is_continue());
10868 let token = req.get_extension::<CsrfToken>().expect("token missing");
10869 assert!(!token.as_str().is_empty());
10870 }
10871 }
10872
10873 #[test]
10874 fn csrf_safe_method_preserves_existing_cookie_token() {
10875 let csrf = CsrfMiddleware::new();
10876 let ctx = test_context();
10877 let mut req = Request::new(crate::request::Method::Get, "/");
10878 req.headers_mut()
10879 .insert("cookie", b"csrf_token=my-existing-token".to_vec());
10880
10881 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10882
10883 let token = req.get_extension::<CsrfToken>().unwrap();
10885 assert_eq!(token.as_str(), "my-existing-token");
10886 }
10887
10888 #[test]
10889 fn csrf_valid_post_stores_token_in_extension() {
10890 let csrf = CsrfMiddleware::new();
10891 let ctx = test_context();
10892 let mut req = Request::new(crate::request::Method::Post, "/submit");
10893
10894 let tk = "valid-token-xyz";
10895 req.headers_mut()
10896 .insert("cookie", format!("csrf_token={}", tk).into_bytes());
10897 req.headers_mut()
10898 .insert("x-csrf-token", tk.as_bytes().to_vec());
10899
10900 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10901 assert!(result.is_continue());
10902 let stored = req.get_extension::<CsrfToken>().unwrap();
10903 assert_eq!(stored.as_str(), tk);
10904 }
10905
10906 #[test]
10907 fn csrf_double_submit_both_empty_strings_rejected() {
10908 let csrf = CsrfMiddleware::new();
10909 let ctx = test_context();
10910 let mut req = Request::new(crate::request::Method::Post, "/");
10911
10912 req.headers_mut().insert("cookie", b"csrf_token=".to_vec());
10914 req.headers_mut().insert("x-csrf-token", b"".to_vec());
10915
10916 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10917 assert!(result.is_break());
10918 }
10919
10920 #[test]
10921 fn csrf_double_submit_matching_empty_rejected() {
10922 let csrf = CsrfMiddleware::new();
10924 let ctx = test_context();
10925 let mut req = Request::new(crate::request::Method::Post, "/");
10926
10927 req.headers_mut().insert("cookie", b"csrf_token=".to_vec());
10928 req.headers_mut().insert("x-csrf-token", b"".to_vec());
10929
10930 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10931 assert!(
10932 result.is_break(),
10933 "Empty matching tokens should be rejected"
10934 );
10935 }
10936
10937 #[test]
10938 fn csrf_header_only_mode_does_not_need_cookie() {
10939 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10940 let ctx = test_context();
10941 let mut req = Request::new(crate::request::Method::Post, "/");
10942
10943 req.headers_mut()
10945 .insert("x-csrf-token", b"header-only-token".to_vec());
10946
10947 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10948 assert!(result.is_continue());
10949 let token = req.get_extension::<CsrfToken>().unwrap();
10950 assert_eq!(token.as_str(), "header-only-token");
10951 }
10952
10953 #[test]
10954 fn csrf_header_only_mode_ignores_mismatched_cookie() {
10955 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10957 let ctx = test_context();
10958 let mut req = Request::new(crate::request::Method::Post, "/");
10959
10960 req.headers_mut()
10961 .insert("cookie", b"csrf_token=different-value".to_vec());
10962 req.headers_mut()
10963 .insert("x-csrf-token", b"header-value".to_vec());
10964
10965 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10966 assert!(result.is_continue(), "HeaderOnly should ignore cookie");
10967 }
10968
10969 #[test]
10970 fn csrf_header_only_mode_rejects_no_header() {
10971 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10972 let ctx = test_context();
10973 let mut req = Request::new(crate::request::Method::Post, "/");
10974 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10976 assert!(result.is_break());
10977 }
10978
10979 #[test]
10980 fn csrf_header_only_error_message_mentions_header() {
10981 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10982 let ctx = test_context();
10983 let mut req = Request::new(crate::request::Method::Post, "/");
10984
10985 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10986 if let ControlFlow::Break(response) = result {
10987 if let ResponseBody::Bytes(body) = response.body_ref() {
10988 let body_str = std::str::from_utf8(body).unwrap();
10989 assert!(
10990 body_str.contains("missing in header"),
10991 "Expected 'missing in header' in: {}",
10992 body_str
10993 );
10994 }
10995 } else {
10996 panic!("Expected Break");
10997 }
10998 }
10999
11000 #[test]
11001 fn csrf_mismatch_error_differs_from_missing_error() {
11002 let csrf = CsrfMiddleware::new();
11003 let ctx = test_context();
11004
11005 let mut req_missing = Request::new(crate::request::Method::Post, "/");
11007 let missing_result = futures_executor::block_on(csrf.before(&ctx, &mut req_missing));
11008 let missing_body = match missing_result {
11009 ControlFlow::Break(r) => match r.body_ref() {
11010 ResponseBody::Bytes(b) => std::str::from_utf8(b).unwrap().to_string(),
11011 ResponseBody::Empty | ResponseBody::Stream(_) => panic!("Expected Bytes"),
11012 },
11013 ControlFlow::Continue => panic!("Expected Break"),
11014 };
11015
11016 let mut req_mismatch = Request::new(crate::request::Method::Post, "/");
11018 req_mismatch
11019 .headers_mut()
11020 .insert("cookie", b"csrf_token=aaa".to_vec());
11021 req_mismatch
11022 .headers_mut()
11023 .insert("x-csrf-token", b"bbb".to_vec());
11024 let mismatch_result = futures_executor::block_on(csrf.before(&ctx, &mut req_mismatch));
11025 let mismatch_body = match mismatch_result {
11026 ControlFlow::Break(r) => match r.body_ref() {
11027 ResponseBody::Bytes(b) => std::str::from_utf8(b).unwrap().to_string(),
11028 ResponseBody::Empty | ResponseBody::Stream(_) => panic!("Expected Bytes"),
11029 },
11030 ControlFlow::Continue => panic!("Expected Break"),
11031 };
11032
11033 assert_ne!(
11035 missing_body, mismatch_body,
11036 "Missing vs mismatch should have different error messages"
11037 );
11038 assert!(missing_body.contains("missing"));
11039 assert!(mismatch_body.contains("mismatch"));
11040 }
11041
11042 #[test]
11043 fn csrf_cookie_not_httponly() {
11044 let csrf = CsrfMiddleware::new();
11046 let ctx = test_context();
11047 let mut req = Request::new(crate::request::Method::Get, "/");
11048
11049 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11050 let response = Response::ok();
11051 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11052
11053 let cookie_value = header_value(&result, "set-cookie").unwrap();
11054 assert!(
11055 !cookie_value.to_lowercase().contains("httponly"),
11056 "CSRF cookie must NOT be HttpOnly (needs JS access), got: {}",
11057 cookie_value
11058 );
11059 }
11060
11061 #[test]
11062 fn csrf_cookie_has_path_slash() {
11063 let csrf = CsrfMiddleware::new();
11064 let ctx = test_context();
11065 let mut req = Request::new(crate::request::Method::Get, "/");
11066
11067 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11068 let response = Response::ok();
11069 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11070
11071 let cookie_value = header_value(&result, "set-cookie").unwrap();
11072 assert!(
11073 cookie_value.contains("Path=/"),
11074 "Cookie should have Path=/, got: {}",
11075 cookie_value
11076 );
11077 }
11078
11079 #[test]
11080 fn csrf_cookie_has_samesite_strict() {
11081 let csrf = CsrfMiddleware::new();
11082 let ctx = test_context();
11083 let mut req = Request::new(crate::request::Method::Get, "/");
11084
11085 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11086 let response = Response::ok();
11087 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11088
11089 let cookie_value = header_value(&result, "set-cookie").unwrap();
11090 assert!(
11091 cookie_value.contains("SameSite=Strict"),
11092 "Cookie should have SameSite=Strict, got: {}",
11093 cookie_value
11094 );
11095 }
11096
11097 #[test]
11098 fn csrf_production_mode_sets_secure_flag() {
11099 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().production(true));
11100 let ctx = test_context();
11101 let mut req = Request::new(crate::request::Method::Get, "/");
11102
11103 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11104 let response = Response::ok();
11105 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11106
11107 let cookie_value = header_value(&result, "set-cookie").unwrap();
11108 assert!(
11109 cookie_value.contains("Secure"),
11110 "Production cookie must have Secure flag, got: {}",
11111 cookie_value
11112 );
11113 }
11114
11115 #[test]
11116 fn csrf_no_set_cookie_on_post_response() {
11117 let csrf = CsrfMiddleware::new();
11119 let ctx = test_context();
11120 let mut req = Request::new(crate::request::Method::Post, "/");
11121
11122 let token = "valid-token";
11123 req.headers_mut()
11124 .insert("cookie", format!("csrf_token={}", token).into_bytes());
11125 req.headers_mut()
11126 .insert("x-csrf-token", token.as_bytes().to_vec());
11127
11128 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11129 let response = Response::ok();
11130 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11131
11132 assert!(
11133 header_value(&result, "set-cookie").is_none(),
11134 "POST response should not set CSRF cookie"
11135 );
11136 }
11137
11138 #[test]
11139 fn csrf_head_method_sets_cookie() {
11140 let csrf = CsrfMiddleware::new();
11141 let ctx = test_context();
11142 let mut req = Request::new(crate::request::Method::Head, "/");
11143
11144 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11145 let response = Response::ok();
11146 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11147
11148 assert!(
11149 header_value(&result, "set-cookie").is_some(),
11150 "HEAD response should set CSRF cookie"
11151 );
11152 }
11153
11154 #[test]
11155 fn csrf_options_method_sets_cookie() {
11156 let csrf = CsrfMiddleware::new();
11157 let ctx = test_context();
11158 let mut req = Request::new(crate::request::Method::Options, "/");
11159
11160 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11161 let response = Response::ok();
11162 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11163
11164 assert!(
11165 header_value(&result, "set-cookie").is_some(),
11166 "OPTIONS response should set CSRF cookie"
11167 );
11168 }
11169
11170 #[test]
11171 fn csrf_rotation_produces_different_token_in_cookie() {
11172 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().rotate_token(true));
11173 let ctx = test_context();
11174 let mut req = Request::new(crate::request::Method::Get, "/");
11175
11176 let old_token = "old-token-value";
11177 req.headers_mut()
11178 .insert("cookie", format!("csrf_token={}", old_token).into_bytes());
11179
11180 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11181 let response = Response::ok();
11182 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11183
11184 let cookie_value = header_value(&result, "set-cookie").unwrap();
11185 assert!(cookie_value.starts_with("csrf_token="));
11190 }
11191
11192 #[test]
11193 fn csrf_no_rotation_skips_set_cookie_when_present() {
11194 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().rotate_token(false));
11195 let ctx = test_context();
11196 let mut req = Request::new(crate::request::Method::Get, "/");
11197
11198 req.headers_mut()
11199 .insert("cookie", b"csrf_token=existing".to_vec());
11200
11201 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11202 let response = Response::ok();
11203 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11204
11205 assert!(
11206 header_value(&result, "set-cookie").is_none(),
11207 "Without rotation, should not re-set existing cookie"
11208 );
11209 }
11210
11211 #[test]
11212 fn csrf_custom_cookie_name_in_set_cookie_response() {
11213 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().cookie_name("XSRF-TOKEN"));
11214 let ctx = test_context();
11215 let mut req = Request::new(crate::request::Method::Get, "/");
11216
11217 let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11218 let response = Response::ok();
11219 let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11220
11221 let cookie_value = header_value(&result, "set-cookie").unwrap();
11222 assert!(
11223 cookie_value.starts_with("XSRF-TOKEN="),
11224 "Custom cookie name should appear in Set-Cookie, got: {}",
11225 cookie_value
11226 );
11227 }
11228
11229 #[test]
11230 fn csrf_custom_header_name_validated() {
11231 let csrf = CsrfMiddleware::with_config(
11232 CsrfConfig::new()
11233 .header_name("X-Custom-CSRF")
11234 .cookie_name("my_csrf"),
11235 );
11236 let ctx = test_context();
11237 let mut req = Request::new(crate::request::Method::Post, "/");
11238
11239 let token = "custom-tok";
11240 req.headers_mut()
11241 .insert("cookie", format!("my_csrf={}", token).into_bytes());
11242 req.headers_mut()
11243 .insert("x-custom-csrf", token.as_bytes().to_vec());
11244
11245 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11246 assert!(result.is_continue());
11247 }
11248
11249 #[test]
11250 fn csrf_custom_header_name_wrong_header_rejected() {
11251 let csrf = CsrfMiddleware::with_config(CsrfConfig::new().header_name("X-Custom-CSRF"));
11252 let ctx = test_context();
11253 let mut req = Request::new(crate::request::Method::Post, "/");
11254
11255 let token = "some-token";
11256 req.headers_mut()
11257 .insert("cookie", format!("csrf_token={}", token).into_bytes());
11258 req.headers_mut()
11260 .insert("x-csrf-token", token.as_bytes().to_vec());
11261
11262 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11263 assert!(result.is_break(), "Wrong header name should be rejected");
11264 }
11265
11266 #[test]
11267 fn csrf_cookie_parsing_multiple_cookies_picks_correct() {
11268 let csrf = CsrfMiddleware::new();
11269 let ctx = test_context();
11270 let mut req = Request::new(crate::request::Method::Post, "/");
11271
11272 let token = "correct-csrf";
11273 req.headers_mut().insert(
11274 "cookie",
11275 format!("session=abc; other=xyz; csrf_token={}; tracking=123", token).into_bytes(),
11276 );
11277 req.headers_mut()
11278 .insert("x-csrf-token", token.as_bytes().to_vec());
11279
11280 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11281 assert!(result.is_continue());
11282 }
11283
11284 #[test]
11285 fn csrf_cookie_parsing_spaces_around_semicolons() {
11286 let csrf = CsrfMiddleware::new();
11287 let ctx = test_context();
11288 let mut req = Request::new(crate::request::Method::Post, "/");
11289
11290 let token = "spaced-token";
11291 req.headers_mut().insert(
11292 "cookie",
11293 format!("session=abc ; csrf_token={} ; other=xyz", token).into_bytes(),
11294 );
11295 req.headers_mut()
11296 .insert("x-csrf-token", token.as_bytes().to_vec());
11297
11298 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11299 assert!(result.is_continue());
11300 }
11301
11302 #[test]
11303 fn csrf_error_response_status_is_403() {
11304 let csrf = CsrfMiddleware::new();
11305 let ctx = test_context();
11306
11307 for method in [
11309 crate::request::Method::Post,
11310 crate::request::Method::Put,
11311 crate::request::Method::Delete,
11312 crate::request::Method::Patch,
11313 ] {
11314 let mut req = Request::new(method, "/");
11315 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11316 match result {
11317 ControlFlow::Break(response) => {
11318 assert_eq!(
11319 response.status(),
11320 StatusCode::FORBIDDEN,
11321 "Expected 403 for {:?}",
11322 method
11323 );
11324 }
11325 ControlFlow::Continue => panic!("Expected Break for {:?}", method),
11326 }
11327 }
11328 }
11329
11330 #[test]
11331 fn csrf_error_body_json_structure() {
11332 let csrf = CsrfMiddleware::new();
11333 let ctx = test_context();
11334 let mut req = Request::new(crate::request::Method::Post, "/");
11335
11336 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11337 if let ControlFlow::Break(response) = result {
11338 if let ResponseBody::Bytes(body) = response.body_ref() {
11339 let body_str = std::str::from_utf8(body).unwrap();
11340 let parsed: serde_json::Value = serde_json::from_str(body_str)
11342 .unwrap_or_else(|e| panic!("Invalid JSON: {}: {}", body_str, e));
11343 assert!(parsed["detail"].is_array());
11344 let detail = &parsed["detail"][0];
11345 assert_eq!(detail["type"], "csrf_error");
11346 assert!(detail["loc"].is_array());
11347 assert_eq!(detail["loc"][0], "header");
11348 assert_eq!(detail["loc"][1], "x-csrf-token");
11349 assert!(detail["msg"].is_string());
11350 } else {
11351 panic!("Expected Bytes body");
11352 }
11353 } else {
11354 panic!("Expected Break");
11355 }
11356 }
11357
11358 #[test]
11359 fn csrf_default_trait() {
11360 let csrf = CsrfMiddleware::default();
11361 assert_eq!(csrf.name(), "CSRF");
11362 let ctx = test_context();
11364 let mut req = Request::new(crate::request::Method::Get, "/");
11365 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11366 assert!(result.is_continue());
11367 }
11368
11369 #[test]
11370 fn csrf_mode_default_is_double_submit() {
11371 assert_eq!(CsrfMode::default(), CsrfMode::DoubleSubmit);
11372 }
11373
11374 #[test]
11375 fn csrf_double_submit_both_present_same_non_empty_passes() {
11376 let csrf = CsrfMiddleware::new();
11378 let ctx = test_context();
11379
11380 let token = "a1b2c3d4e5f6";
11381 let mut req = Request::new(crate::request::Method::Delete, "/resource/1");
11382 req.headers_mut()
11383 .insert("cookie", format!("csrf_token={}", token).into_bytes());
11384 req.headers_mut()
11385 .insert("x-csrf-token", token.as_bytes().to_vec());
11386
11387 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11388 assert!(result.is_continue());
11389 }
11390
11391 #[test]
11392 fn csrf_double_submit_case_sensitive() {
11393 let csrf = CsrfMiddleware::new();
11395 let ctx = test_context();
11396 let mut req = Request::new(crate::request::Method::Post, "/");
11397
11398 req.headers_mut()
11399 .insert("cookie", b"csrf_token=AbCdEf".to_vec());
11400 req.headers_mut().insert("x-csrf-token", b"abcdef".to_vec());
11401
11402 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11403 assert!(
11404 result.is_break(),
11405 "Token comparison should be case-sensitive"
11406 );
11407 }
11408
11409 #[test]
11410 fn csrf_token_cookie_extractor_reads_csrf_cookie() {
11411 use crate::extract::{CookieName, CsrfTokenCookie};
11413 assert_eq!(CsrfTokenCookie::NAME, "csrf_token");
11414 }
11415
11416 #[test]
11417 fn csrf_make_set_cookie_header_value_production() {
11418 let value = CsrfMiddleware::make_set_cookie_header_value("csrf_token", "tok123", true);
11419 let s = std::str::from_utf8(&value).unwrap();
11420 assert!(s.contains("csrf_token=tok123"));
11421 assert!(s.contains("Path=/"));
11422 assert!(s.contains("SameSite=Strict"));
11423 assert!(s.contains("Secure"));
11424 assert!(!s.to_lowercase().contains("httponly"));
11425 }
11426
11427 #[test]
11428 fn csrf_make_set_cookie_header_value_development() {
11429 let value = CsrfMiddleware::make_set_cookie_header_value("csrf_token", "tok123", false);
11430 let s = std::str::from_utf8(&value).unwrap();
11431 assert!(s.contains("csrf_token=tok123"));
11432 assert!(s.contains("Path=/"));
11433 assert!(s.contains("SameSite=Strict"));
11434 assert!(!s.contains("Secure"));
11435 }
11436
11437 #[test]
11438 fn csrf_before_after_full_cycle_get_then_post() {
11439 let csrf = CsrfMiddleware::new();
11441 let ctx = test_context();
11442
11443 let mut get_req = Request::new(crate::request::Method::Get, "/form");
11445 let _ = futures_executor::block_on(csrf.before(&ctx, &mut get_req));
11446 let get_response = Response::ok();
11447 let get_result = futures_executor::block_on(csrf.after(&ctx, &get_req, get_response));
11448
11449 let set_cookie = header_value(&get_result, "set-cookie").expect("GET should set cookie");
11450 let token_value = set_cookie
11452 .strip_prefix("csrf_token=")
11453 .unwrap()
11454 .split(';')
11455 .next()
11456 .unwrap();
11457 assert!(!token_value.is_empty());
11458
11459 let mut post_req = Request::new(crate::request::Method::Post, "/form");
11461 post_req
11462 .headers_mut()
11463 .insert("cookie", format!("csrf_token={}", token_value).into_bytes());
11464 post_req
11465 .headers_mut()
11466 .insert("x-csrf-token", token_value.as_bytes().to_vec());
11467
11468 let result = futures_executor::block_on(csrf.before(&ctx, &mut post_req));
11469 assert!(result.is_continue(), "POST with valid token should pass");
11470 }
11471
11472 #[test]
11473 fn csrf_all_state_changing_methods_require_token() {
11474 let csrf = CsrfMiddleware::new();
11475 let ctx = test_context();
11476
11477 for method in [
11478 crate::request::Method::Post,
11479 crate::request::Method::Put,
11480 crate::request::Method::Delete,
11481 crate::request::Method::Patch,
11482 ] {
11483 let mut req = Request::new(method, "/resource");
11484 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11485 assert!(
11486 result.is_break(),
11487 "{:?} without token should be rejected",
11488 method
11489 );
11490 }
11491 }
11492
11493 #[test]
11494 fn csrf_all_safe_methods_pass_without_token() {
11495 let csrf = CsrfMiddleware::new();
11496 let ctx = test_context();
11497
11498 for method in [
11499 crate::request::Method::Get,
11500 crate::request::Method::Head,
11501 crate::request::Method::Options,
11502 crate::request::Method::Trace,
11503 ] {
11504 let mut req = Request::new(method, "/resource");
11505 let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11506 assert!(
11507 result.is_continue(),
11508 "{:?} should be allowed without token",
11509 method
11510 );
11511 }
11512 }
11513
11514 #[derive(Clone)]
11521 struct OrderRecordingMiddleware {
11522 id: &'static str,
11523 log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11524 }
11525
11526 impl OrderRecordingMiddleware {
11527 fn new(id: &'static str, log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11528 Self { id, log }
11529 }
11530 }
11531
11532 impl Middleware for OrderRecordingMiddleware {
11533 fn before<'a>(
11534 &'a self,
11535 _ctx: &'a RequestContext,
11536 _req: &'a mut Request,
11537 ) -> BoxFuture<'a, ControlFlow> {
11538 let id = self.id;
11539 let log = self.log.clone();
11540 Box::pin(async move {
11541 log.lock().unwrap().push(format!("{id}:before"));
11542 ControlFlow::Continue
11543 })
11544 }
11545
11546 fn after<'a>(
11547 &'a self,
11548 _ctx: &'a RequestContext,
11549 _req: &'a Request,
11550 response: Response,
11551 ) -> BoxFuture<'a, Response> {
11552 let id = self.id;
11553 let log = self.log.clone();
11554 Box::pin(async move {
11555 log.lock().unwrap().push(format!("{id}:after"));
11556 response
11557 })
11558 }
11559
11560 fn name(&self) -> &'static str {
11561 "OrderRecording"
11562 }
11563 }
11564
11565 struct ShortCircuitMiddleware {
11567 id: &'static str,
11568 log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11569 }
11570
11571 impl ShortCircuitMiddleware {
11572 fn new(id: &'static str, log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11573 Self { id, log }
11574 }
11575 }
11576
11577 impl Middleware for ShortCircuitMiddleware {
11578 fn before<'a>(
11579 &'a self,
11580 _ctx: &'a RequestContext,
11581 _req: &'a mut Request,
11582 ) -> BoxFuture<'a, ControlFlow> {
11583 let id = self.id;
11584 let log = self.log.clone();
11585 Box::pin(async move {
11586 log.lock().unwrap().push(format!("{id}:before:break"));
11587 ControlFlow::Break(
11588 Response::with_status(StatusCode::FORBIDDEN)
11589 .body(ResponseBody::Bytes(b"short-circuited".to_vec())),
11590 )
11591 })
11592 }
11593
11594 fn after<'a>(
11595 &'a self,
11596 _ctx: &'a RequestContext,
11597 _req: &'a Request,
11598 response: Response,
11599 ) -> BoxFuture<'a, Response> {
11600 let id = self.id;
11601 let log = self.log.clone();
11602 Box::pin(async move {
11603 log.lock().unwrap().push(format!("{id}:after"));
11604 response
11605 })
11606 }
11607
11608 fn name(&self) -> &'static str {
11609 "ShortCircuit"
11610 }
11611 }
11612
11613 struct RecordingHandler {
11615 log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11616 }
11617
11618 impl RecordingHandler {
11619 fn new(log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11620 Self { log }
11621 }
11622 }
11623
11624 impl Handler for RecordingHandler {
11625 fn call<'a>(
11626 &'a self,
11627 _ctx: &'a RequestContext,
11628 _req: &'a mut Request,
11629 ) -> BoxFuture<'a, Response> {
11630 let log = self.log.clone();
11631 Box::pin(async move {
11632 log.lock().unwrap().push("handler".to_string());
11633 Response::ok().body(ResponseBody::Bytes(b"ok".to_vec()))
11634 })
11635 }
11636 }
11637
11638 #[test]
11639 fn middleware_stack_three_middleware_onion_order() {
11640 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11644
11645 let mut stack = MiddlewareStack::new();
11646 stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11647 stack.push(OrderRecordingMiddleware::new("mw2", log.clone()));
11648 stack.push(OrderRecordingMiddleware::new("mw3", log.clone()));
11649
11650 let handler = RecordingHandler::new(log.clone());
11651 let ctx = test_context();
11652 let mut req = Request::new(crate::request::Method::Get, "/");
11653
11654 let _response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11655
11656 let execution_log = log.lock().unwrap().clone();
11657 assert_eq!(
11658 execution_log,
11659 vec![
11660 "mw1:before",
11661 "mw2:before",
11662 "mw3:before",
11663 "handler",
11664 "mw3:after",
11665 "mw2:after",
11666 "mw1:after",
11667 ]
11668 );
11669 }
11670
11671 #[test]
11672 fn middleware_stack_short_circuit_runs_prior_after_hooks() {
11673 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11681
11682 let mut stack = MiddlewareStack::new();
11683 stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11684 stack.push(ShortCircuitMiddleware::new("mw2", log.clone()));
11685 stack.push(OrderRecordingMiddleware::new("mw3", log.clone()));
11686
11687 let handler = RecordingHandler::new(log.clone());
11688 let ctx = test_context();
11689 let mut req = Request::new(crate::request::Method::Get, "/");
11690
11691 let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11692
11693 assert_eq!(response.status().as_u16(), 403);
11695
11696 let execution_log = log.lock().unwrap().clone();
11697 assert_eq!(
11700 execution_log,
11701 vec!["mw1:before", "mw2:before:break", "mw1:after",]
11702 );
11703 }
11704
11705 #[test]
11706 fn middleware_stack_first_middleware_short_circuits() {
11707 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11711
11712 let mut stack = MiddlewareStack::new();
11713 stack.push(ShortCircuitMiddleware::new("mw1", log.clone()));
11714 stack.push(OrderRecordingMiddleware::new("mw2", log.clone()));
11715
11716 let handler = RecordingHandler::new(log.clone());
11717 let ctx = test_context();
11718 let mut req = Request::new(crate::request::Method::Get, "/");
11719
11720 let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11721 assert_eq!(response.status().as_u16(), 403);
11722
11723 let execution_log = log.lock().unwrap().clone();
11724 assert_eq!(execution_log, vec!["mw1:before:break",]);
11726 }
11727
11728 #[test]
11729 fn middleware_stack_empty_runs_handler_only() {
11730 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11732
11733 let stack = MiddlewareStack::new();
11734 let handler = RecordingHandler::new(log.clone());
11735 let ctx = test_context();
11736 let mut req = Request::new(crate::request::Method::Get, "/");
11737
11738 let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11739 assert_eq!(response.status().as_u16(), 200);
11740
11741 let execution_log = log.lock().unwrap().clone();
11742 assert_eq!(execution_log, vec!["handler"]);
11743 }
11744
11745 #[test]
11746 fn middleware_stack_single_middleware_ordering() {
11747 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11749
11750 let mut stack = MiddlewareStack::new();
11751 stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11752
11753 let handler = RecordingHandler::new(log.clone());
11754 let ctx = test_context();
11755 let mut req = Request::new(crate::request::Method::Get, "/");
11756
11757 let _response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11758
11759 let execution_log = log.lock().unwrap().clone();
11760 assert_eq!(execution_log, vec!["mw1:before", "handler", "mw1:after",]);
11761 }
11762
11763 #[test]
11764 fn middleware_stack_five_middleware_onion_order() {
11765 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11767
11768 let mut stack = MiddlewareStack::new();
11769 stack.push(OrderRecordingMiddleware::new("a", log.clone()));
11770 stack.push(OrderRecordingMiddleware::new("b", log.clone()));
11771 stack.push(OrderRecordingMiddleware::new("c", log.clone()));
11772 stack.push(OrderRecordingMiddleware::new("d", log.clone()));
11773 stack.push(OrderRecordingMiddleware::new("e", log.clone()));
11774
11775 let handler = RecordingHandler::new(log.clone());
11776 let ctx = test_context();
11777 let mut req = Request::new(crate::request::Method::Get, "/");
11778
11779 let _response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11780
11781 let execution_log = log.lock().unwrap().clone();
11782 assert_eq!(
11783 execution_log,
11784 vec![
11785 "a:before", "b:before", "c:before", "d:before", "e:before", "handler", "e:after",
11786 "d:after", "c:after", "b:after", "a:after",
11787 ]
11788 );
11789 }
11790
11791 #[test]
11792 fn middleware_stack_short_circuit_at_end_runs_prior_afters() {
11793 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11800
11801 let mut stack = MiddlewareStack::new();
11802 stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11803 stack.push(OrderRecordingMiddleware::new("mw2", log.clone()));
11804 stack.push(ShortCircuitMiddleware::new("mw3", log.clone()));
11805
11806 let handler = RecordingHandler::new(log.clone());
11807 let ctx = test_context();
11808 let mut req = Request::new(crate::request::Method::Get, "/");
11809
11810 let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11811 assert_eq!(response.status().as_u16(), 403);
11812
11813 let execution_log = log.lock().unwrap().clone();
11814 assert_eq!(
11816 execution_log,
11817 vec![
11818 "mw1:before",
11819 "mw2:before",
11820 "mw3:before:break",
11821 "mw2:after",
11822 "mw1:after",
11823 ]
11824 );
11825 }
11826
11827 struct ModifyingMiddleware {
11829 id: &'static str,
11830 log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11831 }
11832
11833 impl ModifyingMiddleware {
11834 fn new(id: &'static str, log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11835 Self { id, log }
11836 }
11837 }
11838
11839 impl Middleware for ModifyingMiddleware {
11840 fn before<'a>(
11841 &'a self,
11842 _ctx: &'a RequestContext,
11843 req: &'a mut Request,
11844 ) -> BoxFuture<'a, ControlFlow> {
11845 let id = self.id;
11846 let log = self.log.clone();
11847 Box::pin(async move {
11848 req.headers_mut()
11850 .insert(format!("x-{id}-before"), b"true".to_vec());
11851 log.lock().unwrap().push(format!("{id}:before"));
11852 ControlFlow::Continue
11853 })
11854 }
11855
11856 fn after<'a>(
11857 &'a self,
11858 _ctx: &'a RequestContext,
11859 _req: &'a Request,
11860 response: Response,
11861 ) -> BoxFuture<'a, Response> {
11862 let id = self.id;
11863 let log = self.log.clone();
11864 Box::pin(async move {
11865 log.lock().unwrap().push(format!("{id}:after"));
11866 response.header(format!("x-{id}-after"), b"true".to_vec())
11868 })
11869 }
11870
11871 fn name(&self) -> &'static str {
11872 "Modifying"
11873 }
11874 }
11875
11876 #[test]
11877 fn middleware_stack_modifications_accumulate_correctly() {
11878 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11881
11882 let mut stack = MiddlewareStack::new();
11883 stack.push(ModifyingMiddleware::new("mw1", log.clone()));
11884 stack.push(ModifyingMiddleware::new("mw2", log.clone()));
11885 stack.push(ModifyingMiddleware::new("mw3", log.clone()));
11886
11887 let handler = RecordingHandler::new(log.clone());
11888 let ctx = test_context();
11889 let mut req = Request::new(crate::request::Method::Get, "/");
11890
11891 let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11892
11893 assert!(header_value(&response, "x-mw1-after").is_some());
11895 assert!(header_value(&response, "x-mw2-after").is_some());
11896 assert!(header_value(&response, "x-mw3-after").is_some());
11897
11898 assert!(req.headers().contains("x-mw1-before"));
11900 assert!(req.headers().contains("x-mw2-before"));
11901 assert!(req.headers().contains("x-mw3-before"));
11902 }
11903
11904 #[test]
11905 fn layer_wrap_maintains_middleware_order() {
11906 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11908
11909 let layer = Layer::new(OrderRecordingMiddleware::new("layer", log.clone()));
11911
11912 let handler = RecordingHandler::new(log.clone());
11914 let layered_handler = layer.wrap(handler);
11915
11916 let ctx = test_context();
11917 let mut req = Request::new(crate::request::Method::Get, "/");
11918
11919 let _response = futures_executor::block_on(layered_handler.call(&ctx, &mut req));
11921
11922 let execution_log = log.lock().unwrap().clone();
11923 assert_eq!(
11924 execution_log,
11925 vec!["layer:before", "handler", "layer:after",]
11926 );
11927 }
11928}
11929
11930#[cfg(all(test, feature = "compression"))]
11935mod compression_tests {
11936 use super::*;
11937 use crate::request::Method;
11938 use crate::response::ResponseBody;
11939
11940 fn test_context() -> RequestContext {
11941 RequestContext::new(asupersync::Cx::for_testing(), 1)
11942 }
11943
11944 #[test]
11945 fn compression_config_defaults() {
11946 let config = CompressionConfig::default();
11947 assert_eq!(config.min_size, 1024);
11948 assert_eq!(config.level, 6);
11949 assert!(!config.skip_content_types.is_empty());
11950 }
11951
11952 #[test]
11953 fn compression_config_builder() {
11954 let config = CompressionConfig::new().min_size(512).level(9);
11955 assert_eq!(config.min_size, 512);
11956 assert_eq!(config.level, 9);
11957 }
11958
11959 #[test]
11960 fn compression_level_clamped() {
11961 let config = CompressionConfig::new().level(100);
11962 assert_eq!(config.level, 9);
11963
11964 let config = CompressionConfig::new().level(0);
11965 assert_eq!(config.level, 1);
11966 }
11967
11968 #[test]
11969 fn skip_content_type_exact_match() {
11970 let config = CompressionConfig::default();
11971 assert!(config.should_skip_content_type("image/jpeg"));
11972 assert!(config.should_skip_content_type("image/jpeg; charset=utf-8"));
11973 assert!(!config.should_skip_content_type("text/html"));
11974 }
11975
11976 #[test]
11977 fn skip_content_type_prefix_match() {
11978 let config = CompressionConfig::default();
11979 assert!(config.should_skip_content_type("video/mp4"));
11981 assert!(config.should_skip_content_type("video/webm"));
11982 assert!(config.should_skip_content_type("audio/mpeg"));
11983 }
11984
11985 #[test]
11986 fn compression_skips_small_responses() {
11987 let middleware = CompressionMiddleware::new();
11988 let ctx = test_context();
11989
11990 let mut req = Request::new(Method::Get, "/");
11992 req.headers_mut()
11993 .insert("accept-encoding", b"gzip".to_vec());
11994
11995 let response = Response::ok()
11997 .header("content-type", b"text/plain".to_vec())
11998 .body(ResponseBody::Bytes(b"Hello, World!".to_vec()));
11999
12000 let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12002
12003 let has_encoding = result
12005 .headers()
12006 .iter()
12007 .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12008 assert!(!has_encoding, "Small response should not be compressed");
12009 }
12010
12011 #[test]
12012 fn compression_works_for_large_responses() {
12013 let config = CompressionConfig::new().min_size(10); let middleware = CompressionMiddleware::with_config(config);
12015 let ctx = test_context();
12016
12017 let mut req = Request::new(Method::Get, "/");
12019 req.headers_mut()
12020 .insert("accept-encoding", b"gzip".to_vec());
12021
12022 let body = "Hello, World! ".repeat(100);
12024 let original_size = body.len();
12025
12026 let response = Response::ok()
12027 .header("content-type", b"text/plain".to_vec())
12028 .body(ResponseBody::Bytes(body.into_bytes()));
12029
12030 let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12032
12033 let encoding = result
12035 .headers()
12036 .iter()
12037 .find(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12038 assert!(encoding.is_some(), "Large response should be compressed");
12039
12040 let (_, value) = encoding.unwrap();
12041 assert_eq!(value, b"gzip");
12042
12043 let vary = result
12045 .headers()
12046 .iter()
12047 .find(|(name, _)| name.eq_ignore_ascii_case("vary"));
12048 assert!(vary.is_some(), "Should have Vary header");
12049
12050 if let ResponseBody::Bytes(compressed) = result.body_ref() {
12052 assert!(
12053 compressed.len() < original_size,
12054 "Compressed size should be smaller"
12055 );
12056 } else {
12057 panic!("Expected Bytes body");
12058 }
12059 }
12060
12061 #[test]
12062 fn compression_skips_without_accept_encoding() {
12063 let config = CompressionConfig::new().min_size(10);
12064 let middleware = CompressionMiddleware::with_config(config);
12065 let ctx = test_context();
12066
12067 let req = Request::new(Method::Get, "/");
12069
12070 let body = "Hello, World! ".repeat(100);
12071 let response = Response::ok()
12072 .header("content-type", b"text/plain".to_vec())
12073 .body(ResponseBody::Bytes(body.into_bytes()));
12074
12075 let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12076
12077 let has_encoding = result
12079 .headers()
12080 .iter()
12081 .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12082 assert!(!has_encoding, "Should not compress without Accept-Encoding");
12083 }
12084
12085 #[test]
12086 fn compression_skips_already_compressed_content() {
12087 let config = CompressionConfig::new().min_size(10);
12088 let middleware = CompressionMiddleware::with_config(config);
12089 let ctx = test_context();
12090
12091 let mut req = Request::new(Method::Get, "/");
12093 req.headers_mut()
12094 .insert("accept-encoding", b"gzip".to_vec());
12095
12096 let body = "Some image data".repeat(100);
12098 let response = Response::ok()
12099 .header("content-type", b"image/jpeg".to_vec())
12100 .body(ResponseBody::Bytes(body.into_bytes()));
12101
12102 let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12103
12104 let has_encoding = result
12106 .headers()
12107 .iter()
12108 .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12109 assert!(
12110 !has_encoding,
12111 "Should not compress already-compressed content types"
12112 );
12113 }
12114
12115 #[test]
12116 fn compression_skips_if_already_has_content_encoding() {
12117 let config = CompressionConfig::new().min_size(10);
12118 let middleware = CompressionMiddleware::with_config(config);
12119 let ctx = test_context();
12120
12121 let mut req = Request::new(Method::Get, "/");
12123 req.headers_mut()
12124 .insert("accept-encoding", b"gzip".to_vec());
12125
12126 let body = "Hello, World! ".repeat(100);
12128 let response = Response::ok()
12129 .header("content-type", b"text/plain".to_vec())
12130 .header("content-encoding", b"br".to_vec())
12131 .body(ResponseBody::Bytes(body.into_bytes()));
12132
12133 let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12134
12135 let encodings: Vec<_> = result
12137 .headers()
12138 .iter()
12139 .filter(|(name, _)| name.eq_ignore_ascii_case("content-encoding"))
12140 .collect();
12141
12142 assert_eq!(encodings.len(), 1);
12144 assert_eq!(encodings[0].1, b"br");
12145 }
12146
12147 #[test]
12148 fn accepts_gzip_parses_header_correctly() {
12149 let mut req = Request::new(Method::Get, "/");
12153 req.headers_mut()
12154 .insert("accept-encoding", b"gzip".to_vec());
12155 assert!(CompressionMiddleware::accepts_gzip(&req));
12156
12157 let mut req = Request::new(Method::Get, "/");
12159 req.headers_mut()
12160 .insert("accept-encoding", b"deflate, gzip, br".to_vec());
12161 assert!(CompressionMiddleware::accepts_gzip(&req));
12162
12163 let mut req = Request::new(Method::Get, "/");
12165 req.headers_mut()
12166 .insert("accept-encoding", b"gzip;q=1.0, identity;q=0.5".to_vec());
12167 assert!(CompressionMiddleware::accepts_gzip(&req));
12168
12169 let mut req = Request::new(Method::Get, "/");
12171 req.headers_mut().insert("accept-encoding", b"*".to_vec());
12172 assert!(CompressionMiddleware::accepts_gzip(&req));
12173
12174 let mut req = Request::new(Method::Get, "/");
12176 req.headers_mut()
12177 .insert("accept-encoding", b"deflate, br".to_vec());
12178 assert!(!CompressionMiddleware::accepts_gzip(&req));
12179
12180 let req_no_header = Request::new(Method::Get, "/");
12182 assert!(!CompressionMiddleware::accepts_gzip(&req_no_header));
12183 }
12184
12185 #[test]
12186 fn compression_middleware_name() {
12187 let middleware = CompressionMiddleware::new();
12188 assert_eq!(middleware.name(), "Compression");
12189 }
12190}
12191
12192#[cfg(test)]
12197mod request_inspection_tests {
12198 use super::*;
12199 use crate::request::Method;
12200 use crate::response::ResponseBody;
12201
12202 fn test_context() -> RequestContext {
12203 RequestContext::new(asupersync::Cx::for_testing(), 1)
12204 }
12205
12206 #[test]
12207 fn inspection_middleware_default_creates_normal_verbosity() {
12208 let mw = RequestInspectionMiddleware::new();
12209 assert_eq!(mw.verbosity, InspectionVerbosity::Normal);
12210 assert_eq!(mw.slow_threshold_ms, 1000);
12211 assert_eq!(mw.max_body_preview, 2048);
12212 assert_eq!(mw.name(), "RequestInspection");
12213 }
12214
12215 #[test]
12216 fn inspection_middleware_builder_methods() {
12217 let mw = RequestInspectionMiddleware::new()
12218 .verbosity(InspectionVerbosity::Verbose)
12219 .slow_threshold_ms(500)
12220 .max_body_preview(4096)
12221 .log_config(LogConfig::development())
12222 .redact_header("x-api-key");
12223
12224 assert_eq!(mw.verbosity, InspectionVerbosity::Verbose);
12225 assert_eq!(mw.slow_threshold_ms, 500);
12226 assert_eq!(mw.max_body_preview, 4096);
12227 assert!(mw.redact_headers.contains("x-api-key"));
12228 assert!(mw.redact_headers.contains("authorization"));
12230 assert!(mw.redact_headers.contains("cookie"));
12231 }
12232
12233 #[test]
12234 fn inspection_before_continues_processing() {
12235 let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Minimal);
12236 let ctx = test_context();
12237 let mut req = Request::new(Method::Post, "/api/users");
12238
12239 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12240 assert!(result.is_continue());
12241 }
12242
12243 #[test]
12244 fn inspection_after_returns_response_unchanged() {
12245 let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Minimal);
12246 let ctx = test_context();
12247 let mut req = Request::new(Method::Get, "/health");
12248
12249 let _ = futures_executor::block_on(mw.before(&ctx, &mut req));
12251
12252 let response = Response::ok().body(ResponseBody::Bytes(b"OK".to_vec()));
12253
12254 let result = futures_executor::block_on(mw.after(&ctx, &req, response));
12255 assert_eq!(result.status().as_u16(), 200);
12256 assert_eq!(result.body_ref().len(), 2);
12257 }
12258
12259 #[test]
12260 fn inspection_stores_start_extension() {
12261 let mw = RequestInspectionMiddleware::new();
12262 let ctx = test_context();
12263 let mut req = Request::new(Method::Get, "/");
12264
12265 let _ = futures_executor::block_on(mw.before(&ctx, &mut req));
12266
12267 assert!(req.get_extension::<InspectionStart>().is_some());
12269 }
12270
12271 #[test]
12272 fn inspection_all_verbosity_levels_continue() {
12273 for verbosity in [
12274 InspectionVerbosity::Minimal,
12275 InspectionVerbosity::Normal,
12276 InspectionVerbosity::Verbose,
12277 ] {
12278 let mw = RequestInspectionMiddleware::new().verbosity(verbosity);
12279 let ctx = test_context();
12280 let mut req = Request::new(Method::Get, "/test");
12281 req.headers_mut()
12282 .insert("content-type", b"text/plain".to_vec());
12283
12284 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12285 assert!(
12286 result.is_continue(),
12287 "Verbosity {verbosity:?} should continue"
12288 );
12289 }
12290 }
12291
12292 #[test]
12293 fn inspection_verbose_with_json_body() {
12294 let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Verbose);
12295 let ctx = test_context();
12296 let body = br#"{"name":"Alice","age":30}"#;
12297 let mut req = Request::new(Method::Post, "/api/users");
12298 req.headers_mut()
12299 .insert("content-type", b"application/json".to_vec());
12300 req.set_body(Body::Bytes(body.to_vec()));
12301
12302 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12303 assert!(result.is_continue());
12304 }
12305
12306 #[test]
12307 fn inspection_verbose_after_with_json_response() {
12308 let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Verbose);
12309 let ctx = test_context();
12310 let mut req = Request::new(Method::Get, "/api/users/1");
12311
12312 let _ = futures_executor::block_on(mw.before(&ctx, &mut req));
12313
12314 let response = Response::ok()
12315 .header("content-type", b"application/json".to_vec())
12316 .body(ResponseBody::Bytes(br#"{"id":1,"name":"Alice"}"#.to_vec()));
12317
12318 let result = futures_executor::block_on(mw.after(&ctx, &req, response));
12319 assert_eq!(result.status().as_u16(), 200);
12320 }
12321
12322 #[test]
12323 fn inspection_redacts_sensitive_headers() {
12324 let mw = RequestInspectionMiddleware::new();
12325
12326 assert!(mw.redact_headers.contains("authorization"));
12328 assert!(mw.redact_headers.contains("proxy-authorization"));
12329 assert!(mw.redact_headers.contains("cookie"));
12330 assert!(mw.redact_headers.contains("set-cookie"));
12331 }
12332
12333 #[test]
12334 fn inspection_format_headers_redacts() {
12335 let mw = RequestInspectionMiddleware::new().redact_header("x-secret");
12336
12337 let headers = vec![
12338 ("content-type", b"text/plain".as_slice()),
12339 ("x-secret", b"my-secret-value".as_slice()),
12340 ("x-normal", b"visible".as_slice()),
12341 ];
12342
12343 let output = mw.format_inspection_headers(headers.into_iter());
12344 assert!(output.contains("content-type: text/plain"));
12345 assert!(output.contains("x-secret: [REDACTED]"));
12346 assert!(output.contains("x-normal: visible"));
12347 assert!(!output.contains("my-secret-value"));
12348 }
12349
12350 #[test]
12351 fn inspection_format_body_preview_truncates() {
12352 let mw = RequestInspectionMiddleware::new().max_body_preview(10);
12353
12354 let body = b"Hello, World! This is a long body.";
12355 let result = mw.format_body_preview(body, None);
12356 assert!(result.is_some());
12357 let text = result.unwrap();
12358 assert!(text.ends_with("..."));
12359 assert!(text.len() <= 15); }
12361
12362 #[test]
12363 fn inspection_format_body_preview_empty() {
12364 let mw = RequestInspectionMiddleware::new();
12365 assert!(mw.format_body_preview(b"", None).is_none());
12366 }
12367
12368 #[test]
12369 fn inspection_format_body_preview_zero_max() {
12370 let mw = RequestInspectionMiddleware::new().max_body_preview(0);
12371 assert!(mw.format_body_preview(b"hello", None).is_none());
12372 }
12373
12374 #[test]
12375 fn inspection_format_body_preview_json_pretty() {
12376 let mw = RequestInspectionMiddleware::new();
12377 let body = br#"{"key":"value","num":42}"#;
12378 let ct = b"application/json".as_slice();
12379 let result = mw.format_body_preview(body, Some(ct));
12380 assert!(result.is_some());
12381 let text = result.unwrap();
12382 assert!(text.contains('\n'));
12384 assert!(text.contains("\"key\": \"value\""));
12385 }
12386
12387 #[test]
12388 fn inspection_format_body_preview_non_json() {
12389 let mw = RequestInspectionMiddleware::new();
12390 let body = b"Hello, World!";
12391 let ct = b"text/plain".as_slice();
12392 let result = mw.format_body_preview(body, Some(ct));
12393 assert_eq!(result.unwrap(), "Hello, World!");
12394 }
12395
12396 #[test]
12397 fn inspection_format_body_preview_binary() {
12398 let mw = RequestInspectionMiddleware::new();
12399 let body: &[u8] = &[0xFF, 0xFE, 0xFD, 0x00];
12400 let result = mw.format_body_preview(body, None);
12401 assert!(result.is_some());
12402 assert!(result.unwrap().contains("binary"));
12403 }
12404
12405 #[test]
12406 fn try_pretty_json_valid_object() {
12407 let result = try_pretty_json(r#"{"a":"b","c":1}"#);
12408 assert!(result.is_some());
12409 let pretty = result.unwrap();
12410 assert!(pretty.contains('\n'));
12411 assert!(pretty.contains(" \"a\": \"b\""));
12412 }
12413
12414 #[test]
12415 fn try_pretty_json_valid_array() {
12416 let result = try_pretty_json(r"[1,2,3]");
12417 assert!(result.is_some());
12418 let pretty = result.unwrap();
12419 assert!(pretty.contains('\n'));
12420 }
12421
12422 #[test]
12423 fn try_pretty_json_empty_object() {
12424 let result = try_pretty_json("{}");
12425 assert!(result.is_some());
12426 assert_eq!(result.unwrap(), "{}");
12427 }
12428
12429 #[test]
12430 fn try_pretty_json_empty_array() {
12431 let result = try_pretty_json("[]");
12432 assert!(result.is_some());
12433 assert_eq!(result.unwrap(), "[]");
12434 }
12435
12436 #[test]
12437 fn try_pretty_json_not_json() {
12438 assert!(try_pretty_json("hello world").is_none());
12439 assert!(try_pretty_json("12345").is_none());
12440 }
12441
12442 #[test]
12443 fn try_pretty_json_nested() {
12444 let input = r#"{"user":{"name":"Alice","roles":["admin","user"]}}"#;
12445 let result = try_pretty_json(input);
12446 assert!(result.is_some());
12447 let pretty = result.unwrap();
12448 assert!(pretty.contains("\"user\":"));
12449 assert!(pretty.contains("\"name\": \"Alice\""));
12450 assert!(pretty.contains("\"roles\":"));
12451 }
12452
12453 #[test]
12454 fn try_pretty_json_with_escapes() {
12455 let input = r#"{"msg":"hello \"world\""}"#;
12456 let result = try_pretty_json(input);
12457 assert!(result.is_some());
12458 let pretty = result.unwrap();
12459 assert!(pretty.contains(r#"\"world\""#));
12460 }
12461
12462 #[test]
12463 fn inspection_name() {
12464 let mw = RequestInspectionMiddleware::new();
12465 assert_eq!(mw.name(), "RequestInspection");
12466 }
12467
12468 #[test]
12469 fn inspection_default_via_default_trait() {
12470 let mw = RequestInspectionMiddleware::default();
12471 assert_eq!(mw.verbosity, InspectionVerbosity::Normal);
12472 assert_eq!(mw.slow_threshold_ms, 1000);
12473 }
12474
12475 #[test]
12476 fn inspection_with_query_string() {
12477 let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Minimal);
12478 let ctx = test_context();
12479 let mut req = Request::new(Method::Get, "/search");
12480 req.set_query(Some("q=rust&page=1".to_string()));
12481
12482 let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12483 assert!(result.is_continue());
12484 }
12485
12486 #[test]
12487 fn inspection_response_body_stream() {
12488 let mw = RequestInspectionMiddleware::new();
12489 let result = mw.format_response_preview(&ResponseBody::Empty, None);
12490 assert!(result.is_none());
12491 }
12492}
12493
12494#[cfg(test)]
12499mod rate_limit_tests {
12500 use super::*;
12501 use crate::request::Method;
12502 use crate::response::{ResponseBody, StatusCode};
12503 use std::time::{Duration, Instant};
12504
12505 fn test_context() -> RequestContext {
12506 RequestContext::new(asupersync::Cx::for_testing(), 1)
12507 }
12508
12509 fn run_rate_limit_before(mw: &RateLimitMiddleware, req: &mut Request) -> ControlFlow {
12510 let ctx = test_context();
12511 let fut = mw.before(&ctx, req);
12512 futures_executor::block_on(fut)
12513 }
12514
12515 fn run_rate_limit_after(mw: &RateLimitMiddleware, req: &Request, resp: Response) -> Response {
12516 let ctx = test_context();
12517 let fut = mw.after(&ctx, req, resp);
12518 futures_executor::block_on(fut)
12519 }
12520
12521 fn request_with_ip(key: &str) -> Request {
12522 let mut req = Request::new(Method::Get, "/");
12523 req.headers_mut()
12524 .insert("x-forwarded-for", key.as_bytes().to_vec());
12525 req
12526 }
12527
12528 fn rate_limit_entry_keys(
12529 store: &InMemoryRateLimitStore,
12530 algorithm: RateLimitAlgorithm,
12531 ) -> Vec<String> {
12532 match algorithm {
12533 RateLimitAlgorithm::TokenBucket => {
12534 store.token_buckets.lock().entries.keys().cloned().collect()
12535 }
12536 RateLimitAlgorithm::FixedWindow => {
12537 store.fixed_windows.lock().entries.keys().cloned().collect()
12538 }
12539 RateLimitAlgorithm::SlidingWindow => store
12540 .sliding_windows
12541 .lock()
12542 .entries
12543 .keys()
12544 .cloned()
12545 .collect(),
12546 }
12547 }
12548
12549 #[test]
12550 fn rate_limit_default_allows_requests() {
12551 let mw = RateLimitMiddleware::new();
12552 let mut req = Request::new(Method::Get, "/api/test");
12553 req.headers_mut()
12554 .insert("x-forwarded-for", b"192.168.1.1".to_vec());
12555
12556 let result = run_rate_limit_before(&mw, &mut req);
12557 assert!(result.is_continue(), "first request should be allowed");
12558 }
12559
12560 #[test]
12561 fn rate_limit_fixed_window_blocks_after_limit() {
12562 let mw = RateLimitMiddleware::builder()
12563 .requests(3)
12564 .per(Duration::from_secs(60))
12565 .algorithm(RateLimitAlgorithm::FixedWindow)
12566 .key_extractor(IpKeyExtractor)
12567 .build();
12568
12569 for i in 0..3 {
12570 let mut req = Request::new(Method::Get, "/api/test");
12571 req.headers_mut()
12572 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12573 let result = run_rate_limit_before(&mw, &mut req);
12574 assert!(
12575 result.is_continue(),
12576 "request {i} should be allowed within limit"
12577 );
12578 }
12579
12580 let mut req = Request::new(Method::Get, "/api/test");
12582 req.headers_mut()
12583 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12584 let result = run_rate_limit_before(&mw, &mut req);
12585 assert!(result.is_break(), "fourth request should be blocked");
12586
12587 if let ControlFlow::Break(resp) = result {
12589 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
12590 }
12591 }
12592
12593 #[test]
12594 fn rate_limit_different_keys_independent() {
12595 let mw = RateLimitMiddleware::builder()
12596 .requests(2)
12597 .per(Duration::from_secs(60))
12598 .algorithm(RateLimitAlgorithm::FixedWindow)
12599 .key_extractor(IpKeyExtractor)
12600 .build();
12601
12602 for _ in 0..2 {
12604 let mut req = Request::new(Method::Get, "/");
12605 req.headers_mut()
12606 .insert("x-forwarded-for", b"1.1.1.1".to_vec());
12607 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12608 }
12609
12610 let mut req = Request::new(Method::Get, "/");
12612 req.headers_mut()
12613 .insert("x-forwarded-for", b"1.1.1.1".to_vec());
12614 assert!(run_rate_limit_before(&mw, &mut req).is_break());
12615
12616 let mut req = Request::new(Method::Get, "/");
12618 req.headers_mut()
12619 .insert("x-forwarded-for", b"2.2.2.2".to_vec());
12620 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12621 }
12622
12623 #[test]
12624 fn rate_limit_token_bucket_allows_burst() {
12625 let mw = RateLimitMiddleware::builder()
12626 .requests(5)
12627 .per(Duration::from_secs(60))
12628 .algorithm(RateLimitAlgorithm::TokenBucket)
12629 .key_extractor(IpKeyExtractor)
12630 .build();
12631
12632 for i in 0..5 {
12634 let mut req = Request::new(Method::Get, "/");
12635 req.headers_mut()
12636 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12637 let result = run_rate_limit_before(&mw, &mut req);
12638 assert!(result.is_continue(), "burst request {i} should be allowed");
12639 }
12640
12641 let mut req = Request::new(Method::Get, "/");
12643 req.headers_mut()
12644 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12645 assert!(run_rate_limit_before(&mw, &mut req).is_break());
12646 }
12647
12648 #[test]
12649 fn rate_limit_sliding_window_basic() {
12650 let mw = RateLimitMiddleware::builder()
12651 .requests(3)
12652 .per(Duration::from_secs(60))
12653 .algorithm(RateLimitAlgorithm::SlidingWindow)
12654 .key_extractor(IpKeyExtractor)
12655 .build();
12656
12657 for i in 0..3 {
12658 let mut req = Request::new(Method::Get, "/");
12659 req.headers_mut()
12660 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12661 assert!(
12662 run_rate_limit_before(&mw, &mut req).is_continue(),
12663 "sliding window request {i} should be allowed"
12664 );
12665 }
12666
12667 let mut req = Request::new(Method::Get, "/");
12669 req.headers_mut()
12670 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12671 assert!(run_rate_limit_before(&mw, &mut req).is_break());
12672 }
12673
12674 #[test]
12675 fn rate_limit_header_key_extractor() {
12676 let mw = RateLimitMiddleware::builder()
12677 .requests(2)
12678 .per(Duration::from_secs(60))
12679 .algorithm(RateLimitAlgorithm::FixedWindow)
12680 .key_extractor(HeaderKeyExtractor::new("x-api-key"))
12681 .build();
12682
12683 for _ in 0..2 {
12685 let mut req = Request::new(Method::Get, "/");
12686 req.headers_mut().insert("x-api-key", b"key-abc".to_vec());
12687 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12688 }
12689
12690 let mut req = Request::new(Method::Get, "/");
12692 req.headers_mut().insert("x-api-key", b"key-abc".to_vec());
12693 assert!(run_rate_limit_before(&mw, &mut req).is_break());
12694
12695 let mut req = Request::new(Method::Get, "/");
12697 req.headers_mut().insert("x-api-key", b"key-xyz".to_vec());
12698 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12699 }
12700
12701 #[test]
12702 fn rate_limit_path_key_extractor() {
12703 let mw = RateLimitMiddleware::builder()
12704 .requests(1)
12705 .per(Duration::from_secs(60))
12706 .algorithm(RateLimitAlgorithm::FixedWindow)
12707 .key_extractor(PathKeyExtractor)
12708 .build();
12709
12710 let mut req = Request::new(Method::Get, "/api/a");
12711 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12712
12713 let mut req = Request::new(Method::Get, "/api/a");
12715 assert!(run_rate_limit_before(&mw, &mut req).is_break());
12716
12717 let mut req = Request::new(Method::Get, "/api/b");
12719 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12720 }
12721
12722 #[test]
12723 fn rate_limit_no_key_skips_limiting() {
12724 let mw = RateLimitMiddleware::builder()
12725 .requests(1)
12726 .per(Duration::from_secs(60))
12727 .algorithm(RateLimitAlgorithm::FixedWindow)
12728 .key_extractor(HeaderKeyExtractor::new("x-api-key"))
12729 .build();
12730
12731 let mut req = Request::new(Method::Get, "/");
12733 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12734
12735 for _ in 0..10 {
12737 let mut req = Request::new(Method::Get, "/");
12738 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12739 }
12740 }
12741
12742 #[test]
12743 fn rate_limit_response_headers_on_success() {
12744 let mw = RateLimitMiddleware::builder()
12745 .requests(10)
12746 .per(Duration::from_secs(60))
12747 .algorithm(RateLimitAlgorithm::FixedWindow)
12748 .key_extractor(IpKeyExtractor)
12749 .build();
12750
12751 let mut req = Request::new(Method::Get, "/");
12752 req.headers_mut()
12753 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12754 let cf = run_rate_limit_before(&mw, &mut req);
12755 assert!(cf.is_continue());
12756
12757 let resp = Response::with_status(StatusCode::OK);
12758 let resp = run_rate_limit_after(&mw, &req, resp);
12759
12760 let headers = resp.headers();
12762 let has_limit = headers
12763 .iter()
12764 .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-limit"));
12765 let has_remaining = headers
12766 .iter()
12767 .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-remaining"));
12768 let has_reset = headers
12769 .iter()
12770 .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-reset"));
12771
12772 assert!(has_limit, "should have X-RateLimit-Limit header");
12773 assert!(has_remaining, "should have X-RateLimit-Remaining header");
12774 assert!(has_reset, "should have X-RateLimit-Reset header");
12775
12776 let limit_val = headers
12778 .iter()
12779 .find(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-limit"))
12780 .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
12781 .unwrap();
12782 assert_eq!(limit_val, "10");
12783 }
12784
12785 #[test]
12786 fn rate_limit_429_response_has_retry_after() {
12787 let mw = RateLimitMiddleware::builder()
12788 .requests(1)
12789 .per(Duration::from_secs(60))
12790 .algorithm(RateLimitAlgorithm::FixedWindow)
12791 .key_extractor(IpKeyExtractor)
12792 .build();
12793
12794 let mut req = Request::new(Method::Get, "/");
12796 req.headers_mut()
12797 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12798 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12799
12800 let mut req = Request::new(Method::Get, "/");
12802 req.headers_mut()
12803 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12804 let result = run_rate_limit_before(&mw, &mut req);
12805
12806 if let ControlFlow::Break(resp) = result {
12807 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
12808
12809 let has_retry = resp
12811 .headers()
12812 .iter()
12813 .any(|(n, _)| n.eq_ignore_ascii_case("retry-after"));
12814 assert!(has_retry, "429 response should have Retry-After header");
12815
12816 let has_ct = resp
12818 .headers()
12819 .iter()
12820 .any(|(n, v)| n.eq_ignore_ascii_case("content-type") && v == b"application/json");
12821 assert!(has_ct, "429 response should have JSON content type");
12822 } else {
12823 panic!("expected Break(429)");
12824 }
12825 }
12826
12827 #[test]
12828 fn rate_limit_no_headers_when_disabled() {
12829 let mw = RateLimitMiddleware::builder()
12830 .requests(10)
12831 .per(Duration::from_secs(60))
12832 .algorithm(RateLimitAlgorithm::FixedWindow)
12833 .key_extractor(IpKeyExtractor)
12834 .include_headers(false)
12835 .build();
12836
12837 let mut req = Request::new(Method::Get, "/");
12838 req.headers_mut()
12839 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12840 assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12841
12842 let resp = Response::with_status(StatusCode::OK);
12843 let resp = run_rate_limit_after(&mw, &req, resp);
12844
12845 let has_limit = resp
12846 .headers()
12847 .iter()
12848 .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-limit"));
12849 assert!(
12850 !has_limit,
12851 "should NOT have rate limit headers when disabled"
12852 );
12853 }
12854
12855 #[test]
12856 fn rate_limit_custom_retry_message() {
12857 let mw = RateLimitMiddleware::builder()
12858 .requests(1)
12859 .per(Duration::from_secs(60))
12860 .algorithm(RateLimitAlgorithm::FixedWindow)
12861 .key_extractor(IpKeyExtractor)
12862 .retry_message("Slow down, partner!")
12863 .build();
12864
12865 let mut req = Request::new(Method::Get, "/");
12867 req.headers_mut()
12868 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12869 run_rate_limit_before(&mw, &mut req);
12870
12871 let mut req = Request::new(Method::Get, "/");
12873 req.headers_mut()
12874 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12875 if let ControlFlow::Break(resp) = run_rate_limit_before(&mw, &mut req) {
12876 if let ResponseBody::Bytes(body) = resp.body_ref() {
12877 let body_str = std::str::from_utf8(body).unwrap();
12878 assert!(
12879 body_str.contains("Slow down, partner!"),
12880 "expected custom message in body, got: {body_str}"
12881 );
12882 } else {
12883 panic!("expected Bytes body");
12884 }
12885 } else {
12886 panic!("expected Break(429)");
12887 }
12888 }
12889
12890 #[test]
12891 fn rate_limit_ip_extractor_x_forwarded_for() {
12892 let extractor = IpKeyExtractor;
12893 let mut req = Request::new(Method::Get, "/");
12894 req.headers_mut()
12895 .insert("x-forwarded-for", b"1.2.3.4, 5.6.7.8".to_vec());
12896 assert_eq!(extractor.extract_key(&req), Some("1.2.3.4".to_string()));
12897 }
12898
12899 #[test]
12900 fn rate_limit_ip_extractor_x_real_ip() {
12901 let extractor = IpKeyExtractor;
12902 let mut req = Request::new(Method::Get, "/");
12903 req.headers_mut().insert("x-real-ip", b"9.8.7.6".to_vec());
12904 assert_eq!(extractor.extract_key(&req), Some("9.8.7.6".to_string()));
12905 }
12906
12907 #[test]
12908 fn rate_limit_ip_extractor_fallback() {
12909 let extractor = IpKeyExtractor;
12910 let req = Request::new(Method::Get, "/");
12911 assert_eq!(extractor.extract_key(&req), Some("unknown".to_string()));
12912 }
12913
12914 #[test]
12916 fn connected_ip_extractor_with_remote_addr() {
12917 use std::net::{IpAddr, Ipv4Addr};
12918
12919 let extractor = ConnectedIpKeyExtractor;
12920 let mut req = Request::new(Method::Get, "/");
12921 req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))));
12922
12923 assert_eq!(
12924 extractor.extract_key(&req),
12925 Some("192.168.1.100".to_string())
12926 );
12927 }
12928
12929 #[test]
12930 fn connected_ip_extractor_without_remote_addr() {
12931 let extractor = ConnectedIpKeyExtractor;
12932 let req = Request::new(Method::Get, "/");
12933
12934 assert_eq!(extractor.extract_key(&req), None);
12936 }
12937
12938 #[test]
12939 fn connected_ip_extractor_ignores_headers() {
12940 use std::net::{IpAddr, Ipv4Addr};
12941
12942 let extractor = ConnectedIpKeyExtractor;
12943 let mut req = Request::new(Method::Get, "/");
12944 req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
12945 req.headers_mut()
12947 .insert("x-forwarded-for", b"1.2.3.4".to_vec());
12948
12949 assert_eq!(extractor.extract_key(&req), Some("10.0.0.1".to_string()));
12951 }
12952
12953 #[test]
12955 fn trusted_proxy_extractor_from_trusted_proxy() {
12956 use std::net::{IpAddr, Ipv4Addr};
12957
12958 let extractor = TrustedProxyIpKeyExtractor::new().trust_cidr("10.0.0.0/8");
12959
12960 let mut req = Request::new(Method::Get, "/");
12961 req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
12963 req.headers_mut()
12965 .insert("x-forwarded-for", b"203.0.113.50".to_vec());
12966
12967 assert_eq!(
12969 extractor.extract_key(&req),
12970 Some("203.0.113.50".to_string())
12971 );
12972 }
12973
12974 #[test]
12975 fn trusted_proxy_extractor_from_untrusted_direct() {
12976 use std::net::{IpAddr, Ipv4Addr};
12977
12978 let extractor = TrustedProxyIpKeyExtractor::new().trust_cidr("10.0.0.0/8");
12979
12980 let mut req = Request::new(Method::Get, "/");
12981 req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 50))));
12983 req.headers_mut()
12985 .insert("x-forwarded-for", b"1.2.3.4".to_vec());
12986
12987 assert_eq!(
12989 extractor.extract_key(&req),
12990 Some("203.0.113.50".to_string())
12991 );
12992 }
12993
12994 #[test]
12995 fn trusted_proxy_extractor_no_remote_addr() {
12996 let extractor = TrustedProxyIpKeyExtractor::new().trust_loopback();
12997
12998 let mut req = Request::new(Method::Get, "/");
12999 req.headers_mut()
13001 .insert("x-forwarded-for", b"1.2.3.4".to_vec());
13002
13003 assert_eq!(extractor.extract_key(&req), None);
13004 }
13005
13006 #[test]
13007 fn trusted_proxy_extractor_loopback_ipv4() {
13008 use std::net::{IpAddr, Ipv4Addr};
13009
13010 let extractor = TrustedProxyIpKeyExtractor::new().trust_loopback();
13011
13012 let mut req = Request::new(Method::Get, "/");
13013 req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::LOCALHOST)));
13014 req.headers_mut()
13015 .insert("x-forwarded-for", b"8.8.8.8".to_vec());
13016
13017 assert_eq!(extractor.extract_key(&req), Some("8.8.8.8".to_string()));
13018 }
13019
13020 #[test]
13021 fn trusted_proxy_extractor_loopback_ipv6() {
13022 use std::net::{IpAddr, Ipv6Addr};
13023
13024 let extractor = TrustedProxyIpKeyExtractor::new().trust_loopback();
13025
13026 let mut req = Request::new(Method::Get, "/");
13027 req.insert_extension(RemoteAddr(IpAddr::V6(Ipv6Addr::LOCALHOST)));
13028 req.headers_mut()
13029 .insert("x-forwarded-for", b"8.8.8.8".to_vec());
13030
13031 assert_eq!(extractor.extract_key(&req), Some("8.8.8.8".to_string()));
13032 }
13033
13034 #[test]
13035 fn cidr_parsing() {
13036 assert!(parse_cidr("10.0.0.0/8").is_some());
13038 assert!(parse_cidr("192.168.1.0/24").is_some());
13039 assert!(parse_cidr("0.0.0.0/0").is_some());
13040 assert!(parse_cidr("::1/128").is_some());
13041 assert!(parse_cidr("::/0").is_some());
13042
13043 assert!(parse_cidr("10.0.0.0/33").is_none()); assert!(parse_cidr("invalid").is_none());
13046 assert!(parse_cidr("10.0.0.0").is_none()); }
13048
13049 #[test]
13050 fn ip_in_cidr_matching() {
13051 use std::net::{IpAddr, Ipv4Addr};
13052
13053 let cidr_10 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0));
13054
13055 assert!(ip_in_cidr(
13057 IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
13058 cidr_10,
13059 8
13060 ));
13061 assert!(ip_in_cidr(
13062 IpAddr::V4(Ipv4Addr::new(10, 255, 255, 255)),
13063 cidr_10,
13064 8
13065 ));
13066
13067 assert!(!ip_in_cidr(
13069 IpAddr::V4(Ipv4Addr::new(11, 0, 0, 1)),
13070 cidr_10,
13071 8
13072 ));
13073 assert!(!ip_in_cidr(
13074 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
13075 cidr_10,
13076 8
13077 ));
13078 }
13079
13080 #[test]
13081 fn rate_limit_composite_key_extractor() {
13082 let extractor =
13083 CompositeKeyExtractor::new(vec![Box::new(IpKeyExtractor), Box::new(PathKeyExtractor)]);
13084
13085 let mut req = Request::new(Method::Get, "/api/users");
13086 req.headers_mut()
13087 .insert("x-forwarded-for", b"10.0.0.1".to_vec());
13088
13089 let key = extractor.extract_key(&req);
13090 assert_eq!(key, Some("10.0.0.1:/api/users".to_string()));
13091 }
13092
13093 #[test]
13094 fn rate_limit_builder_defaults() {
13095 let mw = RateLimitMiddleware::builder().build();
13096 assert_eq!(DEFAULT_RATE_LIMIT_MAX_KEYS, 65_536);
13097 assert_eq!(mw.config.max_requests, 100);
13098 assert_eq!(mw.config.window, Duration::from_secs(60));
13099 assert_eq!(mw.config.algorithm, RateLimitAlgorithm::TokenBucket);
13100 assert_eq!(mw.config.max_keys, DEFAULT_RATE_LIMIT_MAX_KEYS);
13101 assert_eq!(mw.store.max_keys, DEFAULT_RATE_LIMIT_MAX_KEYS);
13102 assert!(mw.config.include_headers);
13103 }
13104
13105 #[test]
13106 fn rate_limit_builder_applies_custom_key_bound() {
13107 let mw = RateLimitMiddleware::builder().max_keys(7).build();
13108 assert_eq!(mw.config.max_keys, 7);
13109 assert_eq!(mw.store.max_keys, 7);
13110 }
13111
13112 #[test]
13113 fn rate_limit_store_bounds_every_algorithm_and_fails_closed() {
13114 let window = Duration::from_secs(60);
13115
13116 for algorithm in [
13117 RateLimitAlgorithm::TokenBucket,
13118 RateLimitAlgorithm::FixedWindow,
13119 RateLimitAlgorithm::SlidingWindow,
13120 ] {
13121 let store = InMemoryRateLimitStore::with_max_keys(2);
13122 assert!(store.check("resident-a", algorithm, 10, window).allowed);
13123 assert!(store.check("resident-b", algorithm, 10, window).allowed);
13124
13125 let saturated = store.check("unseen", algorithm, 10, window);
13126 assert!(!saturated.allowed, "{algorithm:?} must fail closed");
13127 assert_eq!(saturated.limit, 10);
13128 assert_eq!(saturated.remaining, 0);
13129 assert_eq!(saturated.reset_after_secs, 60);
13130
13131 let keys = rate_limit_entry_keys(&store, algorithm);
13132 assert_eq!(keys.len(), 2, "{algorithm:?} exceeded its key bound");
13133 assert!(keys.contains(&"resident-a".to_string()));
13134 assert!(keys.contains(&"resident-b".to_string()));
13135 assert!(!keys.contains(&"unseen".to_string()));
13136 }
13137 }
13138
13139 #[test]
13140 fn saturated_retry_after_rounds_fractional_windows_up() {
13141 let store = InMemoryRateLimitStore::with_max_keys(0);
13142 let result = store.check(
13143 "unseen",
13144 RateLimitAlgorithm::FixedWindow,
13145 10,
13146 Duration::from_millis(1_500),
13147 );
13148 assert!(!result.allowed);
13149 assert_eq!(result.reset_after_secs, 2);
13150 }
13151
13152 #[test]
13153 fn rate_limit_saturation_returns_429_without_resetting_resident_counter() {
13154 for algorithm in [
13155 RateLimitAlgorithm::TokenBucket,
13156 RateLimitAlgorithm::FixedWindow,
13157 RateLimitAlgorithm::SlidingWindow,
13158 ] {
13159 let mw = RateLimitMiddleware::builder()
13160 .requests(2)
13161 .per(Duration::from_secs(60))
13162 .algorithm(algorithm)
13163 .key_extractor(IpKeyExtractor)
13164 .max_keys(1)
13165 .build();
13166
13167 for _ in 0..2 {
13168 let mut resident = request_with_ip("resident");
13169 assert!(run_rate_limit_before(&mw, &mut resident).is_continue());
13170 }
13171
13172 let mut unseen = request_with_ip("unseen");
13173 let ControlFlow::Break(response) = run_rate_limit_before(&mw, &mut unseen) else {
13174 panic!("{algorithm:?} unseen key must fail closed while saturated");
13175 };
13176 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
13177 assert!(response.headers().iter().any(|(name, value)| {
13178 name.eq_ignore_ascii_case("retry-after") && value.as_slice() == b"60"
13179 }));
13180
13181 let mut resident = request_with_ip("resident");
13182 assert!(
13183 run_rate_limit_before(&mw, &mut resident).is_break(),
13184 "{algorithm:?} resident counter must remain exhausted"
13185 );
13186
13187 let keys = rate_limit_entry_keys(&mw.store, algorithm);
13188 assert_eq!(keys, vec!["resident".to_string()]);
13189 }
13190 }
13191
13192 #[test]
13193 fn rate_limit_store_reclaims_algorithm_specific_stale_entries() {
13194 let window = Duration::from_secs(3_600);
13195
13196 for algorithm in [
13197 RateLimitAlgorithm::TokenBucket,
13198 RateLimitAlgorithm::FixedWindow,
13199 RateLimitAlgorithm::SlidingWindow,
13200 ] {
13201 let store = InMemoryRateLimitStore::with_max_keys(1);
13202 let start = Instant::now();
13203 assert!(
13204 store
13205 .check_at("stale", algorithm, 10, window, start)
13206 .allowed
13207 );
13208
13209 let replacement_at = if algorithm == RateLimitAlgorithm::SlidingWindow {
13210 start + Duration::from_secs(7_201)
13211 } else {
13212 start + Duration::from_secs(3_601)
13213 };
13214
13215 assert!(
13216 store
13217 .check_at("replacement", algorithm, 10, window, replacement_at)
13218 .allowed,
13219 "{algorithm:?} should reclaim its stale entry"
13220 );
13221 assert_eq!(
13222 rate_limit_entry_keys(&store, algorithm),
13223 vec!["replacement".to_string()]
13224 );
13225 }
13226 }
13227
13228 #[test]
13229 fn sliding_window_retains_entries_until_two_windows_are_inactive() {
13230 let algorithm = RateLimitAlgorithm::SlidingWindow;
13231 let window = Duration::from_secs(3_600);
13232 let store = InMemoryRateLimitStore::with_max_keys(1);
13233 let start = Instant::now();
13234 assert!(
13235 store
13236 .check_at("resident", algorithm, 10, window, start)
13237 .allowed
13238 );
13239
13240 assert!(
13241 !store
13242 .check_at(
13243 "too-early",
13244 algorithm,
13245 10,
13246 window,
13247 start + Duration::from_secs(3_601),
13248 )
13249 .allowed
13250 );
13251 assert_eq!(
13252 rate_limit_entry_keys(&store, algorithm),
13253 vec!["resident".to_string()]
13254 );
13255
13256 assert!(
13257 store
13258 .check_at(
13259 "replacement",
13260 algorithm,
13261 10,
13262 window,
13263 start + Duration::from_secs(7_202),
13264 )
13265 .allowed
13266 );
13267 assert_eq!(
13268 rate_limit_entry_keys(&store, algorithm),
13269 vec!["replacement".to_string()]
13270 );
13271 }
13272
13273 #[test]
13274 fn sliding_window_preserves_fractional_progress_when_rotating() {
13275 let algorithm = RateLimitAlgorithm::SlidingWindow;
13276 let window = Duration::from_secs(100);
13277 let store = InMemoryRateLimitStore::with_max_keys(1);
13278 let start = Instant::now();
13279
13280 for _ in 0..10 {
13281 assert!(
13282 store
13283 .check_at("resident", algorithm, 10, window, start)
13284 .allowed
13285 );
13286 }
13287 assert!(
13288 !store
13289 .check_at("resident", algorithm, 10, window, start)
13290 .allowed
13291 );
13292
13293 let half_into_next_window = store.check_at(
13294 "resident",
13295 algorithm,
13296 10,
13297 window,
13298 start + Duration::from_secs(150),
13299 );
13300 assert!(half_into_next_window.allowed);
13301 assert_eq!(half_into_next_window.remaining, 4);
13302 assert_eq!(half_into_next_window.reset_after_secs, 50);
13303 }
13304
13305 #[test]
13306 fn cleanup_uses_each_resident_entry_retention_policy() {
13307 let short_window = Duration::from_secs(1);
13308 let long_window = Duration::from_secs(3_600);
13309
13310 for algorithm in [
13311 RateLimitAlgorithm::TokenBucket,
13312 RateLimitAlgorithm::FixedWindow,
13313 RateLimitAlgorithm::SlidingWindow,
13314 ] {
13315 let store = InMemoryRateLimitStore::with_max_keys(1);
13316 let start = Instant::now();
13317 assert!(
13318 store
13319 .check_at("long-lived", algorithm, 10, long_window, start)
13320 .allowed
13321 );
13322
13323 let result = store.check_at(
13324 "short-window-unseen",
13325 algorithm,
13326 10,
13327 short_window,
13328 start + Duration::from_secs(2),
13329 );
13330 assert!(
13331 !result.allowed,
13332 "{algorithm:?} must not evict a resident using the incoming short window"
13333 );
13334 assert_eq!(
13335 rate_limit_entry_keys(&store, algorithm),
13336 vec!["long-lived".to_string()]
13337 );
13338 }
13339 }
13340
13341 #[test]
13342 fn cleanup_reclaims_short_resident_despite_long_incoming_window() {
13343 let short_window = Duration::from_secs(1);
13344 let long_window = Duration::from_secs(3_600);
13345
13346 for algorithm in [
13347 RateLimitAlgorithm::TokenBucket,
13348 RateLimitAlgorithm::FixedWindow,
13349 RateLimitAlgorithm::SlidingWindow,
13350 ] {
13351 let store = InMemoryRateLimitStore::with_max_keys(1);
13352 let start = Instant::now();
13353 assert!(
13354 store
13355 .check_at("short-lived", algorithm, 10, short_window, start)
13356 .allowed
13357 );
13358
13359 let result = store.check_at(
13360 "long-window-replacement",
13361 algorithm,
13362 10,
13363 long_window,
13364 start + Duration::from_secs(3),
13365 );
13366 assert!(
13367 result.allowed,
13368 "{algorithm:?} must reclaim a stale resident independently of the incoming window"
13369 );
13370 assert_eq!(
13371 rate_limit_entry_keys(&store, algorithm),
13372 vec!["long-window-replacement".to_string()]
13373 );
13374 }
13375 }
13376
13377 #[test]
13378 fn rate_limit_cleanup_is_throttled_to_one_sweep_per_second() {
13379 let algorithm = RateLimitAlgorithm::SlidingWindow;
13380 let window = Duration::from_secs(100);
13381 let store = InMemoryRateLimitStore::with_max_keys(1);
13382 let start = Instant::now();
13383 assert!(
13384 store
13385 .check_at("stale", algorithm, 10, window, start)
13386 .allowed
13387 );
13388
13389 assert!(
13390 !store
13391 .check_at(
13392 "initial-sweep",
13393 algorithm,
13394 10,
13395 window,
13396 start + Duration::from_millis(199_500),
13397 )
13398 .allowed
13399 );
13400 assert!(
13401 !store
13402 .check_at(
13403 "first-unseen",
13404 algorithm,
13405 10,
13406 window,
13407 start + Duration::from_millis(200_100),
13408 )
13409 .allowed
13410 );
13411 assert!(
13412 !store
13413 .check_at(
13414 "second-unseen",
13415 algorithm,
13416 10,
13417 window,
13418 start + Duration::from_millis(200_400),
13419 )
13420 .allowed
13421 );
13422 assert_eq!(
13423 rate_limit_entry_keys(&store, algorithm),
13424 vec!["stale".to_string()]
13425 );
13426
13427 assert!(
13428 store
13429 .check_at(
13430 "replacement",
13431 algorithm,
13432 10,
13433 window,
13434 start + Duration::from_millis(200_500),
13435 )
13436 .allowed
13437 );
13438 assert_eq!(
13439 rate_limit_entry_keys(&store, algorithm),
13440 vec!["replacement".to_string()]
13441 );
13442 }
13443
13444 #[test]
13445 fn zero_window_fails_closed_without_retaining_keys() {
13446 let store = InMemoryRateLimitStore::with_max_keys(1);
13447
13448 for algorithm in [
13449 RateLimitAlgorithm::TokenBucket,
13450 RateLimitAlgorithm::FixedWindow,
13451 RateLimitAlgorithm::SlidingWindow,
13452 ] {
13453 let result = store.check("never-retained", algorithm, 10, Duration::ZERO);
13454 assert!(
13455 !result.allowed,
13456 "{algorithm:?} zero window must fail closed"
13457 );
13458 assert_eq!(result.reset_after_secs, 1);
13459 assert!(rate_limit_entry_keys(&store, algorithm).is_empty());
13460 }
13461 }
13462
13463 #[test]
13464 fn rate_limit_builder_per_minute() {
13465 let mw = RateLimitMiddleware::builder()
13466 .requests(50)
13467 .per_minute(2)
13468 .algorithm(RateLimitAlgorithm::SlidingWindow)
13469 .build();
13470 assert_eq!(mw.config.max_requests, 50);
13471 assert_eq!(mw.config.window, Duration::from_secs(120));
13472 assert_eq!(mw.config.algorithm, RateLimitAlgorithm::SlidingWindow);
13473 }
13474
13475 #[test]
13476 fn rate_limit_builder_per_hour() {
13477 let mw = RateLimitMiddleware::builder()
13478 .requests(1000)
13479 .per_hour(1)
13480 .build();
13481 assert_eq!(mw.config.window, Duration::from_secs(3600));
13482 }
13483
13484 #[test]
13485 fn rate_limit_middleware_name() {
13486 let mw = RateLimitMiddleware::new();
13487 assert_eq!(mw.name(), "RateLimit");
13488 }
13489
13490 #[test]
13491 fn rate_limit_default_via_default_trait() {
13492 let mw = RateLimitMiddleware::default();
13493 assert_eq!(mw.config.max_requests, 100);
13494 }
13495
13496 #[test]
13501 fn etag_middleware_generates_etag_for_get() {
13502 let mw = ETagMiddleware::new();
13503 let ctx = test_context();
13504 let req = Request::new(crate::request::Method::Get, "/resource");
13505
13506 let response = Response::ok()
13508 .header("content-type", b"application/json".to_vec())
13509 .body(ResponseBody::Bytes(br#"{"status":"ok"}"#.to_vec()));
13510
13511 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13512
13513 let etag = response
13515 .headers()
13516 .iter()
13517 .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13518 assert!(etag.is_some(), "Response should have ETag header");
13519
13520 let etag_value = std::str::from_utf8(&etag.unwrap().1).unwrap();
13522 assert!(etag_value.starts_with('"'), "ETag should start with quote");
13523 assert!(etag_value.ends_with('"'), "ETag should end with quote");
13524 }
13525
13526 #[test]
13527 fn etag_middleware_returns_304_on_match() {
13528 let mw = ETagMiddleware::new();
13529 let ctx = test_context();
13530
13531 let req1 = Request::new(crate::request::Method::Get, "/resource");
13533 let body = br#"{"status":"ok"}"#.to_vec();
13534 let response1 = Response::ok().body(ResponseBody::Bytes(body.clone()));
13535 let response1 = futures_executor::block_on(mw.after(&ctx, &req1, response1));
13536
13537 let etag = response1
13538 .headers()
13539 .iter()
13540 .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
13541 .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
13542 .unwrap();
13543
13544 let mut req2 = Request::new(crate::request::Method::Get, "/resource");
13546 req2.headers_mut()
13547 .insert("if-none-match", etag.as_bytes().to_vec());
13548
13549 let response2 = Response::ok().body(ResponseBody::Bytes(body));
13550 let response2 = futures_executor::block_on(mw.after(&ctx, &req2, response2));
13551
13552 assert_eq!(response2.status().as_u16(), 304);
13554 assert!(response2.body_ref().is_empty());
13555 }
13556
13557 #[test]
13558 fn etag_middleware_returns_full_response_on_mismatch() {
13559 let mw = ETagMiddleware::new();
13560 let ctx = test_context();
13561
13562 let mut req = Request::new(crate::request::Method::Get, "/resource");
13563 req.headers_mut()
13564 .insert("if-none-match", b"\"old-etag\"".to_vec());
13565
13566 let body = br#"{"status":"updated"}"#.to_vec();
13567 let response = Response::ok().body(ResponseBody::Bytes(body.clone()));
13568 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13569
13570 assert_eq!(response.status().as_u16(), 200);
13572 assert!(!response.body_ref().is_empty());
13573 }
13574
13575 #[test]
13576 fn etag_middleware_weak_etag_generation() {
13577 let config = ETagConfig::new().weak(true);
13578 let mw = ETagMiddleware::with_config(config);
13579 let ctx = test_context();
13580 let req = Request::new(crate::request::Method::Get, "/resource");
13581
13582 let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13583 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13584
13585 let etag = response
13586 .headers()
13587 .iter()
13588 .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
13589 .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
13590 .unwrap();
13591
13592 assert!(etag.starts_with("W/"), "Weak ETag should start with W/");
13593 }
13594
13595 #[test]
13596 fn etag_middleware_skips_post_requests() {
13597 let mw = ETagMiddleware::new();
13598 let ctx = test_context();
13599 let req = Request::new(crate::request::Method::Post, "/resource");
13600
13601 let response = Response::ok().body(ResponseBody::Bytes(b"created".to_vec()));
13602 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13603
13604 let etag = response
13606 .headers()
13607 .iter()
13608 .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13609 assert!(etag.is_none(), "POST should not have ETag");
13610 }
13611
13612 #[test]
13613 fn etag_middleware_handles_head_requests() {
13614 let mw = ETagMiddleware::new();
13615 let ctx = test_context();
13616 let req = Request::new(crate::request::Method::Head, "/resource");
13617
13618 let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13619 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13620
13621 let etag = response
13623 .headers()
13624 .iter()
13625 .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13626 assert!(etag.is_some(), "HEAD should have ETag");
13627 }
13628
13629 #[test]
13630 fn etag_middleware_disabled_mode() {
13631 let config = ETagConfig::new().mode(ETagMode::Disabled);
13632 let mw = ETagMiddleware::with_config(config);
13633 let ctx = test_context();
13634 let req = Request::new(crate::request::Method::Get, "/resource");
13635
13636 let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13637 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13638
13639 let etag = response
13641 .headers()
13642 .iter()
13643 .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13644 assert!(etag.is_none(), "Disabled mode should not add ETag");
13645 }
13646
13647 #[test]
13648 fn etag_middleware_min_size_filter() {
13649 let config = ETagConfig::new().min_size(1000);
13650 let mw = ETagMiddleware::with_config(config);
13651 let ctx = test_context();
13652 let req = Request::new(crate::request::Method::Get, "/resource");
13653
13654 let response = Response::ok().body(ResponseBody::Bytes(b"small".to_vec()));
13656 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13657
13658 let etag = response
13660 .headers()
13661 .iter()
13662 .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13663 assert!(etag.is_none(), "Small body should not get ETag");
13664 }
13665
13666 #[test]
13667 fn etag_middleware_preserves_existing_etag() {
13668 let config = ETagConfig::new().mode(ETagMode::Manual);
13669 let mw = ETagMiddleware::with_config(config);
13670 let ctx = test_context();
13671
13672 let mut req = Request::new(crate::request::Method::Get, "/resource");
13674 req.headers_mut()
13675 .insert("if-none-match", b"\"custom-etag\"".to_vec());
13676
13677 let response = Response::ok()
13679 .header("etag", b"\"custom-etag\"".to_vec())
13680 .body(ResponseBody::Bytes(b"data".to_vec()));
13681 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13682
13683 assert_eq!(response.status().as_u16(), 304);
13685 }
13686
13687 #[test]
13688 fn etag_middleware_wildcard_if_none_match() {
13689 let mw = ETagMiddleware::new();
13690 let ctx = test_context();
13691 let mut req = Request::new(crate::request::Method::Get, "/resource");
13692 req.headers_mut().insert("if-none-match", b"*".to_vec());
13693
13694 let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13695 let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13696
13697 assert_eq!(response.status().as_u16(), 304);
13699 }
13700
13701 #[test]
13702 fn etag_middleware_weak_comparison_matches() {
13703 let mw = ETagMiddleware::new();
13704 let ctx = test_context();
13705
13706 let req1 = Request::new(crate::request::Method::Get, "/resource");
13708 let body = b"test data".to_vec();
13709 let response1 = Response::ok().body(ResponseBody::Bytes(body.clone()));
13710 let response1 = futures_executor::block_on(mw.after(&ctx, &req1, response1));
13711
13712 let etag = response1
13713 .headers()
13714 .iter()
13715 .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
13716 .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
13717 .unwrap();
13718
13719 let mut req2 = Request::new(crate::request::Method::Get, "/resource");
13721 let weak_etag = format!("W/{}", etag);
13722 req2.headers_mut()
13723 .insert("if-none-match", weak_etag.as_bytes().to_vec());
13724
13725 let response2 = Response::ok().body(ResponseBody::Bytes(body));
13726 let response2 = futures_executor::block_on(mw.after(&ctx, &req2, response2));
13727
13728 assert_eq!(response2.status().as_u16(), 304);
13730 }
13731
13732 #[test]
13733 fn etag_middleware_name() {
13734 let mw = ETagMiddleware::new();
13735 assert_eq!(mw.name(), "ETagMiddleware");
13736 }
13737
13738 #[test]
13739 fn etag_config_builder() {
13740 let config = ETagConfig::new()
13741 .mode(ETagMode::Auto)
13742 .weak(true)
13743 .min_size(512);
13744
13745 assert_eq!(config.mode, ETagMode::Auto);
13746 assert!(config.weak);
13747 assert_eq!(config.min_size, 512);
13748 }
13749
13750 #[test]
13751 fn etag_generates_consistent_hash() {
13752 let etag1 = ETagMiddleware::generate_etag(b"hello world", false);
13754 let etag2 = ETagMiddleware::generate_etag(b"hello world", false);
13755 assert_eq!(etag1, etag2);
13756
13757 let etag3 = ETagMiddleware::generate_etag(b"hello world!", false);
13759 assert_ne!(etag1, etag3);
13760 }
13761}