Ferrit Explore
中文·繁體·EN·日本語 Sign in Register
cielxl / veld / src / http / response.rs
use super::headers::HeaderMap;
use super::request::Version;
use super::status::HttpStatusCode;
use bytes::Bytes;
use std::fs::File;
use std::path::PathBuf;
use std::sync::Arc;

/// Server token shown in the default error-page footer and `Server` header,
/// e.g. `veld/0.1.0` (analogous to nginx's `nginx/1.20.1`).
const SERVER_TOKEN: &str = concat!("veld/", env!("CARGO_PKG_VERSION"));

/// Self-contained (no external assets / JS) styled default error page.
/// Placeholders `__CODE__`, `__REASON__`, `__SERVER__` are substituted at
/// render time.
const ERROR_PAGE_TEMPLATE: &str = r##"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>__CODE__ __REASON__</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{height:100%}
  body{display:flex;align-items:center;justify-content:center;overflow:hidden;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;
    color:#e8e8ea;
    background:radial-gradient(1200px 620px at 50% -12%,#2a1206 0%,#0b0b0f 56%,#06060a 100%)}
  .grid{position:fixed;inset:0;z-index:0;opacity:.55;
    background-image:linear-gradient(rgba(255,255,255,.035) 1px,transparent 1px),
      linear-gradient(90deg,rgba(255,255,255,.035) 1px,transparent 1px);
    background-size:44px 44px;
    -webkit-mask-image:radial-gradient(circle at 50% 34%,#000,transparent 72%);
    mask-image:radial-gradient(circle at 50% 34%,#000,transparent 72%)}
  .wrap{position:relative;z-index:1;text-align:center;padding:2rem}
  .code{font-family:"SFMono-Regular",ui-monospace,Menlo,Consolas,monospace;
    font-weight:800;font-size:clamp(6rem,23vw,14rem);line-height:.92;letter-spacing:-.045em;
    background:linear-gradient(180deg,#ff8a4c 0%,#f74c00 48%,#b3320a 100%);
    -webkit-background-clip:text;background-clip:text;color:transparent;
    animation:glow 3.4s ease-in-out infinite}
  @keyframes glow{0%,100%{filter:drop-shadow(0 0 26px rgba(247,76,0,.35))}
    50%{filter:drop-shadow(0 0 54px rgba(247,76,0,.7))}}
  .reason{margin-top:.5rem;font-size:clamp(1.15rem,3.6vw,1.9rem);font-weight:700;color:#f4f4f6}
  .desc{margin-top:.95rem;font-size:.96rem;color:#9a9aa3;line-height:1.6}
  .actions{margin-top:1.7rem}
  .btn{display:inline-flex;align-items:center;gap:.55rem;padding:.72rem 1.4rem;border-radius:999px;
    font-size:.96rem;font-weight:700;text-decoration:none;color:#1a0a02;
    background:linear-gradient(180deg,#ff8a4c,#f74c00);
    box-shadow:0 10px 30px rgba(247,76,0,.42);transition:transform .16s ease,box-shadow .16s ease}
  .btn:hover{transform:translateY(-2px);box-shadow:0 14px 40px rgba(247,76,0,.6)}
  .btn .arrow{transition:transform .16s ease}
  .btn:hover .arrow{transform:translateX(4px)}
  .bar{margin:2rem auto 1.1rem;width:min(420px,72vw);height:1px;
    background:linear-gradient(90deg,transparent,rgba(247,76,0,.65),transparent)}
  .brand{display:inline-flex;align-items:center;gap:.55rem;
    font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.84rem;color:#c9c9cf;letter-spacing:.06em}
  .brand .dot{width:8px;height:8px;border-radius:50%;background:#f74c00;
    box-shadow:0 0 10px #f74c00;animation:blink 1.6s steps(2,start) infinite}
  @keyframes blink{50%{opacity:.25}}
</style>
</head>
<body>
<div class="grid"></div>
<div class="wrap">
  <div class="code">__CODE__</div>
  <div class="reason">__REASON__</div>
  <div class="desc">抱歉,请求的页面无法访问。</div>
  <div class="actions"><a class="btn" href="https://netditec.com">前往官网 netditec.com <span class="arrow">&#8594;</span></a></div>
  <div class="bar"></div>
  <div class="brand"><span class="dot"></span>__SERVER__</div>
</div>
</body>
</html>
"##;

/// Response body
#[derive(Debug, Clone)]
pub enum Body {
    Empty,
    Bytes(Bytes),
    File(PathBuf, u64), // path, size
    /// An already-open, read-only file descriptor plus its size.  Allows the
    /// connection layer to `sendfile(2)` directly from a cached descriptor
    /// without re-opening the file.
    FileFd(Arc<File>, u64),
    /// A fully precomputed cached response.  The status line, headers, and
    /// (for small files) the body are serialized once at cache-fill time so
    /// the hot path performs no allocation or formatting -- just a single
    /// `write` (small files) or a header `write` plus `sendfile` (large
    /// files).  The boolean marks a HEAD request (headers only).
    Cached(Arc<CachedResponse>, bool),
}

/// A precomputed, ready-to-send response for a cached static file.
#[derive(Debug)]
pub struct CachedResponse {
    /// Full response bytes (status line + headers + body) for files small
    /// enough to keep resident; sent in a single `write`.  `None` for large
    /// files, which are streamed via `sendfile`.
    pub full: Option<Bytes>,
    /// Status line + headers + terminating CRLF (no body).  Used for the
    /// `sendfile` path and for HEAD requests.
    pub header_block: Bytes,
    /// Open, read-only descriptor for the `sendfile` path.
    pub file: Arc<File>,
    /// File size in bytes.
    pub size: u64,
}

impl Body {
    pub fn len(&self) -> usize {
        match self {
            Body::Empty => 0,
            Body::Bytes(b) => b.len(),
            Body::File(_, size) => *size as usize,
            Body::FileFd(_, size) => *size as usize,
            Body::Cached(c, _) => c.size as usize,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Get body as bytes (for non-file bodies)
    pub fn as_bytes(&self) -> Option<&[u8]> {
        match self {
            Body::Bytes(b) => Some(b),
            _ => None,
        }
    }

    /// Convert into inner bytes if this is a Bytes variant.
    pub fn into_bytes(self) -> Option<Bytes> {
        match self {
            Body::Bytes(b) => Some(b),
            _ => None,
        }
    }
}

impl From<Vec<u8>> for Body {
    fn from(v: Vec<u8>) -> Self {
        Body::Bytes(Bytes::from(v))
    }
}

impl From<Bytes> for Body {
    fn from(b: Bytes) -> Self {
        Body::Bytes(b)
    }
}

impl From<String> for Body {
    fn from(s: String) -> Self {
        Body::Bytes(Bytes::from(s))
    }
}

/// HTTP response
#[derive(Debug, Clone)]
pub struct Response {
    pub status: HttpStatusCode,
    pub version: Version,
    pub headers: HeaderMap,
    pub body: Body,
    pub close_connection: bool,
}

impl Response {
    /// Create a new response with default 200 OK status.
    pub fn new() -> Self {
        Self {
            status: HttpStatusCode::OK,
            version: Version::Http11,
            headers: HeaderMap::new(),
            body: Body::Empty,
            close_connection: false,
        }
    }

    /// Builder-style: set the status code.
    pub fn status(mut self, status: HttpStatusCode) -> Self {
        self.status = status;
        self
    }

    /// Builder-style: set a header.
    pub fn header(mut self, name: &str, value: &str) -> Self {
        self.headers.insert(name, value);
        self
    }

    /// Builder-style: set the body from bytes.
    pub fn with_body_bytes(mut self, data: Vec<u8>) -> Self {
        let len = data.len();
        if !self.headers.contains("content-length") {
            self.headers.insert("Content-Length", len.to_string());
        }
        self.body = Body::Bytes(Bytes::from(data));
        self
    }

    /// Builder-style: set the body from a Body enum or Vec<u8>.
    pub fn body(mut self, body: impl Into<Body>) -> Self {
        self.body = body.into();
        self
    }

    /// Builder-style: set the body from a string.
    pub fn body_str(self, s: &str) -> Self {
        self.with_body_bytes(s.as_bytes().to_vec())
    }

    /// Builder-style: set close_connection.
    pub fn close(mut self, close: bool) -> Self {
        self.close_connection = close;
        self
    }

    /// Get a reference to the body bytes, if present.
    pub fn body_bytes(&self) -> Option<&[u8]> {
        self.body.as_bytes()
    }

    /// Get a reference to the body bytes, if present (alias).
    pub fn body_bytes_ref(&self) -> Option<&[u8]> {
        self.body.as_bytes()
    }

    pub fn builder() -> ResponseBuilder {
        ResponseBuilder::new()
    }

    pub fn ok() -> Self {
        Response::new()
    }

    /// Build an nginx-style default error page branded for veld.
    ///
    /// Mirrors nginx's default body (`<center>` heading + `<hr>` + a server
    /// token footer) so the look is familiar, and sets the `Server` header.
    pub fn error_page(status: HttpStatusCode) -> Self {
        let code = status.as_u16();
        let body = ERROR_PAGE_TEMPLATE
            .replace("__CODE__", &code.to_string())
            .replace("__REASON__", status.reason_phrase())
            .replace("__SERVER__", SERVER_TOKEN);
        Response::new()
            .status(status)
            .header("Server", "veld")
            .header("Content-Type", "text/html; charset=utf-8")
            .with_body_bytes(body.into_bytes())
    }

    pub fn not_found() -> Self {
        Self::error_page(HttpStatusCode::NOT_FOUND)
    }

    pub fn bad_request() -> Self {
        Self::error_page(HttpStatusCode::BAD_REQUEST)
    }

    pub fn internal_error() -> Self {
        Self::error_page(HttpStatusCode::INTERNAL_SERVER_ERROR)
    }

    pub fn not_modified() -> Self {
        Response::new().status(HttpStatusCode::NOT_MODIFIED)
    }

    pub fn redirect(url: &str) -> Self {
        Response::new()
            .status(HttpStatusCode::MOVED_PERMANENTLY)
            .header("Location", url)
    }

    pub fn gateway_timeout() -> Self {
        Self::error_page(HttpStatusCode::GATEWAY_TIMEOUT)
    }

    pub fn bad_gateway() -> Self {
        Self::error_page(HttpStatusCode::BAD_GATEWAY)
    }

    /// Serialize the response to bytes (status line + headers + body).
    ///
    /// Pre-calculates the total size and performs a single allocation,
    /// writing the status line, headers, and body directly into the
    /// buffer without intermediate allocations.
    pub fn to_bytes(&self) -> Vec<u8> {
        let reason = self.status.reason_phrase();
        // Status line: version + ' ' + status_code(3 digits) + ' ' + reason + \r\n
        let status_line_len = self.version.as_str().len() + 1 + 3 + 1 + reason.len() + 2;
        let header_block_len = self.headers.serialized_len();
        let body_len = self.body.len();
        // +2 for the blank line between headers and body
        let total = status_line_len + header_block_len + 2 + body_len;

        let mut buf = Vec::with_capacity(total);

        // Status line -- write status code as ASCII digits directly to
        // avoid the temporary String allocation from u16::to_string().
        buf.extend_from_slice(self.version.as_str().as_bytes());
        buf.push(b' ');
        push_status_code(&mut buf, self.status.as_u16());
        buf.push(b' ');
        buf.extend_from_slice(reason.as_bytes());
        buf.extend_from_slice(b"\r\n");

        // Headers -- write directly into our buffer instead of creating
        // an intermediate Vec via headers.to_bytes().
        self.headers.write_to(&mut buf);

        // End of headers
        buf.extend_from_slice(b"\r\n");

        // Body (only for Bytes variant)
        if let Body::Bytes(ref b) = self.body {
            buf.extend_from_slice(b);
        }

        buf
    }

    /// Serialize only the status line and headers (no body).
    ///
    /// Useful for sendfile-style responses where the headers are sent as
    /// one write and the body is sent directly from disk via the kernel,
    /// avoiding a userspace copy of the file contents.
    pub fn to_header_bytes(&self) -> Vec<u8> {
        let reason = self.status.reason_phrase();
        let status_line_len = self.version.as_str().len() + 1 + 3 + 1 + reason.len() + 2;
        let header_block_len = self.headers.serialized_len();
        // +2 for the blank line terminating the header block
        let total = status_line_len + header_block_len + 2;

        let mut buf = Vec::with_capacity(total);

        // Status line
        buf.extend_from_slice(self.version.as_str().as_bytes());
        buf.push(b' ');
        push_status_code(&mut buf, self.status.as_u16());
        buf.push(b' ');
        buf.extend_from_slice(reason.as_bytes());
        buf.extend_from_slice(b"\r\n");

        // Headers
        self.headers.write_to(&mut buf);

        // End of headers
        buf.extend_from_slice(b"\r\n");

        buf
    }

    /// Set a header (mutating version)
    pub fn set_header(&mut self, name: &str, value: &str) {
        self.headers.insert(name, value);
    }
}

impl Default for Response {
    fn default() -> Self {
        Self::new()
    }
}

/// Response builder (alternative API)
pub struct ResponseBuilder {
    status: HttpStatusCode,
    version: Version,
    headers: HeaderMap,
    close_connection: bool,
}

impl ResponseBuilder {
    pub fn new() -> Self {
        Self {
            status: HttpStatusCode::OK,
            version: Version::Http11,
            headers: HeaderMap::new(),
            close_connection: false,
        }
    }

    pub fn status(mut self, status: HttpStatusCode) -> Self {
        self.status = status;
        self
    }

    pub fn version(mut self, version: Version) -> Self {
        self.version = version;
        self
    }

    pub fn header(mut self, name: &str, value: &str) -> Self {
        self.headers.insert(name, value);
        self
    }

    pub fn close(mut self, close: bool) -> Self {
        self.close_connection = close;
        self
    }

    pub fn build(self) -> Response {
        Response {
            status: self.status,
            version: self.version,
            headers: self.headers,
            body: Body::Empty,
            close_connection: self.close_connection,
        }
    }

    pub fn with_body_bytes(self, data: Vec<u8>) -> Response {
        let len = data.len();
        let mut headers = self.headers;
        if !headers.contains("content-length") {
            headers.insert("Content-Length", len.to_string());
        }
        Response {
            status: self.status,
            version: self.version,
            headers,
            body: Body::Bytes(Bytes::from(data)),
            close_connection: self.close_connection,
        }
    }

    pub fn body_str(self, s: &str) -> Response {
        self.with_body_bytes(s.as_bytes().to_vec())
    }
}

impl Default for ResponseBuilder {
    fn default() -> Self {
        Self::new()
    }
}

/// Write a 3-digit HTTP status code directly into the buffer as ASCII bytes.
///
/// This avoids the heap allocation that `u16::to_string()` would create.
/// HTTP status codes are always in the range 100-599, so exactly 3 digits.
#[inline]
fn push_status_code(buf: &mut Vec<u8>, code: u16) {
    // Safe: code is always 100..=599, producing valid ASCII digit bytes.
    buf.push(b'0' + (code / 100) as u8);
    buf.push(b'0' + ((code / 10) % 10) as u8);
    buf.push(b'0' + (code % 10) as u8);
}