moella/
host.rs

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    /// `cert_path` have to be relative to $PWD, not the config
179    #[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        // set extensions
270        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    // addons
652    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}