Skip to main content

folk_plugin_http/
config.rs

1use std::net::SocketAddr;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use ipnet::IpNet;
6use serde::{Deserialize, Serialize};
7
8/// HTTP plugin configuration.
9///
10/// ```toml
11/// [http]
12/// listen = "0.0.0.0:8080"
13/// read_timeout = "10s"
14/// write_timeout = "30s"
15/// max_request_size = "10mb"
16/// access_log = false
17/// trusted_proxies = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
18/// h2c = false
19///
20/// [http.tls]
21/// cert = "/path/to/cert.pem"
22/// key = "/path/to/key.pem"
23///
24/// [http.compression]
25/// enabled = true
26/// algorithms = ["gzip", "br", "zstd"]
27/// min_size = 256
28/// ```
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(default)]
31pub struct HttpConfig {
32    /// Listening address. Default: 0.0.0.0:8080
33    pub listen: SocketAddr,
34    /// Max time to read request body. Default: 10s
35    #[serde(with = "humantime_serde")]
36    pub read_timeout: Duration,
37    /// Max time to write response. Default: 30s
38    #[serde(with = "humantime_serde")]
39    pub write_timeout: Duration,
40    /// Maximum request body size. Accepts: "10mb", "512kb", "1gb", or raw bytes as integer.
41    /// Default: "10mb".
42    #[serde(with = "human_bytes")]
43    pub max_request_size: usize,
44    /// Enable HTTP access logging (method, uri, status, duration). Default: false
45    pub access_log: bool,
46    /// Trusted proxy subnets for X-Forwarded-For parsing.
47    /// When a request comes from a trusted proxy, the real client IP
48    /// is extracted from X-Forwarded-For. Default: empty (trust no proxies).
49    #[serde(default)]
50    pub trusted_proxies: Vec<IpNet>,
51    /// TLS configuration. If set, the server listens on HTTPS.
52    /// HTTP/2 via ALPN is negotiated automatically when TLS is enabled.
53    #[serde(default)]
54    pub tls: Option<TlsConfig>,
55    /// Enable HTTP/2 cleartext (h2c) — HTTP/2 without TLS. Default: false.
56    #[serde(default)]
57    pub h2c: bool,
58    /// Response compression. Default: disabled.
59    #[serde(default)]
60    pub compression: CompressionConfig,
61    /// Lua hook pipeline. Default: empty (no hooks).
62    #[serde(default)]
63    pub hooks: Vec<HookConfig>,
64}
65
66/// TLS certificate + key paths.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct TlsConfig {
69    /// Path to PEM-encoded certificate chain.
70    pub cert: PathBuf,
71    /// Path to PEM-encoded private key.
72    pub key: PathBuf,
73}
74
75/// Response compression configuration.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(default)]
78pub struct CompressionConfig {
79    /// Enable response compression. Default: false.
80    pub enabled: bool,
81    /// Compression algorithms in priority order. Default: ["gzip", "br", "zstd"].
82    /// Supported: "gzip", "br" (brotli), "zstd", "deflate".
83    pub algorithms: Vec<CompressionAlgorithm>,
84    /// Minimum response body size (bytes) to compress. Default: 256.
85    #[serde(with = "human_bytes")]
86    pub min_size: usize,
87}
88
89impl Default for CompressionConfig {
90    fn default() -> Self {
91        Self {
92            enabled: false,
93            algorithms: vec![
94                CompressionAlgorithm::Gzip,
95                CompressionAlgorithm::Br,
96                CompressionAlgorithm::Zstd,
97            ],
98            min_size: 256,
99        }
100    }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum CompressionAlgorithm {
106    Gzip,
107    Br,
108    Zstd,
109    Deflate,
110}
111
112impl Default for HttpConfig {
113    fn default() -> Self {
114        Self {
115            listen: "0.0.0.0:8080".parse().unwrap(),
116            read_timeout: Duration::from_secs(10),
117            write_timeout: Duration::from_secs(30),
118            max_request_size: 10 * 1024 * 1024, // 10 MiB
119            access_log: false,
120            trusted_proxies: Vec::new(),
121            tls: None,
122            h2c: false,
123            compression: CompressionConfig::default(),
124            hooks: Vec::new(),
125        }
126    }
127}
128
129// ── Hook pipeline configuration ───────────────────────────────────────────────
130
131/// Whether a hook runs synchronously (in the request critical path) or
132/// asynchronously (fire-and-forget, outside the critical path).
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
134#[serde(rename_all = "snake_case")]
135pub enum HookMode {
136    Sync,
137    Async,
138}
139
140/// What to do when a sync hook fails (Lua error or timeout).
141#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
142#[serde(rename_all = "snake_case")]
143pub enum HookErrorBehavior {
144    /// Ignore the error and continue (default).
145    #[default]
146    FailOpen,
147    /// Abort the request with HTTP 500.
148    FailClosed,
149}
150
151/// One `[[http.hooks]]` entry.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct HookConfig {
154    /// Pipeline event: "request.before", "request.error",
155    /// "response.headers", "response.after".
156    pub event: String,
157    /// Path to the Lua script (relative to the working directory).
158    pub lua: PathBuf,
159    /// Execution mode. Default: sync.
160    #[serde(default = "default_hook_mode")]
161    pub mode: HookMode,
162    /// Timeout for sync hooks in milliseconds. Default: 5.
163    #[serde(default = "default_timeout_ms")]
164    pub timeout_ms: u64,
165    /// Error handling for sync hooks. Default: fail_open.
166    #[serde(default)]
167    pub on_error: HookErrorBehavior,
168}
169
170fn default_hook_mode() -> HookMode {
171    HookMode::Sync
172}
173
174fn default_timeout_ms() -> u64 {
175    5
176}
177
178// ── Byte-size parsing ─────────────────────────────────────────────────────────
179
180/// Parse a human-readable byte size: "10mb", "512kb", "1gb", "256b", or a plain integer.
181/// Case-insensitive. Accepts both "mb" and "mib" (binary interpretation throughout).
182pub fn parse_byte_size(s: &str) -> Result<usize, String> {
183    let s = s.trim().to_lowercase();
184
185    // Try plain integer first
186    if let Ok(n) = s.parse::<usize>() {
187        return Ok(n);
188    }
189
190    let (num_part, multiplier) = if let Some(n) = s.strip_suffix("gib") {
191        (n, 1024 * 1024 * 1024)
192    } else if let Some(n) = s.strip_suffix("gb") {
193        (n, 1024 * 1024 * 1024)
194    } else if let Some(n) = s.strip_suffix("mib") {
195        (n, 1024 * 1024)
196    } else if let Some(n) = s.strip_suffix("mb") {
197        (n, 1024 * 1024)
198    } else if let Some(n) = s.strip_suffix("kib") {
199        (n, 1024)
200    } else if let Some(n) = s.strip_suffix("kb") {
201        (n, 1024)
202    } else if let Some(n) = s.strip_suffix("b") {
203        (n, 1)
204    } else {
205        return Err(format!("invalid byte size: {s:?}"));
206    };
207
208    let num: usize = num_part
209        .trim()
210        .parse()
211        .map_err(|_| format!("invalid byte size number: {num_part:?}"))?;
212
213    Ok(num * multiplier)
214}
215
216mod human_bytes {
217    use serde::{Deserialize, Deserializer, Serializer, de};
218
219    pub fn serialize<S: Serializer>(value: &usize, ser: S) -> Result<S::Ok, S::Error> {
220        ser.serialize_u64(*value as u64)
221    }
222
223    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<usize, D::Error> {
224        #[derive(Deserialize)]
225        #[serde(untagged)]
226        enum ByteSize {
227            Str(String),
228            Num(usize),
229        }
230
231        match ByteSize::deserialize(de)? {
232            ByteSize::Num(n) => Ok(n),
233            ByteSize::Str(s) => super::parse_byte_size(&s).map_err(de::Error::custom),
234        }
235    }
236}