zenith-http1 0.1.0

Zenith HTTP/1.1 协议解析器(RFC 7230):零堆分配热路径、流式解析、CRLF 防注入
Documentation
//! HTTP/1.1 响应序列化器
//!
//! 将 CanonicalResponse 序列化为 HTTP/1.1 线路格式字节 (RFC 7230 §3)。
//!
//! # 格式
//! ```text
//! HTTP/1.1 {status_code} {reason_phrase}\r\n
//! {Header-Name}: {Header-Value}\r\n
//! ...
//! \r\n
//! {body}
//! ```
//!
//! # 设计
//! - 零堆分配热路径:写入调用方提供的 `&mut Vec<u8>`
//! - 自动补全 Content-Length(若缺失且 body 非空)
//! - CRLF 注入防护:拒绝包含 `\r\n` 的头部值(二次校验,上游已拦截)
//! - RFC 7230 §3.2.4 严格合规:头部字段名仅允许 token 字符

use zenith_api::CanonicalResponse;

/// 响应序列化错误
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseSerializeError {
    /// 头部值包含 CRLF(注入攻击)
    CrlfInjection,
    /// 头部名称包含非法字符
    InvalidHeaderName,
}

impl std::fmt::Display for ResponseSerializeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::CrlfInjection => write!(f, "CRLF injection detected in header"),
            Self::InvalidHeaderName => write!(f, "invalid header name character"),
        }
    }
}

impl std::error::Error for ResponseSerializeError {}

/// 获取状态码对应的 Reason Phrase (RFC 7231 §6)
#[inline]
fn reason_phrase(status: u16) -> &'static str {
    match status {
        100 => "Continue",
        101 => "Switching Protocols",
        200 => "OK",
        201 => "Created",
        202 => "Accepted",
        203 => "Non-Authoritative Information",
        204 => "No Content",
        205 => "Reset Content",
        206 => "Partial Content",
        301 => "Moved Permanently",
        302 => "Found",
        303 => "See Other",
        304 => "Not Modified",
        307 => "Temporary Redirect",
        308 => "Permanent Redirect",
        400 => "Bad Request",
        401 => "Unauthorized",
        402 => "Payment Required",
        403 => "Forbidden",
        404 => "Not Found",
        405 => "Method Not Allowed",
        406 => "Not Acceptable",
        407 => "Proxy Authentication Required",
        408 => "Request Timeout",
        409 => "Conflict",
        410 => "Gone",
        411 => "Length Required",
        412 => "Precondition Failed",
        413 => "Payload Too Large",
        414 => "URI Too Long",
        415 => "Unsupported Media Type",
        416 => "Range Not Satisfiable",
        417 => "Expectation Failed",
        421 => "Misdirected Request",
        422 => "Unprocessable Entity",
        425 => "Too Early",
        426 => "Upgrade Required",
        428 => "Precondition Required",
        429 => "Too Many Requests",
        431 => "Request Header Fields Too Large",
        451 => "Unavailable For Legal Reasons",
        500 => "Internal Server Error",
        501 => "Not Implemented",
        502 => "Bad Gateway",
        503 => "Service Unavailable",
        504 => "Gateway Timeout",
        505 => "HTTP Version Not Supported",
        _ => "Unknown",
    }
}

/// HTTP/1.1 响应序列化器
///
/// 将规范化响应序列化为符合 RFC 7230 的 HTTP/1.1 线路格式。
#[derive(Debug)]
pub struct Http1ResponseEncoder;

