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 #[serde(default)]
55 pub lys: Option<Lys>,
56 pub assistant: Option<Assistant>,
58}
59
60#[derive(Clone, Debug, Deserialize)]
67#[serde(untagged)]
68pub enum Store {
69 Socket(PathBuf),
70 Network(NetworkStore),
71}
72
73#[derive(Clone, Debug, Deserialize)]
76pub struct NetworkStore {
77 #[serde(default)]
78 pub endpoint: Option<String>,
79 #[serde(default)]
80 pub server_name: Option<String>,
81 #[serde(flatten)]
84 pub other: BTreeMap<String, serde_json::Value>,
85}
86
87impl NetworkStore {
88 fn check(&self, name: &str) -> Result<(), String> {
89 if let Some(field) = self.other.keys().next() {
90 return Err(format!(
91 "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"
92 ));
93 }
94 let endpoint = self.endpoint.as_deref().unwrap_or_default();
95 if !crate::client::network::endpoint_parses(endpoint) {
96 return Err(format!(
97 "door_store_endpoint_invalid: store {name}: endpoint {endpoint:?} is not <host>:<port> with a port from 1 to 65535"
98 ));
99 }
100 let server_name = self.server_name.as_deref().unwrap_or_default();
101 if server_name.is_empty() {
102 return Err(format!(
103 "door_store_server_name_missing: store {name}: a network store names the server_name its TLS certificate is verified for"
104 ));
105 }
106 crate::client::network::name(server_name)
107 .map(drop)
108 .map_err(|why| format!("door_store_server_name_invalid: store {name}: {why}"))
109 }
110}
111
112#[derive(Clone, Debug, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub struct Assistant {
116 pub socket: PathBuf,
118 #[serde(default = "assistant_frame")]
120 pub frame_bytes: NonZeroUsize,
121 #[serde(default = "assistant_person")]
123 pub max_per_person: NonZeroUsize,
124 #[serde(default = "assistant_store")]
126 pub max_per_store: NonZeroUsize,
127 #[serde(default = "assistant_frame")]
129 pub events_after_bytes: NonZeroUsize,
130}
131
132const fn assistant_frame() -> NonZeroUsize {
133 NonZeroUsize::MIN.saturating_add(262_143)
134}
135const fn assistant_person() -> NonZeroUsize {
136 NonZeroUsize::MIN.saturating_add(1)
137}
138const fn assistant_store() -> NonZeroUsize {
139 NonZeroUsize::MIN.saturating_add(31)
140}
141
142#[derive(Debug, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct Lys {
146 pub issuer: String,
148 pub client_id: String,
150 pub secret_file: PathBuf,
152 pub redirect: String,
154}
155
156const fn timeout() -> NonZeroU64 {
157 NonZeroU64::MIN.saturating_add(29_999)
158}
159
160const fn connections() -> NonZeroUsize {
161 NonZeroUsize::MIN.saturating_add(63)
162}
163
164impl Config {
165 pub fn parse(bytes: &[u8]) -> Result<Self, String> {
171 let config: Self = serde_json::from_slice(bytes)
172 .map_err(|error| format!("the door configuration is not valid: {error}"))?;
173 config.check()?;
174 Ok(config)
175 }
176
177 fn check(&self) -> Result<(), String> {
178 super::children::check(&self.children, &self.stores)?;
179 if self
180 .assistant
181 .as_ref()
182 .is_some_and(|assistant| !assistant.socket.is_absolute())
183 {
184 return Err("assistant.socket must be absolute".to_owned());
185 }
186 if !self.listen.ip().is_loopback() {
187 return Err("listen must be a loopback address behind a TLS proxy".to_owned());
188 }
189 match &self.lys {
190 Some(_) => {
191 let origin = url::Url::parse(&self.origin)
192 .map_err(|_| "origin is not an address".to_owned())?;
193 if origin.scheme() != "https"
194 || origin.origin().ascii_serialization() != self.origin
195 {
196 return Err(
197 "origin must be an https origin with no path, as the browser sends it"
198 .to_owned(),
199 );
200 }
201 }
202 None => local_origin(&self.origin)?,
203 }
204 if !self.files.is_absolute() {
205 return Err("files must be an absolute folder".to_owned());
206 }
207 if self.stores.is_empty() {
208 return Err("stores must name at least one store".to_owned());
209 }
210 for (name, store) in &self.stores {
211 if name.is_empty()
212 || !name
213 .bytes()
214 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
215 {
216 return Err(format!(
217 "store name {name:?} must be letters, digits, - or _"
218 ));
219 }
220 match store {
221 Store::Socket(socket) if !socket.is_absolute() => {
222 return Err(format!("store socket for {name} must be an absolute path"));
223 }
224 Store::Socket(_) => {}
225 Store::Network(network) => network.check(name)?,
226 }
227 }
228 let Some(lys) = &self.lys else {
229 return Ok(());
230 };
231 let issuer =
232 url::Url::parse(&lys.issuer).map_err(|_| "lys issuer is not an address".to_owned())?;
233 if !matches!(issuer.scheme(), "http" | "https") {
234 return Err("lys issuer must be an http or https address".to_owned());
235 }
236 if lys.client_id.is_empty() || lys.client_id.chars().any(char::is_control) {
237 return Err("lys client_id must be a name with no control character".to_owned());
238 }
239 if !lys.secret_file.is_absolute() {
240 return Err("lys secret_file must be an absolute path".to_owned());
241 }
242 if lys.redirect != format!("{}/callback", self.origin) {
243 return Err("lys redirect must be the origin's /callback".to_owned());
244 }
245 Ok(())
246 }
247
248 #[must_use]
250 pub const fn through_lys(&self) -> bool {
251 self.lys.is_some()
252 }
253
254 #[must_use]
256 pub const fn timeout(&self) -> Duration {
257 Duration::from_millis(self.timeout_ms.get())
258 }
259
260 pub fn addresses(&self, trust: &Trust) -> Result<BTreeMap<String, Address>, String> {
266 self.stores
267 .iter()
268 .map(|(name, store)| {
269 let address = match store {
270 Store::Socket(socket) => Address::Socket(socket.clone()),
271 Store::Network(network) => Address::Network(
272 Remote::new(
273 network.endpoint.as_deref().unwrap_or_default(),
274 network.server_name.as_deref().unwrap_or_default(),
275 trust,
276 )
277 .map_err(|why| {
278 format!("door_store_endpoint_invalid: store {name}: {why}")
279 })?,
280 ),
281 };
282 Ok((name.clone(), address))
283 })
284 .collect()
285 }
286
287 pub fn site(&self, trust: &Trust) -> Result<Site, String> {
293 Ok(Site {
294 origin: self.origin.clone(),
295 files: self.files.clone(),
296 stores: self.addresses(trust)?,
297 store_children: self
298 .children
299 .iter()
300 .filter_map(|child| {
301 child.store.as_ref().map(|store| {
302 (
303 store.clone(),
304 (child.name.clone(), std::sync::Arc::clone(&child.given_up)),
305 )
306 })
307 })
308 .collect(),
309 timeout: self.timeout(),
310 packed: Packed::default(),
311 })
312 }
313
314 #[must_use]
316 pub const fn limits(&self) -> Limits {
317 Limits {
318 max_connections: self.max_connections.get(),
319 read_timeout: READ_TIMEOUT,
320 head_cap: HEAD_CAP,
321 body_cap: BODY_CAP,
322 }
323 }
324}
325
326fn local_origin(origin: &str) -> Result<(), String> {
330 let refused = || {
331 format!(
332 "door_local_origin_not_loopback: origin {origin} is not an http or https origin on \
333 127.0.0.1, localhost or [::1] with no path; a door with no lys block serves only \
334 this machine, and a lys block is needed for any other origin"
335 )
336 };
337 let parsed = url::Url::parse(origin).map_err(|_| refused())?;
338 let loopback = parsed.host_str().is_some_and(crate::loopback_http::host);
339 if !matches!(parsed.scheme(), "http" | "https")
340 || !loopback
341 || parsed.origin().ascii_serialization() != origin
342 {
343 return Err(refused());
344 }
345 Ok(())
346}
347
348#[cfg(test)]
349#[path = "config_tests.rs"]
350mod tests;
351
352#[derive(Clone, Copy)]
356pub struct SignInBounds {
357 pub max_pending: NonZeroUsize,
358 pub session_seconds: NonZeroU64,
359 pub connect_ms: NonZeroU64,
360 pub total_ms: NonZeroU64,
361}
362
363impl Default for SignInBounds {
364 fn default() -> Self {
365 Self {
366 max_pending: NonZeroUsize::MIN.saturating_add(511),
367 session_seconds: NonZeroU64::MIN.saturating_add(43_199),
368 connect_ms: NonZeroU64::MIN.saturating_add(4_999),
369 total_ms: timeout(),
370 }
371 }
372}
373
374impl SignInBounds {
375 pub fn from_env() -> Result<Self, String> {
381 let defaults = Self::default();
382 let max_pending = positive_env(
383 "HAEMATITE_PENDING_SIGNINS",
384 defaults.max_pending.get() as u64,
385 )?;
386 let bounds = Self {
387 max_pending: usize::try_from(max_pending.get())
388 .ok()
389 .and_then(NonZeroUsize::new)
390 .ok_or("HAEMATITE_PENDING_SIGNINS exceeds this machine's range")?,
391 session_seconds: positive_env(
392 "HAEMATITE_SESSION_SECONDS",
393 defaults.session_seconds.get(),
394 )?,
395 connect_ms: positive_env(
396 "HAEMATITE_LYS_CONNECT_TIMEOUT_MS",
397 defaults.connect_ms.get(),
398 )?,
399 total_ms: positive_env("HAEMATITE_LYS_TIMEOUT_MS", defaults.total_ms.get())?,
400 };
401 if bounds.connect_ms > bounds.total_ms {
402 return Err(
403 "HAEMATITE_LYS_CONNECT_TIMEOUT_MS exceeds HAEMATITE_LYS_TIMEOUT_MS".to_owned(),
404 );
405 }
406 if std::time::Instant::now()
407 .checked_add(Duration::from_millis(bounds.total_ms.get()))
408 .is_none()
409 {
410 return Err("HAEMATITE_LYS_TIMEOUT_MS exceeds the monotonic clock".to_owned());
411 }
412 Ok(bounds)
413 }
414}
415
416pub(crate) fn positive_env(name: &str, default: u64) -> Result<NonZeroU64, String> {
417 match std::env::var(name) {
418 Ok(value) => value
419 .parse()
420 .map_err(|_| format!("{name} must be a positive integer")),
421 Err(std::env::VarError::NotPresent) => {
422 NonZeroU64::new(default).ok_or_else(|| format!("{name} has a zero default"))
423 }
424 Err(std::env::VarError::NotUnicode(_)) => Err(format!("{name} must be a positive integer")),
425 }
426}