links/config/
global.rs

1//! Global redirector server configuration.
2
3use std::{
4	collections::HashMap,
5	fmt::{Display, Formatter, Result as FmtResult},
6	net::{IpAddr, Ipv6Addr},
7	path::PathBuf,
8	sync::Arc,
9};
10
11use parking_lot::RwLock;
12use rand::{distributions::Alphanumeric, Rng};
13use tracing::{debug, instrument, warn};
14
15use super::{CertificateSource, DefaultCertificateSource, ListenAddress, LogLevel};
16use crate::{
17	config::partial::Partial, server::Protocol, stats::StatisticCategories, store::BackendType,
18	util::A_YEAR,
19};
20
21/// Global configuration for the links redirector server. This is the more
22/// idiomatic, easier to use (in rust code), and shareable-across-threads
23/// version, which can be updated from a [`Partial`].
24#[derive(Debug)]
25pub struct Config {
26	inner: RwLock<ConfigInner>,
27	file: Option<PathBuf>,
28}
29
30impl Config {
31	/// Create a new `Config` instance using the provided file path as the
32	/// configuration file. Configuration data is parsed from environment
33	/// variables, the config file, and command-line arguments, in that order.
34	/// If there is an error with the configuration file or any other
35	/// configuration source, no error is emitted. Instead, a warning is logged,
36	/// and the other configuration sources are used.
37	///
38	/// # IO
39	/// This function performs synchronous file IO, and should therefore not be
40	/// used inside of an asynchronous context.
41	#[must_use]
42	pub fn new(file: Option<PathBuf>) -> Self {
43		let config = ConfigInner::default();
44
45		let config = Self {
46			inner: RwLock::new(config),
47			file,
48		};
49		config.update();
50		config
51	}
52
53	/// Create a new static reference to a new `Config` instance using the
54	/// provided file path as the configuration file. Configuration data is
55	/// parsed from environment variables, the config file, and command-line
56	/// arguments, in that order. If there is an error with the configuration
57	/// file or any other configuration source, no error is emitted. Instead, a
58	/// warning is logged, and the other configuration sources are used.
59	///
60	/// # Memory
61	/// Because this function leaks memory with no (safe) way of freeing it,
62	/// care should be taken not to call this function an unbounded number of
63	/// times.
64	///
65	/// # IO
66	/// This function performs synchronous file IO, and should therefore not be
67	/// used inside of an asynchronous context.
68	#[must_use]
69	pub fn new_static(file: Option<PathBuf>) -> &'static Self {
70		Box::leak(Box::new(Self::new(file)))
71	}
72
73	/// Update this config from environment variables, config file, and
74	/// command-line arguments. This function starts with defaults for each
75	/// option, then updates those from environment variables, then from the
76	/// config file, then from command-line arguments, and finally overwrites
77	/// this `Config`'s options with those newly-parsed ones.
78	///
79	/// # IO
80	/// This function performs synchronous file IO, and should therefore not be
81	/// used inside of an asynchronous context.
82	#[instrument(level = "info", fields(%self))]
83	pub fn update(&self) {
84		let mut config = ConfigInner::default();
85
86		config.update_from_partial(&Partial::from_env_vars());
87
88		if let Some(ref file) = *self.file() {
89			match Partial::from_file(file) {
90				Ok(partial) => config.update_from_partial(&partial),
91				Err(err) => warn!("Could not read configuration from file: {err}"),
92			}
93		}
94
95		config.update_from_partial(&Partial::from_args());
96
97		debug!(new_config = ?config, "Configuration reloaded");
98
99		*self.inner.write() = config;
100	}
101
102	/// Generate a redirector configuration from the options defined in this
103	/// global links config.
104	#[must_use]
105	pub fn redirector(&self) -> Redirector {
106		Redirector {
107			hsts: self.hsts(),
108			send_alt_svc: self.send_alt_svc(),
109			send_server: self.send_server(),
110			send_csp: self.send_csp(),
111			statistics: self.statistics(),
112		}
113	}
114
115	/// Get the configured log level
116	#[must_use]
117	pub fn log_level(&self) -> LogLevel {
118		self.inner.read().log_level
119	}
120
121	/// Get the RPC API token
122	#[must_use]
123	pub fn token(&self) -> Arc<str> {
124		Arc::clone(&self.inner.read().token)
125	}
126
127	/// Get the list of listener addresses
128	#[must_use]
129	pub fn listeners(&self) -> Vec<ListenAddress> {
130		self.inner.read().listeners.clone()
131	}
132
133	/// Get the types of statistics to collect
134	#[must_use]
135	pub fn statistics(&self) -> StatisticCategories {
136		self.inner.read().statistics
137	}
138
139	/// Get the default TLS certificate source
140	#[must_use]
141	pub fn default_certificate(&self) -> DefaultCertificateSource {
142		self.inner.read().default_certificate.clone()
143	}
144
145	/// Get the TLS certificate configuration
146	#[must_use]
147	pub fn certificates(&self) -> Vec<CertificateSource> {
148		self.inner.read().certificates.clone()
149	}
150
151	/// Get the `hsts` configuration option
152	#[must_use]
153	pub fn hsts(&self) -> Hsts {
154		self.inner.read().hsts
155	}
156
157	/// Get the `https_redirect` configuration option
158	#[must_use]
159	pub fn https_redirect(&self) -> bool {
160		self.inner.read().https_redirect
161	}
162
163	/// Get the `send_alt_svc` configuration option
164	#[must_use]
165	pub fn send_alt_svc(&self) -> bool {
166		self.inner.read().send_alt_svc
167	}
168
169	/// Get the `send_server` configuration option
170	#[must_use]
171	pub fn send_server(&self) -> bool {
172		self.inner.read().send_server
173	}
174
175	/// Get the `send_csp` configuration option
176	#[must_use]
177	pub fn send_csp(&self) -> bool {
178		self.inner.read().send_csp
179	}
180
181	/// Get the store type
182	#[must_use]
183	pub fn store(&self) -> BackendType {
184		self.inner.read().store
185	}
186
187	/// Get the store backend configuration
188	#[must_use]
189	pub fn store_config(&self) -> HashMap<String, String> {
190		self.inner.read().store_config.clone()
191	}
192
193	/// Get the configuration file path
194	#[must_use]
195	pub const fn file(&self) -> &Option<PathBuf> {
196		&self.file
197	}
198}
199
200impl Display for Config {
201	fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
202		fmt.debug_struct("Config")
203			.field("log_level", &(self.log_level()).to_string())
204			.field(
205				"token",
206				&(self.token())
207					.chars()
208					.take(3)
209					.chain("...".chars())
210					.collect::<String>(),
211			)
212			.field("listeners", &serde_json::to_string(&self.listeners()))
213			.field("statistics", &serde_json::to_string(&self.statistics()))
214			.field("default_certificate", &self.default_certificate())
215			.field("certificates", &self.certificates())
216			.field("hsts", &self.hsts())
217			.field("https_redirect", &self.https_redirect())
218			.field("send_alt_svc", &self.send_alt_svc())
219			.field("send_server", &self.send_server())
220			.field("send_csp", &self.send_csp())
221			.field("store", &self.store())
222			.field("store_config", &self.store_config())
223			.field("file", &self.file())
224			.finish()
225	}
226}
227
228/// Actual configuration storage inside of a [`Config`]
229#[derive(Debug, PartialEq)]
230#[expect(clippy::struct_excessive_bools)]
231struct ConfigInner {
232	/// Minimum level of logs to be collected/displayed. Debug and trace levels
233	/// may expose secret information, so are not recommended for production
234	/// deployments.
235	pub log_level: LogLevel,
236	/// API token, used for authentication of gRPC clients
237	pub token: Arc<str>,
238	/// Addresses on which the links redirector server will listen on
239	pub listeners: Vec<ListenAddress>,
240	/// Which types of statistics should be collected
241	pub statistics: StatisticCategories,
242	/// Default TLS certificate source
243	pub default_certificate: DefaultCertificateSource,
244	/// TLS certificate sources
245	pub certificates: Vec<CertificateSource>,
246	/// HTTP Strict Transport Security setting on redirect
247	pub hsts: Hsts,
248	/// Redirect incoming HTTP requests to HTTPS first, before the actual
249	/// external redirect
250	pub https_redirect: bool,
251	/// Send the `Alt-Svc` header advertising `h2` (HTTP/2.0 with TLS) support
252	/// on port 443
253	pub send_alt_svc: bool,
254	/// Send the `Server` header
255	pub send_server: bool,
256	/// Send the `Content-Security-Policy` header
257	pub send_csp: bool,
258	/// The store backend type
259	pub store: BackendType,
260	/// The store backend configuration
261	pub store_config: HashMap<String, String>,
262}
263
264impl ConfigInner {
265	/// Update the config from a [`Partial`]. This overwrites all fields of this
266	/// [`Config`] from the provided [`Partial`], if they are set in that
267	/// partial config.
268	fn update_from_partial(&mut self, partial: &Partial) {
269		if let Some(log_level) = partial.log_level {
270			self.log_level = log_level;
271		}
272
273		if let Some(ref token) = partial.token {
274			self.token = Arc::from(token.as_str());
275		}
276
277		if let Some(ref listeners) = partial.listeners {
278			self.listeners.clone_from(listeners);
279		}
280
281		if let Some(statistics) = partial.statistics {
282			self.statistics = statistics;
283		}
284
285		if let Some(ref default_certificate) = partial.default_certificate {
286			self.default_certificate = default_certificate.clone();
287		}
288
289		if let Some(ref certificates) = partial.certificates {
290			self.certificates.clone_from(certificates);
291		}
292
293		if let Some(hsts) = partial.hsts() {
294			self.hsts = hsts;
295		}
296
297		if let Some(https_redirect) = partial.https_redirect {
298			self.https_redirect = https_redirect;
299		}
300
301		if let Some(send_alt_svc) = partial.send_alt_svc {
302			self.send_alt_svc = send_alt_svc;
303		}
304
305		if let Some(send_server) = partial.send_server {
306			self.send_server = send_server;
307		}
308
309		if let Some(send_csp) = partial.send_csp {
310			self.send_csp = send_csp;
311		}
312
313		if let Some(store) = partial.store {
314			self.store = store;
315		}
316
317		if let Some(ref store_config) = partial.store_config {
318			self.store_config
319				.extend(store_config.iter().map(|(k, v)| (k.clone(), v.clone())));
320		}
321	}
322}
323
324impl Default for ConfigInner {
325	fn default() -> Self {
326		Self {
327			log_level: LogLevel::default(),
328			token: rand::thread_rng()
329				.sample_iter(&Alphanumeric)
330				.take(32)
331				.map(char::from)
332				.collect::<String>()
333				.into(),
334			listeners: vec![
335				ListenAddress {
336					protocol: Protocol::Http,
337					address: None,
338					port: None,
339				},
340				ListenAddress {
341					protocol: Protocol::Https,
342					address: None,
343					port: None,
344				},
345				ListenAddress {
346					protocol: Protocol::Grpc,
347					address: Some(IpAddr::V6(Ipv6Addr::LOCALHOST)),
348					port: None,
349				},
350				ListenAddress {
351					protocol: Protocol::Grpcs,
352					address: None,
353					port: None,
354				},
355			],
356			statistics: StatisticCategories::default(),
357			https_redirect: false,
358			default_certificate: DefaultCertificateSource::None,
359			certificates: Vec::default(),
360			hsts: Hsts::default(),
361			send_alt_svc: false,
362			send_server: true,
363			send_csp: true,
364			store: BackendType::default(),
365			store_config: HashMap::with_capacity(0),
366		}
367	}
368}
369
370/// Configuration of a redirector. Can be generated from a [`Config`]. This is
371/// separate from the actual `Config`, because it shouldn't/can't change during
372/// the course of processing a redirect.
373#[derive(Copy, Clone, Debug, PartialEq, Eq)]
374pub struct Redirector {
375	/// HTTP Strict Transport Security configuration
376	pub hsts: Hsts,
377	/// Send the `Alt-Svc` header advertising `h2` (HTTP/2.0 with TLS) support
378	/// on port 443
379	pub send_alt_svc: bool,
380	/// Send the `Server` header
381	pub send_server: bool,
382	/// Send the `Content-Security-Policy` header
383	pub send_csp: bool,
384	/// The categories of statistics to collect
385	pub statistics: StatisticCategories,
386}
387
388/// HTTP Strict Transport Security configuration settings and `max-age` in
389/// seconds for the links redirector.
390///
391/// The `max-age` indicates for how long the server's HSTS setting should be
392/// saved by browsers, with 2 years (63072000 seconds) being recommended. For
393/// preloading to work, `max-age` must be at least 1 year (31536000 seconds).
394/// Setting `max-age` to 0 will clear a browser's HSTS setting for the
395/// redirection website on next request, allowing it to perform HTTP (without
396/// TLS) requests again.
397///
398/// # Caution:
399/// The `IncludeSubDomains` and `Preload` settings may have lasting unintended
400/// effects on unrelated HTTP servers (current and future) running on subdomains
401/// of the links host, and may even render those websites unusable for months or
402/// years by requiring browsers to use HTTPS (with TLS) *exclusively* when doing
403/// HTTP requests to those domains. The `Enable` setting, however, only impacts
404/// the exact domain it is used from, so should only impact the links redirector
405/// server itself. It is recommended to start testing HSTS (especially
406/// `IncludeSubDomains` and `Preload`) with a short `max-age` initially, and to
407/// test any possible impact on other websites hosted on the same domain and on
408/// its subdomains.
409///
410/// See also:
411/// - <https://hstspreload.org/>
412/// - <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>
413/// - <https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security>
414#[derive(Copy, Clone, Debug, PartialEq, Eq)]
415pub enum Hsts {
416	/// Don't send the HTTP Strict Transport Security header
417	Disable,
418	/// Send the HSTS header without the `preload` or `includeSubDomains`
419	/// attributes
420	Enable(u32),
421	/// Send the HSTS header with the `includeSubDomains` attribute, but without
422	/// `preload`
423	///
424	/// # Caution:
425	/// This may have temporary unintended effects on unrelated HTTP servers
426	/// running on subdomains of the links host. Make sure that this won't cause
427	/// any problems before enabling it and try a short max-age first.
428	/// More info on <https://hstspreload.org/>,
429	/// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>,
430	/// and <https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security>.
431	IncludeSubDomains(u32),
432	/// Send the HSTS header with the `preload` and `includeSubDomains`
433	/// attributes
434	///
435	/// # Caution:
436	/// This may have lasting unintended effects on unrelated HTTP servers
437	/// (current and future) running on subdomains of the links host, and may
438	/// even render those websites unusable for months or years.
439	///
440	/// Read <https://hstspreload.org/>,
441	/// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>,
442	/// and <https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security>, and
443	/// make sure that this won't cause any problems before enabling it.
444	Preload(u32),
445}
446
447impl Default for Hsts {
448	fn default() -> Self {
449		Self::Enable(2 * A_YEAR)
450	}
451}
452
453#[cfg(test)]
454mod tests {
455	use super::*;
456	use crate::stats::StatisticType;
457
458	#[test]
459	fn config_inner_update_from_partial_all() {
460		let mut inner = ConfigInner::default();
461		let empty_partial = Partial::default();
462		let full_partial = Partial::from_toml(include_str!("../../example-config.toml")).unwrap();
463
464		inner.update_from_partial(&empty_partial);
465
466		assert_eq!(inner, ConfigInner {
467			// This would otherwise be randomly generated and fail the test
468			token: Arc::clone(&inner.token),
469			..Default::default()
470		});
471
472		inner.update_from_partial(&full_partial);
473
474		assert_ne!(inner, ConfigInner {
475			// This would otherwise be randomly generated and fail the test
476			token: Arc::clone(&inner.token),
477			..Default::default()
478		});
479	}
480
481	#[test]
482	fn config_inner_update_from_partial_overwrite_listeners() {
483		let mut inner = ConfigInner::default();
484		let first = Partial {
485			listeners: Some(vec![ListenAddress {
486				protocol: Protocol::Http,
487				address: Some("::1".parse().unwrap()),
488				port: None,
489			}]),
490			..Default::default()
491		};
492		let second = Partial {
493			listeners: Some(vec![]),
494			..Default::default()
495		};
496
497		inner.update_from_partial(&first);
498
499		assert!(!inner.listeners.is_empty());
500
501		inner.update_from_partial(&second);
502
503		assert!(inner.listeners.is_empty());
504	}
505
506	#[test]
507	fn config_inner_update_from_partial_overwrite_statistics() {
508		let mut inner = ConfigInner::default();
509		let first = Partial {
510			statistics: Some(StatisticCategories::ALL),
511			..Default::default()
512		};
513		let second = Partial {
514			statistics: Some(StatisticCategories::NONE),
515			..Default::default()
516		};
517
518		inner.update_from_partial(&first);
519
520		assert!(inner.statistics.specifies(StatisticType::Request));
521
522		inner.update_from_partial(&second);
523
524		assert!(!inner.statistics.specifies(StatisticType::Request));
525	}
526}