Skip to main content

ecat_security/
lib.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2use http::{Request, StatusCode};
3use security_rust::Scanner;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::{Context as TaskCtx, Poll};
8
9pub use security_rust::{AttackCategory, DetectionResult, ScannerBuilder, Severity};
10
11#[derive(Debug, thiserror::Error)]
12pub enum SecurityError {
13    #[error("attack blocked: {0}")]
14    AttackBlocked(String),
15    /// 请求体超过 body_limit 上限(读体阶段即拒绝,不进入扫描)。
16    #[error("request body too large")]
17    BodyTooLarge,
18    #[error("inner error: {0}")]
19    Inner(#[from] Box<dyn std::error::Error + Send + Sync>),
20}
21
22impl SecurityError {
23    pub fn to_http_status(&self) -> StatusCode {
24        match self {
25            Self::AttackBlocked(_) => StatusCode::FORBIDDEN,
26            Self::BodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
27            Self::Inner(_) => StatusCode::INTERNAL_SERVER_ERROR,
28        }
29    }
30}
31
32/// 拦截结果映射为 HTTP 响应:攻击拦截为 403,请求体超限为 413,内部
33/// 错误为 500。内部错误的原始信息只进日志,响应体保持通用文案,避免
34/// 把内部错误细节(文件路径、SQL、堆栈)泄露给客户端。
35impl axum::response::IntoResponse for SecurityError {
36    fn into_response(self) -> axum::response::Response {
37        let status = self.to_http_status();
38        let body = match &self {
39            Self::AttackBlocked(types) => {
40                format!(r#"{{"error":"attack blocked","types":"{types}"}}"#)
41            }
42            Self::BodyTooLarge => r#"{"error":"request body too large"}"#.to_string(),
43            Self::Inner(e) => {
44                tracing::error!(error = %e, "security middleware internal error");
45                r#"{"error":"internal server error"}"#.to_string()
46            }
47        };
48        (status, body).into_response()
49    }
50}
51
52/// Wraps `security_rust::Scanner` with convenient constructors.
53pub struct SecurityScanner {
54    scanner: Scanner,
55}
56
57impl SecurityScanner {
58    /// Create scanner with default detector configuration.
59    pub fn new() -> Self {
60        Self {
61            scanner: Scanner::default(),
62        }
63    }
64
65    /// Scan a single string through all detectors.
66    pub fn scan(&self, input: &str) -> Vec<DetectionResult> {
67        self.scanner.scan(input)
68    }
69
70    /// Scan multiple request parts (path, headers, body, etc.).
71    pub fn scan_parts(&self, parts: &[&str]) -> Vec<DetectionResult> {
72        let mut results = Vec::with_capacity(parts.len() * 2);
73        for part in parts {
74            results.extend(self.scanner.scan(part));
75        }
76        results
77    }
78
79    /// Scan request body bytes. Converts to string for analysis.
80    pub fn scan_body(&self, body: &[u8]) -> Vec<DetectionResult> {
81        if let Ok(s) = std::str::from_utf8(body) {
82            self.scanner.scan(s)
83        } else {
84            Vec::new()
85        }
86    }
87}
88
89impl Default for SecurityScanner {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95/// Logs detections and returns a blocking error when a High/Critical attack
96/// was found. Shared by the header-scanning and body-scanning middlewares.
97fn evaluate(results: &[DetectionResult]) -> Option<SecurityError> {
98    let mut blocked = false;
99    for r in results {
100        tracing::warn!(
101            attack_type = %r.attack_type,
102            category = ?r.category,
103            severity = ?r.severity,
104            matched = %r.matched_pattern,
105            "attack detected"
106        );
107        // jwt_attack 的宽正则(ey..ey.. 匹配一切标准 JWT)会误伤合法 token:
108        // 服务端鉴权由 JwtAuthLayer 验签把关(alg:none/伪造签名在验签层拒绝),
109        // 此处仅记日志不拦截。
110        if r.attack_type != "jwt_attack"
111            && matches!(r.severity, Severity::High | Severity::Critical)
112        {
113            blocked = true;
114        }
115    }
116    if blocked {
117        let attack_types: Vec<String> = results.iter().map(|r| r.attack_type.to_string()).collect();
118        return Some(SecurityError::AttackBlocked(attack_types.join(", ")));
119    }
120    None
121}
122
123/// 百分号解码,仅用于扫描检测;原始 URI 在响应/日志/转发中保持不变。
124/// 无效的 % 序列原样保留,非 UTF-8 解码结果按 replacement 字符处理。
125fn percent_decode(s: &str) -> String {
126    let bytes = s.as_bytes();
127    let mut out = Vec::with_capacity(bytes.len());
128    let mut i = 0;
129    while i < bytes.len() {
130        if bytes[i] == b'%'
131            && i + 2 < bytes.len()
132            && let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2]))
133        {
134            out.push(h * 16 + l);
135            i += 3;
136            continue;
137        }
138        out.push(bytes[i]);
139        i += 1;
140    }
141    String::from_utf8_lossy(&out).into_owned()
142}
143
144fn hex_val(b: u8) -> Option<u8> {
145    match b {
146        b'0'..=b'9' => Some(b - b'0'),
147        b'a'..=b'f' => Some(b - b'a' + 10),
148        b'A'..=b'F' => Some(b - b'A' + 10),
149        _ => None,
150    }
151}
152
153/// Builds the scan list from URI and headers (shared by both middlewares).
154/// URI 先做百分号解码再扫描:`?q=SELECT%20*%20FROM%20users` 若不解码会绕过
155/// 要求字面空白的 SQLi 正则。解码仅用于检测,URI 本身不变。
156/// 代理拓扑头(X-Forwarded-For/X-Real-IP/Forwarded 等)由网关重写,携带
157/// 内网 IP(如 docker 网关 172.x),SSRF 检测会误伤内网部署,跳过不扫。
158/// Authorization 同理:JWT 是本站正常鉴权流量,jwt_attack 规则会误报,
159/// 扫描跳过(token 的校验由 JwtAuthLayer 负责)。
160fn is_proxy_header(name: &http::header::HeaderName) -> bool {
161    matches!(
162        name.as_str(),
163        "x-forwarded-for"
164            | "x-real-ip"
165            | "forwarded"
166            | "x-forwarded-host"
167            | "x-forwarded-proto"
168            | "x-forwarded-port"
169            | "authorization"
170    )
171}
172
173fn request_parts<B>(req: &Request<B>) -> Vec<String> {
174    let mut parts: Vec<String> = Vec::new();
175    parts.push(percent_decode(&req.uri().to_string()));
176    for (name, value) in req.headers() {
177        if is_proxy_header(name) {
178            continue;
179        }
180        if let Ok(v) = value.to_str() {
181            parts.push(v.to_string());
182        }
183    }
184    parts
185}
186
187// ── Tower Layer ──
188
189#[derive(Clone)]
190pub struct SecurityLayer {
191    scanner: Arc<SecurityScanner>,
192}
193
194impl SecurityLayer {
195    pub fn new() -> Self {
196        Self {
197            scanner: Arc::new(SecurityScanner::new()),
198        }
199    }
200}
201
202impl Default for SecurityLayer {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208impl<S> tower::Layer<S> for SecurityLayer {
209    type Service = SecurityService<S>;
210
211    fn layer(&self, inner: S) -> Self::Service {
212        SecurityService {
213            inner,
214            scanner: Arc::clone(&self.scanner),
215        }
216    }
217}
218
219#[derive(Clone)]
220pub struct SecurityService<S> {
221    inner: S,
222    scanner: Arc<SecurityScanner>,
223}
224
225impl<S, B> tower::Service<Request<B>> for SecurityService<S>
226where
227    S: tower::Service<Request<B>> + Send + 'static,
228    S::Future: Send + 'static,
229    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
230    // 拦截时直接返回 403 响应而非 Err:Err 经服务端 no_error 归一到
231    // Infallible 时触发 unreachable panic,会打崩 worker 线程
232    S::Response: From<axum::response::Response> + Send,
233    B: Send + 'static,
234{
235    type Response = S::Response;
236    type Error = SecurityError;
237    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
238
239    fn poll_ready(&mut self, cx: &mut TaskCtx<'_>) -> Poll<Result<(), Self::Error>> {
240        self.inner
241            .poll_ready(cx)
242            .map_err(|e| SecurityError::Inner(e.into()))
243    }
244
245    fn call(&mut self, req: Request<B>) -> Self::Future {
246        let scanner = Arc::clone(&self.scanner);
247        let parts = request_parts(&req);
248        let strings: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
249        let results = scanner.scan_parts(&strings);
250
251        if let Some(err) = evaluate(&results) {
252            use axum::response::IntoResponse;
253            let resp: S::Response = err.into_response().into();
254            return Box::pin(async move { Ok(resp) });
255        }
256
257        let fut = self.inner.call(req);
258        Box::pin(async move { fut.await.map_err(|e| SecurityError::Inner(e.into())) })
259    }
260}
261
262// ── Body-scanning variant ──
263
264/// Tower layer that scans URI, headers, **and** the request body.
265///
266/// The body is read exactly once (up to [`body_limit`](Self::body_limit)
267/// bytes) and passed through to the inner service, so handlers still receive
268/// the full payload. Use this instead of [`SecurityLayer`] when request
269/// bodies must also be checked for SQLi/XSS payloads.
270#[derive(Clone)]
271pub struct SecurityBodyLayer {
272    scanner: Arc<SecurityScanner>,
273    body_limit: usize,
274}
275
276impl SecurityBodyLayer {
277    pub fn new() -> Self {
278        Self {
279            scanner: Arc::new(SecurityScanner::new()),
280            body_limit: 10 * 1024 * 1024,
281        }
282    }
283
284    /// Maximum body size (in bytes) that will be buffered and scanned.
285    /// Larger bodies are rejected with a 413 (Payload Too Large) rather
286    /// than buffered.
287    pub fn body_limit(mut self, limit: usize) -> Self {
288        self.body_limit = limit;
289        self
290    }
291}
292
293impl Default for SecurityBodyLayer {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299impl<S> tower::Layer<S> for SecurityBodyLayer {
300    type Service = SecurityBodyService<S>;
301
302    fn layer(&self, inner: S) -> Self::Service {
303        SecurityBodyService {
304            inner,
305            scanner: Arc::clone(&self.scanner),
306            body_limit: self.body_limit,
307        }
308    }
309}
310
311#[derive(Clone)]
312pub struct SecurityBodyService<S> {
313    inner: S,
314    scanner: Arc<SecurityScanner>,
315    body_limit: usize,
316}
317
318impl<S> tower::Service<Request<axum::body::Body>> for SecurityBodyService<S>
319where
320    S: tower::Service<Request<axum::body::Body>> + Clone + Send + 'static,
321    S::Future: Send + 'static,
322    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
323{
324    type Response = S::Response;
325    type Error = SecurityError;
326    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
327
328    fn poll_ready(&mut self, cx: &mut TaskCtx<'_>) -> Poll<Result<(), Self::Error>> {
329        self.inner
330            .poll_ready(cx)
331            .map_err(|e| SecurityError::Inner(e.into()))
332    }
333
334    fn call(&mut self, req: Request<axum::body::Body>) -> Self::Future {
335        let scanner = Arc::clone(&self.scanner);
336        let body_limit = self.body_limit;
337        let mut inner = self.inner.clone();
338        let parts = request_parts(&req);
339        let strings: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
340        let header_results = scanner.scan_parts(&strings);
341
342        Box::pin(async move {
343            let (parts, body) = req.into_parts();
344            // Read the body exactly once; the collected bytes become the new
345            // body so downstream handlers can still access the payload.
346            let bytes = match axum::body::to_bytes(body, body_limit).await {
347                Ok(bytes) => bytes,
348                Err(e) => {
349                    // LengthLimitError = 超限:413;其余读体错误为 500
350                    let inner: Box<dyn std::error::Error + Send + Sync> = e.into_inner();
351                    if inner
352                        .downcast_ref::<http_body_util::LengthLimitError>()
353                        .is_some()
354                    {
355                        return Err(SecurityError::BodyTooLarge);
356                    }
357                    return Err(SecurityError::Inner(inner));
358                }
359            };
360
361            let mut results = header_results;
362            results.extend(scanner.scan_body(&bytes));
363
364            if let Some(err) = evaluate(&results) {
365                return Err(err);
366            }
367
368            let req = Request::from_parts(parts, axum::body::Body::from(bytes));
369            inner
370                .call(req)
371                .await
372                .map_err(|e| SecurityError::Inner(e.into()))
373        })
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn scanner_detects_sql_injection() {
383        let s = SecurityScanner::new();
384        let results = s.scan("SELECT * FROM users; DROP TABLE users;");
385        assert!(!results.is_empty());
386    }
387
388    #[test]
389    fn scanner_detects_xss() {
390        let s = SecurityScanner::new();
391        let results = s.scan("<script>alert('xss')</script>");
392        assert!(!results.is_empty());
393    }
394
395    #[test]
396    fn scanner_clean_input_no_detection() {
397        let s = SecurityScanner::new();
398        let results = s.scan("hello world");
399        assert!(results.is_empty());
400    }
401
402    #[test]
403    fn scanner_scan_parts_aggregates() {
404        let s = SecurityScanner::new();
405        let results = s.scan_parts(&["clean", "<script>x</script>"]);
406        assert!(!results.is_empty());
407    }
408
409    #[test]
410    fn attack_blocked_maps_to_403() {
411        use axum::response::IntoResponse;
412        let resp = SecurityError::AttackBlocked("sqli".into()).into_response();
413        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
414    }
415
416    #[test]
417    fn inner_error_maps_to_500() {
418        use axum::response::IntoResponse;
419        let resp = SecurityError::Inner(Box::new(std::io::Error::other("boom"))).into_response();
420        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
421    }
422
423    #[test]
424    fn layer_constructs() {
425        let _layer = SecurityLayer::new();
426    }
427
428    #[test]
429    fn layer_default_constructs() {
430        let _layer: SecurityLayer = Default::default();
431    }
432
433    #[test]
434    fn body_layer_constructs() {
435        let _layer = SecurityBodyLayer::new().body_limit(1024);
436    }
437
438    #[test]
439    fn body_layer_default_constructs() {
440        let _layer: SecurityBodyLayer = Default::default();
441    }
442
443    #[tokio::test]
444    async fn body_layer_blocks_attack_in_body() {
445        use tower::Layer as _;
446        use tower::ServiceExt;
447
448        let layer = SecurityBodyLayer::new();
449        let svc = layer.layer(tower::service_fn(|_: Request<axum::body::Body>| async {
450            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
451        }));
452
453        let req = http::Request::builder()
454            .method("POST")
455            .uri("/submit")
456            .body(axum::body::Body::from("<script>alert('xss')</script>"))
457            .unwrap();
458        let result = svc.oneshot(req).await;
459        assert!(matches!(result, Err(SecurityError::AttackBlocked(_))));
460    }
461
462    #[tokio::test]
463    async fn body_over_limit_maps_to_413() {
464        use tower::Layer as _;
465        use tower::ServiceExt;
466
467        let layer = SecurityBodyLayer::new().body_limit(8);
468        let svc = layer.layer(tower::service_fn(|_: Request<axum::body::Body>| async {
469            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
470        }));
471
472        let req = http::Request::builder()
473            .method("POST")
474            .uri("/submit")
475            .body(axum::body::Body::from("x".repeat(64)))
476            .unwrap();
477        let result = svc.oneshot(req).await;
478        assert!(matches!(result, Err(SecurityError::BodyTooLarge)));
479        assert_eq!(
480            SecurityError::BodyTooLarge.to_http_status(),
481            StatusCode::PAYLOAD_TOO_LARGE
482        );
483    }
484
485    #[tokio::test]
486    async fn inner_error_response_body_does_not_leak_detail() {
487        use axum::response::IntoResponse;
488        let resp = SecurityError::Inner(Box::new(std::io::Error::other(
489            "secret-db-dsn=s3://user:pass",
490        )))
491        .into_response();
492        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
493        let (_, body) = resp.into_parts();
494        let bytes = axum::body::to_bytes(body, 4096).await.unwrap();
495        let text = String::from_utf8_lossy(&bytes);
496        assert!(!text.contains("secret-db-dsn"), "leaked detail: {text}");
497        assert!(text.contains("internal server error"), "got: {text}");
498    }
499
500    #[tokio::test]
501    async fn attack_blocked_response_body_shape() {
502        use axum::response::IntoResponse;
503        let resp = SecurityError::AttackBlocked("sqli, xss".into()).into_response();
504        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
505        let (_, body) = resp.into_parts();
506        let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
507        let text = String::from_utf8_lossy(&bytes);
508        assert_eq!(
509            text,
510            r#"{"error":"attack blocked","types":"sqli, xss"}"#.to_string()
511        );
512    }
513
514    #[tokio::test]
515    async fn body_too_large_response_body_shape() {
516        use axum::response::IntoResponse;
517        let resp = SecurityError::BodyTooLarge.into_response();
518        assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
519        let (_, body) = resp.into_parts();
520        let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
521        let text = String::from_utf8_lossy(&bytes);
522        assert_eq!(text, r#"{"error":"request body too large"}"#.to_string());
523    }
524
525    #[test]
526    fn request_parts_include_uri_and_headers() {
527        let req = http::Request::builder()
528            .uri("/search?q=abc")
529            .header("X-Custom", "val-1")
530            .body(())
531            .unwrap();
532        let parts = request_parts(&req);
533        assert_eq!(
534            parts,
535            vec!["/search?q=abc".to_string(), "val-1".to_string()]
536        );
537    }
538
539    #[test]
540    fn percent_decode_handles_encoded_sql_chars() {
541        assert_eq!(
542            percent_decode("/q?x=SELECT%20*%20FROM%20users"),
543            "/q?x=SELECT * FROM users"
544        );
545        assert_eq!(percent_decode("1%27%20OR%20%271%27%3D%271"), "1' OR '1'='1");
546        assert_eq!(
547            percent_decode("/clean?q=hello%20world"),
548            "/clean?q=hello world"
549        );
550        assert_eq!(percent_decode("%3cscript%3e"), "<script>");
551        // 无效 % 序列原样保留
552        assert_eq!(percent_decode("/%zz%"), "/%zz%");
553        assert_eq!(percent_decode("/100%25"), "/100%");
554    }
555
556    #[test]
557    fn request_parts_percent_decode_uri_only() {
558        let req = http::Request::builder()
559            .uri("/search?q=SELECT%20*%20FROM%20users")
560            .header("X-Custom", "val%201")
561            .body(())
562            .unwrap();
563        let parts = request_parts(&req);
564        // URI 解码后进入扫描列表;header 原样保留(header 无编码层)
565        assert_eq!(parts[0], "/search?q=SELECT * FROM users");
566        assert_eq!(parts[1], "val%201");
567    }
568
569    #[tokio::test]
570    async fn header_layer_blocks_attack_in_uri() {
571        use tower::Layer as _;
572        use tower::ServiceExt;
573
574        let layer = SecurityLayer::new();
575        let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
576            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
577        }));
578
579        // URI 不允许空格,SQLi 正则需字面空白 → 用 URI 合法字符即可命中的
580        // javascript: XSS 载荷
581        let req = http::Request::builder()
582            .uri("/redirect?url=javascript:alert(1)")
583            .body(())
584            .unwrap();
585        let resp = svc.oneshot(req).await.expect("blocked as response");
586        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
587    }
588
589    #[tokio::test]
590    async fn header_layer_blocks_attack_in_header() {
591        use tower::Layer as _;
592        use tower::ServiceExt;
593
594        let layer = SecurityLayer::new();
595        let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
596            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
597        }));
598
599        let req = http::Request::builder()
600            .uri("/clean")
601            .header("X-Trace", "<script>alert(1)</script>")
602            .body(())
603            .unwrap();
604        let resp = svc.oneshot(req).await.expect("blocked as response");
605        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
606    }
607
608    #[tokio::test]
609    async fn header_layer_blocks_encoded_sqli_in_uri() {
610        use tower::Layer as _;
611        use tower::ServiceExt;
612
613        let layer = SecurityLayer::new();
614        let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
615            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
616        }));
617
618        // %20 编码空格:解码后 `SELECT * FROM users` 命中 SQLi 正则
619        let req = http::Request::builder()
620            .uri("/search?q=SELECT%20*%20FROM%20users")
621            .body(())
622            .unwrap();
623        let resp = svc.oneshot(req).await.expect("blocked as response");
624        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
625    }
626
627    #[tokio::test]
628    async fn header_layer_blocks_encoded_single_quote_sqli_in_uri() {
629        use tower::Layer as _;
630        use tower::ServiceExt;
631
632        let layer = SecurityLayer::new();
633        let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
634            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
635        }));
636
637        // %27 编码单引号 + %20 编码空格:解码后 `1' OR '1'='1` 命中 SQLi 正则
638        let req = http::Request::builder()
639            .uri("/login?u=1%27%20OR%20%271%27%3D%271")
640            .body(())
641            .unwrap();
642        let resp = svc.oneshot(req).await.expect("blocked as response");
643        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
644    }
645
646    #[tokio::test]
647    async fn header_layer_passes_clean_encoded_query_through() {
648        use tower::Layer as _;
649        use tower::ServiceExt;
650
651        let layer = SecurityLayer::new();
652        let svc = layer.layer(tower::service_fn(|req: Request<()>| async move {
653            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
654                req.uri().path().to_string(),
655            )))
656        }));
657
658        // 正常请求含 %20 编码空格不应误拦截
659        let req = http::Request::builder()
660            .uri("/search?q=hello%20world")
661            .body(())
662            .unwrap();
663        let resp = svc.oneshot(req).await.expect("clean request passes");
664        let (_, body) = resp.into_parts();
665        let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
666        assert_eq!(String::from_utf8_lossy(&bytes), "/search");
667    }
668
669    #[tokio::test]
670    async fn header_layer_passes_proxy_headers_with_internal_ip() {
671        use tower::Layer as _;
672        use tower::ServiceExt;
673
674        let layer = SecurityLayer::new();
675        let svc = layer.layer(tower::service_fn(|req: Request<()>| async move {
676            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
677                req.uri().path().to_string(),
678            )))
679        }));
680
681        // nginx 网关注入的代理头携带 docker 内网 IP,不应触发 SSRF 拦截
682        let req = http::Request::builder()
683            .uri("/api/v1/booking/dates?region_id=1")
684            .header("X-Real-IP", "172.19.0.1")
685            .header("X-Forwarded-For", "172.19.0.1")
686            .body(())
687            .unwrap();
688        let resp = svc.oneshot(req).await.expect("proxy headers pass");
689        assert_eq!(resp.status(), StatusCode::OK);
690    }
691
692    #[tokio::test]
693    async fn header_layer_passes_clean_request_through() {
694        use tower::Layer as _;
695        use tower::ServiceExt;
696
697        let layer = SecurityLayer::new();
698        let svc = layer.layer(tower::service_fn(|req: Request<()>| async move {
699            Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
700                req.uri().path().to_string(),
701            )))
702        }));
703
704        let req = http::Request::builder().uri("/clean").body(()).unwrap();
705        let resp = svc.oneshot(req).await.expect("clean request passes");
706        let (_, body) = resp.into_parts();
707        let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
708        assert_eq!(String::from_utf8_lossy(&bytes), "/clean");
709    }
710
711    #[tokio::test]
712    async fn body_layer_passes_clean_body_through() {
713        use tower::Layer as _;
714        use tower::ServiceExt;
715
716        let layer = SecurityBodyLayer::new();
717        let svc = layer.layer(tower::service_fn(
718            |req: Request<axum::body::Body>| async move {
719                let (_, body) = req.into_parts();
720                let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
721                Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
722                    bytes,
723                )))
724            },
725        ));
726
727        let req = http::Request::builder()
728            .method("POST")
729            .uri("/submit")
730            .body(axum::body::Body::from("hello world"))
731            .unwrap();
732        let resp = svc.oneshot(req).await.expect("clean body passes through");
733        let (_, body) = resp.into_parts();
734        let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
735        assert_eq!(&bytes[..], b"hello world");
736    }
737}