Ferrit Explore
中文·繁體·EN·日本語 Sign in Register
cielxl / veld / src / handler / mod.rs
//! Request handling pipeline.
//!
//! This module orchestrates the full lifecycle of an incoming HTTP request
//! through a chain of handlers that mirror nginx's processing phases:
//!
//! 1. **Rewrite** -- apply URL rewrite / redirect rules.
//! 2. **Static file** -- serve the (possibly rewritten) URI from the document
//!    root.
//! 3. **Error page** -- when the upstream handler produces an error status
//!    (4xx / 5xx), substitute a custom or built-in error page.

pub mod error_page;
pub mod file_cache;
pub mod index;
pub mod rewrite;
pub mod static_file;

// ---------------------------------------------------------------------------
// Public re-exports
// ---------------------------------------------------------------------------

pub use self::error_page::ErrorPageHandler;
pub use self::index::IndexHandler;
pub use self::rewrite::{RewriteHandler, RewriteResult, RewriteRule};
pub use self::static_file::StaticFileHandler;

use tracing::debug;

use crate::http::request::Request;
use crate::http::response::Response;
use crate::http::status::HttpStatusCode;

// ---------------------------------------------------------------------------
// HandlerChain
// ---------------------------------------------------------------------------

/// Top-level request processor that chains the rewrite, static file, and
/// error page handlers.
///
/// The chain is intentionally stateless -- all handler instances are supplied
/// at call time through a [`HandlerConfig`] reference so that the same chain
/// can be shared across threads without locking.
pub struct HandlerChain;

impl HandlerChain {
    /// Process an incoming HTTP request through the full handler pipeline.
    ///
    /// # Processing order
    ///
    /// 1. The rewrite rules in `config` are evaluated against the request
    ///    URI.
    ///    - A `Redirect` result produces an HTTP 301/302 response
    ///      immediately.
    ///    - A `Return` result forces an arbitrary status code and body.
    ///    - A `Rewrite` result silently rewrites the URI before continuing.
    ///    - No match passes the original URI through unchanged.
    /// 2. The (possibly rewritten) request is dispatched to the static file
    ///    handler.
    /// 3. If the static file handler returns an error status (>= 400), the
    ///    error page handler is consulted.  It first tries a configured
    ///    custom page; if none is available it falls back to a built-in
    ///    default.
    pub fn process(request: &Request, config: &HandlerConfig) -> Response {
        // ---- Phase 1: rewrite rules ----
        let rewrite_result = config.rewrite_handler.apply(&request.uri);

        let rewritten_uri: Option<String> = match rewrite_result {
            Some(RewriteResult::Redirect(url, code)) => {
                debug!(
                    original = %request.uri,
                    target = %url,
                    code = code,
                    "rewrite: redirect"
                );

                let status = HttpStatusCode::from(code);
                return Response::builder()
                    .status(status)
                    .header("Location", &url)
                    .header("Content-Length", "0")
                    .build();
            }
            Some(RewriteResult::Return(code, body)) => {
                debug!(
                    original = %request.uri,
                    code = code,
                    "rewrite: forced return"
                );

                let status = HttpStatusCode::from(code);
                return Response::builder()
                    .status(status)
                    .header("Content-Type", "text/plain; charset=utf-8")
                    .header("Content-Length", &body.len().to_string())
                    .body_str(&body);
            }
            Some(RewriteResult::Rewrite(new_uri)) => {
                debug!(
                    original = %request.uri,
                    rewritten = %new_uri,
                    "rewrite: internal rewrite"
                );
                Some(new_uri)
            }
            None => None,
        };

        // ---- Phase 2: static file serving ----
        let effective_request;
        let active_request = match &rewritten_uri {
            Some(new_uri) => {
                effective_request = rewrite_request_uri(request, new_uri);
                &effective_request
            }
            None => request,
        };

        let response = config.static_file_handler.handle(active_request);

        // ---- Phase 3: error page fallback ----
        if response.status.is_error() {
            let status = response.status;

            // Try a custom error page first.
            if let Some(custom) = config.error_page_handler.handle(status) {
                debug!(status = status.as_u16(), "serving custom error page");
                return custom;
            }

            // Fall back to the built-in default.
            debug!(
                status = status.as_u16(),
                "no custom error page, using built-in default"
            );
            return ErrorPageHandler::default_error_page(status);
        }

        response
    }
}

