mod body;
pub(crate) mod content_headers;
pub mod mount;
mod paid;
pub(crate) mod proof;
pub mod range;
pub mod reach;
pub(crate) mod resolve;
pub mod route;
pub mod seams;
pub use body::{EndHook, HttpBody, exact as exact_body, with_hook as body_with_hook};
pub use paid::HttpReadRuntime;
pub(crate) use paid::ReadFinalizer;
pub use reach::{Reachability, TtlReachability};
pub use route::{BadUrl, ParsedUrl, Query, RepoPrefix, Target, is_http_object_path, parse};
pub use seams::{
AdmitDecision, AdmitRequest, Admitted, HttpAdmission, HttpSeams, NoAdmission, NoTakedown,
NoTokens, PreparedProof, ProofServer, ProofSource, TakedownGate, TakedownVerdict, TokenGate,
UnsupportedProofs,
};
#[cfg(not(target_arch = "wasm32"))]
pub type HttpHeaderValues<'a> = dyn Fn(&str) -> Vec<String> + Sync + 'a;
#[cfg(target_arch = "wasm32")]
pub type HttpHeaderValues<'a> = dyn Fn(&str) -> Vec<String> + 'a;
use crate::{Code, ServerError};
pub const METRIC_HTTP_REACH_CAPPED: &str = "mkit_server_http_reach_capped_total";
pub const METRIC_HTTP_INLINE_CAPPED: &str = "mkit_server_http_inline_capped_total";
pub const DEFAULT_MAX_INLINE_OBJECT_BYTES: u64 = 64 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct HttpObjectsConfig {
pub max_walk_objects: usize,
pub admit_reads: bool,
pub redirect_public_refs: bool,
pub read_deadline: core::time::Duration,
pub read_reconcile_grace: core::time::Duration,
pub reachability_lag_ms: u64,
pub reach_cache_entries: usize,
pub max_inline_object_bytes: u64,
pub http_decode_budget: u64,
pub max_proof_content_bytes: u64,
pub max_proof_bundle_bytes: u64,
}
impl Default for HttpObjectsConfig {
fn default() -> Self {
Self {
max_walk_objects: 50_000,
admit_reads: false,
redirect_public_refs: false,
read_deadline: core::time::Duration::from_mins(5),
read_reconcile_grace: core::time::Duration::from_mins(1),
reachability_lag_ms: 60_000,
reach_cache_entries: 65_536,
max_inline_object_bytes: DEFAULT_MAX_INLINE_OBJECT_BYTES,
http_decode_budget: 256 << 20,
max_proof_content_bytes: 8 << 20,
max_proof_bundle_bytes: 64 << 20,
}
}
}
impl HttpObjectsConfig {
pub fn validate(&self, extract_min_bytes: u64) -> Result<(), ServerError> {
if self.max_proof_content_bytes == 0
|| self.max_proof_bundle_bytes == 0
|| self.max_proof_bundle_bytes > 64 << 20
|| self.read_deadline.is_zero()
|| self.read_reconcile_grace.is_zero()
|| self.max_walk_objects == 0
|| self.reachability_lag_ms == 0
|| self.reach_cache_entries == 0
|| self.max_inline_object_bytes < extract_min_bytes.saturating_add(10)
|| self.http_decode_budget < self.max_inline_object_bytes
{
return Err(ServerError::invalid_argument("invalid HTTP object limits"));
}
Ok(())
}
}
#[derive(Clone, Copy)]
pub struct RedactedQuery<'a>(&'a str);
impl<'a> RedactedQuery<'a> {
#[must_use]
pub fn new(query: &'a str) -> Self {
Self(query)
}
}
impl core::fmt::Debug for RedactedQuery<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("[redacted]")
}
}
impl core::fmt::Display for RedactedQuery<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("[redacted]")
}
}
pub struct HttpObjectRequest<'a> {
pub method: &'a str,
pub raw_path: &'a str,
pub raw_query: Option<RedactedQuery<'a>>,
pub headers: &'a HttpHeaderValues<'a>,
pub header_names: &'a [&'a str],
}
impl core::fmt::Debug for HttpObjectRequest<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("HttpObjectRequest")
.field("method", &self.method)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct HttpObjectResponse {
pub status: u16,
pub headers: Vec<(&'static str, String)>,
pub body: HttpBody,
}
const SECURITY_HEADERS: [(&str, &str); 3] = [
("X-Content-Type-Options", "nosniff"),
("Content-Security-Policy", "sandbox; default-src 'none'"),
("Referrer-Policy", "no-referrer"),
];
const NOT_FOUND_BODY: &[u8] = b"not found";
impl HttpObjectResponse {
#[must_use]
pub fn new(status: u16) -> Self {
Self {
status,
headers: SECURITY_HEADERS
.iter()
.map(|(name, value)| (*name, (*value).to_owned()))
.collect(),
body: HttpBody::Empty,
}
}
#[must_use]
pub fn with_header(mut self, name: &'static str, value: impl Into<String>) -> Self {
self.headers.push((name, value.into()));
self
}
#[must_use]
pub fn error(status: u16) -> Self {
Self::new(status).with_header("Cache-Control", "no-store")
}
#[must_use]
pub fn not_found() -> Self {
let mut response = Self::error(404)
.with_header("Content-Type", "text/plain; charset=utf-8")
.with_header("Content-Length", NOT_FOUND_BODY.len().to_string());
response.body = HttpBody::Bytes(bytes::Bytes::from_static(NOT_FOUND_BODY));
response
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
}
pub(crate) fn cache_control(ref_path: bool, private: bool) -> &'static str {
match (ref_path, private) {
(false, false) => "public, max-age=31536000, immutable",
(false, true) => "private, max-age=31536000, immutable",
(true, false) => "public, no-cache",
(true, true) => "private, no-cache",
}
}
#[derive(Debug)]
pub(crate) enum Fail {
NotFound,
Forbidden,
ProofRange,
Unavailable,
}
impl Fail {
pub(crate) fn from_server_error(error: &ServerError) -> Self {
match error.code() {
Code::NotFound => Self::NotFound,
Code::PermissionDenied | Code::Unauthenticated => Self::Forbidden,
_ => Self::Unavailable,
}
}
pub(crate) fn code(&self) -> Code {
match self {
Self::NotFound => Code::NotFound,
Self::ProofRange => Code::OutOfRange,
Self::Forbidden => Code::PermissionDenied,
Self::Unavailable => Code::Unavailable,
}
}
pub(crate) fn into_response(self) -> HttpObjectResponse {
match self {
Self::NotFound => HttpObjectResponse::not_found(),
Self::ProofRange => HttpObjectResponse::error(416),
Self::Forbidden => HttpObjectResponse::error(403),
Self::Unavailable => HttpObjectResponse::error(503),
}
}
}
impl From<resolve::Miss> for Fail {
fn from(miss: resolve::Miss) -> Self {
match miss {
resolve::Miss::NotFound => Self::NotFound,
resolve::Miss::Capped | resolve::Miss::Unavailable => Self::Unavailable,
}
}
}