folk-plugin-http 0.2.3

HTTP plugin for Folk — accepts connections via hyper and dispatches to PHP workers
Documentation
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;

use ipnet::IpNet;
use serde::{Deserialize, Serialize};

/// HTTP plugin configuration.
///
/// ```toml
/// [http]
/// listen = "0.0.0.0:8080"
/// read_timeout = "10s"
/// write_timeout = "30s"
/// max_request_size = "10mb"
/// access_log = false
/// trusted_proxies = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
/// h2c = false
///
/// [http.tls]
/// cert = "/path/to/cert.pem"
/// key = "/path/to/key.pem"
///
/// [http.compression]
/// enabled = true
/// algorithms = ["gzip", "br", "zstd"]
/// min_size = 256
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HttpConfig {
    /// Listening address. Default: 0.0.0.0:8080
    pub listen: SocketAddr,
    /// Max time to read request body. Default: 10s
    #[serde(with = "humantime_serde")]
    pub read_timeout: Duration,
    /// Max time to write response. Default: 30s
    #[serde(with = "humantime_serde")]
    pub write_timeout: Duration,
    /// Maximum request body size. Accepts: "10mb", "512kb", "1gb", or raw bytes as integer.
    /// Default: "10mb".
    #[serde(with = "human_bytes")]
    pub max_request_size: usize,
    /// Enable HTTP access logging (method, uri, status, duration). Default: false
    pub access_log: bool,
    /// Trusted proxy subnets for X-Forwarded-For parsing.
    /// When a request comes from a trusted proxy, the real client IP
    /// is extracted from X-Forwarded-For. Default: empty (trust no proxies).
    #[serde(default)]
    pub trusted_proxies: Vec<IpNet>,
    /// TLS configuration. If set, the server listens on HTTPS.
    /// HTTP/2 via ALPN is negotiated automatically when TLS is enabled.
    #[serde(default)]
    pub tls: Option<TlsConfig>,
    /// Enable HTTP/2 cleartext (h2c) — HTTP/2 without TLS. Default: false.
    #[serde(default)]
    pub h2c: bool,
    /// Response compression. Default: disabled.
    #[serde(default)]
    pub compression: CompressionConfig,
}

/// TLS certificate + key paths.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
    /// Path to PEM-encoded certificate chain.
    pub cert: PathBuf,
    /// Path to PEM-encoded private key.
    pub key: PathBuf,
}

/// Response compression configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CompressionConfig {
    /// Enable response compression. Default: false.
    pub enabled: bool,
    /// Compression algorithms in priority order. Default: ["gzip", "br", "zstd"].
    /// Supported: "gzip", "br" (brotli), "zstd", "deflate".
    pub algorithms: Vec<CompressionAlgorithm>,
    /// Minimum response body size (bytes) to compress. Default: 256.
    #[serde(with = "human_bytes")]
    pub min_size: usize,
}

impl Default for CompressionConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            algorithms: vec![
                CompressionAlgorithm::Gzip,
                CompressionAlgorithm::Br,
                CompressionAlgorithm::Zstd,
            ],
            min_size: 256,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompressionAlgorithm {
    Gzip,
    Br,
    Zstd,
    Deflate,
}

impl Default for HttpConfig {
    fn default() -> Self {
        Self {
            listen: "0.0.0.0:8080".parse().unwrap(),
            read_timeout: Duration::from_secs(10),
            write_timeout: Duration::from_secs(30),
            max_request_size: 10 * 1024 * 1024, // 10 MiB
            access_log: false,
            trusted_proxies: Vec::new(),
            tls: None,
            h2c: false,
            compression: CompressionConfig::default(),
        }
    }
}

/// Parse a human-readable byte size: "10mb", "512kb", "1gb", "256b", or a plain integer.
/// Case-insensitive. Accepts both "mb" and "mib" (binary interpretation throughout).
pub fn parse_byte_size(s: &str) -> Result<usize, String> {
    let s = s.trim().to_lowercase();

    // Try plain integer first
    if let Ok(n) = s.parse::<usize>() {
        return Ok(n);
    }

    let (num_part, multiplier) = if let Some(n) = s.strip_suffix("gib") {
        (n, 1024 * 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("gb") {
        (n, 1024 * 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("mib") {
        (n, 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("mb") {
        (n, 1024 * 1024)
    } else if let Some(n) = s.strip_suffix("kib") {
        (n, 1024)
    } else if let Some(n) = s.strip_suffix("kb") {
        (n, 1024)
    } else if let Some(n) = s.strip_suffix("b") {
        (n, 1)
    } else {
        return Err(format!("invalid byte size: {s:?}"));
    };

    let num: usize = num_part
        .trim()
        .parse()
        .map_err(|_| format!("invalid byte size number: {num_part:?}"))?;

    Ok(num * multiplier)
}

mod human_bytes {
    use serde::{Deserialize, Deserializer, Serializer, de};

    pub fn serialize<S: Serializer>(value: &usize, ser: S) -> Result<S::Ok, S::Error> {
        ser.serialize_u64(*value as u64)
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<usize, D::Error> {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum ByteSize {
            Str(String),
            Num(usize),
        }

        match ByteSize::deserialize(de)? {
            ByteSize::Num(n) => Ok(n),
            ByteSize::Str(s) => super::parse_byte_size(&s).map_err(de::Error::custom),
        }
    }
}