impl Http1ResponseEncoder {
    /// 将 CanonicalResponse 序列化为 HTTP/1.1 线路格式
    ///
    /// # 参数
    /// - `response`: 规范化响应
    /// - `out`: 输出缓冲区(调用方预分配,零堆分配热路径)
    ///
    /// # 返回
    /// 写入的字节数,或序列化错误
    ///
    /// # 安全
    /// - 自动补全 Content-Length(若缺失且 body 非空且无 Transfer-Encoding)
    /// - 拒绝 CRLF 注入(二次校验)
    /// - 头部名称仅允许 RFC 7230 §3.2.6 token 字符
    pub fn encode(
        response: &CanonicalResponse,
        out: &mut Vec<u8>,
    ) -> Result<usize, ResponseSerializeError> {
        let start = out.len();

        // 状态行: HTTP/1.1 {status} {reason}\r\n
        out.extend_from_slice(b"HTTP/1.1 ");
        out.extend_from_slice(response.status_code.to_string().as_bytes());
        out.push(b' ');
        out.extend_from_slice(reason_phrase(response.status_code).as_bytes());
        out.extend_from_slice(b"\r\n");

        // 检查是否已有 Content-Length / Transfer-Encoding
        let mut has_content_length = false;
        let mut has_transfer_encoding = false;

        // 头部
        for header in response.headers_iter() {
            let name = header.name_str();
            let value = header.value_str();

            // CRLF 注入二次校验
            if value.contains('\r') || value.contains('\n') {
                return Err(ResponseSerializeError::CrlfInjection);
            }

            // 头部名称 token 校验(含非空拒绝,fail-closed)
            if !zenith_api::normalize::is_valid_header_name(name) {
                return Err(ResponseSerializeError::InvalidHeaderName);
            }

            let lower = name.to_ascii_lowercase();
            if lower == "content-length" {
                has_content_length = true;
            }
            if lower == "transfer-encoding" {
                has_transfer_encoding = true;
            }

            out.extend_from_slice(name.as_bytes());
            out.extend_from_slice(b": ");
            out.extend_from_slice(value.as_bytes());
            out.extend_from_slice(b"\r\n");
        }

        // 自动补全 Content-Length(若未设置且非 chunked)
        // 严格 RFC 7230 §3.3.2:
        // - 已知 body 长度(含 0 字节)且无 Transfer-Encoding 时 SHOULD 生成 Content-Length
        // - 1xx / 204 / 304 MUST NOT 发送 Content-Length(禁止在这些代码上发送 body)
        let body = response.body();
        let status = response.status_code;
        let forbid_content_length = matches!(status, 100..=199) || status == 204 || status == 304;
        if !forbid_content_length && !has_content_length && !has_transfer_encoding {
            out.extend_from_slice(b"content-length: ");
            out.extend_from_slice(body.len().to_string().as_bytes());
            out.extend_from_slice(b"\r\n");
        }

        // 空行
        out.extend_from_slice(b"\r\n");

        // Body:1xx/204/304 为无体状态码(MUST NOT 发送 body),
        // 即使应用误设了 body 也不写出,避免"无长度头 + body"的响应走私/解析歧义。
        if !forbid_content_length && !body.is_empty() {
            out.extend_from_slice(body);
        }

        Ok(out.len() - start)
    }

