Repository: static-web-server/static-web-server (language: rust)

Issue:
Add support for virtual hosting
Please consider adding support for [Virtual Hosting](https://en.wikipedia.org/wiki/Virtual_hosting#Name-based). From configuration point of view it could be as simple as a flag enabling this feature that would imply that the value of the `Host` http header would be appended to `SERVER_ROOT`.


Discussion / hints:
> this feature that would imply that the value of the `Host` http header would be appended to `SERVER_ROOT`.

It could be but I think that maybe we should give this feature a [TOML config](https://sws.joseluisq.net/configuration/config-file/#toml-file-manifest) place under the `[advanced]` section to let users use custom root dirs per domain instead of just append whatever comes in the `Host` value.

Internally we could keep a list of domains/root dir pairs and try to match the domain value(s) against the incoming `Host` header value.

Fix patch:
diff --git a/Makefile b/Makefile
index 1334cb83..c2862be6 100644
--- a/Makefile
+++ b/Makefile
@@ -139,7 +139,7 @@ docker.image.debian:
 ########## Production tasks ###########
 #######################################
 
-# Compile release binary 
+# Compile release binary
 define build_release =
 	set -e
 	set -u
@@ -196,7 +196,7 @@ define build_release_shrink =
 	echo "Releases size shrinking completed!"
 endef
 
-# Creates release files (tarballs, zipballs) 
+# Creates release files (tarballs, zipballs)
 define build_release_files =
 	set -e
 	set -u
diff --git a/docs/content/configuration/config-file.md b/docs/content/configuration/config-file.md
index 7f1ca566..bf06dc38 100644
--- a/docs/content/configuration/config-file.md
+++ b/docs/content/configuration/config-file.md
@@ -143,7 +143,7 @@ So they are equivalent to each other **except** for the `-w, --config-file` opti
 
 The TOML `[advanced]` section is intended for more complex features.
 
-For example [Custom HTTP Headers](../features/custom-http-headers.md) or [Custom URL Redirects](../features/url-redirects.md).
+For example [Custom HTTP Headers](../features/custom-http-headers.md), [Custom URL Redirects](../features/url-redirects.md), [URL Rewrites](../features/url-rewrites.md), or [Virtual Hosting](../features/virtual-hosting.md)
 
 ### Precedence
 
diff --git a/docs/content/features/virtual-hosting.md b/docs/content/features/virtual-hosting.md
new file mode 100644
index 00000000..ba9efe72
--- /dev/null
+++ b/docs/content/features/virtual-hosting.md
@@ -0,0 +1,29 @@
+# Virtual Hosting
+
+**SWS** provides rudimentary support for name-based [virtual hosting](https://en.wikipedia.org/wiki/Virtual_hosting#Name-based). This allows you to serve files from different root directories depending on the ["Host" header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/host) of the request, with all other settings staying the same.
+
+!!! warning "All other settings are the same!"
+    Each virtual host has to have all the same settings (aside from `root`). If using TLS, your certificates will have to cover all virtual host names as Subject Alternative Names (SANs). Also, beware of other conflicting settings like redirects and rewrites. If you find yourself needing different settings for different virtual hosts, it is recommended to run multiple instances of SWS.
+
+Virtual hosting can be useful for serving more than one static website from the same SWS instance, if it's not otherwise feasible to run multiple instances of SWS. Browsers will automatically send a `Host` header which matches the hostname in the URL bar, which is how HTTP servers are able to tell which "virtual" host that the client is accessing.
+
+By default, SWS will always serve files from the main `root` directory. If you configure virtual hosting and the "Host" header matches, SWS will instead look for files in an alternate root directory you specify.
+
+## Examples
+
+```toml
+# By default, all requests are served from here
+root = "/var/www/html"
+
+[advanced]
+
+[[advanced.virtual-hosts]]
+# But if the "Host" header matches this...
+host = "sales.example.com"
+# ...then files will be served from here instead
+root = "/var/sales/html"
+
+[[advanced.virtual-hosts]]
+host = "blog.example.com"
+root = "/var/blog/html"
+```
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index c60149ec..5c98861a 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -162,6 +162,7 @@ nav:
     - 'Trailing Slash Redirect': 'features/trailing-slash-redirect.md'
     - 'Ignore Files': 'features/ignore-files.md'
     - 'Health endpoint': 'features/health-endpoint.md'
+    - 'Virtual Hosting': 'features/virtual-hosting.md'
   - 'Platforms & Architectures': 'platforms-architectures.md'
   - 'Migrating from v1 to v2': 'migration.md'
   - 'Changelog v2 (stable)': 'https://github.com/static-web-server/static-web-server/blob/master/CHANGELOG.md'
diff --git a/src/handler.rs b/src/handler.rs
index 74482567..9add7e4e 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -25,7 +25,7 @@ use crate::{
     redirects, rewrites, security_headers,
     settings::{file::RedirectsKind, Advanced},
     static_files::{self, HandleOpts},
-    Error, Result,
+    virtual_hosts, Error, Result,
 };
 
 #[cfg(feature = "directory-listing")]
@@ -100,7 +100,7 @@ impl RequestHandler {
         let headers = req.headers();
         let uri = req.uri();
 
-        let base_path = &self.opts.root_dir;
+        let mut base_path = &self.opts.root_dir;
         let mut uri_path = uri.path().to_owned();
         let uri_query = uri.query();
         #[cfg(feature = "directory-listing")]
@@ -347,6 +347,11 @@ impl RequestHandler {
                         return Ok(resp);
                     }
                 }
+
+                // If the "Host" header matches any virtual_host, change the root dir
+                if let Some(root) = virtual_hosts::get_real_root(&advanced.virtual_hosts, headers) {
+                    base_path = root;
+                }
             }
 
             let uri_path = &uri_path;
diff --git a/src/lib.rs b/src/lib.rs
index d0bd3473..5c20b201 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -146,6 +146,7 @@ pub mod static_files;
 #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
 pub mod tls;
 pub mod transport;
+pub mod virtual_hosts;
 #[cfg(windows)]
 #[cfg_attr(docsrs, doc(cfg(windows)))]
 pub mod winservice;
diff --git a/src/settings/file.rs b/src/settings/file.rs
index a199f2c2..cb2dd4ed 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -90,6 +90,16 @@ pub struct Rewrites {
     pub redirect: Option<RedirectsKind>,
 }
 
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(rename_all = "kebab-case")]
+/// Represents virtual hosts with different root directories
+pub struct VirtualHosts {
+    /// The value to check for in the "Host" header
+    pub host: String,
+    /// The root directory for this virtual host
+    pub root: Option<PathBuf>,
+}
+
 /// Advanced server options only available in configuration file mode.
 #[derive(Debug, Serialize, Deserialize, Clone)]
 #[serde(rename_all = "kebab-case")]
@@ -100,6 +110,8 @@ pub struct Advanced {
     pub rewrites: Option<Vec<Rewrites>>,
     /// Redirects
     pub redirects: Option<Vec<Redirects>>,
+    /// Name-based virtual hosting
+    pub virtual_hosts: Option<Vec<VirtualHosts>>,
 }
 
 /// General server options available in configuration file mode.
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 470e0b58..0429bee8 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -13,7 +13,7 @@ use hyper::StatusCode;
 use regex::Regex;
 use std::path::PathBuf;
 
-use crate::{logger, Context, Result};
+use crate::{helpers, logger, Context, Result};
 
 pub mod cli;
 pub mod file;
@@ -53,6 +53,14 @@ pub struct Redirects {
     pub kind: StatusCode,
 }
 
+/// The `VirtualHosts` file options.
+pub struct VirtualHosts {
+    /// The value to check for in the "Host" header
+    pub host: String,
+    /// The root directory for this virtual host
+    pub root: PathBuf,
+}
+
 /// The `advanced` file options.
 pub struct Advanced {
     /// Headers list.
@@ -61,6 +69,8 @@ pub struct Advanced {
     pub rewrites: Option<Vec<Rewrites>>,
     /// Redirects list.
     pub redirects: Option<Vec<Redirects>>,
+    /// Name-based virtual hosting
+    pub virtual_hosts: Option<Vec<VirtualHosts>>,
 }
 
 /// The full server CLI and File options.
@@ -403,10 +413,37 @@ impl Settings {
                     _ => None,
                 };
 
+                // 3. Virtual hosts assignment
+                let vhosts_entries = match advanced.virtual_hosts {
+                    Some(vhosts_entries) => {
+                        let mut vhosts_vec: Vec<VirtualHosts> = Vec::new();
+
+                        for vhosts_entry in vhosts_entries.iter() {
+                            if let Some(root) = vhosts_entry.root.to_owned() {
+                                // Make sure path is valid
+                                let root_dir = helpers::get_valid_dirpath(&root)
+                                    .with_context(|| "root directory for virtual host was not found or inaccessible")?;
+                                tracing::debug!(
+                                    "added virtual host: {} -> {}",
+                                    vhosts_entry.host,
+                                    root_dir.display()
+                                );
+                                vhosts_vec.push(VirtualHosts {
+                                    host: vhosts_entry.host.to_owned(),
+                                    root: root_dir,
+                                });
+                            }
+                        }
+                        Some(vhosts_vec)
+                    }
+                    _ => None,
+                };
+
                 settings_advanced = Some(Advanced {
                     headers: headers_entries,
                     rewrites: rewrites_entries,
                     redirects: redirects_entries,
+                    virtual_hosts: vhosts_entries,
                 });
             }
         } else if log_init {
diff --git a/src/virtual_hosts.rs b/src/virtual_hosts.rs
new file mode 100644
index 00000000..ef74c646
--- /dev/null
+++ b/src/virtual_hosts.rs
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+// This file is part of Static Web Server.
+// See https://static-web-server.net/ for more information
+// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
+
+//! Module that allows to rewrite request URLs with pattern matching support.
+//!
+
+use hyper::{header::HOST, HeaderMap};
+use std::path::PathBuf;
+
+use crate::settings::VirtualHosts;
+
+/// It returns different root dir if the "Host" header matches a virtual hostname.
+pub fn get_real_root<'a>(
+    vhosts_vec: &'a Option<Vec<VirtualHosts>>,
+    headers: &HeaderMap,
+) -> Option<&'a PathBuf> {
+    if let Some(vhosts) = vhosts_vec {
+        if let Ok(host_str) = headers.get(HOST)?.to_str() {
+            for vhost in vhosts {
+                if vhost.host == host_str {
+                    return Some(&vhost.root);
+                }
+            }
+        }
+    }
+    None
+}


