Skip to main content

zenith_http1/
response.rs

1//! HTTP/1.1 响应序列化器
2//!
3//! 将 CanonicalResponse 序列化为 HTTP/1.1 线路格式字节 (RFC 7230 §3)。
4//!
5//! # 格式
6//! ```text
7//! HTTP/1.1 {status_code} {reason_phrase}\r\n
8//! {Header-Name}: {Header-Value}\r\n
9//! ...
10//! \r\n
11//! {body}
12//! ```
13//!
14//! # 设计
15//! - 零堆分配热路径:写入调用方提供的 `&mut Vec<u8>`
16//! - 自动补全 Content-Length(若缺失且 body 非空)
17//! - CRLF 注入防护:拒绝包含 `\r\n` 的头部值(二次校验,上游已拦截)
18//! - RFC 7230 §3.2.4 严格合规:头部字段名仅允许 token 字符
19
20use zenith_api::CanonicalResponse;
21
22/// 响应序列化错误
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ResponseSerializeError {
25    /// 头部值包含 CRLF(注入攻击)
26    CrlfInjection,
27    /// 头部名称包含非法字符
28    InvalidHeaderName,
29}
30
31impl std::fmt::Display for ResponseSerializeError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::CrlfInjection => write!(f, "CRLF injection detected in header"),
35            Self::InvalidHeaderName => write!(f, "invalid header name character"),
36        }
37    }
38}
39
40impl std::error::Error for ResponseSerializeError {}
41
42/// 获取状态码对应的 Reason Phrase (RFC 7231 §6)
43#[inline]
44fn reason_phrase(status: u16) -> &'static str {
45    match status {
46        100 => "Continue",
47        101 => "Switching Protocols",
48        200 => "OK",
49        201 => "Created",
50        202 => "Accepted",
51        203 => "Non-Authoritative Information",
52        204 => "No Content",
53        205 => "Reset Content",
54        206 => "Partial Content",
55        301 => "Moved Permanently",
56        302 => "Found",
57        303 => "See Other",
58        304 => "Not Modified",
59        307 => "Temporary Redirect",
60        308 => "Permanent Redirect",
61        400 => "Bad Request",
62        401 => "Unauthorized",
63        402 => "Payment Required",
64        403 => "Forbidden",
65        404 => "Not Found",
66        405 => "Method Not Allowed",
67        406 => "Not Acceptable",
68        407 => "Proxy Authentication Required",
69        408 => "Request Timeout",
70        409 => "Conflict",
71        410 => "Gone",
72        411 => "Length Required",
73        412 => "Precondition Failed",
74        413 => "Payload Too Large",
75        414 => "URI Too Long",
76        415 => "Unsupported Media Type",
77        416 => "Range Not Satisfiable",
78        417 => "Expectation Failed",
79        421 => "Misdirected Request",
80        422 => "Unprocessable Entity",
81        425 => "Too Early",
82        426 => "Upgrade Required",
83        428 => "Precondition Required",
84        429 => "Too Many Requests",
85        431 => "Request Header Fields Too Large",
86        451 => "Unavailable For Legal Reasons",
87        500 => "Internal Server Error",
88        501 => "Not Implemented",
89        502 => "Bad Gateway",
90        503 => "Service Unavailable",
91        504 => "Gateway Timeout",
92        505 => "HTTP Version Not Supported",
93        _ => "Unknown",
94    }
95}
96
97/// HTTP/1.1 响应序列化器
98///
99/// 将规范化响应序列化为符合 RFC 7230 的 HTTP/1.1 线路格式。
100#[derive(Debug)]
101pub struct Http1ResponseEncoder;
102
103impl Http1ResponseEncoder {
104    /// 将 CanonicalResponse 序列化为 HTTP/1.1 线路格式
105    ///
106    /// # 参数
107    /// - `response`: 规范化响应
108    /// - `out`: 输出缓冲区(调用方预分配,零堆分配热路径)
109    ///
110    /// # 返回
111    /// 写入的字节数,或序列化错误
112    ///
113    /// # 安全
114    /// - 自动补全 Content-Length(若缺失且 body 非空且无 Transfer-Encoding)
115    /// - 拒绝 CRLF 注入(二次校验)
116    /// - 头部名称仅允许 RFC 7230 §3.2.6 token 字符
117    pub fn encode(
118        response: &CanonicalResponse,
119        out: &mut Vec<u8>,
120    ) -> Result<usize, ResponseSerializeError> {
121        let start = out.len();
122
123        // 状态行: HTTP/1.1 {status} {reason}\r\n
124        out.extend_from_slice(b"HTTP/1.1 ");
125        out.extend_from_slice(response.status_code.to_string().as_bytes());
126        out.push(b' ');
127        out.extend_from_slice(reason_phrase(response.status_code).as_bytes());
128        out.extend_from_slice(b"\r\n");
129
130        // 检查是否已有 Content-Length / Transfer-Encoding
131        let mut has_content_length = false;
132        let mut has_transfer_encoding = false;
133
134        // 头部
135        for header in response.headers_iter() {
136            let name = header.name_str();
137            let value = header.value_str();
138
139            // CRLF 注入二次校验
140            if value.contains('\r') || value.contains('\n') {
141                return Err(ResponseSerializeError::CrlfInjection);
142            }
143
144            // 头部名称 token 校验(含非空拒绝,fail-closed)
145            if !zenith_api::normalize::is_valid_header_name(name) {
146                return Err(ResponseSerializeError::InvalidHeaderName);
147            }
148
149            let lower = name.to_ascii_lowercase();
150            if lower == "content-length" {
151                has_content_length = true;
152            }
153            if lower == "transfer-encoding" {
154                has_transfer_encoding = true;
155            }
156
157            out.extend_from_slice(name.as_bytes());
158            out.extend_from_slice(b": ");
159            out.extend_from_slice(value.as_bytes());
160            out.extend_from_slice(b"\r\n");
161        }
162
163        // 自动补全 Content-Length(若未设置且非 chunked)
164        // 严格 RFC 7230 §3.3.2:
165        // - 已知 body 长度(含 0 字节)且无 Transfer-Encoding 时 SHOULD 生成 Content-Length
166        // - 1xx / 204 / 304 MUST NOT 发送 Content-Length(禁止在这些代码上发送 body)
167        let body = response.body();
168        let status = response.status_code;
169        let forbid_content_length = matches!(status, 100..=199) || status == 204 || status == 304;
170        if !forbid_content_length && !has_content_length && !has_transfer_encoding {
171            out.extend_from_slice(b"content-length: ");
172            out.extend_from_slice(body.len().to_string().as_bytes());
173            out.extend_from_slice(b"\r\n");
174        }
175
176        // 空行
177        out.extend_from_slice(b"\r\n");
178
179        // Body:1xx/204/304 为无体状态码(MUST NOT 发送 body),
180        // 即使应用误设了 body 也不写出,避免"无长度头 + body"的响应走私/解析歧义。
181        if !forbid_content_length && !body.is_empty() {
182            out.extend_from_slice(body);
183        }
184
185        Ok(out.len() - start)
186    }
187
188    /// 便捷方法:序列化为新的 Vec
189    #[inline]
190    pub fn encode_to_vec(response: &CanonicalResponse) -> Result<Vec<u8>, ResponseSerializeError> {
191        let mut out = Vec::with_capacity(256 + response.body().len());
192        Http1ResponseEncoder::encode(response, &mut out)?;
193        Ok(out)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use zenith_api::CanonicalResponse;
201
202    #[test]
203    fn test_encode_simple_response() {
204        let mut resp = CanonicalResponse::new(200);
205        resp.add_header(b"content-type", b"text/plain").unwrap();
206        resp.set_body(b"Hello, World!");
207
208        let mut out = Vec::new();
209        let n = Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
210        let s = String::from_utf8(out).unwrap();
211
212        assert!(s.starts_with("HTTP/1.1 200 OK\r\n"));
213        assert!(s.contains("content-type: text/plain\r\n"));
214        assert!(s.contains("content-length: 13\r\n"));
215        assert!(s.ends_with("\r\nHello, World!"));
216        assert_eq!(n, s.len());
217    }
218
219    #[test]
220    fn test_encode_no_body() {
221        let resp = CanonicalResponse::new(204);
222        let mut out = Vec::new();
223        Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
224        let s = String::from_utf8(out).unwrap();
225
226        assert!(s.starts_with("HTTP/1.1 204 No Content\r\n"));
227        assert!(s.ends_with("\r\n\r\n"));
228        // 204 不应有 Content-Length
229        assert!(!s.contains("content-length"));
230    }
231
232    #[test]
233    fn test_encode_with_explicit_content_length() {
234        let mut resp = CanonicalResponse::new(200);
235        resp.add_header(b"content-length", b"42").unwrap();
236        resp.set_body(b"Hello");
237
238        let mut out = Vec::new();
239        Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
240        let s = String::from_utf8(out).unwrap();
241
242        // 不应重复添加 Content-Length
243        let cl_count = s.matches("content-length").count();
244        assert_eq!(cl_count, 1);
245        assert!(s.contains("content-length: 42\r\n"));
246    }
247
248    #[test]
249    fn test_encode_crlf_injection_blocked() {
250        let mut resp = CanonicalResponse::new(200);
251        // 第一层防御:CanonicalResponse::add_header 拒绝 CRLF
252        let add_result = resp.add_header(b"x-evil", b"val\r\nInjected: yes");
253        assert!(add_result.is_err(), "add_header must reject CRLF in value");
254
255        // add_header 失败,头部未添加,encode 应成功(无恶意头部)
256        let mut out = Vec::new();
257        let result = Http1ResponseEncoder::encode(&resp, &mut out);
258        assert!(result.is_ok(), "encode should succeed on clean response");
259    }
260
261    #[test]
262    fn test_encode_reason_phrases() {
263        for (code, phrase) in &[
264            (200u16, "OK"),
265            (404, "Not Found"),
266            (500, "Internal Server Error"),
267            (301, "Moved Permanently"),
268            (429, "Too Many Requests"),
269            (451, "Unavailable For Legal Reasons"),
270        ] {
271            let resp = CanonicalResponse::new(*code);
272            let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
273            let s = String::from_utf8(out).unwrap();
274            assert!(s.starts_with(&format!("HTTP/1.1 {code} {phrase}\r\n")));
275        }
276    }
277
278    #[test]
279    fn test_encode_unknown_status() {
280        let resp = CanonicalResponse::new(599);
281        let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
282        let s = String::from_utf8(out).unwrap();
283        assert!(s.starts_with("HTTP/1.1 599 Unknown\r\n"));
284    }
285
286    #[test]
287    fn test_encode_to_vec() {
288        let mut resp = CanonicalResponse::new(200);
289        resp.set_body(b"test");
290        let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
291        assert!(!out.is_empty());
292        assert!(out.windows(4).any(|w| w == b"test"));
293    }
294
295    #[test]
296    fn test_encode_multiple_headers() {
297        let mut resp = CanonicalResponse::new(200);
298        resp.add_header(b"x-custom-1", b"value1").unwrap();
299        resp.add_header(b"x-custom-2", b"value2").unwrap();
300        resp.add_header(b"server", b"Zenith/1.0").unwrap();
301        resp.set_body(b"OK");
302
303        let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
304        assert!(s.contains("x-custom-1: value1\r\n"));
305        assert!(s.contains("x-custom-2: value2\r\n"));
306        assert!(s.contains("server: Zenith/1.0\r\n"));
307        assert!(s.ends_with("\r\nOK"));
308    }
309
310    #[test]
311    fn test_encode_empty_body_with_content_type() {
312        let mut resp = CanonicalResponse::new(200);
313        resp.add_header(b"content-type", b"application/json").unwrap();
314        // HTTP/1.1 keep-alive 合规:空 body 也必须显式声明 Content-Length: 0(RFC 7230 §3.3.2)
315        let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
316        assert!(s.contains("content-type: application/json\r\n"));
317        // 空 body(非 1xx/204/304)必须补 Content-Length: 0,避免 keep-alive 客户端挂起
318        assert!(
319            s.contains("content-length: 0"),
320            "empty-body 200 response must include Content-Length: 0 for keep-alive compliance. Raw: {s}"
321        );
322    }
323
324    #[test]
325    fn test_reason_phrase_all_variants() {
326        let codes = [100, 101, 200, 201, 204, 206, 301, 302, 304, 400, 401, 403, 404, 500];
327        for &code in &codes {
328            let resp = CanonicalResponse::new(code);
329            let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
330            assert!(!s.contains("Unknown"), "status {code} should have known reason phrase");
331        }
332    }
333}