mkit_server/http_objects/
mod.rs1mod body;
21pub(crate) mod content_headers;
22pub mod mount;
23mod paid;
24pub(crate) mod proof;
25pub mod range;
26pub mod reach;
27pub(crate) mod resolve;
28pub mod route;
29pub mod seams;
30
31pub use body::{EndHook, HttpBody, exact as exact_body, with_hook as body_with_hook};
32pub use paid::HttpReadRuntime;
33pub(crate) use paid::ReadFinalizer;
34pub use reach::{Reachability, TtlReachability};
35pub use route::{BadUrl, ParsedUrl, Query, RepoPrefix, Target, is_http_object_path, parse};
36pub use seams::{
37 AdmitDecision, AdmitRequest, Admitted, HttpAdmission, HttpSeams, NoAdmission, NoTakedown,
38 NoTokens, PreparedProof, ProofServer, ProofSource, TakedownGate, TakedownVerdict, TokenGate,
39 UnsupportedProofs,
40};
41
42#[cfg(not(target_arch = "wasm32"))]
44pub type HttpHeaderValues<'a> = dyn Fn(&str) -> Vec<String> + Sync + 'a;
45#[cfg(target_arch = "wasm32")]
47pub type HttpHeaderValues<'a> = dyn Fn(&str) -> Vec<String> + 'a;
48use crate::{Code, ServerError};
49
50pub const METRIC_HTTP_REACH_CAPPED: &str = "mkit_server_http_reach_capped_total";
54pub const METRIC_HTTP_INLINE_CAPPED: &str = "mkit_server_http_inline_capped_total";
57
58pub const DEFAULT_MAX_INLINE_OBJECT_BYTES: u64 = 64 << 20;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[non_exhaustive]
64pub struct HttpObjectsConfig {
65 pub max_walk_objects: usize,
70 pub admit_reads: bool,
72 pub redirect_public_refs: bool,
74 pub read_deadline: core::time::Duration,
76 pub read_reconcile_grace: core::time::Duration,
78 pub reachability_lag_ms: u64,
81 pub reach_cache_entries: usize,
83 pub max_inline_object_bytes: u64,
86 pub http_decode_budget: u64,
90 pub max_proof_content_bytes: u64,
92 pub max_proof_bundle_bytes: u64,
94}
95
96impl Default for HttpObjectsConfig {
97 fn default() -> Self {
98 Self {
99 max_walk_objects: 50_000,
100 admit_reads: false,
101 redirect_public_refs: false,
102 read_deadline: core::time::Duration::from_mins(5),
103 read_reconcile_grace: core::time::Duration::from_mins(1),
104 reachability_lag_ms: 60_000,
105 reach_cache_entries: 65_536,
106 max_inline_object_bytes: DEFAULT_MAX_INLINE_OBJECT_BYTES,
107 http_decode_budget: 256 << 20,
108 max_proof_content_bytes: 8 << 20,
109 max_proof_bundle_bytes: 64 << 20,
110 }
111 }
112}
113
114impl HttpObjectsConfig {
115 pub fn validate(&self, extract_min_bytes: u64) -> Result<(), ServerError> {
121 if self.max_proof_content_bytes == 0
122 || self.max_proof_bundle_bytes == 0
123 || self.max_proof_bundle_bytes > 64 << 20
124 || self.read_deadline.is_zero()
125 || self.read_reconcile_grace.is_zero()
126 || self.max_walk_objects == 0
127 || self.reachability_lag_ms == 0
128 || self.reach_cache_entries == 0
129 || self.max_inline_object_bytes < extract_min_bytes.saturating_add(10)
130 || self.http_decode_budget < self.max_inline_object_bytes
131 {
132 return Err(ServerError::invalid_argument("invalid HTTP object limits"));
133 }
134 Ok(())
135 }
136}
137
138#[derive(Clone, Copy)]
141pub struct RedactedQuery<'a>(&'a str);
142
143impl<'a> RedactedQuery<'a> {
144 #[must_use]
146 pub fn new(query: &'a str) -> Self {
147 Self(query)
148 }
149}
150
151impl core::fmt::Debug for RedactedQuery<'_> {
152 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153 f.write_str("[redacted]")
154 }
155}
156
157impl core::fmt::Display for RedactedQuery<'_> {
158 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159 f.write_str("[redacted]")
160 }
161}
162
163pub struct HttpObjectRequest<'a> {
167 pub method: &'a str,
169 pub raw_path: &'a str,
171 pub raw_query: Option<RedactedQuery<'a>>,
175 pub headers: &'a HttpHeaderValues<'a>,
177 pub header_names: &'a [&'a str],
180}
181
182impl core::fmt::Debug for HttpObjectRequest<'_> {
183 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
185 f.debug_struct("HttpObjectRequest")
186 .field("method", &self.method)
187 .finish_non_exhaustive()
188 }
189}
190
191#[derive(Debug)]
195pub struct HttpObjectResponse {
196 pub status: u16,
198 pub headers: Vec<(&'static str, String)>,
200 pub body: HttpBody,
202}
203
204const SECURITY_HEADERS: [(&str, &str); 3] = [
206 ("X-Content-Type-Options", "nosniff"),
207 ("Content-Security-Policy", "sandbox; default-src 'none'"),
208 ("Referrer-Policy", "no-referrer"),
209];
210
211const NOT_FOUND_BODY: &[u8] = b"not found";
213
214impl HttpObjectResponse {
215 #[must_use]
217 pub fn new(status: u16) -> Self {
218 Self {
219 status,
220 headers: SECURITY_HEADERS
221 .iter()
222 .map(|(name, value)| (*name, (*value).to_owned()))
223 .collect(),
224 body: HttpBody::Empty,
225 }
226 }
227
228 #[must_use]
230 pub fn with_header(mut self, name: &'static str, value: impl Into<String>) -> Self {
231 self.headers.push((name, value.into()));
232 self
233 }
234
235 #[must_use]
237 pub fn error(status: u16) -> Self {
238 Self::new(status).with_header("Cache-Control", "no-store")
239 }
240
241 #[must_use]
243 pub fn not_found() -> Self {
244 let mut response = Self::error(404)
245 .with_header("Content-Type", "text/plain; charset=utf-8")
246 .with_header("Content-Length", NOT_FOUND_BODY.len().to_string());
247 response.body = HttpBody::Bytes(bytes::Bytes::from_static(NOT_FOUND_BODY));
248 response
249 }
250
251 #[must_use]
253 pub fn header(&self, name: &str) -> Option<&str> {
254 self.headers
255 .iter()
256 .find(|(n, _)| n.eq_ignore_ascii_case(name))
257 .map(|(_, v)| v.as_str())
258 }
259}
260
261pub(crate) fn cache_control(ref_path: bool, private: bool) -> &'static str {
264 match (ref_path, private) {
265 (false, false) => "public, max-age=31536000, immutable",
266 (false, true) => "private, max-age=31536000, immutable",
267 (true, false) => "public, no-cache",
268 (true, true) => "private, no-cache",
269 }
270}
271
272#[derive(Debug)]
274pub(crate) enum Fail {
275 NotFound,
277 Forbidden,
279 ProofRange,
281 Unavailable,
283}
284
285impl Fail {
286 pub(crate) fn from_server_error(error: &ServerError) -> Self {
290 match error.code() {
291 Code::NotFound => Self::NotFound,
292 Code::PermissionDenied | Code::Unauthenticated => Self::Forbidden,
293 _ => Self::Unavailable,
294 }
295 }
296
297 pub(crate) fn code(&self) -> Code {
299 match self {
300 Self::NotFound => Code::NotFound,
301 Self::ProofRange => Code::OutOfRange,
302 Self::Forbidden => Code::PermissionDenied,
303 Self::Unavailable => Code::Unavailable,
304 }
305 }
306
307 pub(crate) fn into_response(self) -> HttpObjectResponse {
308 match self {
309 Self::NotFound => HttpObjectResponse::not_found(),
310 Self::ProofRange => HttpObjectResponse::error(416),
311 Self::Forbidden => HttpObjectResponse::error(403),
312 Self::Unavailable => HttpObjectResponse::error(503),
313 }
314 }
315}
316
317impl From<resolve::Miss> for Fail {
318 fn from(miss: resolve::Miss) -> Self {
319 match miss {
320 resolve::Miss::NotFound => Self::NotFound,
321 resolve::Miss::Capped | resolve::Miss::Unavailable => Self::Unavailable,
322 }
323 }
324}