#[cfg(feature = "governor")] use std::num::NonZero; #[cfg(feature = "cf-worker")] #[allow(unsafe_code)] mod json { use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = JSON)] pub fn stringify(value: &JsValue) -> String; } } /// Application configuration #[derive(Debug, Clone, serde::Deserialize)] pub struct Config { /// The listen address pub listen: Option<String>, /// Sandbox configuration #[serde(default)] pub sandbox: SandboxConfig, /// Send Cache-Control headers pub enable_cache: bool, /// Index page configuration pub index_redirect: IndexConfig, /// Whether to only serve media with a known safe signature pub allow_unknown: bool, /// Fetch configuration pub fetch: FetchConfig, /// Post-processing configuration pub post_process: PostProcessConfig, #[cfg(not(feature = "cf-worker"))] /// The maximum number of X-Forwarded-For headers to allow pub max_x_forwarded_for: u8, #[cfg(feature = "governor")] /// Governor configuration pub rate_limit: Vec<RateLimitConfig>, } /// Sandbox configuration #[derive(Debug, Clone, serde::Deserialize)] #[non_exhaustive] pub enum SandboxConfig { /// No sandboxing #[serde(rename = "none")] NoSandbox, /// AppArmor sandboxing #[cfg(feature = "apparmor")] #[serde(rename = "apparmor")] AppArmor(AppArmorConfig), } impl Default for SandboxConfig { fn default() -> Self { Self::NoSandbox } } /// AppArmor configuration #[cfg(feature = "apparmor")] #[derive(Debug, Clone, serde::Deserialize)] pub struct AppArmorConfig { /// The profile to transition to after initialization is complete pub serve: String, /// The AppArmor hat to use when processing media pub image_hat: String, } /// Governor configuration #[cfg(feature = "governor")] #[derive(Debug, Clone, serde::Deserialize)] pub struct RateLimitConfig { /// The key to use for rate limiting headers pub key: Option<String>, /// The rate limit replenish interval in milliseconds pub replenish_every: u64, /// The rate limit burst size pub burst: NonZero<u32>, /// The minimum request duration in milliseconds for this rate limit to apply pub min_request_duration: Option<u64>, } #[cfg(feature = "cf-worker")] #[derive(Debug, thiserror::Error)] /// Configuration error pub enum CfConfigError { #[error("Failed to convert env")] /// Failed to convert the environment to a JS object EnvConversion, #[error("Failed to parse JSON")] /// Failed to parse JSON JsonParse(#[from] serde_json::Error), } #[cfg(feature = "cf-worker")] impl Config { /// Load the configuration from the Cloudflare Worker environment pub fn load_from_cf_env(env: worker::Env) -> Result<Self, CfConfigError> { use wasm_bindgen::JsCast; let obj = env.dyn_into().map_err(|_| CfConfigError::EnvConversion)?; let json = json::stringify(&obj); serde_json::from_str(&json).map_err(CfConfigError::JsonParse) } } impl Default for Config { fn default() -> Self { Config { listen: Some("127.0.0.1:3000".to_string()), enable_cache: false, sandbox: SandboxConfig::default(), fetch: FetchConfig { #[cfg(not(feature = "cf-worker"))] addr_family: AddrFamilyConfig::Both, allow_http: false, via: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).to_string(), user_agent: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")) .to_string(), }, index_redirect: IndexConfig::Message(format!( "Welcome to {}", concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), )), allow_unknown: false, post_process: PostProcessConfig { enable_redirects: false, normalization: NormalizationPolicy::Opportunistic, allow_svg_passthrough: false, }, #[cfg(not(feature = "cf-worker"))] max_x_forwarded_for: 0, #[cfg(feature = "governor")] rate_limit: vec![RateLimitConfig { key: None, replenish_every: 2000, burst: NonZero::new(32).unwrap(), min_request_duration: None, }], } } } /// Fetch configuration #[derive(Debug, Clone, serde::Deserialize)] pub struct FetchConfig { /// The address family to use #[cfg(not(feature = "cf-worker"))] #[serde(default)] pub addr_family: AddrFamilyConfig, /// Whether to allow HTTP requests pub allow_http: bool, /// The via string to use when fetching media pub via: String, /// The user agent to use when fetching media pub user_agent: String, } /// Address family configuration #[cfg(not(feature = "cf-worker"))] #[derive(Debug, Clone, Copy, serde::Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum AddrFamilyConfig { /// Prefer IPv4 V4Only, /// Prefer IPv6 V6Only, /// Use both IPv4 and IPv6 Both, } #[cfg(not(feature = "cf-worker"))] impl Default for AddrFamilyConfig { fn default() -> Self { Self::Both } } /// Post-processing configuration #[derive(Debug, Clone, serde::Deserialize)] pub struct PostProcessConfig { /// Opportunistically redirect to the original URL if the media is not modified /// /// Potentially leaks the user's IP address and other metadata pub enable_redirects: bool, /// Whether to normalize media files when the request specifically asks for a format pub normalization: NormalizationPolicy, /// Whether to allow SVG passthrough /// /// This opens up the possibility of SVG-based attacks pub allow_svg_passthrough: bool, } /// Normalization policy #[derive(Copy, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum NormalizationPolicy { /// Ignore the requested format and return the original Never, /// Don't convert the media if we don't have to /// /// /// This is the default for Cloudflare Workers Lazy, /// Only return the requested format if other conversions are necessary /// /// This is the default for local environments Opportunistic, /// Always make an attempt to return the requested format Aggressive, } impl Default for NormalizationPolicy { fn default() -> Self { Self::Opportunistic } } #[derive(Debug, Clone, serde::Deserialize)] #[serde(untagged)] /// Index page configuration pub enum IndexConfig { /// Redirect to a URL #[allow(missing_docs)] Redirect { permanent: bool, url: String }, /// Display a message Message(String), }