Skip to main content

mkit_server/http_objects/
mount.rs

1//! Read-only mount policy shared by the native and Workers adapters.
2use super::{HttpBody, HttpObjectResponse};
3use crate::pipeline::ADMISSION_EXPOSE_HEADERS;
4use crate::url_token::{UrlTokenConfig, UrlTokenConfigError};
5
6/// Public key document, outside repository bearer and admission gates.
7pub const KEY_PATH: &str = "/.well-known/mkit-url-token-keys.json";
8
9/// Explicit adapter opt-in. Indexed and HTTP pipeline configuration are also required.
10#[derive(Debug, Clone, Default)]
11pub struct HttpMountOptions {
12    /// Allowed origins, compared exactly; empty permits every origin with `*`.
13    pub cors_origins: Vec<String>,
14}
15
16/// Apply SPEC-HTTP-OBJECTS ยง8 to every response, including adapter failures.
17pub fn apply_cors(
18    response: &mut HttpObjectResponse,
19    origin: Option<&str>,
20    opts: &HttpMountOptions,
21) {
22    response
23        .headers
24        .retain(|(name, _)| !name.to_ascii_lowercase().starts_with("access-control-"));
25    if opts.cors_origins.is_empty() {
26        response
27            .headers
28            .push(("Access-Control-Allow-Origin", "*".into()));
29    } else {
30        let mut vary = Vec::new();
31        response.headers.retain(|(name, value)| {
32            if name.eq_ignore_ascii_case("Vary") {
33                vary.extend(
34                    value
35                        .split(',')
36                        .map(str::trim)
37                        .filter(|v| !v.is_empty())
38                        .map(str::to_owned),
39                );
40                false
41            } else {
42                true
43            }
44        });
45        if !vary
46            .iter()
47            .any(|value| value.eq_ignore_ascii_case("Origin") || value == "*")
48        {
49            vary.push("Origin".into());
50        }
51        response.headers.push(("Vary", vary.join(", ")));
52        if let Some(origin) =
53            origin.filter(|o| opts.cors_origins.iter().any(|allowed| allowed == o))
54        {
55            response
56                .headers
57                .push(("Access-Control-Allow-Origin", origin.into()));
58        }
59    }
60    response
61        .headers
62        .push(("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS".into()));
63    response.headers.push(("Access-Control-Allow-Headers", "Range, If-None-Match, If-Range, Payment-Authorization, PAYMENT-SIGNATURE, Authorization, Accept-Payment".into()));
64    let mut expose = "ETag, Content-Range, Accept-Ranges, Content-Length, X-Mkit-Commit, X-Mkit-Object, X-Mkit-Object-Type, WWW-Authenticate, Payment-Receipt, PAYMENT-REQUIRED, PAYMENT-RESPONSE, Link".to_owned();
65    for header in ADMISSION_EXPOSE_HEADERS {
66        if !expose
67            .split(',')
68            .any(|existing| existing.trim().eq_ignore_ascii_case(header))
69        {
70            expose.push_str(", ");
71            expose.push_str(header);
72        }
73    }
74    response
75        .headers
76        .push(("Access-Control-Expose-Headers", expose));
77}
78
79/// Serve active and retained public keys without any bearer or payment check.
80#[must_use]
81pub fn key_document(config: &UrlTokenConfig, method: &str) -> HttpObjectResponse {
82    match method {
83        "OPTIONS" => HttpObjectResponse::new(204).with_header("Allow", "GET, HEAD, OPTIONS"),
84        "GET" | "HEAD" => {
85            let json = config.keys().key_set_json(config.ttl_ms());
86            let mut response = HttpObjectResponse::new(200)
87                .with_header("Content-Type", "application/json")
88                .with_header("Cache-Control", "public, max-age=300")
89                .with_header("Content-Length", json.len().to_string());
90            if method == "GET" {
91                response.body = HttpBody::Bytes(json.into());
92            }
93            response
94        }
95        _ => HttpObjectResponse::error(405).with_header("Allow", "GET, HEAD, OPTIONS"),
96    }
97}
98
99/// Reject active or retained token keys reused by another deployment role.
100///
101/// # Errors
102/// `Keys` on any public-key collision. No seed material is exported.
103pub fn check_key_separation(
104    config: &UrlTokenConfig,
105    others: &[[u8; 32]],
106) -> Result<(), UrlTokenConfigError> {
107    if config
108        .keys()
109        .public_keys()
110        .any(|public| others.contains(&public))
111    {
112        Err(UrlTokenConfigError::Keys)
113    } else {
114        Ok(())
115    }
116}