1use std::collections::BTreeMap;
9use std::net::SocketAddr;
10use std::num::{NonZeroU64, NonZeroUsize};
11use std::path::PathBuf;
12use std::time::Duration;
13
14use serde::Deserialize;
15
16use super::listen::Limits;
17use super::packed::Packed;
18use super::route::Site;
19use crate::client::Address;
20use crate::client::network::{Remote, Trust};
21
22const HEAD_CAP: usize = 16 * 1024;
24const BODY_CAP: usize = 1024 * 1024;
26const READ_TIMEOUT: Duration = Duration::from_secs(10);
28
29#[derive(Debug, Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct Config {
33 #[serde(default)]
35 pub children: Vec<super::children::Config>,
36 pub listen: SocketAddr,
38 pub origin: String,
40 pub files: PathBuf,
42 pub stores: BTreeMap<String, Store>,
45 #[serde(default = "timeout")]
47 pub timeout_ms: NonZeroU64,
48 #[serde(default = "connections")]
50 pub max_connections: NonZeroUsize,
51 pub lys: Lys,
53 pub assistant: Option<Assistant>,
55}
56
57#[derive(Clone, Debug, Deserialize)]
64#[serde(untagged)]
65pub enum Store {
66 Socket(PathBuf),
67 Network(NetworkStore),
68}
69
70#[derive(Clone, Debug, Deserialize)]
73pub struct NetworkStore {
74 #[serde(default)]
75 pub endpoint: Option<String>,
76 #[serde(default)]
77 pub server_name: Option<String>,
78 #[serde(flatten)]
81 pub other: BTreeMap<String, serde_json::Value>,
82}
83
84impl NetworkStore {
85 fn check(&self, name: &str) -> Result<(), String> {
86 if let Some(field) = self.other.keys().next() {
87 return Err(format!(
88 "door_store_field_unknown: store {name}: {field:?} is not a network store field; a network store names endpoint and server_name and is reached only over verified TLS"
89 ));
90 }
91 let endpoint = self.endpoint.as_deref().unwrap_or_default();
92 if !crate::client::network::endpoint_parses(endpoint) {
93 return Err(format!(
94 "door_store_endpoint_invalid: store {name}: endpoint {endpoint:?} is not <host>:<port> with a port from 1 to 65535"
95 ));
96 }
97 let server_name = self.server_name.as_deref().unwrap_or_default();
98 if server_name.is_empty() {
99 return Err(format!(
100 "door_store_server_name_missing: store {name}: a network store names the server_name its TLS certificate is verified for"
101 ));
102 }
103 crate::client::network::name(server_name)
104 .map(drop)
105 .map_err(|why| format!("door_store_server_name_invalid: store {name}: {why}"))
106 }
107}
108
109#[derive(Clone, Debug, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct Assistant {
113 pub socket: PathBuf,
115 #[serde(default = "assistant_frame")]
117 pub frame_bytes: NonZeroUsize,
118 #[serde(default = "assistant_person")]
120 pub max_per_person: NonZeroUsize,
121 #[serde(default = "assistant_store")]
123 pub max_per_store: NonZeroUsize,
124 #[serde(default = "assistant_frame")]
126 pub events_after_bytes: NonZeroUsize,
127}
128
129const fn assistant_frame() -> NonZeroUsize {
130 NonZeroUsize::MIN.saturating_add(262_143)
131}
132const fn assistant_person() -> NonZeroUsize {
133 NonZeroUsize::MIN.saturating_add(1)
134}
135const fn assistant_store() -> NonZeroUsize {
136 NonZeroUsize::MIN.saturating_add(31)
137}
138
139#[derive(Debug, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct Lys {
143 pub issuer: String,
145 pub client_id: String,
147 pub secret_file: PathBuf,
149 pub redirect: String,
151}
152
153const fn timeout() -> NonZeroU64 {
154 NonZeroU64::MIN.saturating_add(29_999)
155}
156
157const fn connections() -> NonZeroUsize {
158 NonZeroUsize::MIN.saturating_add(63)
159}
160
161impl Config {
162 pub fn parse(bytes: &[u8]) -> Result<Self, String> {
168 let config: Self = serde_json::from_slice(bytes)
169 .map_err(|error| format!("the door configuration is not valid: {error}"))?;
170 config.check()?;
171 Ok(config)
172 }
173
174 fn check(&self) -> Result<(), String> {
175 super::children::check(&self.children, &self.stores)?;
176 if self
177 .assistant
178 .as_ref()
179 .is_some_and(|assistant| !assistant.socket.is_absolute())
180 {
181 return Err("assistant.socket must be absolute".to_owned());
182 }
183 if !self.listen.ip().is_loopback() {
184 return Err("listen must be a loopback address behind a TLS proxy".to_owned());
185 }
186 let origin =
187 url::Url::parse(&self.origin).map_err(|_| "origin is not an address".to_owned())?;
188 if origin.scheme() != "https" || origin.origin().ascii_serialization() != self.origin {
189 return Err(
190 "origin must be an https origin with no path, as the browser sends it".to_owned(),
191 );
192 }
193 if !self.files.is_absolute() {
194 return Err("files must be an absolute folder".to_owned());
195 }
196 if self.stores.is_empty() {
197 return Err("stores must name at least one store".to_owned());
198 }
199 for (name, store) in &self.stores {
200 if name.is_empty()
201 || !name
202 .bytes()
203 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
204 {
205 return Err(format!(
206 "store name {name:?} must be letters, digits, - or _"
207 ));
208 }
209 match store {
210 Store::Socket(socket) if !socket.is_absolute() => {
211 return Err(format!("store socket for {name} must be an absolute path"));
212 }
213 Store::Socket(_) => {}
214 Store::Network(network) => network.check(name)?,
215 }
216 }
217 let issuer = url::Url::parse(&self.lys.issuer)
218 .map_err(|_| "lys issuer is not an address".to_owned())?;
219 if !matches!(issuer.scheme(), "http" | "https") {
220 return Err("lys issuer must be an http or https address".to_owned());
221 }
222 if self.lys.client_id.is_empty() || self.lys.client_id.chars().any(char::is_control) {
223 return Err("lys client_id must be a name with no control character".to_owned());
224 }
225 if !self.lys.secret_file.is_absolute() {
226 return Err("lys secret_file must be an absolute path".to_owned());
227 }
228 if self.lys.redirect != format!("{}/callback", self.origin) {
229 return Err("lys redirect must be the origin's /callback".to_owned());
230 }
231 Ok(())
232 }
233
234 #[must_use]
236 pub const fn timeout(&self) -> Duration {
237 Duration::from_millis(self.timeout_ms.get())
238 }
239
240 pub fn addresses(&self, trust: &Trust) -> Result<BTreeMap<String, Address>, String> {
246 self.stores
247 .iter()
248 .map(|(name, store)| {
249 let address = match store {
250 Store::Socket(socket) => Address::Socket(socket.clone()),
251 Store::Network(network) => Address::Network(
252 Remote::new(
253 network.endpoint.as_deref().unwrap_or_default(),
254 network.server_name.as_deref().unwrap_or_default(),
255 trust,
256 )
257 .map_err(|why| {
258 format!("door_store_endpoint_invalid: store {name}: {why}")
259 })?,
260 ),
261 };
262 Ok((name.clone(), address))
263 })
264 .collect()
265 }
266
267 pub fn site(&self, trust: &Trust) -> Result<Site, String> {
273 Ok(Site {
274 origin: self.origin.clone(),
275 files: self.files.clone(),
276 stores: self.addresses(trust)?,
277 store_children: self
278 .children
279 .iter()
280 .filter_map(|child| {
281 child.store.as_ref().map(|store| {
282 (
283 store.clone(),
284 (child.name.clone(), std::sync::Arc::clone(&child.given_up)),
285 )
286 })
287 })
288 .collect(),
289 timeout: self.timeout(),
290 packed: Packed::default(),
291 })
292 }
293
294 #[must_use]
296 pub const fn limits(&self) -> Limits {
297 Limits {
298 max_connections: self.max_connections.get(),
299 read_timeout: READ_TIMEOUT,
300 head_cap: HEAD_CAP,
301 body_cap: BODY_CAP,
302 }
303 }
304}
305
306#[cfg(test)]
307#[path = "config_tests.rs"]
308mod tests;
309
310#[derive(Clone, Copy)]
314pub struct SignInBounds {
315 pub max_pending: NonZeroUsize,
316 pub session_seconds: NonZeroU64,
317 pub connect_ms: NonZeroU64,
318 pub total_ms: NonZeroU64,
319}
320
321impl Default for SignInBounds {
322 fn default() -> Self {
323 Self {
324 max_pending: NonZeroUsize::MIN.saturating_add(511),
325 session_seconds: NonZeroU64::MIN.saturating_add(43_199),
326 connect_ms: NonZeroU64::MIN.saturating_add(4_999),
327 total_ms: timeout(),
328 }
329 }
330}
331
332impl SignInBounds {
333 pub fn from_env() -> Result<Self, String> {
339 let defaults = Self::default();
340 let max_pending = positive_env(
341 "HAEMATITE_PENDING_SIGNINS",
342 defaults.max_pending.get() as u64,
343 )?;
344 let bounds = Self {
345 max_pending: usize::try_from(max_pending.get())
346 .ok()
347 .and_then(NonZeroUsize::new)
348 .ok_or("HAEMATITE_PENDING_SIGNINS exceeds this machine's range")?,
349 session_seconds: positive_env(
350 "HAEMATITE_SESSION_SECONDS",
351 defaults.session_seconds.get(),
352 )?,
353 connect_ms: positive_env(
354 "HAEMATITE_LYS_CONNECT_TIMEOUT_MS",
355 defaults.connect_ms.get(),
356 )?,
357 total_ms: positive_env("HAEMATITE_LYS_TIMEOUT_MS", defaults.total_ms.get())?,
358 };
359 if bounds.connect_ms > bounds.total_ms {
360 return Err(
361 "HAEMATITE_LYS_CONNECT_TIMEOUT_MS exceeds HAEMATITE_LYS_TIMEOUT_MS".to_owned(),
362 );
363 }
364 if std::time::Instant::now()
365 .checked_add(Duration::from_millis(bounds.total_ms.get()))
366 .is_none()
367 {
368 return Err("HAEMATITE_LYS_TIMEOUT_MS exceeds the monotonic clock".to_owned());
369 }
370 Ok(bounds)
371 }
372}
373
374pub(crate) fn positive_env(name: &str, default: u64) -> Result<NonZeroU64, String> {
375 match std::env::var(name) {
376 Ok(value) => value
377 .parse()
378 .map_err(|_| format!("{name} must be a positive integer")),
379 Err(std::env::VarError::NotPresent) => {
380 NonZeroU64::new(default).ok_or_else(|| format!("{name} has a zero default"))
381 }
382 Err(std::env::VarError::NotUnicode(_)) => Err(format!("{name} must be a positive integer")),
383 }
384}