// ---------------------------------------------------------------------------
// Handler configuration
// ---------------------------------------------------------------------------

/// Aggregated configuration for the request processing pipeline.
///
/// Each field corresponds to one phase of the [`HandlerChain`] pipeline.
#[derive(Debug, Clone)]
pub struct HandlerConfig {
    /// URL rewrite rules evaluated before static file serving.
    pub rewrite_handler: RewriteHandler,

    /// Serves static files from the document root.
    pub static_file_handler: StaticFileHandler,

    /// Serves custom error pages for 4xx / 5xx responses.
    pub error_page_handler: ErrorPageHandler,
}

impl HandlerConfig {
    /// Create a new handler configuration.
    pub fn new(
        rewrite_handler: RewriteHandler,
        static_file_handler: StaticFileHandler,
        error_page_handler: ErrorPageHandler,
    ) -> Self {
        Self {
            rewrite_handler,
            static_file_handler,
            error_page_handler,
        }
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Produce a clone of `request` with the URI (and derived path/query)
/// replaced by `new_uri`.
///
/// All other fields -- method, version, headers, body, remote address, and
/// connection id -- are preserved from the original request.
fn rewrite_request_uri(request: &Request, new_uri: &str) -> Request {
    let mut rewritten = request.clone();

    let (path, query) = match new_uri.find('?') {
        Some(pos) => (
            new_uri[..pos].to_string(),
            Some(new_uri[pos + 1..].to_string()),
        ),
        None => (new_uri.to_string(), None),
    };

    rewritten.uri = new_uri.to_string();
    rewritten.path = path;
    rewritten.query = query;

    rewritten
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::request::{Method, Version};

    /// Build a minimal GET request for testing.
    fn test_request(uri: &str) -> Request {
        Request::new(Method::GET, uri.to_string(), Version::Http11)
    }

    /// Build a `HandlerConfig` with no rewrite rules, a static file handler
    /// rooted at `root`, and an empty error page map.
    fn test_config(root: std::path::PathBuf) -> HandlerConfig {
        HandlerConfig::new(
            RewriteHandler::new(vec![]),
            StaticFileHandler::with_defaults(root.clone()),
            ErrorPageHandler::new(root, std::collections::HashMap::new()),
        )
    }

    // -- rewrite_request_uri ------------------------------------------------

    #[test]
    fn rewrite_preserves_path_only() {
        let req = test_request("/old");
        let rewritten = rewrite_request_uri(&req, "/new");
        assert_eq!(rewritten.uri, "/new");
        assert_eq!(rewritten.path, "/new");
        assert!(rewritten.query.is_none());
    }

    #[test]
    fn rewrite_preserves_query_string() {
        let req = test_request("/search?q=test&page=1");
        let rewritten = rewrite_request_uri(&req, "/results?q=test&page=1");
        assert_eq!(rewritten.uri, "/results?q=test&page=1");
        assert_eq!(rewritten.path, "/results");
        assert_eq!(rewritten.query.as_deref(), Some("q=test&page=1"));
    }

    #[test]
    fn rewrite_preserves_method_and_version() {
        let req = test_request("/old");
        let rewritten = rewrite_request_uri(&req, "/new");
        assert_eq!(rewritten.method, Method::GET);
        assert_eq!(rewritten.version, Version::Http11);
    }

    // -- HandlerChain: rewrite phase ----------------------------------------

    #[test]
    fn process_redirect_short_circuits() {
        let rules = vec![RewriteRule {
            pattern: "/old".into(),
            replacement: "/new".into(),
            redirect: true,
            permanent: true,
            last: true,
        }];

        let root = std::env::temp_dir().join("veld_chain_test_redirect");
        let _ = std::fs::create_dir_all(&root);

        let config = HandlerConfig::new(
            RewriteHandler::new(rules),
            StaticFileHandler::with_defaults(root.clone()),
            ErrorPageHandler::new(root.clone(), std::collections::HashMap::new()),
        );

        let request = test_request("/old");
        let response = HandlerChain::process(&request, &config);

        assert!(response.status.is_redirection());
        assert_eq!(response.status.as_u16(), 301);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn process_return_forces_response() {
        let config = HandlerConfig::new(
            RewriteHandler::with_return(503, "Maintenance"),
            StaticFileHandler::with_defaults(std::path::PathBuf::from("/tmp")),
            ErrorPageHandler::new(
                std::path::PathBuf::from("/tmp"),
                std::collections::HashMap::new(),
            ),
        );

        let request = test_request("/anything");
        let response = HandlerChain::process(&request, &config);

        assert_eq!(response.status.as_u16(), 503);
    }

    // -- HandlerChain: error page fallback ----------------------------------

    #[test]
    fn process_falls_back_to_default_error_page() {
        // Request a path that does not exist -> 404 from static file handler
        // -> default error page from ErrorPageHandler.
        let root = std::env::temp_dir().join("veld_chain_test_fallback");
        let _ = std::fs::create_dir_all(&root);

        let config = test_config(root.clone());

        let request = test_request("/does-not-exist.html");
        let response = HandlerChain::process(&request, &config);

        // Should be a 404 using the built-in error page.
        assert_eq!(response.status.as_u16(), 404);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn process_serves_custom_error_page() {
        let root = std::env::temp_dir().join("veld_chain_test_custom_err");
        let _ = std::fs::create_dir_all(&root);

        // Create a custom 404 page.
        std::fs::write(root.join("err_404.html"), "<h1>Custom 404</h1>").unwrap();

        let mut error_pages = std::collections::HashMap::new();
        error_pages.insert(404u16, "/err_404.html".to_string());

        let config = HandlerConfig::new(
            RewriteHandler::new(vec![]),
            StaticFileHandler::with_defaults(root.clone()),
            ErrorPageHandler::new(root.clone(), error_pages),
        );

        let request = test_request("/does-not-exist.html");
        let response = HandlerChain::process(&request, &config);

        assert_eq!(response.status.as_u16(), 404);

        let _ = std::fs::remove_dir_all(&root);
    }

    // -- HandlerChain: rewrite + static file integration --------------------

    #[test]
    fn process_rewrite_then_serve() {
        let root = std::env::temp_dir().join("veld_chain_test_rewrite_serve");
        let _ = std::fs::create_dir_all(&root);

        // Create a file at the rewritten destination.
        std::fs::write(root.join("actual.html"), "<h1>Hello</h1>").unwrap();

        let rules = vec![RewriteRule {
            pattern: "/virtual".into(),
            replacement: "/actual.html".into(),
            redirect: false,
            permanent: false,
            last: true,
        }];

        let config = HandlerConfig::new(
            RewriteHandler::new(rules),
            StaticFileHandler::with_defaults(root.clone()),
            ErrorPageHandler::new(root.clone(), std::collections::HashMap::new()),
        );

        let request = test_request("/virtual");
        let response = HandlerChain::process(&request, &config);

        assert_eq!(response.status.as_u16(), 200);

        let _ = std::fs::remove_dir_all(&root);
    }

    // -- HandlerConfig construction -----------------------------------------

    #[test]
    fn handler_config_new() {
        let root = std::path::PathBuf::from("/var/www");
        let config = HandlerConfig::new(
            RewriteHandler::new(vec![]),
            StaticFileHandler::with_defaults(root.clone()),
            ErrorPageHandler::new(root, std::collections::HashMap::new()),
        );
        assert_eq!(config.rewrite_handler.rules().len(), 0);
    }
}