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