maincopy-server 0.1.0

Self-hosted publishing server with exact previews and explicit release approval
Documentation
use std::fmt;

use base64::{Engine as _, engine::general_purpose::STANDARD};
use sha2::{Digest as _, Sha256};

use axum::{
    body::Body,
    extract::Path,
    http::{
        HeaderMap, HeaderValue, StatusCode,
        header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_NONE_MATCH},
    },
    response::Response,
};

const ADMIN_STYLESHEET: &[u8] = include_bytes!("../../frontend/admin/site.css");
const NOSTR_LOGIN_SCRIPT: &[u8] = include_bytes!("../../frontend/admin/nostr-login.js");
const ADMIN_ASSET_CONTEXT: &str = "maincopy admin frontend bundle v1";
const ADMIN_ASSET_PREFIX: &str = "admin-b3-v1-";
const STYLESHEET_NAME: &str = "site.css";
const NOSTR_LOGIN_SCRIPT_NAME: &str = "nostr-login.js";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct AdminAssetBundleDigest([u8; 32]);

impl AdminAssetBundleDigest {
    fn current() -> Self {
        let mut hasher = blake3::Hasher::new_derive_key(ADMIN_ASSET_CONTEXT);
        for (name, bytes) in [
            (STYLESHEET_NAME, ADMIN_STYLESHEET),
            (NOSTR_LOGIN_SCRIPT_NAME, NOSTR_LOGIN_SCRIPT),
        ] {
            hasher.update(&(name.len() as u64).to_be_bytes());
            hasher.update(name.as_bytes());
            hasher.update(&(bytes.len() as u64).to_be_bytes());
            hasher.update(bytes);
        }
        Self(*hasher.finalize().as_bytes())
    }

    fn parse(value: &str) -> Option<Self> {
        let encoded = value.strip_prefix(ADMIN_ASSET_PREFIX)?;
        if encoded.len() != 64 {
            return None;
        }
        let decoded = blake3::Hash::from_hex(encoded).ok()?;
        (decoded.to_hex().as_str() == encoded).then(|| Self(*decoded.as_bytes()))
    }
}

impl fmt::Display for AdminAssetBundleDigest {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "{ADMIN_ASSET_PREFIX}{}",
            blake3::Hash::from_bytes(self.0).to_hex()
        )
    }
}

pub(crate) fn stylesheet_path() -> String {
    format!(
        "/admin/assets/{}/{STYLESHEET_NAME}",
        AdminAssetBundleDigest::current()
    )
}

pub(super) fn nostr_login_script_path() -> String {
    format!(
        "/admin/assets/{}/{NOSTR_LOGIN_SCRIPT_NAME}",
        AdminAssetBundleDigest::current()
    )
}

pub(super) fn nostr_login_script_integrity() -> String {
    format!(
        "sha256-{}",
        STANDARD.encode(Sha256::digest(NOSTR_LOGIN_SCRIPT))
    )
}

pub(crate) async fn get(
    Path((digest, name)): Path<(String, String)>,
    headers: HeaderMap,
) -> Response {
    let Some(digest) = AdminAssetBundleDigest::parse(&digest) else {
        return not_found();
    };
    if digest != AdminAssetBundleDigest::current() {
        return not_found();
    }
    let (bytes, content_type) = match name.as_str() {
        STYLESHEET_NAME => (ADMIN_STYLESHEET, "text/css; charset=utf-8"),
        NOSTR_LOGIN_SCRIPT_NAME => (NOSTR_LOGIN_SCRIPT, "text/javascript; charset=utf-8"),
        _ => return not_found(),
    };

    let etag = HeaderValue::from_str(&format!("\"{digest}\""))
        .expect("an admin asset digest always forms a valid ETag");
    if headers
        .get_all(IF_NONE_MATCH)
        .iter()
        .any(|candidate| candidate == etag)
    {
        let mut response = Response::new(Body::empty());
        *response.status_mut() = StatusCode::NOT_MODIFIED;
        response.headers_mut().insert(ETAG, etag);
        return response;
    }

    let mut response = Response::new(Body::from(bytes));
    *response.status_mut() = StatusCode::OK;
    response
        .headers_mut()
        .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
    response.headers_mut().insert(
        CONTENT_LENGTH,
        HeaderValue::from_str(&bytes.len().to_string())
            .expect("the bounded asset length is a valid header value"),
    );
    response.headers_mut().insert(ETAG, etag);
    response
}

fn not_found() -> Response {
    let mut response = Response::new(Body::empty());
    *response.status_mut() = StatusCode::NOT_FOUND;
    response
}

#[cfg(test)]
mod tests {
    use axum::{Router, http::Request, routing::get as route_get};
    use tower::ServiceExt as _;

    use super::*;

    #[test]
    fn stylesheet_path_is_content_addressed_and_stable() {
        let path = stylesheet_path();
        let expected = AdminAssetBundleDigest::current().to_string();

        assert_eq!(path, format!("/admin/assets/{expected}/{STYLESHEET_NAME}"));
        assert_eq!(
            AdminAssetBundleDigest::parse(&expected),
            Some(AdminAssetBundleDigest::current())
        );
        assert!(ADMIN_STYLESHEET.len() < 16 * 1024);
        let stylesheet = std::str::from_utf8(ADMIN_STYLESHEET).unwrap();
        assert!(!stylesheet.contains("@import"));
        assert!(!stylesheet.contains("url("));
    }

    #[tokio::test]
    async fn only_the_exact_embedded_admin_assets_are_served() {
        let router = Router::new().route("/admin/assets/{digest}/{name}", route_get(get));
        let response = router
            .clone()
            .oneshot(
                Request::builder()
                    .uri(stylesheet_path())
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(response.headers()[CONTENT_TYPE], "text/css; charset=utf-8");

        let script = router
            .clone()
            .oneshot(
                Request::builder()
                    .uri(nostr_login_script_path())
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(script.status(), StatusCode::OK);
        assert_eq!(
            script.headers()[CONTENT_TYPE],
            "text/javascript; charset=utf-8"
        );
        assert_eq!(
            script.headers()[CONTENT_LENGTH],
            NOSTR_LOGIN_SCRIPT.len().to_string()
        );
        let etag = script.headers()[ETAG].clone();
        let cached = router
            .clone()
            .oneshot(
                Request::builder()
                    .uri(nostr_login_script_path())
                    .header(IF_NONE_MATCH, etag)
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(cached.status(), StatusCode::NOT_MODIFIED);

        let response = router
            .oneshot(
                Request::builder()
                    .uri(stylesheet_path().replace(STYLESHEET_NAME, "other.css"))
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }
}