//! Handling of multiple [`Host`]s on one instance of Kvarn.
//!
//! A single [`Host`] contains the certificate, caches, and preferences
//! which are needed to run a domain.
//!
//! This also implements the logic needed for [`rustls`] to resolve which [`Host`]
//! to use for a connection. This is done by having an optional default and
//! other defined by their SNI (or `host` header in HTTP/1). If no host is matched,
//! the request is dropped, like when a [unsecure connection is sent to a secure port](Host).
use crate::prelude::{internals::*, *};
#[cfg(feature = "https")]
use rustls::{
server::{ClientHello, ResolvesServerCert},
sign, ServerConfig,
};
/// A set of settings for a [virtual host](https://en.wikipedia.org/wiki/Virtual_hosting),
/// allowing multiple DNS entries (domain names) to share a single IP address.
///
/// This is an integral part of Kvarn; the ability to host multiple
/// webpages on a single instance without crosstalk and with high performance
/// makes it a viable option.
///
/// Let's talk about the relations of [`Host::unsecure`] and [`PortDescriptor::unsecure`].
/// A host can be secure and contain a certificate (if the `https` feature is enabled).
/// A [`PortDescriptor`] can accept HTTPS or HTTP requests. [`PortDescriptor::new`] will
/// set up the descriptor to accept only HTTP requests if none of the hosts contains a certificate.
/// It accepts only HTTPS messages if any of the hosts have a certificate. Then, connections to
/// all the other hosts with no certificate are rejected.
///
/// For example, in [the reference implementation](https://github.com/Icelk/kvarn-reference),
/// I use [`PortDescriptor::unsecure`] to bind port 80 and
/// [`PortDescriptor::new`] to bind port 443. Then, all hosts are reachable on port 80,
/// but only the ones with a certificate on port 443.
///
/// # Examples
///
/// See [`RunConfig::execute()`].
#[must_use]
pub struct Host {
/// The name of the host, will be used in matching the requests [SNI hostname](rustls::server::ClientHello::server_name())
/// and `host` header to get the requested host to handle the request.
pub name: String,
/// The alternative names this host is recognized by.
/// This should probably be empty unless your certificate also covers these names.
pub alternative_names: Vec<String>,
/// The certificate of this host, if any.
#[cfg(feature = "https")]
pub certificate: Option<Arc<sign::CertifiedKey>>,
/// Base path of all data for this host.
///
/// If you enabled the `fs` feature (enabled by default),
/// the public files are in the directory `<path>/public`
/// (`public` by default; see [`Options::public_data_dir`]).
///
/// Also, all extensions should use this to access data on disk.
pub path: PathBuf,
/// The extensions of this host.
pub extensions: Extensions,
/// The file cache of this host.
///
/// The caches are separated to limit the performance fluctuations of
/// multiple hosts on the same instance.
///
/// Can be used to clear the cache and to pass to the read functions in [`read`].
pub file_cache: Option<FileCache>,
/// The response cache of this host.
/// See [`comprash`] and [`Host::file_cache`] for more info.
pub response_cache: Option<ResponseCache>,
/// The [`LimitManager`] checking for spam attacks
/// for this host.
///
/// Having this host-specific enables different virtual
/// hosts to have varying degrees of strictness.
pub limiter: LimitManager,
/// Settings for handling caching of responses with the `vary` header.
pub vary: Vary,
/// Other settings.
pub options: Options,
/// Preferences and options for compression.
pub compression_options: comprash::CompressionOptions,
// also add to debug implementation when inserting new field
}
impl Host {
/// Creates a new [`Host`].
/// Will read PEM encoded certificates in the specified locations
/// and return an non-secure host if parsing fails.
///
/// To achieve greater security, use [`Host::with_http_to_https_redirect`] and call [`Host::with_hsts`].
///
/// See [`Host::unsecure`] for a non-failing function,
/// available regardless of features.
///
/// # Errors
///
/// Will return any error from [`get_certified_key()`] with a [`Host`] containing no certificates.
#[cfg(feature = "https")]
pub fn try_read_fs(
host_name: impl AsRef<str>,
cert_path: impl AsRef<Path>,
private_key_path: impl AsRef<Path>,
path: impl AsRef<Path>,
extensions: Extensions,
options: Options,
) -> Result<Self, (CertificateError, Self)> {
let cert = get_certified_key(cert_path, private_key_path);
match cert {
Ok((cert, pk)) => Ok(Self::new(host_name, cert, pk, path, extensions, options)),
Err(err) => Err((err, Self::unsecure(host_name, path, extensions, options))),
}
}
/// Same as [`Self::try_read_fs`], but extracts the host name from the certificate.
/// This also doesn't fall back to [`Self::unsecure`], as we don't know the name if the
/// certificate couldn't be parsed.
///
/// # Panics
///
/// Panics if the parsed certificate `.is_empty()` or if the first certificate in the parsed chain is invalid.
///
/// # Errors
///
/// Will return any error from [`get_certified_key()`].
#[cfg(all(feature = "https", feature = "auto-hostname"))]
pub fn read_fs_name_from_cert(
cert_path: impl AsRef<Path>,
private_key_path: impl AsRef<Path>,
path: impl AsRef<Path>,
extensions: Extensions,
options: Options,
) -> Result<Self, CertificateError> {
let cert = get_certified_key(cert_path, private_key_path);
match cert {
Ok((cert, pk)) => Ok(Self::new_name_from_cert(
cert, pk, path, extensions, options,
)),
Err(err) => Err(err),
}
}
/// Creates a new [`Host`] from the [`rustls`]
/// `cert` and `pk`. When they are in files, consider [`Self::new`]
/// which reads from files.
///
/// See the considerations of [`Self::new`] for security.
///
/// # Examples
///
/// ```ignore
/// # use kvarn::prelude::*;
/// let certificate =
/// rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
/// let cert = vec![rustls::Certificate(certificate.serialize_der().unwrap())];
/// let pk = rustls::PrivateKey(certificate.serialize_private_key_der());
/// let pk = Arc::new(rustls::sign::any_supported_type(&pk).unwrap());
///
/// Host::new(
/// "localhost",
/// cert,
/// pk,
/// "tests",
/// Extensions::default(),
/// host::Options::default(),
/// );
/// ```
#[cfg(feature = "https")]
pub fn new(
name: impl AsRef<str>,
cert: Vec<rustls::Certificate>,
pk: Arc<dyn sign::SigningKey>,
path: impl AsRef<Path>,
extensions: Extensions,
options: Options,
) -> Self {
let cert = sign::CertifiedKey::new(cert, pk);
Self {
name: name.as_ref().to_owned(),
alternative_names: Vec::new(),
certificate: Some(Arc::new(cert)),
path: path.as_ref().to_path_buf(),
extensions,
file_cache: Some(RwLock::new(Cache::default())),
response_cache: Some(RwLock::new(Cache::default())),
options,
limiter: LimitManager::default(),
vary: Vary::default(),
compression_options: comprash::CompressionOptions::default(),
}
}
/// Same as [`Self::new`], but extracts the host name from the certificate.
///
/// # Panics
///
/// Panics if `cert.is_empty()` or if the first certificate in `cert` is invalid.
#[cfg(all(feature = "https", feature = "auto-hostname"))]
pub fn new_name_from_cert(
cert: Vec<rustls::Certificate>,
pk: Arc<dyn sign::SigningKey>,
path: impl AsRef<Path>,
extensions: Extensions,
options: Options,
) -> Self {
use x509_parser::prelude::FromDer;
let tbs = x509_parser::certificate::X509Certificate::from_der(&cert[0].0)
.expect("certificate invalid, failed to get host name")
.1
.tbs_certificate;
let name = tbs.subject().to_string();
let mut alt_names = Vec::new();
let alt_name = tbs
.subject_alternative_name()
.expect("alternative name extension of certificate invalid");
if let Some(alt_name) = alt_name {
for name in &alt_name.value.general_names {
alt_names.push(name.to_string());
}
}
let mut me = Self::new(name, cert, pk, path, extensions, options);
me.alternative_names = alt_names;
println!("Host: {}, alt: {:?}", me.name, me.alternative_names);
me
}
/// Creates a new [`Host`] without a certificate.
///
/// This host will only support non-encrypted HTTP/1 connections.
/// Consider enabling the `https` feature and use a self-signed certificate or one from [Let's Encrypt](https://letsencrypt.org/).
pub fn unsecure(
host_name: impl AsRef<str>,
path: impl AsRef<Path>,
extensions: Extensions,
options: Options,
) -> Self {
Self {
name: host_name.as_ref().to_owned(),
alternative_names: Vec::new(),
#[cfg(feature = "https")]
certificate: None,
path: path.as_ref().to_path_buf(),
extensions,
file_cache: Some(RwLock::new(Cache::default())),
response_cache: Some(RwLock::new(Cache::default())),
options,
limiter: LimitManager::default(),
vary: Vary::default(),
compression_options: comprash::CompressionOptions::default(),
}
}
/// Same as [`Host::try_read_fs`] with [`Host::with_http_to_https_redirect`].
/// This does however consider the error from [`Host::try_read_fs`] to be ok.
/// We log it as an [`log::Level::Error`]
/// and continue without encryption.
///
/// Consider running [`Host::try_read_fs`] with [`Host::with_http_to_https_redirect`]
/// and [`Host::with_hsts`] to harden the system.
#[cfg(feature = "https")]
pub fn http_redirect_or_unsecure(
host_name: &'static str,
cert_path: impl AsRef<Path>,
private_key_path: impl AsRef<Path>,
path: impl AsRef<Path>,
extensions: Extensions,
options: Options,
) -> Self {
match Host::try_read_fs(
host_name,
cert_path,
private_key_path,
path,
extensions,
options,
) {
Ok(mut host) => {
host.with_http_to_https_redirect();
host
}
Err((err, host_without_cert)) => {
error!(
"Failed to get certificate! Not running host on HTTPS. {:?}",
err
);
host_without_cert
}
}
}
/// Adds extensions to redirect unsecure HTTP requests to the secure HTTPS URI.
///
/// See [`Extensions::with_http_to_https_redirect`].
#[cfg(feature = "https")]
pub fn with_http_to_https_redirect(&mut self) -> &mut Self {
self.extensions.with_http_to_https_redirect();
self
}
/// Enables [HSTS](https://en.wikipedia.org/wiki/HSTS) on this [`Host`].
///
/// The [`Package`] extension has a priority of `8`.
///
/// You should be careful using this feature.
/// If you do not plan to have a certificate for your domain
/// for at least the following two years, take a look in the source code,
/// copy paste it and lower the `max-age`.
///
/// Also see [hstspreload.org](https://hstspreload.org/)
#[cfg(feature = "https")]
pub fn with_hsts(&mut self) -> &mut Self {
struct Ext;
impl extensions::PackageCall for Ext {
fn call<'a>(
&'a self,
response: &'a mut Response<()>,
request: &'a FatRequest,
_: &'a Host,
) -> RetFut<'a, ()> {
if request.uri().scheme_str() == Some("https") {
response
.headers_mut()
.entry("strict-transport-security")
.or_insert(HeaderValue::from_static(
"max-age=63072000; includeSubDomains; preload",
));
}
ready(())
}
}
self.extensions
.add_package(Box::new(Ext), extensions::Id::new(8, "Adding HSTS header"));
self
}
/// Add an alternative name to this host.
///
/// See [the fiels](Self::alternative_names) for more details.
pub fn add_alternative_name(&mut self, name: impl AsRef<str>) -> &mut Self {
self.alternative_names.push(name.as_ref().to_owned());
self
}
/// Disables client cache on this host.
///
/// This makes all [`comprash::ClientCachePreference`]s `no-store`.
/// Use Kvarn extensions' `force_cache` to force certain files to cache.
pub fn disable_client_cache(&mut self) -> &mut Self {
self.options.disable_client_cache();
self
}
/// Disables the file system cache for this host.
/// This can cause degraded performance under heavy load,
/// but reduces the memoy used.
pub fn disable_fs_cache(&mut self) -> &mut Self {
self.file_cache = None;
self
}
/// Disables the response cache for this host.
/// This can cause degraded performance under heavy load,
/// but reduces the memoy used.
pub fn disable_response_cache(&mut self) -> &mut Self {
self.response_cache = None;
self
}
/// Disables all server caches.
/// This can cause degraded performance under heavy load,
/// but reduces the memoy used.
///
/// Right now calls [`Self::disable_fs_cache`] and [`Self::disable_response_cache`].
pub fn disable_server_cache(&mut self) -> &mut Self {
self.disable_fs_cache().disable_response_cache()
}
/// Whether or not this this host is secured with a certificate.
///
/// See [`Host::certificate`].
#[cfg(feature = "https")]
#[inline]
pub fn is_secure(&self) -> bool {
self.certificate.is_some()
}
/// Whether or not this this host is secured with a certificate.
///
/// See [`Host::certificate`].
#[cfg(not(feature = "https"))]
#[inline]
pub(crate) fn is_secure(&self) -> bool {
false
}
/// Set the brotli compression level. 1-10, lower values are faster, but compress less.
///
/// See [some benchmarks](https://quixdb.github.io/squash-benchmark/#results) for more context.
#[cfg(feature = "br")]
#[inline]
pub fn set_brotli_level(&mut self, level: u32) -> &mut Self {
self.compression_options.brotli_level = level;
self
}
/// Set the gzip compression level. 1-10, lower values are faster, but compress less.
///
/// See [some benchmarks](https://quixdb.github.io/squash-benchmark/#results) for more context.
#[cfg(feature = "gzip")]
#[inline]
pub fn set_gzip_level(&mut self, level: u32) -> &mut Self {
self.compression_options.gzip_level = level;
self
}
}
impl Debug for Host {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct(utils::ident_str!(Host));
utils::fmt_fields!(
s,
(self.name),
(self.alternative_names),
#[cfg(feature = "https")]
(self.certificate, &"[internal certificate]".as_clean()),
(self.path),
(self.extensions, &"[internal extension data]".as_clean()),
(self.file_cache, &"[internal cache]".as_clean()),
(self.response_cache, &"[internal cache]".as_clean()),
(self.limiter),
(self.vary),
(self.options),
(self.compression_options),
);
s.finish()
}
}
/// Options for [`Host`].
/// Values wrapped in [`Option`]s usually use hardcoded defaults when the value is [`None`].
///
/// This can easily be cloned to be shared across multiple hosts.
#[derive(Debug, Clone)]
#[must_use]
pub struct Options {
// # Miscellaneous
/// Will be the default for folders; `/js/` will resolve to `/js/<folder_default>`.
/// E.g. `/posts/` -> `/posts/index.html`
///
/// If no value is passed, `index.html` is assumed.
pub folder_default: Option<String>,
/// Will be the default for unspecified file extensions; `/foobar.` will resolve to `/foobar.<extension_default>`.
/// E.g. `/index.` -> `/index.html`
///
/// If no value is passed, `html` is assumed.
pub extension_default: Option<String>,
/// Default data directory for public files.
/// Default is `public`.
pub public_data_dir: Option<PathBuf>,
/// Default directory for overriding HTTP error responses.
/// Default is `errors`.
pub errors_dir: Option<PathBuf>,
// # Cache
/// Returns `cache-control` header to be `no-store` by default, if enabled.
///
/// Useful if you have a developing site and don't want traditionally static content to be in the client cache.
pub disable_client_cache: bool,
/// Disables further caching by sending a [`StatusCode::NOT_MODIFIED`] when the
/// `if-modified-since` header is sent and the resource is fresh.
pub disable_if_modified_since: bool,
/// Filter to not cache certain [`StatusCode`]s.
///
/// See [`CacheAction`] and [`default_status_code_cache_filter`] for more info.
pub status_code_cache_filter: fn(StatusCode) -> CacheAction,
// # Extensions
/// Disables file system access for public files.
///
/// This still enables custom error messages and reading of files through extensions.
pub disable_fs: bool,
}
impl Options {
/// Creates a new [`Options`] with default settings.
///
/// All [`Option`]s are [`None`] and all booleans are `false`.
/// [`Self::status_code_cache_filter`] uses [`default_status_code_cache_filter`].
pub fn new() -> Self {
Self {
folder_default: None,
extension_default: None,
public_data_dir: None,
errors_dir: None,
disable_client_cache: false,
disable_if_modified_since: false,
status_code_cache_filter: default_status_code_cache_filter,
disable_fs: false,
}
}
/// Disables client cache on this host.
///
/// This makes all [`comprash::ClientCachePreference`]s `no-store`.
/// Use Kvarn extensions' `force_cache` to force certain files to cache.
pub fn disable_client_cache(&mut self) -> &mut Self {
self.disable_client_cache = true;
self
}
/// Disables accessing the file system for public files.
///
/// See [`Self::disable_fs`] for more info.
pub fn disable_fs(&mut self) -> &mut Self {
self.disable_fs = true;
self
}
/// Sets the directory (relative to the [`Host::path`]) to fetch data for the web in.
/// Defaults to `public`.
pub fn set_public_data_dir(&mut self, path: impl AsRef<Path>) -> &mut Self {
self.public_data_dir = Some(path.as_ref().to_path_buf());
self
}
/// Sets the directory (relative to the [`Host::path`]) to get HTTP error overrides from.
/// Defaults to `errors`.
pub fn set_errors_dir(&mut self, path: impl AsRef<Path>) -> &mut Self {
self.errors_dir = Some(path.as_ref().to_path_buf());
self
}
/// Gets the [`Self::folder_default`], as used by Kvarn.
/// Uses the default specified there.
#[must_use]
pub fn get_folder_default(&self) -> &str {
self.folder_default.as_deref().unwrap_or("index.html")
}
/// Gets the [`Self::extension_default`], as used by Kvarn.
/// Uses the default specified there.
#[must_use]
pub fn get_extension_default(&self) -> &str {
self.extension_default.as_deref().unwrap_or("html")
}
/// Gets the [`Self::public_data_dir`], as used by Kvarn.
/// Uses the default specified there.
#[must_use]
pub fn get_public_data_dir(&self) -> &Path {
self.public_data_dir
.as_deref()
.unwrap_or_else(|| Path::new("public"))
}
/// Gets the [`Self::errors_dir`], as used by Kvarn.
/// Uses the default specified there.
#[must_use]
pub fn get_errors_dir(&self) -> &Path {
self.public_data_dir
.as_deref()
.unwrap_or_else(|| Path::new("errors"))
}
}
impl Default for Options {
fn default() -> Self {
Self::new()
}
}
/// A builder of [`Collection`]. See [`Collection::builder()`].
#[derive(Debug)]
#[must_use]
pub struct CollectionBuilder(Collection);
impl CollectionBuilder {
/// Adds `host` to the builder.
/// This will match the `host` header and SNI hostname for [`Host.name`].
///
/// If it is the first call to this function, [`Self::set_pre_host_limiter`] is called
/// with [`Host::limiter`].
#[inline]
pub fn insert(mut self, host: Host) -> Self {
self.check_secure(&host);
if self.0.first.is_none() {
self.0.first = Some(host.name.clone());
self.0.pre_host_limiter = host.limiter.clone();
}
// it's important to insert the alt-names first, as they might contain the main name.
// If that happens, the inserted reference is overridden below.
for alt_name in &host.alternative_names {
self.0
.by_name
.insert(alt_name.clone(), HostValue::Ref(host.name.clone()));
}
self.0
.by_name
.insert(host.name.clone(), HostValue::Host(host));
self
}
/// Adds a default `host` which is the fallback for all requests with a requested host
/// which does not match any host added using [`Self::insert`].
///
/// > This is not needed when debugging, as the first [`Host`] to be inserted is used
/// > for requests to `localhost`.
///
/// **NOTE:** This should be used with care as all secure connections to this server
/// with a SNI hostname that is not registered in this [`Collection`], the client will get
/// a security warning, as the certificate of the default host is used.
/// This is fine with HTTP, as that only delivers the website.
///
/// # Panics
///
/// Panics if this function is called twice on the same struct.
/// You should only set the default once.
#[inline]
pub fn default(mut self, host: Host) -> Self {
assert!(
self.0.default.is_none(),
"Can not set default host multiple times."
);
self.0.default = Some(host.name.clone());
self.insert(host)
}
fn check_secure(&mut self, host: &Host) {
if host.is_secure() {
self.0.has_secure = true;
}
}
/// Sets the limiter used before any data is read. Only when the [`LimitManager`] returns
/// [`LimitAction::Drop`] will this do anything here.
#[inline]
pub fn set_pre_host_limiter(mut self, limiter: LimitManager) -> Self {
self.0.pre_host_limiter = limiter;
self
}
/// Puts the inner [`Collection`] in a [`Arc`] and returns it.
///
/// This works great with the overall flow of Kvarn. See [`RunConfig::execute()`] for an example.
#[inline]
#[must_use]
pub fn build(self) -> Arc<Collection> {
Arc::new(self.into_inner())
}
/// Converts `self` to a [`Collection`].
#[inline]
pub fn into_inner(self) -> Collection {
self.0
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)] // we want direct access to the host.
enum HostValue {
Host(Host),
Ref(String),
}
impl HostValue {
fn as_host(&self) -> Option<&Host> {
match self {
Self::Host(h) => Some(h),
Self::Ref(_) => None,
}
}
}
/// A collection of [`Host`]s, with an optional default and
/// arbitrarily many others, indexed by [`Host.name`].
///
/// Tries to route to the host with it's name.
/// If no host with a matching name is found, it'll fall back to [`default`](Self::get_default), if
/// that's [`Some`].
///
/// > When called from `localhost`, the first host added is always used.
///
/// If the feature `https` is enabled, [`rustls::server::ResolvesServerCert`] is implemented
/// using the pattern described above.
#[derive(Debug)]
#[must_use]
pub struct Collection {
default: Option<String>,
by_name: HashMap<String, HostValue>,
first: Option<String>,
has_secure: bool,
pre_host_limiter: LimitManager,
}
impl Collection {
/// Creates a new [`CollectionBuilder`] with `default_host` as the default.
#[inline]
pub fn builder() -> CollectionBuilder {
CollectionBuilder(Self {
default: None,
by_name: HashMap::new(),
first: None,
has_secure: false,
pre_host_limiter: LimitManager::default(),
})
}
/// Creates a `Host` without certification, using the directories `./public` and `./templates`.
/// The host is the default. See [`host`] for more info.
#[inline]
pub fn simple_non_secure(default_host_name: &'static str, extensions: Extensions) -> Self {
Self::builder()
.default(Host::unsecure(
default_host_name,
".",
extensions,
Options::default(),
))
.into_inner()
}
/// Returns a reference to the default [`Host`].
///
/// Use [`Self::get_from_request`] to get the appropriate host.
#[inline]
#[must_use]
pub fn get_default(&self) -> Option<&Host> {
self.default
.as_ref()
.and_then(|default| self.get_host(default))
}
/// Get a [`Host`] by name.
#[inline]
#[must_use]
pub fn get_host(&self, name: &str) -> Option<&Host> {
match self.by_name.get(name) {
Some(v) => match v {
HostValue::Host(h) => Some(h),
HostValue::Ref(r) => Some(
self.by_name
.get(r)
.and_then(HostValue::as_host)
.expect("internal error when resolving host"),
),
},
None => None,
}
}
/// Get a [`Host`] by name, and returns the [`default`](Self::get_default) if none were found.
#[inline]
#[must_use]
pub fn get_or_default(&self, name: &str) -> Option<&Host> {
self.get_host(name)
.or_else(|| name.strip_suffix('.').and_then(|name| self.get_host(name)))
.or_else(|| self.get_default())
.or_else(|| {
let base_host = name.split(':').next();
if base_host == Some("localhost")
|| base_host == Some("127.0.0.1")
|| base_host == Some("::1")
|| base_host == Some("[::1]")
{
self.first.as_ref().and_then(|host| self.get_host(host))
} else {
None
}
})
}
/// Get a [`Host`] by name, if any, and returns it or the [`default`](Self::get_default)
/// if `name` is [`None`] or [`Self::get_or_default`] returns [`None`].
#[inline]
#[must_use]
pub fn get_option_or_default(&self, name: Option<&str>) -> Option<&Host> {
match name {
Some(host) => self.get_or_default(host),
None => self.get_default(),
}
}
/// Get the host depending on [`header::HOST`] and the `sni_hostname`.
#[inline]
pub fn get_from_request<'a>(
&'a self,
request: &Request<Body>,
sni_hostname: Option<&str>,
) -> Option<&'a Host> {
fn get_header(headers: &HeaderMap) -> Option<&str> {
headers
.get(header::HOST)
.map(HeaderValue::to_str)
.and_then(Result::ok)
}
let host = sni_hostname.or_else(|| get_header(request.headers()));
self.get_option_or_default(host)
}
/// Returns if any [`Host`]s are [`Host::is_secure`].
#[inline]
#[must_use]
pub fn has_secure(&self) -> bool {
self.has_secure
}
/// Makes a [`rustls::ServerConfig`] from [`Self`].
///
/// This takes [`Self`] in an [`Arc`] and clones it.
///
/// You should not have to call this, since [`PortDescriptor::new`] and [`PortDescriptor::https`] calls it internally.
/// Though, you could use the [`host`] system by itself, without the rest of Kvarn.
#[cfg(feature = "https")]
#[inline]
#[must_use]
pub fn make_config(self: &Arc<Self>) -> ServerConfig {
let mut config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_cert_resolver(self.clone());
config.alpn_protocols = alpn();
config
}
/// Gets the `pre_host_limiter` to manage limits before any of the request is read, or even a
/// TLS session is initiated.
pub(crate) fn limiter(&self) -> &LimitManager {
&self.pre_host_limiter
}
/// Clears all response caches.
#[inline]
pub async fn clear_response_caches(&self) {
for host in self.by_name.values().filter_map(HostValue::as_host) {
if let Some(cache) = &host.response_cache {
cache.write().await.clear();
}
}
}
/// Clears a single `uri` in `host`.
/// If `host` is `""` or `"default"`, the [default](Self::get_default) host is used.
///
/// # Returns
///
/// (if host was found, cleared page)
///
/// This will probably become a error enum in the future.
///
/// It will never return (false, true).
pub async fn clear_page(&self, host: &str, uri: &Uri) -> (bool, bool) {
let key = UriKey::path_and_query(uri);
let mut found = false;
let mut cleared = false;
if host.is_empty() || host == "default" {
if let Some(cache) = self
.get_default()
.as_ref()
.and_then(|h| h.response_cache.as_ref())
{
found = true;
let mut lock = cache.write().await;
if key
.call_all(|key| lock.remove(key).into_option())
.1
.is_some()
{
cleared = true;
}
}
} else if let Some(host) = self.get_host(host) {
found = true;
if let Some(cache) = &host.response_cache {
let mut lock = cache.write().await;
if key
.call_all(|key| lock.remove(key).into_option())
.1
.is_some()
{
cleared = true;
}
}
}
(found, cleared)
}
/// Clears all file caches.
#[inline]
pub async fn clear_file_caches(&self) {
for host in self.by_name.values().filter_map(HostValue::as_host) {
if let Some(cache) = &host.file_cache {
cache.write().await.clear();
}
}
}
/// Clears a single `path` in `host`.
/// If `host` is `""` or `"default"`, the [default](Self::get_default) host is used.
///
/// # Returns
///
/// (if host was found, cleared page)
///
/// This will probably become a error enum in the future.
///
/// It will never return (false, true).
///
/// This iterates over all caches and [locks](RwLock::write) them, which takes a lot of time.
/// Though, it's not blocking.
pub async fn clear_file(&self, host: &str, path: impl AsRef<Path>) -> (bool, bool) {
let path = path.as_ref();
let mut found = false;
let mut cleared = false;
if host.is_empty() || host == "default" {
if let Some(cache) = self
.get_default()
.as_ref()
.and_then(|h| h.file_cache.as_ref())
{
found = true;
let mut lock = cache.write().await;
if lock.remove(path).into_option().is_some() {
cleared = true;
}
}
} else if let Some(host) = self.get_host(host) {
found = true;
if let Some(cache) = &host.file_cache {
let mut lock = cache.write().await;
if lock.remove(path).into_option().is_some() {
cleared = true;
}
}
}
(found, cleared)
}
}
#[cfg(feature = "https")]
impl ResolvesServerCert for Collection {
#[inline]
fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<sign::CertifiedKey>> {
// Mostly returns true, since we have a default
// Will however return false if certificate is not present
// in found host or default host.
self.get_option_or_default(client_hello.server_name())
.and_then(|host| host.certificate.as_ref())
.cloned()
}
}
/// All the supported ALPN protocols.
///
/// > ***Note:** this is often not needed, as the ALPN protocols
/// are set in [`host::Collection::make_config()`].*
#[must_use]
pub fn alpn() -> Vec<Vec<u8>> {
let vec = vec![
#[cfg(feature = "http2")]
b"h2".to_vec(),
b"http/1.1".to_vec(),
];
vec
}
/// Per host filter output of whether or not to cache a response with some [`StatusCode`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub enum CacheAction {
/// Cache this status code response.
Cache,
/// Don't cache, in hope following responses have status codes which can be cached.
Drop,
}
impl CacheAction {
/// Returns [`Self::Cache`] if `cache` is true. Else [`Self::Drop`].
pub fn from_cache(cache: bool) -> Self {
if cache {
Self::Cache
} else {
Self::Drop
}
}
/// Returns [`Self::Drop`] if `drop` is true. Else [`Self::Cache`].
pub fn from_drop(drop: bool) -> Self {
Self::from_cache(!drop)
}
/// Returns true if `self` is [`Self::Cache`].
#[must_use]
pub fn into_cache(self) -> bool {
matches!(self, Self::Cache)
}
/// Returns true if `self` is [`Self::Drop`].
#[must_use]
pub fn into_drop(self) -> bool {
matches!(self, Self::Drop)
}
}
/// This is the default for [`Options::status_code_cache_filter`].
///
/// This caches the request on every [`StatusCode`] except
/// - client errors, however `404 Not Found` and `410 Gone` are still cached
/// - any [informational response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#information_responses)
/// - [304 Not Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304)
pub fn default_status_code_cache_filter(code: StatusCode) -> CacheAction {
CacheAction::from_drop(matches!(code.as_u16(), 400..=403 | 405..=409 | 411..=499|100..=199|304))
}
/// An error regarding creation of a [`rustls::sign::CertifiedKey`].
#[cfg(feature = "https")]
#[derive(Debug)]
pub enum CertificateError {
/// An error occurred while reading from the fs.
Io(io::Error),
/// The private key is of improper format.
ImproperPrivateKeyFormat,
/// THe certificate (public key) is of improper format.
ImproperCertificateFormat,
/// No key was found.
NoKey,
/// The private key doesn't match the public key.
InvalidPrivateKey,
}
#[cfg(feature = "https")]
impl From<io::Error> for CertificateError {
#[inline]
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
/// A pair of [`rustls::Certificate`] and [`sign::SigningKey`].
///
/// Returned from [`get_certified_key`].
#[cfg(feature = "https")]
pub type CertKeyPair = (Vec<rustls::Certificate>, Arc<dyn sign::SigningKey>);
/// Extracts a [`sign::CertifiedKey`] from `cert_path` and `private_key_path`.
///
/// # Errors
///
/// Will return any errors while reading the files, or any parsing errors.
#[cfg(feature = "https")]
pub fn get_certified_key(
cert_path: impl AsRef<Path>,
private_key_path: impl AsRef<Path>,
) -> Result<CertKeyPair, CertificateError> {
let mut chain = io::BufReader::new(std::fs::File::open(&cert_path)?);
let mut private_key = io::BufReader::new(std::fs::File::open(&private_key_path)?);
let mut private_keys = Vec::with_capacity(4);
private_keys.extend(match rustls_pemfile::pkcs8_private_keys(&mut private_key) {
Ok(key) => key,
Err(_) => return Err(CertificateError::ImproperPrivateKeyFormat),
});
if private_keys.is_empty() {
private_keys.extend(match rustls_pemfile::rsa_private_keys(&mut private_key) {
Ok(key) => key,
Err(_) => return Err(CertificateError::ImproperPrivateKeyFormat),
});
}
let key = match private_keys.into_iter().next() {
Some(key) => rustls::PrivateKey(key),
None => return Err(CertificateError::NoKey),
};
let key = sign::any_supported_type(&key).map_err(|_| CertificateError::InvalidPrivateKey)?;
let chain = match rustls_pemfile::certs(&mut chain) {
Ok(cert) => cert,
Err(_) => return Err(CertificateError::ImproperCertificateFormat),
};
let chain = chain.into_iter().map(rustls::Certificate).collect();
Ok((chain, key))
}