feat: virtual hosting support (#252)
Diff
Makefile | 4 +--
docs/content/configuration/config-file.md | 2 +-
docs/content/features/virtual-hosting.md | 29 ++++++++++++++++++++++++-
docs/mkdocs.yml | 1 +-
src/handler.rs | 9 +++++--
src/lib.rs | 1 +-
src/settings/file.rs | 12 ++++++++++-
src/settings/mod.rs | 39 +++++++++++++++++++++++++++++++-
src/virtual_hosts.rs | 29 ++++++++++++++++++++++++-
tests/settings.rs | 28 +++++++++++++++++++++++-
tests/toml/config.toml | 10 ++++++++-
11 files changed, 158 insertions(+), 6 deletions(-)
@@ -139,7 +139,7 @@ docker.image.debian:
define build_release =
set -e
set -u
@@ -196,7 +196,7 @@ define build_release_shrink =
echo "Releases size shrinking completed!"
endef
define build_release_files =
set -e
set -u
@@ -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
@@ -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"
```
@@ -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'
@@ -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 let Some(root) = virtual_hosts::get_real_root(&advanced.virtual_hosts, headers) {
base_path = root;
}
}
let uri_path = &uri_path;
@@ -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;
@@ -90,6 +90,16 @@ pub struct Rewrites {
pub redirect: Option<RedirectsKind>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct VirtualHosts {
pub host: String,
pub root: Option<PathBuf>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
@@ -100,6 +110,8 @@ pub struct Advanced {
pub rewrites: Option<Vec<Rewrites>>,
pub redirects: Option<Vec<Redirects>>,
pub virtual_hosts: Option<Vec<VirtualHosts>>,
}
@@ -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,
}
pub struct VirtualHosts {
pub host: String,
pub root: PathBuf,
}
pub struct Advanced {
@@ -61,6 +69,8 @@ pub struct Advanced {
pub rewrites: Option<Vec<Rewrites>>,
pub redirects: Option<Vec<Redirects>>,
pub virtual_hosts: Option<Vec<VirtualHosts>>,
}
@@ -403,10 +413,37 @@ impl Settings {
_ => None,
};
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() {
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 {
@@ -0,0 +1,29 @@
use hyper::{header::HOST, HeaderMap};
use std::path::PathBuf;
use crate::settings::VirtualHosts;
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
}
@@ -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")
}
}
}
}
@@ -126,3 +126,13 @@ destination = "/assets/$1.$2"
[[advanced.rewrites]]
source = "/abc/**/*.{svg,jxl}"
destination = "/assets/favicon.ico"
[[advanced.virtual-hosts]]
host = "example.com"
root = "docker"
[[advanced.virtual-hosts]]
host = "localhost"
root = "docker/abc"