Test patch:
diff --git a/tests/settings.rs b/tests/settings.rs
new file mode 100644
index 00000000..77d08484
--- /dev/null
+++ b/tests/settings.rs
@@ -0,0 +1,28 @@
+#![forbid(unsafe_code)]
+#![deny(warnings)]
+#![deny(rust_2018_idioms)]
+#![deny(dead_code)]
+
+#[cfg(test)]
+mod tests {
+    use static_web_server::settings::file::Settings;
+    use std::path::{Path, PathBuf};
+
+    #[tokio::test]
+    async fn toml_file_parsing() {
+        let config_path = Path::new("tests/toml/config.toml");
+        let settings = Settings::read(config_path).unwrap();
+        let root = settings.general.unwrap().root.unwrap();
+        assert_eq!(root, PathBuf::from("docker/public"));
+
+        let virtual_hosts = settings.advanced.unwrap().virtual_hosts.unwrap();
+        let expected_roots = [PathBuf::from("docker"), PathBuf::from("docker/abc")];
+        for vhost in virtual_hosts {
+            if let Some(other_root) = &vhost.root {
+                assert!(expected_roots.contains(other_root));
+            } else {
+                panic!("Could not determine value of advanced.virtual-hosts.root")
+            }
+        }
+    }
+}
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index 07964266..d7a68212 100644
--- a/tests/toml/config.toml
+++ b/tests/toml/config.toml
@@ -126,3 +126,13 @@ destination = "/assets/$1.$2"
 [[advanced.rewrites]]
 source = "/abc/**/*.{svg,jxl}"
 destination = "/assets/favicon.ico"
+
+### Name-based virtual hosting
+
+[[advanced.virtual-hosts]]
+host = "example.com"
+root = "docker"
+
+[[advanced.virtual-hosts]]
+host = "localhost"
+root = "docker/abc"