1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use crate::config::{CustomExtensions, ExtensionBundles, Result};
5use kvarn::prelude::ToCompactString;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Serialize, Deserialize, Default, Clone)]
9#[serde(deny_unknown_fields)]
10pub struct Limiter {
11 max_requests_per_interval: usize,
12 interval: f64,
13 check_one_in_n_requests: usize,
14}
15#[derive(Debug, Serialize, Deserialize, Clone)]
16pub enum Limit {
17 Limit(Limiter),
18 AllowAll,
19}
20
21#[derive(Debug, Serialize, Deserialize, Default, Clone)]
22#[serde(deny_unknown_fields)]
23pub struct HostOptions {
24 disable_fs: Option<bool>,
25 disable_client_cache: Option<bool>,
26 disable_server_cache: Option<bool>,
27 disable_response_cache: Option<bool>,
28 disable_fs_cache: Option<bool>,
29 hsts: Option<bool>,
30 brotli_level: Option<u32>,
31 gzip_level: Option<u32>,
32 zstd_level: Option<i32>,
33 brotli_oneshot_level: Option<u32>,
34 gzip_oneshot_level: Option<u32>,
35 zstd_oneshot_level: Option<i32>,
36 folder_default: Option<String>,
37 extension_default: Option<String>,
38 public_data_directory: Option<String>,
39 alternative_names: Option<Vec<String>>,
40 limiter: Option<Limit>,
41}
42impl HostOptions {
43 fn resolve(self) -> kvarn::host::Options {
44 let mut options = kvarn::host::Options::new();
45
46 if let Some(b) = self.disable_fs {
47 options.disable_fs = b;
48 }
49 if let Some(b) = self.disable_client_cache {
50 options.disable_client_cache = b;
51 }
52 if let Some(d) = self.folder_default {
53 options.folder_default = Some(d.to_compact_string());
54 }
55 if let Some(d) = self.extension_default {
56 options.extension_default = Some(d.to_compact_string());
57 }
58 if let Some(d) = self.public_data_directory {
59 options.public_data_dir = Some(d.into());
60 }
61
62 options
63 }
64}
65#[derive(Debug, Serialize, Deserialize, Clone)]
66#[serde(deny_unknown_fields)]
67pub enum SearchEngineKind {
68 Simple,
69 Lossless,
70}
71#[derive(Debug, Serialize, Deserialize, Clone)]
72#[serde(deny_unknown_fields)]
73pub enum SearchEngineIgnoreExtensions {
74 ExtendDefaults(Vec<String>),
75 Only(Vec<String>),
76}
77#[derive(Debug, Serialize, Deserialize, Clone)]
78#[serde(deny_unknown_fields)]
79pub struct SearchEngineAddon {
80 api_route: String,
81 kind: SearchEngineKind,
82 response_hits_limit: Option<u32>,
83 query_max_length: Option<u32>,
84 query_max_terms: Option<u32>,
85 additional_paths: Option<Vec<String>>,
86 ignore_paths: Option<Vec<String>>,
87 ignore_extensions: Option<SearchEngineIgnoreExtensions>,
88 index_wordpress_sitemap: Option<bool>,
89}
90#[derive(Debug, Serialize, Deserialize, Clone)]
91#[serde(deny_unknown_fields)]
92pub struct AutomaticCertificate {
93 contact: Option<String>,
94 account_path: Option<String>,
95 force_renew_on_start: Option<bool>,
96}
97#[derive(Debug, Serialize, Deserialize, Clone)]
98#[serde(deny_unknown_fields)]
99pub enum HostAddon {
100 SearchEngine(SearchEngineAddon),
101 AutomaticCertificate(AutomaticCertificate),
102}
103
104#[derive(Debug, Serialize, Deserialize, Clone)]
105#[serde(deny_unknown_fields)]
106pub enum Host {
107 Plain {
108 cert: String,
109 pk: String,
110 path: String,
111 auto_cert: Option<bool>,
112 name: Option<String>,
113 extensions: Vec<String>,
114 options: Option<HostOptions>,
115 addons: Option<Vec<HostAddon>>,
116 },
117 TryCertificatesOrUnencrypted {
118 name: String,
119 cert: String,
120 pk: String,
121 path: String,
122 auto_cert: Option<bool>,
123 extensions: Vec<String>,
124 options: Option<HostOptions>,
125 addons: Option<Vec<HostAddon>>,
126 },
127 Http {
128 name: String,
129 path: String,
130 extensions: Vec<String>,
131 options: Option<HostOptions>,
132 addons: Option<Vec<HostAddon>>,
133 },
134}
135impl Host {
136 async fn resolve_extensions(
137 selected: &[String],
138 extensions: &ExtensionBundles,
139 host: &kvarn::host::Host,
140 custom_exts: &CustomExtensions,
141 has_auto_cert: bool,
142 ) -> Result<kvarn::Extensions> {
143 let mut exts = selected.iter();
144 if let Some(first) = exts.next() {
145 let ext = extensions
146 .get(first.as_str())
147 .ok_or_else(|| format!("Didn't find an extension bundle with name {first}"))?;
148 let mut main = crate::extension::build_extensions(
149 ext.0.clone(),
150 host,
151 custom_exts,
152 Path::new(&ext.1)
153 .parent()
154 .expect("config file is in no directory"),
155 has_auto_cert,
156 )
157 .await?;
158 for ext in exts {
159 let ext = extensions
160 .get(ext.as_str())
161 .ok_or_else(|| format!("Didn't find an extension bundle with name {ext}"))?;
162 main = crate::extension::build_extensions_inherit(
163 ext.0.clone(),
164 main,
165 host,
166 custom_exts,
167 Path::new(&ext.1)
168 .parent()
169 .expect("config file is in no directory"),
170 )
171 .await?;
172 }
173 Ok(main)
174 } else {
175 Ok(kvarn::Extensions::new())
176 }
177 }
178 #[allow(clippy::too_many_arguments)]
180 async fn assemble(
181 mut host: kvarn::host::Host,
182 cert_path: Option<(PathBuf, PathBuf)>,
183 exts: Vec<String>,
184 ext_bundles: &ExtensionBundles,
185 options: HostOptions,
186 addons: Vec<HostAddon>,
187 custom_exts: &CustomExtensions,
188 execute_extensions_addons: bool,
189 config_dir: &Path,
190 root_config_dir: &Path,
191 has_auto_cert: bool,
192 dev: bool,
193 ) -> Result<CloneableHost> {
194 let opts_clone = options.clone();
195 if let Some(true) = options.disable_fs_cache {
196 host.disable_fs_cache();
197 }
198 if let Some(true) = options.disable_response_cache {
199 host.disable_response_cache();
200 }
201 if let Some(true) = options.disable_server_cache {
202 host.disable_server_cache();
203 }
204 if let Some(level) = options.brotli_level {
205 if !(1..=10).contains(&level) {
206 return Err("Brotli level has to be in the range 1..=10".into());
207 }
208 host.set_brotli_level(level);
209 }
210 if let Some(level) = options.gzip_level {
211 if !(1..=10).contains(&level) {
212 return Err("GZIP level has to be in the range 1..=10".into());
213 }
214 host.set_gzip_level(level);
215 }
216 if let Some(level) = options.zstd_level {
217 if !(1..=22).contains(&level) {
218 return Err("Zstd level has to be in the range 1..=10".into());
219 }
220 if (20..=22).contains(&level) {
221 log::warn!("Using a very high compression for zstd. This is not recommended.");
222 }
223 host.set_zstd_level(level);
224 }
225 if let Some(level) = options.brotli_oneshot_level {
226 if !(1..=10).contains(&level) {
227 return Err("Brotli level has to be in the range 1..=10".into());
228 }
229 host.set_brotli_level_oneshot(level);
230 }
231 if let Some(level) = options.gzip_oneshot_level {
232 if !(1..=10).contains(&level) {
233 return Err("GZIP level has to be in the range 1..=10".into());
234 }
235 host.set_gzip_level_oneshot(level);
236 }
237 if let Some(level) = options.zstd_oneshot_level {
238 if !(1..=22).contains(&level) {
239 return Err("Zstd level has to be in the range 1..=10".into());
240 }
241 if (20..=22).contains(&level) {
242 log::warn!("Using a very high compression for zstd. This is not recommended.");
243 }
244 host.set_zstd_level_oneshot(level);
245 }
246 if let Some(alts) = options.alternative_names {
247 for alt in alts {
248 host.add_alternative_name(alt);
249 }
250 }
251 if let Some(limiter) = &options.limiter {
252 match limiter {
253 Limit::Limit(opts) => {
254 host.limiter = kvarn::limiting::Manager::new(
255 opts.max_requests_per_interval,
256 opts.check_one_in_n_requests,
257 opts.interval,
258 );
259 }
260 Limit::AllowAll => {
261 host.limiter.disable();
262 }
263 }
264 }
265
266 let mut se_handles = Vec::new();
267 let mut cert_collection_senders = Vec::new();
268
269 if execute_extensions_addons {
271 let mut extensions =
272 Self::resolve_extensions(&exts, ext_bundles, &host, custom_exts, has_auto_cert)
273 .await?;
274 for addon in &addons {
275 match addon {
276 HostAddon::SearchEngine(config) => {
277 let mut opts = kvarn_search::Options::new();
278 opts.kind = match config.kind {
279 SearchEngineKind::Simple => kvarn_search::IndexKind::Simple,
280 SearchEngineKind::Lossless => kvarn_search::IndexKind::Lossless,
281 };
282 if let Some(i) = config.response_hits_limit {
283 opts.response_hits_limit = i as _;
284 }
285 if let Some(i) = config.query_max_length {
286 opts.query_max_length = i as _;
287 }
288 if let Some(i) = config.query_max_terms {
289 opts.query_max_terms = i as _;
290 }
291 if let Some(b) = config.index_wordpress_sitemap {
292 opts.index_wordpress_sitemap = b;
293 }
294 if let Some(ignored) = &config.ignore_paths {
295 let mut v = Vec::with_capacity(ignored.len());
296 for ignored in ignored {
297 match http::Uri::try_from(ignored) {
298 Ok(uri) => v.push(uri),
299 Err(err) => {
300 return Err(format!(
301 "Failed to parse ignored path (search engine): {err}"
302 ))
303 }
304 }
305 }
306 opts.ignore_paths = v;
307 }
308 match &config.ignore_extensions {
309 Some(SearchEngineIgnoreExtensions::Only(v)) => {
310 opts.ignore_extensions.clone_from(v);
311 }
312 Some(SearchEngineIgnoreExtensions::ExtendDefaults(v)) => {
313 opts.ignore_extensions.extend_from_slice(v);
314 }
315 None => {}
316 }
317 if let Some(v) = &config.additional_paths {
318 let mut paths = Vec::with_capacity(v.len());
319 for path in v {
320 let path = http::Uri::from_maybe_shared(
321 kvarn::prelude::Bytes::from(path.as_bytes().to_vec()),
322 )
323 .map_err(|err| {
324 format!(
325 "Invalid path given to search engine addisional_paths: {err:?}"
326 )
327 })?;
328 paths.push(path);
329 }
330 opts.additional_paths = paths;
331 }
332
333 let handle = kvarn_search::mount_search(
334 &mut extensions,
335 config.api_route.clone(),
336 opts,
337 )
338 .await;
339 se_handles.push(handle);
340 }
341 HostAddon::AutomaticCertificate(config) => {
342 struct CachedRx<T> {
343 rx: Option<tokio::sync::oneshot::Receiver<T>>,
344 t: Option<T>,
345 }
346 impl<T> CachedRx<T> {
347 fn new(rx: tokio::sync::oneshot::Receiver<T>) -> Self {
348 Self {
349 rx: Some(rx),
350 t: None,
351 }
352 }
353 async fn rx(&mut self) -> &T {
354 if let Some(ref t) = self.t {
355 t
356 } else {
357 let rx = self.rx.take().unwrap();
358 let t = rx.await.unwrap();
359 self.t.insert(t)
360 }
361 }
362 }
363
364 let email = config
365 .contact
366 .as_ref()
367 .map(|contact| {
368 if let Some(mail) = contact.strip_prefix("mailto:") {
369 Ok(mail)
370 } else {
371 Err(format!(
372 "AutomaticCertificate contact needs to be in the format \
373 `mailto:you@example.org`. You provided `{}`.",
374 contact
375 ))
376 }
377 })
378 .transpose()?;
379 let creds = config.account_path.clone().unwrap_or_else(|| {
380 if let Some(email) = email {
381 format!("lets-encrypt-credentials-{email}.ron")
382 } else {
383 "lets-encrypt-credentials.ron".into()
384 }
385 });
386
387 let Some((cert_path, pk_path)) = &cert_path else {
388 return Err(
389 "You cannot use `AutomaticCertificate` on an HTTP-only host!"
390 .to_owned(),
391 );
392 };
393 let has_cert = { host.certificate.read().unwrap().is_some() };
394 let (tx, rx) = tokio::sync::oneshot::channel();
395
396 cert_collection_senders.push(tx);
397 let host_name = host.name.clone();
398 let rx = Arc::new(tokio::sync::Mutex::new(CachedRx::new(rx)));
399
400 kvarn_extensions::certificate::mount(
401 move |key| {
402 let rx = rx.clone();
403 let host_name = host_name.clone();
404 async move {
405 let mut rx = rx.lock().await;
406 let collection: &Arc<kvarn::host::Collection> = rx.rx().await;
407 let host = collection
408 .get_host(&host_name)
409 .expect("we were created with a host of this name");
410
411 log::info!("Set automatic cert on {}!", host.name);
412 host.live_set_certificate(key);
413 }
414 },
415 &host,
416 &mut extensions,
417 !has_cert || config.force_renew_on_start.unwrap_or(false),
418 config.contact.clone(),
419 root_config_dir.join(creds),
420 cert_path,
421 pk_path,
422 dev,
423 )
424 .await;
425 }
426 }
427 }
428 host.extensions = extensions;
429 if let Some(true) = options.hsts {
430 host.with_hsts();
431 }
432 for handle in &se_handles {
433 handle.index_all(&host).await;
434 }
435 }
436
437 Ok(CloneableHost {
438 host,
439 exts,
440 options: opts_clone,
441 addons,
442 cert_path,
443
444 config_dir: config_dir.to_path_buf(),
445 root_config_dir: root_config_dir.to_path_buf(),
446
447 search_engine_handles: se_handles,
448 cert_collection_senders,
449 has_auto_cert,
450 })
451 }
452 fn add_auto_cert(
453 addons: Option<Vec<HostAddon>>,
454 auto_cert: Option<bool>,
455 ) -> (bool, Vec<HostAddon>) {
456 let mut addons = addons.unwrap_or_default();
457
458 let mut contains = addons
459 .iter()
460 .any(|i| matches!(i, HostAddon::AutomaticCertificate(_)));
461
462 if auto_cert.unwrap_or(false) && !contains {
463 addons.push(HostAddon::AutomaticCertificate(AutomaticCertificate {
464 contact: None,
465 account_path: None,
466 force_renew_on_start: None,
467 }));
468 contains = true;
469 }
470 (contains, addons)
471 }
472 pub async fn resolve(
473 self,
474 ext_bundles: &ExtensionBundles,
475 custom_exts: &CustomExtensions,
476 config_dir: &Path,
477 root_config_dir: &Path,
478 dev: bool,
479 ) -> Result<CloneableHost> {
480 match self {
481 Host::Plain {
482 cert,
483 pk,
484 path,
485 name: name_override,
486 auto_cert,
487 extensions,
488 options,
489 addons,
490 } => {
491 let (contains_auto_cert, addons) = Self::add_auto_cert(addons, auto_cert);
492 let options = options.unwrap_or_default();
493 let opts = options.clone().resolve();
494 let cert_path = config_dir.join(cert);
495 let pk_path = config_dir.join(pk);
496 let host = match (name_override, contains_auto_cert) {
497 (Some(name), false) => kvarn::host::Host::try_read_fs(
498 name,
499 cert_path.to_string_lossy(),
500 pk_path.to_string_lossy(),
501 config_dir.join(path).to_string_lossy(),
502 kvarn::Extensions::empty(),
503 opts,
504 )
505 .map_err(|(err, _)| {
506 format!(
507 "Failed when reading certificate \
508 ({cert_path:?})/private key ({pk_path:?}): {err:?}"
509 )
510 })?,
511 (None, false) => kvarn::host::Host::read_fs_name_from_cert(
512 cert_path.to_string_lossy(),
513 pk_path.to_string_lossy(),
514 config_dir.join(path).to_string_lossy(),
515 kvarn::Extensions::empty(),
516 opts,
517 )
518 .map_err(|err| {
519 format!(
520 "Failed when reading certificate \
521 ({cert_path:?})/private key ({pk_path:?}): {err:?}"
522 )
523 })?,
524 (Some(name), true) => kvarn::host::Host::try_read_fs(
525 name,
526 cert_path.to_string_lossy(),
527 pk_path.to_string_lossy(),
528 config_dir.join(path).to_string_lossy(),
529 kvarn::Extensions::empty(),
530 opts,
531 )
532 .unwrap_or_else(|(_, host)| host),
533 (None, true) => {
534 return Err("Tried to create secure host \
535 with automatic certificates but without a domain name. \
536 We can't know which domain it is!"
537 .into());
538 }
539 };
540 Self::assemble(
541 host,
542 Some((cert_path, pk_path)),
543 extensions,
544 ext_bundles,
545 options,
546 addons,
547 custom_exts,
548 false,
549 config_dir,
550 root_config_dir,
551 contains_auto_cert,
552 dev,
553 )
554 .await
555 }
556 Host::TryCertificatesOrUnencrypted {
557 name,
558 cert,
559 pk,
560 path,
561 auto_cert,
562 extensions,
563 options,
564 addons,
565 } => {
566 let (contains_auto_cert, addons) = Self::add_auto_cert(addons, auto_cert);
567 let cert_path = config_dir.join(cert);
568 let pk_path = config_dir.join(pk);
569 let options = options.unwrap_or_default();
570 let opts = options.clone().resolve();
571
572 let host = kvarn::host::Host::try_read_fs(
573 name,
574 cert_path.to_string_lossy(),
575 pk_path.to_string_lossy(),
576 config_dir.join(path).to_string_lossy(),
577 kvarn::Extensions::empty(),
578 opts,
579 );
580 let host = if contains_auto_cert {
581 host.unwrap_or_else(|(_, host)| host)
582 } else {
583 host
584 .unwrap_or_else(|(err, host)| {
585 log::error!("Failed when reading certificate ({cert_path:?})/private key ({pk_path:?}): {err:?}");
586 host
587 })
588 };
589 Self::assemble(
590 host,
591 Some((cert_path, pk_path)),
592 extensions,
593 ext_bundles,
594 options,
595 addons,
596 custom_exts,
597 false,
598 config_dir,
599 root_config_dir,
600 contains_auto_cert,
601 dev,
602 )
603 .await
604 }
605 Host::Http {
606 name,
607 path,
608 extensions,
609 options,
610 addons,
611 } => {
612 let options = options.unwrap_or_default();
613 let opts = options.clone().resolve();
614 let host = kvarn::host::Host::unsecure(
615 name,
616 config_dir.join(path).to_string_lossy(),
617 kvarn::Extensions::empty(),
618 opts,
619 );
620 Self::assemble(
621 host,
622 None,
623 extensions,
624 ext_bundles,
625 options,
626 addons.unwrap_or_default(),
627 custom_exts,
628 false,
629 config_dir,
630 root_config_dir,
631 false,
632 dev,
633 )
634 .await
635 }
636 }
637 }
638}
639
640pub struct CloneableHost {
641 pub host: kvarn::host::Host,
642 pub exts: Vec<String>,
643 pub options: HostOptions,
644 pub addons: Vec<HostAddon>,
645
646 pub cert_path: Option<(PathBuf, PathBuf)>,
647
648 pub config_dir: PathBuf,
649 pub root_config_dir: PathBuf,
650
651 pub search_engine_handles: Vec<kvarn_search::SearchEngineHandle>,
653 pub cert_collection_senders: Vec<tokio::sync::oneshot::Sender<Arc<kvarn::host::Collection>>>,
654 pub has_auto_cert: bool,
655}
656impl CloneableHost {
657 pub async fn clone_with_extensions(
658 &self,
659 exts: &ExtensionBundles,
660 custom_exts: &CustomExtensions,
661 execute_extensions_addons: bool,
662 dev: bool,
663 ) -> Result<Self> {
664 Host::assemble(
665 self.host.clone_without_extensions(),
666 self.cert_path.clone(),
667 self.exts.clone(),
668 exts,
669 self.options.clone(),
670 self.addons.clone(),
671 custom_exts,
672 execute_extensions_addons,
673 &self.config_dir,
674 &self.root_config_dir,
675 self.has_auto_cert,
676 dev,
677 )
678 .await
679 }
680}