Skip to main content

zenith_http1/
smuggling.rs

1//! HTTP/1.1 请求走私检测器
2//!
3//! 检测经典的 HTTP 请求走私攻击:
4//! - CL.TE: 上游使用 Content-Length,下游使用 Transfer-Encoding
5//! - TE.CL: 上游使用 Transfer-Encoding,下游使用 Content-Length
6//! - TE.TE: 存在多个 Transfer-Encoding 值
7//!
8//! 防护策略:严格 RFC 7230 §3.3.1,Content-Length 与 Transfer-Encoding 互斥。
9
10use crate::types::Http1Error;
11use std::borrow::Cow;
12
13/// 请求走私类型
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum SmugglingKind {
16    /// CL.TE:同时存在 Content-Length 和 Transfer-Encoding
17    ClTe,
18    /// TE.CL:同时存在 Transfer-Encoding 和 Content-Length(同上,攻击方向相反)
19    TeCl,
20    /// TE.TE:存在多个或非标准 Transfer-Encoding
21    TeTe,
22    /// Host 头值包含 CRLF
23    HostInjection,
24    /// 头部值包含 CRLF 注入
25    HeaderInjection,
26    /// 双 Content-Length
27    DoubleContentLength,
28    /// 双 Host 头(RFC 7230 §5.4:客户端不得发送多个 Host,
29    /// 前后端取词差分是经典走私向量)
30    DuplicateHost,
31}
32
33impl SmugglingKind {
34    /// 获取简短描述
35    #[inline]
36    pub fn as_str(&self) -> &'static str {
37        match self {
38            Self::ClTe => "CL.TE",
39            Self::TeCl => "TE.CL",
40            Self::TeTe => "TE.TE",
41            Self::HostInjection => "Host Injection",
42            Self::HeaderInjection => "Header Injection",
43            Self::DoubleContentLength => "Double Content-Length",
44            Self::DuplicateHost => "Duplicate Host",
45        }
46    }
47}
48
49/// 请求走私检测器
50///
51/// 在请求解析阶段对头部进行检测,识别不一致或恶意构造。
52#[derive(Debug, Default, Clone)]
53pub struct SmugglingDetector;
54
55impl SmugglingDetector {
56    /// 创建检测器
57    #[inline]
58    pub fn new() -> Self {
59        Self
60    }
61
62    /// 检测头部序列中的走私攻击
63    ///
64    /// # 参数
65    /// - `headers`: [(name, value)] 头部列表(名称未规范化也可,内部会小写化)
66    ///
67    /// # 返回
68    /// - Ok(()) : 通过检测
69    /// - Err((SmugglingKind, String)) : 检测到的走私类型与说明
70    ///
71    /// # 性能
72    /// 头部名称已小写化的调用方(如 [`crate::parser::Http1Parser`] 经
73    /// `parse_header_line` 规范化后)应优先使用 [`Self::detect_already_lowercased`],
74    /// 跳过每头一次的 `to_ascii_lowercase` 分配。
75    pub fn detect<N, V>(
76        &self,
77        headers: &[(N, V)],
78    ) -> Result<(), (SmugglingKind, String)>
79    where
80        N: AsRef<str>,
81        V: AsRef<str>,
82    {
83        self.detect_impl(headers, |s| Cow::Owned(s.to_ascii_lowercase()))
84    }
85
86    /// 共享检测逻辑(内部实现)
87    ///
88    /// 通过闭包 `normalize` 控制头部名称是否小写化:
89    /// - [`Self::detect`] 传入 `to_ascii_lowercase` 闭包(每头分配一次 `String`)
90    /// - [`Self::detect_already_lowercased`] 传入恒等闭包(零分配,`Cow::Borrowed`)
91    fn detect_impl<N, V>(
92        &self,
93        headers: &[(N, V)],
94        normalize: impl for<'a> Fn(&'a str) -> Cow<'a, str>,
95    ) -> Result<(), (SmugglingKind, String)>
96    where
97        N: AsRef<str>,
98        V: AsRef<str>,
99    {
100        let mut has_content_length = false;
101        let mut content_length_count: usize = 0;
102        let mut transfer_encoding_values: Vec<String> = Vec::with_capacity(4);
103        let mut has_te = false;
104        let mut host_count: usize = 0;
105        // CL / TE 首个出现位置(用于区分 CL.TE 与 TE.CL 两个攻击方向)
106        let mut cl_position: Option<usize> = None;
107        let mut te_position: Option<usize> = None;
108
109        for (idx, (name, value)) in headers.iter().enumerate() {
110            let value = value.as_ref();
111            let n = normalize(name.as_ref());
112
113            match &*n {
114                "content-length" => {
115                    content_length_count += 1;
116                    if content_length_count > 1 {
117                        return Err((
118                            SmugglingKind::DoubleContentLength,
119                            "multiple Content-Length".into(),
120                        ));
121                    }
122                    // 严格校验为十进制数字
123                    if !value.chars().all(|c| c.is_ascii_digit()) {
124                        return Err((
125                            SmugglingKind::ClTe,
126                            "Content-Length not decimal".into(),
127                        ));
128                    }
129                    has_content_length = true;
130                    if cl_position.is_none() {
131                        cl_position = Some(idx);
132                    }
133                }
134                "transfer-encoding" => {
135                    has_te = true;
136                    if te_position.is_none() {
137                        te_position = Some(idx);
138                    }
139                    // 解析多值(逗号分隔)
140                    for v in value.split(',') {
141                        let v = v.trim().to_ascii_lowercase();
142                        if !v.is_empty() {
143                            transfer_encoding_values.push(v);
144                        }
145                    }
146                }
147                "host" => {
148                    host_count += 1;
149                    if host_count > 1 {
150                        // RFC 7230 §5.4:多个 Host 头必须拒绝(前后端取词差分走私向量)
151                        return Err((
152                            SmugglingKind::DuplicateHost,
153                            "multiple Host headers".into(),
154                        ));
155                    }
156                    if value.contains('\r') || value.contains('\n') {
157                        return Err((
158                            SmugglingKind::HostInjection,
159                            "host contains CRLF".into(),
160                        ));
161                    }
162                }
163                _ => {}
164            }
165
166            // 1. 头部值 CRLF 注入检测(在特定头检测之后)
167            if value.contains("\r") || value.contains("\n") {
168                return Err((
169                    SmugglingKind::HeaderInjection,
170                    format!("header '{n}' value contains CRLF"),
171                ));
172            }
173            // 控制字符检测
174            if Self::contains_invalid_control(value) {
175                return Err((
176                    SmugglingKind::HeaderInjection,
177                    format!("header '{n}' contains invalid control chars"),
178                ));
179            }
180        }
181
182        // 2. CL 和 TE 共存 → 按头部出现顺序区分攻击方向:
183        //    CL 在前 → CL.TE(上游用 CL、下游用 TE)
184        //    TE 在前 → TE.CL(上游用 TE、下游用 CL)
185        if has_content_length && has_te {
186            let kind = match (cl_position, te_position) {
187                (Some(cl), Some(te)) if te < cl => SmugglingKind::TeCl,
188                _ => SmugglingKind::ClTe,
189            };
190            return Err((
191                kind,
192                "Content-Length and Transfer-Encoding both present".into(),
193            ));
194        }
195
196        // 3. TE.TE: 多个 chunked 值或 chunked 不是最后
197        if has_te {
198            // HTTP-002:RFC 7230 §3.3.1 已废弃 `identity` 作为 Transfer-Encoding
199            // 值(该值无传输编码语义且是前后端差分走私向量),出现即拒绝(fail-closed),
200            // 不再豁免。此检查须先于 chunked 位置判断,确保含 identity 的 TE 一律拒绝。
201            if transfer_encoding_values.iter().any(|v| v == "identity") {
202                return Err((
203                    SmugglingKind::TeTe,
204                    "deprecated 'identity' in Transfer-Encoding (RFC 7230)".into(),
205                ));
206            }
207            let chunked_positions: Vec<usize> = transfer_encoding_values
208                .iter()
209                .enumerate()
210                .filter_map(|(i, v)| (v == "chunked").then_some(i))
211                .collect();
212            if chunked_positions.len() > 1 {
213                return Err((
214                    SmugglingKind::TeTe,
215                    "multiple 'chunked' in Transfer-Encoding".into(),
216                ));
217            }
218            if let Some(pos) = chunked_positions.first().copied()
219                && pos != transfer_encoding_values.len() - 1 {
220                    return Err((
221                        SmugglingKind::TeTe,
222                        "'chunked' not last in Transfer-Encoding".into(),
223                    ));
224                }
225            // TE 存在但无 chunked → 走私嫌疑(fail-closed),与 parser 的 TE 语义统一
226            if chunked_positions.is_empty() {
227                return Err((
228                    SmugglingKind::TeTe,
229                    "Transfer-Encoding without 'chunked'".into(),
230                ));
231            }
232        }
233
234        // 4. Host 头缺失(HTTP/1.1 必须存在)不在此处直接报错,
235        // 由解析器结合版本上下文决定
236
237        Ok(())
238    }
239
240    /// 检测头部序列中的走私攻击(假设头部名称已小写化)
241    ///
242    /// 与 [`Self::detect`] 语义一致,但假设调用方已将头部名称小写化
243    /// (如 [`crate::parser::Http1Parser`] 的 `parse_header_line` 已执行
244    /// `to_ascii_lowercase`),跳过每头一次的 `to_ascii_lowercase` 分配。
245    ///
246    /// # 安全性
247    /// 若传入未小写化的头部名称,匹配将失败(如 `Content-Length` 不会匹配
248    /// `"content-length"`),可能导致漏检。调用方必须保证名称已小写。
249    pub fn detect_already_lowercased<N, V>(
250        &self,
251        headers: &[(N, V)],
252    ) -> Result<(), (SmugglingKind, String)>
253    where
254        N: AsRef<str>,
255        V: AsRef<str>,
256    {
257        self.detect_impl(headers, |s| Cow::Borrowed(s))
258    }
259
260    /// 便捷:直接返回 Http1Error
261    pub fn detect_err<N, V>(
262        &self,
263        headers: &[(N, V)],
264    ) -> Result<(), Http1Error>
265    where
266        N: AsRef<str>,
267        V: AsRef<str>,
268    {
269        self.detect(headers).map_err(|(k, m)| {
270            Http1Error::SmugglingDetected(format!("{}: {}", k.as_str(), m))
271        })
272    }
273
274    /// 便捷:直接返回 Http1Error(假设头部名称已小写化)
275    ///
276    /// 与 [`Self::detect_err`] 语义一致,但委托 [`Self::detect_already_lowercased`],
277    /// 跳过 `to_ascii_lowercase` 分配。调用方必须保证头部名称已小写化。
278    pub fn detect_err_already_lowercased<N, V>(
279        &self,
280        headers: &[(N, V)],
281    ) -> Result<(), Http1Error>
282    where
283        N: AsRef<str>,
284        V: AsRef<str>,
285    {
286        self.detect_already_lowercased(headers).map_err(|(k, m)| {
287            Http1Error::SmugglingDetected(format!("{}: {}", k.as_str(), m))
288        })
289    }
290
291    /// 检测非法控制字符
292    #[inline]
293    fn contains_invalid_control(s: &str) -> bool {
294        // RFC 7230 §3.2.4:field-content 仅允许 VCHAR/obs-text/SP/HTAB,
295        // 其余控制字符全量拒绝:0x00-0x08, 0x0b, 0x0c, 0x0e-0x1f, 0x7f
296        // (0x0a/0x0d 由 CRLF 检查单独覆盖;HTAB 0x09 为合法 OWS)。
297        s.chars().any(|c| {
298            let code = c as u32;
299            matches!(code, 0x00..=0x08 | 0x0b | 0x0c | 0x0e..=0x1f | 0x7f)
300        })
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_clean_headers() {
310        let d = SmugglingDetector::new();
311        let headers = vec![
312            ("Host".to_string(), "example.com".to_string()),
313            ("Content-Length".to_string(), "13".to_string()),
314            ("Accept".to_string(), "text/plain".to_string()),
315        ];
316        assert!(d.detect(&headers).is_ok());
317    }
318
319    #[test]
320    fn test_cl_te_smuggling() {
321        let d = SmugglingDetector::new();
322        let headers = vec![
323            ("Host".to_string(), "example.com".to_string()),
324            ("Content-Length".to_string(), "0".to_string()),
325            ("Transfer-Encoding".to_string(), "chunked".to_string()),
326        ];
327        let r = d.detect(&headers);
328        assert!(r.is_err());
329        assert_eq!(r.unwrap_err().0, SmugglingKind::ClTe);
330    }
331
332    #[test]
333    fn test_te_te_multiple_chunked() {
334        let d = SmugglingDetector::new();
335        let headers = vec![
336            ("Host".to_string(), "example.com".to_string()),
337            (
338                "Transfer-Encoding".to_string(),
339                "chunked, chunked".to_string(),
340            ),
341        ];
342        let r = d.detect(&headers);
343        assert_eq!(r.unwrap_err().0, SmugglingKind::TeTe);
344    }
345
346    #[test]
347    fn test_te_te_chunked_not_last() {
348        let d = SmugglingDetector::new();
349        let headers = vec![
350            ("Host".to_string(), "example.com".to_string()),
351            (
352                "Transfer-Encoding".to_string(),
353                "chunked, identity".to_string(),
354            ),
355        ];
356        let r = d.detect(&headers);
357        assert_eq!(r.unwrap_err().0, SmugglingKind::TeTe);
358    }
359
360    #[test]
361    fn test_double_content_length() {
362        let d = SmugglingDetector::new();
363        let headers = vec![
364            ("Host".to_string(), "example.com".to_string()),
365            ("Content-Length".to_string(), "10".to_string()),
366            ("Content-Length".to_string(), "20".to_string()),
367        ];
368        let r = d.detect(&headers);
369        assert_eq!(r.unwrap_err().0, SmugglingKind::DoubleContentLength);
370    }
371
372    #[test]
373    fn test_header_injection_crlf() {
374        let d = SmugglingDetector::new();
375        let headers = vec![
376            ("Host".to_string(), "example.com".to_string()),
377            ("X-Test".to_string(), "val\r\nEvil: yes".to_string()),
378        ];
379        let r = d.detect(&headers);
380        assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
381    }
382
383    #[test]
384    fn test_host_injection() {
385        let d = SmugglingDetector::new();
386        let headers = vec![("Host".to_string(), "example.com\r\nX: y".to_string())];
387        let r = d.detect(&headers);
388        assert_eq!(r.unwrap_err().0, SmugglingKind::HostInjection);
389    }
390
391    #[test]
392    fn test_duplicate_host_rejected() {
393        // RFC 7230 §5.4:多个 Host 头必须拒绝(前后端取词差分走私向量)
394        let d = SmugglingDetector::new();
395        let headers = vec![
396            ("Host".to_string(), "a.com".to_string()),
397            ("Host".to_string(), "b.com".to_string()),
398        ];
399        let r = d.detect(&headers);
400        assert_eq!(r.unwrap_err().0, SmugglingKind::DuplicateHost);
401    }
402
403    #[test]
404    fn test_duplicate_host_case_insensitive() {
405        // 头部名大小写不敏感:HOST + host 同样构成双 Host
406        let d = SmugglingDetector::new();
407        let headers = vec![
408            ("HOST".to_string(), "a.com".to_string()),
409            ("host".to_string(), "a.com".to_string()),
410        ];
411        let r = d.detect(&headers);
412        assert_eq!(r.unwrap_err().0, SmugglingKind::DuplicateHost);
413    }
414
415    #[test]
416    fn test_valid_te_chunked() {
417        let d = SmugglingDetector::new();
418        let headers = vec![
419            ("Host".to_string(), "example.com".to_string()),
420            ("Transfer-Encoding".to_string(), "gzip, chunked".to_string()),
421        ];
422        assert!(d.detect(&headers).is_ok());
423    }
424
425    #[test]
426    fn test_invalid_control_chars() {
427        let d = SmugglingDetector::new();
428        let headers = vec![
429            ("Host".to_string(), "example.com".to_string()),
430            ("X".to_string(), "val\x00ue".to_string()),
431        ];
432        let r = d.detect(&headers);
433        assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
434    }
435
436    #[test]
437    fn test_smuggling_kind_all_variants() {
438        let kinds = [
439            SmugglingKind::ClTe,
440            SmugglingKind::TeCl,
441            SmugglingKind::TeTe,
442            SmugglingKind::HostInjection,
443            SmugglingKind::HeaderInjection,
444            SmugglingKind::DoubleContentLength,
445        ];
446        for k in kinds.iter() {
447            let s = k.as_str();
448            assert!(!s.is_empty());
449        }
450    }
451
452    #[test]
453    fn test_smuggling_kind_as_str() {
454        assert_eq!(SmugglingKind::ClTe.as_str(), "CL.TE");
455        assert_eq!(SmugglingKind::TeCl.as_str(), "TE.CL");
456        assert_eq!(SmugglingKind::TeTe.as_str(), "TE.TE");
457        assert_eq!(SmugglingKind::HostInjection.as_str(), "Host Injection");
458        assert_eq!(SmugglingKind::HeaderInjection.as_str(), "Header Injection");
459        assert_eq!(SmugglingKind::DoubleContentLength.as_str(), "Double Content-Length");
460    }
461
462    #[test]
463    fn test_smuggling_detector_default() {
464        let d = SmugglingDetector;
465        let headers = vec![("Host".to_string(), "example.com".to_string())];
466        assert!(d.detect(&headers).is_ok());
467    }
468
469    #[test]
470    fn test_smuggling_detector_new() {
471        let d = SmugglingDetector::new();
472        let headers = vec![("Host".to_string(), "example.com".to_string())];
473        assert!(d.detect(&headers).is_ok());
474    }
475
476    #[test]
477    fn test_detect_err_wraps_correctly() {
478        let d = SmugglingDetector::new();
479        let headers = vec![
480            ("Host".to_string(), "example.com".to_string()),
481            ("Content-Length".to_string(), "10".to_string()),
482            ("Content-Length".to_string(), "20".to_string()),
483        ];
484        let r = d.detect_err(&headers);
485        assert!(r.is_err());
486        let err = r.unwrap_err();
487        assert!(matches!(err, Http1Error::SmugglingDetected(_)));
488        let err_str = err.to_string();
489        assert!(err_str.contains("Double Content-Length"));
490    }
491
492    #[test]
493    fn test_content_length_not_decimal() {
494        let d = SmugglingDetector::new();
495        let headers = vec![
496            ("Host".to_string(), "example.com".to_string()),
497            ("Content-Length".to_string(), "12a3".to_string()),
498        ];
499        let r = d.detect(&headers);
500        assert!(r.is_err());
501        assert_eq!(r.unwrap_err().0, SmugglingKind::ClTe);
502    }
503
504    #[test]
505    fn test_content_length_negative_rejected() {
506        let d = SmugglingDetector::new();
507        let headers = vec![
508            ("Host".to_string(), "example.com".to_string()),
509            ("Content-Length".to_string(), "-5".to_string()),
510        ];
511        let r = d.detect(&headers);
512        assert!(r.is_err());
513    }
514
515    #[test]
516    fn test_transfer_encoding_whitespace() {
517        let d = SmugglingDetector::new();
518        let headers = vec![
519            ("Host".to_string(), "example.com".to_string()),
520            ("Transfer-Encoding".to_string(), "  chunked  ".to_string()),
521        ];
522        assert!(d.detect(&headers).is_ok());
523    }
524
525    #[test]
526    fn test_transfer_encoding_mixed_case() {
527        let d = SmugglingDetector::new();
528        let headers = vec![
529            ("Host".to_string(), "example.com".to_string()),
530            ("Transfer-Encoding".to_string(), "Chunked".to_string()),
531        ];
532        let r = d.detect(&headers);
533        assert!(r.is_ok());
534    }
535
536    #[test]
537    fn test_header_name_case_insensitive() {
538        let d = SmugglingDetector::new();
539        let headers = vec![
540            ("HOST".to_string(), "example.com".to_string()),
541            ("content-length".to_string(), "10".to_string()),
542        ];
543        assert!(d.detect(&headers).is_ok());
544    }
545
546    #[test]
547    fn test_null_byte_in_header_value() {
548        let d = SmugglingDetector::new();
549        let headers = vec![
550            ("Host".to_string(), "example.com".to_string()),
551            ("X-Test".to_string(), "val\x00ue".to_string()),
552        ];
553        let r = d.detect(&headers);
554        assert!(r.is_err());
555        assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
556    }
557
558    #[test]
559    fn test_vtab_in_header_value() {
560        let d = SmugglingDetector::new();
561        let headers = vec![
562            ("Host".to_string(), "example.com".to_string()),
563            ("X-Test".to_string(), "val\x0bue".to_string()),
564        ];
565        let r = d.detect(&headers);
566        assert!(r.is_err());
567        assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
568    }
569
570    #[test]
571    fn test_formfeed_in_header_value() {
572        let d = SmugglingDetector::new();
573        let headers = vec![
574            ("Host".to_string(), "example.com".to_string()),
575            ("X-Test".to_string(), "val\x0cue".to_string()),
576        ];
577        let r = d.detect(&headers);
578        assert!(r.is_err());
579        assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
580    }
581
582    #[test]
583    fn test_cr_only_in_header_value() {
584        let d = SmugglingDetector::new();
585        let headers = vec![
586            ("Host".to_string(), "example.com".to_string()),
587            ("X-Test".to_string(), "val\revil".to_string()),
588        ];
589        let r = d.detect(&headers);
590        assert!(r.is_err());
591        assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
592    }
593
594    #[test]
595    fn test_lf_only_in_header_value() {
596        let d = SmugglingDetector::new();
597        let headers = vec![
598            ("Host".to_string(), "example.com".to_string()),
599            ("X-Test".to_string(), "val\nevil".to_string()),
600        ];
601        let r = d.detect(&headers);
602        assert!(r.is_err());
603        assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
604    }
605
606    #[test]
607    fn test_te_cl_direction_distinguished() {
608        // TE 在前、CL 在后 → TE.CL 攻击方向(上游用 TE、下游用 CL)
609        let d = SmugglingDetector::new();
610        let headers = vec![
611            ("Host".to_string(), "example.com".to_string()),
612            ("Transfer-Encoding".to_string(), "chunked".to_string()),
613            ("Content-Length".to_string(), "0".to_string()),
614        ];
615        let r = d.detect(&headers);
616        assert!(r.is_err());
617        assert_eq!(
618            r.unwrap_err().0,
619            SmugglingKind::TeCl,
620            "TE 在前必须判定为 TE.CL"
621        );
622    }
623
624    #[test]
625    fn test_cl_te_direction_distinguished() {
626        // CL 在前、TE 在后 → CL.TE 攻击方向(上游用 CL、下游用 TE)
627        let d = SmugglingDetector::new();
628        let headers = vec![
629            ("Host".to_string(), "example.com".to_string()),
630            ("Content-Length".to_string(), "0".to_string()),
631            ("Transfer-Encoding".to_string(), "chunked".to_string()),
632        ];
633        let r = d.detect(&headers);
634        assert!(r.is_err());
635        assert_eq!(
636            r.unwrap_err().0,
637            SmugglingKind::ClTe,
638            "CL 在前必须判定为 CL.TE"
639        );
640    }
641
642    #[test]
643    fn test_te_without_chunked_rejected() {
644        // TE 存在但无 chunked 且非 identity → fail-closed 拒绝
645        let d = SmugglingDetector::new();
646        let headers = vec![
647            ("Host".to_string(), "example.com".to_string()),
648            ("Transfer-Encoding".to_string(), "gzip".to_string()),
649        ];
650        let r = d.detect(&headers);
651        assert_eq!(r.unwrap_err().0, SmugglingKind::TeTe);
652    }
653
654    #[test]
655    fn test_te_identity_rejected() {
656        // HTTP-002:RFC 7230 已废弃 TE: identity,必须拒绝(不再豁免)
657        let d = SmugglingDetector::new();
658        let headers = vec![
659            ("Host".to_string(), "example.com".to_string()),
660            ("Transfer-Encoding".to_string(), "identity".to_string()),
661        ];
662        let r = d.detect(&headers);
663        assert_eq!(
664            r.unwrap_err().0,
665            SmugglingKind::TeTe,
666            "TE: identity 必须按 TeTe 拒绝(fail-closed)"
667        );
668    }
669
670    #[test]
671    fn test_single_transfer_encoding_chunked() {
672        let d = SmugglingDetector::new();
673        let headers = vec![
674            ("Host".to_string(), "example.com".to_string()),
675            ("Transfer-Encoding".to_string(), "chunked".to_string()),
676        ];
677        assert!(d.detect(&headers).is_ok());
678    }
679
680    #[test]
681    fn test_empty_headers() {
682        let d = SmugglingDetector::new();
683        let headers: Vec<(String, String)> = vec![];
684        assert!(d.detect(&headers).is_ok());
685    }
686
687    #[test]
688    fn test_smuggling_kind_debug() {
689        let k = SmugglingKind::ClTe;
690        let s = format!("{:?}", k);
691        assert!(!s.is_empty());
692    }
693
694    #[test]
695    fn test_smuggling_kind_clone() {
696        let k = SmugglingKind::ClTe;
697        let k2 = k;
698        assert_eq!(k, k2);
699    }
700}