yumechi-no-kuni-proxy-worker/src/config.rs
eternal-flame-AD 4c98ae337b
tiered rate limiting
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-19 03:23:59 -06:00

238 lines
6.8 KiB
Rust

#[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),
}