csaf-crud 0.3.4

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Static file serving from embedded assets.

use bytes::Bytes;
use http::{Response, StatusCode};

use crate::app_state::AppState;
use crate::router::{self, Body};

/// Serve a static file by path.
pub async fn serve_static(
    _state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let file_path = router::path_param(&params, "path").unwrap_or_default();

    // Prevent path traversal. Component-based check (rejects `..`,
    // absolute paths, drive / UNC prefixes, and NUL) rather than a naive
    // `contains("..")`, which both misses encodings and false-rejects
    // legitimate names containing "..".
    if !csaf_core::path_security::is_safe_relative_path(&file_path) {
        return Response::builder()
            .status(StatusCode::FORBIDDEN)
            .body(Body::from("Forbidden"))
            .unwrap_or_default();
    }

    let content_type = mime_guess::from_path(&file_path)
        .first_raw()
        .unwrap_or("application/octet-stream");

    // Serve from embedded static assets.
    let data: Option<Bytes> = match file_path.as_str() {
        "css/custom.css" => Some(Bytes::from_static(include_bytes!(
            "../static/css/custom.css"
        ))),
        "js/theme.js" => Some(Bytes::from_static(include_bytes!("../static/js/theme.js"))),
        "img/logo.png" => Some(Bytes::from_static(include_bytes!("../static/img/logo.png"))),
        "fonts/roboto/roboto-regular.ttf" => Some(Bytes::from_static(include_bytes!(
            "../static/fonts/roboto/roboto-regular.ttf"
        ))),
        "fonts/roboto/roboto-bold.ttf" => Some(Bytes::from_static(include_bytes!(
            "../static/fonts/roboto/roboto-bold.ttf"
        ))),
        "fonts/roboto-mono/roboto-mono-regular.ttf" => Some(Bytes::from_static(include_bytes!(
            "../static/fonts/roboto-mono/roboto-mono-regular.ttf"
        ))),
        "fonts/roboto-mono/roboto-mono-bold.ttf" => Some(Bytes::from_static(include_bytes!(
            "../static/fonts/roboto-mono/roboto-mono-bold.ttf"
        ))),
        _ => None,
    };

    match data {
        Some(bytes) => Response::builder()
            .status(StatusCode::OK)
            .header("content-type", content_type)
            .header("cache-control", "public, max-age=86400")
            .body(Body::from(bytes))
            .unwrap_or_default(),
        None => Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(Body::from("Static file not found"))
            .unwrap_or_default(),
    }
}