    /// 便捷方法:序列化为新的 Vec
    #[inline]
    pub fn encode_to_vec(response: &CanonicalResponse) -> Result<Vec<u8>, ResponseSerializeError> {
        let mut out = Vec::with_capacity(256 + response.body().len());
        Http1ResponseEncoder::encode(response, &mut out)?;
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use zenith_api::CanonicalResponse;

    #[test]
    fn test_encode_simple_response() {
        let mut resp = CanonicalResponse::new(200);
        resp.add_header(b"content-type", b"text/plain").unwrap();
        resp.set_body(b"Hello, World!");

        let mut out = Vec::new();
        let n = Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
        let s = String::from_utf8(out).unwrap();

        assert!(s.starts_with("HTTP/1.1 200 OK\r\n"));
        assert!(s.contains("content-type: text/plain\r\n"));
        assert!(s.contains("content-length: 13\r\n"));
        assert!(s.ends_with("\r\nHello, World!"));
        assert_eq!(n, s.len());
    }

    #[test]
    fn test_encode_no_body() {
        let resp = CanonicalResponse::new(204);
        let mut out = Vec::new();
        Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
        let s = String::from_utf8(out).unwrap();

        assert!(s.starts_with("HTTP/1.1 204 No Content\r\n"));
        assert!(s.ends_with("\r\n\r\n"));
        // 204 不应有 Content-Length
        assert!(!s.contains("content-length"));
    }

    #[test]
    fn test_encode_with_explicit_content_length() {
        let mut resp = CanonicalResponse::new(200);
        resp.add_header(b"content-length", b"42").unwrap();
        resp.set_body(b"Hello");

        let mut out = Vec::new();
        Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
        let s = String::from_utf8(out).unwrap();

        // 不应重复添加 Content-Length
        let cl_count = s.matches("content-length").count();
        assert_eq!(cl_count, 1);
        assert!(s.contains("content-length: 42\r\n"));
    }

    #[test]
    fn test_encode_crlf_injection_blocked() {
        let mut resp = CanonicalResponse::new(200);
        // 第一层防御:CanonicalResponse::add_header 拒绝 CRLF
        let add_result = resp.add_header(b"x-evil", b"val\r\nInjected: yes");
        assert!(add_result.is_err(), "add_header must reject CRLF in value");

        // add_header 失败,头部未添加,encode 应成功(无恶意头部)
        let mut out = Vec::new();
        let result = Http1ResponseEncoder::encode(&resp, &mut out);
        assert!(result.is_ok(), "encode should succeed on clean response");
    }

    #[test]
    fn test_encode_reason_phrases() {
        for (code, phrase) in &[
            (200u16, "OK"),
            (404, "Not Found"),
            (500, "Internal Server Error"),
            (301, "Moved Permanently"),
            (429, "Too Many Requests"),
            (451, "Unavailable For Legal Reasons"),
        ] {
            let resp = CanonicalResponse::new(*code);
            let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
            let s = String::from_utf8(out).unwrap();
            assert!(s.starts_with(&format!("HTTP/1.1 {code} {phrase}\r\n")));
        }
    }

    #[test]
    fn test_encode_unknown_status() {
        let resp = CanonicalResponse::new(599);
        let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
        let s = String::from_utf8(out).unwrap();
        assert!(s.starts_with("HTTP/1.1 599 Unknown\r\n"));
    }

    #[test]
    fn test_encode_to_vec() {
        let mut resp = CanonicalResponse::new(200);
        resp.set_body(b"test");
        let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
        assert!(!out.is_empty());
        assert!(out.windows(4).any(|w| w == b"test"));
    }

    #[test]
    fn test_encode_multiple_headers() {
        let mut resp = CanonicalResponse::new(200);
        resp.add_header(b"x-custom-1", b"value1").unwrap();
        resp.add_header(b"x-custom-2", b"value2").unwrap();
        resp.add_header(b"server", b"Zenith/1.0").unwrap();
        resp.set_body(b"OK");

        let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
        assert!(s.contains("x-custom-1: value1\r\n"));
        assert!(s.contains("x-custom-2: value2\r\n"));
        assert!(s.contains("server: Zenith/1.0\r\n"));
        assert!(s.ends_with("\r\nOK"));
    }

    #[test]
    fn test_encode_empty_body_with_content_type() {
        let mut resp = CanonicalResponse::new(200);
        resp.add_header(b"content-type", b"application/json").unwrap();
        // HTTP/1.1 keep-alive 合规:空 body 也必须显式声明 Content-Length: 0(RFC 7230 §3.3.2)
        let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
        assert!(s.contains("content-type: application/json\r\n"));
        // 空 body(非 1xx/204/304)必须补 Content-Length: 0,避免 keep-alive 客户端挂起
        assert!(
            s.contains("content-length: 0"),
            "empty-body 200 response must include Content-Length: 0 for keep-alive compliance. Raw: {s}"
        );
    }

    #[test]
    fn test_reason_phrase_all_variants() {
        let codes = [100, 101, 200, 201, 204, 206, 301, 302, 304, 400, 401, 403, 404, 500];
        for &code in &codes {
            let resp = CanonicalResponse::new(code);
            let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
            assert!(!s.contains("Unknown"), "status {code} should have known reason phrase");
        }
    }
}