Skip to main content

mkit_server/http_objects/
seams.rs

1//! The hand-off points later work packages fill. Every default is inert: no
2//! tokens, no admission, no takedown, and proofs answer 416.
3
4use std::sync::Arc;
5
6use mkit_core::hash::Hash;
7
8use super::body::EndHook;
9use super::reach::{Reachability, TtlReachability};
10use super::{HttpObjectResponse, HttpObjectsConfig};
11use crate::Procedure;
12use crate::repo::RepoId;
13use crate::{BoxFuture, MaybeSend, MaybeSync, Redacted, ServerError};
14
15/// §3 step 5, filled by WP-4.15: check a present token's syntax, key id and
16/// signature **before** the repository lookup. The result is held until the
17/// repository's visibility is known; a public repository ignores it. The
18/// token is never logged.
19pub trait TokenGate: MaybeSend + MaybeSync {
20    /// Retain a redacted verified statement until visibility is known.
21    fn precheck(
22        &self,
23        token: &Redacted,
24        now_ms: i64,
25    ) -> Result<crate::url_token::Prechecked, crate::url_token::TokenRejected>;
26    /// Configured maximum token lifetime, used by stateless binding checks.
27    fn ttl_ms(&self) -> u64;
28}
29
30/// No keys are configured; private reads fail with the uniform 404.
31#[derive(Debug, Clone, Copy, Default)]
32pub struct NoTokens;
33impl TokenGate for NoTokens {
34    fn precheck(
35        &self,
36        _: &Redacted,
37        _: i64,
38    ) -> Result<crate::url_token::Prechecked, crate::url_token::TokenRejected> {
39        Err(crate::url_token::TokenRejected)
40    }
41    fn ttl_ms(&self) -> u64 {
42        0
43    }
44}
45impl TokenGate for crate::url_token::UrlTokenConfig {
46    fn precheck(
47        &self,
48        token: &Redacted,
49        now_ms: i64,
50    ) -> Result<crate::url_token::Prechecked, crate::url_token::TokenRejected> {
51        self.precheck(token.expose(), now_ms)
52    }
53    fn ttl_ms(&self) -> u64 {
54        self.ttl_ms()
55    }
56}
57
58/// One admitted read (§7), filled by WP-4.13, which adds the request's
59/// credential headers.
60#[derive(Debug)]
61#[non_exhaustive]
62pub struct AdmitRequest<'a> {
63    /// The selected repository.
64    pub repo: &'a RepoId,
65    /// `HttpGetObject` or `HttpGetRefPath`: the hook's `procedure` (§7).
66    pub procedure: Procedure,
67    /// A HEAD declares the GET byte count but sends no body.
68    pub head: bool,
69    /// A ref path rather than an object id.
70    pub ref_path: bool,
71    /// The selected GET body length, after ordinary Range or proof selection.
72    pub declared_bytes: u64,
73    /// Selected payment credentials; never contains Bearer credentials.
74    pub credential_headers: &'a [crate::pipeline::CredentialHeader],
75}
76
77/// What an admitted read adds to its 200 or 206.
78#[derive(Default)]
79pub struct Admitted {
80    /// Success means the response is `private` (§5.3).
81    pub private: bool,
82    /// Passthrough headers such as `Payment-Receipt`.
83    pub headers: Vec<(&'static str, String)>,
84    /// Called once when the body ends (`ReadServed{bytes}`), also for a HEAD
85    /// with zero bytes.
86    pub on_end: Option<EndHook>,
87}
88
89impl core::fmt::Debug for Admitted {
90    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91        f.debug_struct("Admitted")
92            .field("private", &self.private)
93            .field("on_end", &self.on_end.is_some())
94            .finish_non_exhaustive()
95    }
96}
97
98/// The admission verdict.
99#[derive(Debug)]
100pub enum AdmitDecision {
101    /// Serve the content.
102    Allow(Admitted),
103    /// Answer with this instead (a 402 challenge).
104    Respond(HttpObjectResponse),
105}
106
107/// §3 step 11: read Admission, when configured.
108pub trait HttpAdmission: MaybeSend + MaybeSync {
109    /// Whether read Admission is configured. A 304 then selects the private
110    /// cache policy, without calling [`Self::admit`] (§5.3).
111    fn is_configured(&self) -> bool {
112        true
113    }
114
115    /// Admit or challenge one read.
116    fn admit<'a>(
117        &'a self,
118        request: &'a AdmitRequest<'a>,
119    ) -> BoxFuture<'a, Result<AdmitDecision, ServerError>>;
120}
121
122/// No read Admission is configured.
123#[derive(Debug, Clone, Copy, Default)]
124pub struct NoAdmission;
125
126impl HttpAdmission for NoAdmission {
127    fn is_configured(&self) -> bool {
128        false
129    }
130
131    fn admit<'a>(
132        &'a self,
133        _: &'a AdmitRequest<'a>,
134    ) -> BoxFuture<'a, Result<AdmitDecision, ServerError>> {
135        Box::pin(async { Ok(AdmitDecision::Allow(Admitted::default())) })
136    }
137}
138
139/// The takedown verdict for a resolved leaf.
140#[derive(Debug)]
141pub enum TakedownVerdict {
142    /// Continue.
143    Clear,
144    /// §3 step 7: blocked but not yet tombstoned, the uniform 404.
145    NotFound,
146    /// §3 step 8: answer 451 with this response.
147    Respond(HttpObjectResponse),
148}
149
150/// §3 steps 7 and 8, filled by WP-5.9a: tombstones and blocks.
151pub trait TakedownGate: MaybeSend + MaybeSync {
152    /// The reachability walk's per-object stop predicate: never descend
153    /// through a blocked or tombstoned manifest.
154    fn stops_descent(&self, _repo: &RepoId, _id: &Hash) -> bool {
155        false
156    }
157
158    /// Decide for a leaf already proven a reachable member. A cached
159    /// reachability proof skips the walk and its [`Self::stops_descent`], so
160    /// this must also refuse a chunk that only a blocked or tombstoned
161    /// manifest reaches (§4).
162    fn check<'a>(
163        &'a self,
164        repo: &'a RepoId,
165        leaf: &'a Hash,
166    ) -> BoxFuture<'a, Result<TakedownVerdict, ServerError>>;
167}
168
169/// Nothing is blocked or tombstoned.
170#[derive(Debug, Clone, Copy, Default)]
171pub struct NoTakedown;
172
173impl TakedownGate for NoTakedown {
174    fn check<'a>(
175        &'a self,
176        _: &'a RepoId,
177        _: &'a Hash,
178    ) -> BoxFuture<'a, Result<TakedownVerdict, ServerError>> {
179        Box::pin(async { Ok(TakedownVerdict::Clear) })
180    }
181}
182
183/// Canonical repository objects supplied to a proof builder. Reads verify
184/// membership and integrity and share one bounded decode allowance.
185pub trait ProofSource: MaybeSend {
186    /// Read one canonical object; never concatenated extracted file bytes.
187    fn read(&mut self, id: Hash) -> BoxFuture<'_, Result<Vec<u8>, ServerError>>;
188}
189
190/// Selected proof. Preparation constructs no Merkle or Bao proofs.
191#[derive(Debug, Clone)]
192pub struct PreparedProof {
193    /// Published-reachable commit or remix.
194    pub commit: Hash,
195    /// Leaf matched by the exact decoded path.
196    pub leaf: Hash,
197    /// Path below the commit's tree.
198    pub path: Vec<Vec<u8>>,
199    /// Inclusive content range, or canonical Object selector.
200    pub range: Option<(u64, u64)>,
201    /// Exact encoded GET length, checked before Admission.
202    pub encoded_len: u64,
203    /// Cross-chunk range uses MKDS; all other selections use MKDP.
204    pub span: bool,
205}
206
207/// Build only the already selected representation, after common Admission.
208pub trait ProofServer: MaybeSend + MaybeSync {
209    /// Whether the adapter can build proofs. Unsupported selections return
210    /// 416 before admission, rather than reserving an unservable request.
211    fn is_supported(&self) -> bool {
212        true
213    }
214    /// Return encoded bytes; the common path enforces the planned length.
215    fn build<'a>(
216        &'a self,
217        request: &'a PreparedProof,
218        source: &'a mut dyn ProofSource,
219    ) -> BoxFuture<'a, Result<Vec<u8>, ServerError>>;
220}
221
222/// Workers gain canonical-object prefetch in WP-4.14b-2.
223#[derive(Debug, Clone, Copy, Default)]
224pub struct UnsupportedProofs;
225impl ProofServer for UnsupportedProofs {
226    fn is_supported(&self) -> bool {
227        false
228    }
229    fn build<'a>(
230        &'a self,
231        _: &'a PreparedProof,
232        _: &'a mut dyn ProofSource,
233    ) -> BoxFuture<'a, Result<Vec<u8>, ServerError>> {
234        Box::pin(async {
235            Err(ServerError::new(
236                crate::Code::OutOfRange,
237                "proof unsupported",
238            ))
239        })
240    }
241}
242
243/// Every seam of one HTTP-objects deployment.
244#[derive(Clone)]
245pub struct HttpSeams {
246    /// §3 step 5 (WP-4.15).
247    pub tokens: Arc<dyn TokenGate>,
248    /// Retains asynchronous read finalization on cancellation. Required for reservations.
249    pub read_runtime: Option<super::HttpReadRuntime>,
250    /// §3 step 11 (WP-4.13).
251    pub admission: Arc<dyn HttpAdmission>,
252    /// §3 steps 7-8 (WP-5.9a).
253    pub takedown: Arc<dyn TakedownGate>,
254    /// Proof representations (WP-4.14b).
255    pub proofs: Arc<dyn ProofServer>,
256    /// Reachability answers (WP-5.3a).
257    pub reachability: Arc<dyn Reachability>,
258}
259
260impl HttpSeams {
261    /// The inert defaults for `cfg`.
262    #[must_use]
263    pub fn new(cfg: &HttpObjectsConfig) -> Self {
264        Self {
265            tokens: Arc::new(NoTokens),
266            read_runtime: None,
267            admission: Arc::new(NoAdmission),
268            takedown: Arc::new(NoTakedown),
269            proofs: Arc::new(UnsupportedProofs),
270            reachability: Arc::new(TtlReachability::new(
271                cfg.reachability_lag_ms,
272                cfg.reach_cache_entries,
273            )),
274        }
275    }
276}
277
278impl core::fmt::Debug for HttpSeams {
279    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280        f.debug_struct("HttpSeams").finish_non_exhaustive()
281    }
282}