Merge pull request #101 from joseluisq/feature/configration_file
feat: configuration file support
It allows to adjust the server using a settings file in TOML format via the new "-w, --config-file" CLI option and its "SERVER_CONFIG_FILE" env.
This feature fundamentally provides two main option groups: "general" and "advanced" options.
Diff
Cargo.lock | 102 ++++++++++-
Cargo.toml | 7 +-
LICENSE-MIT | 2 +-
README.md | 6 +-
docs/content/configuration/command-line-arguments.md | 7 +-
docs/content/configuration/config-file.md | 92 +++++++++-
docs/content/configuration/environment-variables.md | 3 +-
docs/content/features/custom-http-headers.md | 66 ++++++-
docs/content/getting-started.md | 6 +-
docs/content/index.md | 4 +-
docs/content/license.md | 2 +-
docs/mkdocs.yml | 6 +-
src/bin/server.rs | 2 +-
src/config.rs | 174 +-----------------
src/custom_headers.rs | 22 ++-
src/handler.rs | 14 +-
src/helpers.rs | 40 +++-
src/lib.rs | 8 +-
src/server.rs | 97 ++++-----
src/settings/cli.rs | 182 +++++++++++++++++-
src/settings/file.rs | 173 ++++++++++++++++-
src/settings/mod.rs | 210 ++++++++++++++++++++-
tests/toml/config.toml | 68 ++++++-
23 files changed, 1052 insertions(+), 241 deletions(-)
@@ -9,6 +9,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -123,6 +132,15 @@ dependencies = [
]
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"memchr",
]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -312,6 +330,20 @@ dependencies = [
]
[[package]]
name = "globset"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
"fnv",
"log",
"regex",
"serde",
]
[[package]]
name = "h2"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -401,6 +433,16 @@ dependencies = [
]
[[package]]
name = "http-serde"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d98b3d9662de70952b14c4840ee0f37e23973542a363e2275f4b9d024ff6cca"
dependencies = [
"http",
"serde",
]
[[package]]
name = "httparse"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -722,6 +764,23 @@ dependencies = [
]
[[package]]
name = "regex"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -774,6 +833,35 @@ dependencies = [
]
[[package]]
name = "serde"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_ignored"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c2c7d39d14f2f2ea82239de71594782f186fd03501ac81f0ce08e674819ff2f"
dependencies = [
"serde",
]
[[package]]
name = "sha-1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -863,8 +951,10 @@ dependencies = [
"bytes",
"form_urlencoded",
"futures-util",
"globset",
"headers",
"http",
"http-serde",
"humansize",
"hyper",
"listenfd",
@@ -873,6 +963,8 @@ dependencies = [
"percent-encoding",
"pin-project",
"rustls-pemfile",
"serde",
"serde_ignored",
"signal-hook",
"signal-hook-tokio",
"structopt",
@@ -881,6 +973,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tokio-util",
"toml",
"tracing",
"tracing-subscriber",
]
@@ -1025,6 +1118,15 @@ dependencies = [
]
[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
"serde",
]
[[package]]
name = "tower-service"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1,7 +1,7 @@
[package]
name = "static-web-server"
version = "2.7.1"
authors = ["Jose Quintana <https://git.io/joseluisq>"]
authors = ["Jose Quintana <https://joseluisq.net>"]
license = "MIT OR Apache-2.0"
description = "A blazing fast and asynchronous web server for static files-serving."
repository = "https://github.com/joseluisq/static-web-server"
@@ -51,6 +51,11 @@ tokio-util = { version = "0.7", default-features = false, features = ["io"] }
tracing = { version = "0.1", default-features = false, features = ["std"] }
tracing-subscriber = { version = "0.3", default-features = false, features = ["smallvec", "parking_lot", "fmt", "ansi", "tracing-log"] }
form_urlencoded = "1.0"
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_ignored = "0.1"
toml = "0.5"
http-serde = "1.1"
globset = { version = "0.4", features = ["serde1"] }
[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.tikv-jemallocator]
version = "0.4"
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2019-present Jose Quintana <https://git.io/joseluisq>
Copyright (c) 2019-present Jose Quintana <https://joseluisq.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
@@ -52,8 +52,10 @@ It's cross-platform and available for `Linux`, `macOS`, `Windows` and `FreeBSD`
- Optional directory listing.
- [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) support.
- Basic HTTP Authentication.
- Customizable HTTP Response Headers for specific file requests via glob patterns.
- Fallback pages for 404 errors useful for Single-page applications.
- Configurable using CLI arguments, environment variables or a file.
- Default and custom error pages.
- Configurable using CLI arguments or environment variables.
- First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available.
- Ability to accept a socket listener as a file descriptor for use in sandboxing and on-demand applications (E.g [systemd](http://0pointer.de/blog/projects/socket-activation.html)).
- Cross-platform. Binaries available for Linux, macOS, Windows & FreeBSD x86_64 / ARM.
@@ -83,4 +85,4 @@ Feel free to send some [Pull request](https://github.com/joseluisq/static-web-se
This work is primarily distributed under the terms of both the [MIT license](LICENSE-MIT) and the [Apache License (Version 2.0)](LICENSE-APACHE).
© 2019-present [Jose Quintana](https://git.io/joseluisq)
© 2019-present [Jose Quintana](https://joseluisq.net)
@@ -10,8 +10,8 @@ The server can be configured via the following command-line arguments.
```
$ static-web-server -h
static-web-server 2.7.0
Jose Quintana <https://git.io/joseluisq>
static-web-server 2.8.0
Jose Quintana <https://joseluisq.net>
A blazing fast and asynchronous web server for static files-serving.
USAGE:
@@ -31,6 +31,9 @@ OPTIONS:
-x, --compression <compression>
Gzip, Deflate or Brotli compression on demand determined by the Accept-Encoding header and applied to text-
based web file types only [env: SERVER_COMPRESSION=] [default: true]
-w, --config-file <config-file>
Server TOML configuration file path [env: SERVER_CONFIG_FILE=]
-j, --cors-allow-headers <cors-allow-headers>
Specify an optional CORS list of allowed headers separated by comas. Default "origin, content-type". It
requires `--cors-allow-origins` to be used along with [env: SERVER_CORS_ALLOW_HEADERS=] [default: origin,
@@ -0,0 +1,92 @@
# TOML Configuration File
**`SWS`** can be configured using a [TOML](https://toml.io/en/) file in order to adjust the general server features as well as other advanced ones.
It's disabled by default and can be enabled by passing an *string file path* via the `-w, --config-file` option or its equivalent [SERVER_CONFIG_FILE](./../configuration/environment-variables.md#server_config_file) env.
## TOML File (Manifest)
Below just an example showing all features with its default values.
```toml
[general]
#### Address & Root dir
host = "::"
port = 8087
root = "docker/public"
#### Logging
log-level = "trace"
#### Cache Control headers
cache-control-headers = true
#### Auto Compression
compression = true
#### Error pages
page404 = "docker/public/404.html"
page50x = "docker/public/50x.html"
#### HTTP/2 + TLS
http2 = false
http2-tls-cert = ""
http2-tls-key = ""
#### Security headers
security-headers = true
#### CORS
cors-allow-origins = ""
cors-allow-headers = ""
#### Directoy listing
directory-listing = false
directory-listing-order = 6
#### Basich Authentication
basic-auth = ""
#### File descriptor binding
# fd = ""
#### Worker threads
threads-multiplier = 1
#### Grace period after a graceful shutdown
grace-period = 0
#### Page fallback for 404s
page-fallback = ""
[advanced]
#### ....
```
### General options
The TOML `[general]` section allows to adjust the current options actually available via the CLI/ENVs ones.
So they are equivalent each other **except** the `-w, --config-file` option which is omitted and can not be used for obvious reasons.
!!! info "Config file based features are optional"
All server feature options via the configuration file are optional and can be omitted as needed.
### Advanced options
The TOML `[advanced]` section is intended for more complex features.
### Precendence
Whatever config file based feature option will take precedence over its CLI or ENV equivalent.
## Usage
The following command runs the server using an specific `config.toml` file.
```sh
static-web-server -w config.toml
```
@@ -18,6 +18,9 @@ Optional file descriptor number (e.g. `0`) to inherit an already-opened TCP list
### SERVER_ROOT
Relative or absolute root directory path of static files. Default `./public`.
### SERVER_CONFIG_FILE
The Server configuration file path in TOML format. See [The TOML Configuration File](../configuration/config-file.md).
### SERVER_GRACE_PERIOD
Defines a grace period in seconds after a `SIGTERM` signal is caught which will delay the server before to shut it down gracefully. The maximum value is `255` seconds. Default value is `0` (no delay).
@@ -0,0 +1,66 @@
# Custom HTTP Headers
**`SWS`** allows to customize the server [HTTP Response headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) on demand.
## Structure
The Server HTTP response headers should be defined mainly as [Array of Tables](https://toml.io/en/v1.0.0#array-of-tables).
Each table entry should have two key/value pairs:
- One `source` key containing an string *glob pattern*.
- One `headers` key containing a [set or hash table](https://toml.io/en/v1.0.0#table) describing plain HTTP headers to apply.
A particular set of HTTP headers can only be applied when a `source` matches against the request uri.
!!! info "Custom HTTP headers take precedence over existing ones"
Whatever custom HTTP header could **replace** an existing one if it was previously defined (E.g server default headers) and matches its `source`.
The headers order is important since it determine its precedence.
**Example:** if the feature `--cache-control-headers=true` is enabled but also a custom `cache-control` header was defined then the custom header will have priority.
### Source
Source is a [Glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) that should match against the uri that is requesting a resource file.
### Headers
A set of valid plain [HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) to be applied.
## Examples
Below some examples of how to customize server HTTP headers in three variants.
### Oneline version
```toml
[advanced]
[[advanced.headers]]
source = "**/*.{js,css}"
headers = { Access-Control-Allow-Origin = "*", X-XSS-PROTECTION = "1; mode=block" }
```
### Multiline version
```toml
[advanced]
[[advanced.headers]]
source = "*.html"
[advanced.headers.headers]
Cache-Control = "public, max-age=36000"
Content-Security-Policy = "frame-ancestors 'self'"
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
```
### Multiline version with explicit header key (dotted)
```toml
[advanced]
[[advanced.headers]]
source = "**/*.{jpg,jpeg,png,ico,gif}"
headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
```
@@ -15,6 +15,6 @@ docker run --rm -it -p 8787:80 joseluisq/static-web-server:2
!!! info "Docker Tip"
You can specify a Docker volume like `-v $HOME/my-public-dir:/public` in order to overwrite the default root directory. See [Docker examples](features/docker.md).
To see the available options type `static-web-server -h` or go to the [Command-line arguments](./configuration/command-line-arguments.md) section.
Or if you are looking for more advanced examples then have a look at [the features](./features/http1.md) section.
- Type `static-web-server --help` or go to the [Command-line arguments](./configuration/command-line-arguments.md) section.
- See how to configure the server using a [configuration file](configuration/config-file.md).
- Have also a look at [the features](./features/http1.md) section for more advanced examples.
@@ -56,8 +56,10 @@ It's cross-platform and available for `Linux`, `macOS`, `Windows` and `FreeBSD`
- Optional directory listing.
- [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) support.
- Basic HTTP Authentication.
- Customizable HTTP Response Headers for specific file requests via glob patterns.
- Fallback pages for 404 errors useful for Single-page applications.
- Configurable using CLI arguments, environment variables or a file.
- Default and custom error pages.
- Configurable using CLI arguments or environment variables.
- First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available.
- Ability to accept a socket listener as a file descriptor for use in sandboxing and on-demand applications (E.g [systemd](http://0pointer.de/blog/projects/socket-activation.html)).
- Cross-platform. Binaries available for Linux, macOS, Windows & FreeBSD x86_64 / ARM.
@@ -2,4 +2,4 @@
This work is primarily distributed under the terms of both the [MIT license](https://github.com/joseluisq/static-web-server/blob/master/LICENSE-MIT) and the [Apache License (Version 2.0)](https://github.com/joseluisq/static-web-server/blob/master/LICENSE-APACHE).
© 2019-present [Jose Quintana](https://git.io/joseluisq)
© 2019-present [Jose Quintana](https://github.com/joseluisq)
@@ -28,7 +28,7 @@ theme:
- content.code.annotate
- content.tabs.link
- header.autohide
- navigation.expand
- navigation.indexes
@@ -103,6 +103,8 @@ markdown_extensions:
- pymdownx.highlight:
linenums: true
linenums_style: pymdownx-inline
- pymdownx.tabbed:
alternate_style: true
plugins:
@@ -118,6 +120,7 @@ nav:
- 'Configuration':
- 'Command Line Arguments': 'configuration/command-line-arguments.md'
- 'Environment Variables': 'configuration/environment-variables.md'
- 'TOML Configuration File': 'configuration/config-file.md'
- 'Building from Source': 'building-from-source.md'
- 'Features':
- 'HTTP/1': 'features/http1.md'
@@ -135,6 +138,7 @@ nav:
- 'File Descriptor Socket Passing': './features/file-descriptor-socket-passing.md'
- 'Worker Threads Customization': 'features/worker-threads.md'
- 'Error Pages': 'features/error-pages.md'
- 'Custom HTTP Headers': 'features/custom-http-headers.md'
- 'Platforms & Architectures': 'platforms-architectures.md'
- 'Migration from v1 to v2': 'migration.md'
- 'Changelog v2 (latest stable)': 'https://github.com/joseluisq/static-web-server/blob/master/CHANGELOG.md'
@@ -10,7 +10,7 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
use static_web_server::{Result, Server};
fn main() -> Result {
Server::new().run()?;
Server::new()?.run()?;
Ok(())
}
@@ -1,174 +0,0 @@
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(about, author)]
pub struct Config {
#[structopt(long, short = "a", default_value = "::", env = "SERVER_HOST")]
pub host: String,
#[structopt(long, short = "p", default_value = "80", env = "SERVER_PORT")]
pub port: u16,
#[structopt(
long,
short = "f",
env = "SERVER_LISTEN_FD",
conflicts_with_all(&["host", "port"])
)]
pub fd: Option<usize>,
#[structopt(
long,
short = "n",
default_value = "1",
env = "SERVER_THREADS_MULTIPLIER"
)]
pub threads_multiplier: usize,
#[structopt(long, short = "d", default_value = "./public", env = "SERVER_ROOT")]
pub root: String,
#[structopt(
long,
default_value = "./public/50x.html",
env = "SERVER_ERROR_PAGE_50X"
)]
pub page50x: String,
#[structopt(
long,
default_value = "./public/404.html",
env = "SERVER_ERROR_PAGE_404"
)]
pub page404: String,
#[structopt(long, default_value = "", env = "SERVER_FALLBACK_PAGE")]
pub page_fallback: String,
#[structopt(long, short = "g", default_value = "error", env = "SERVER_LOG_LEVEL")]
pub log_level: String,
#[structopt(
long,
short = "c",
default_value = "",
env = "SERVER_CORS_ALLOW_ORIGINS"
)]
pub cors_allow_origins: String,
#[structopt(
long,
short = "j",
default_value = "origin, content-type",
env = "SERVER_CORS_ALLOW_HEADERS"
)]
pub cors_allow_headers: String,
#[structopt(
long,
short = "t",
parse(try_from_str),
default_value = "false",
env = "SERVER_HTTP2_TLS"
)]
pub http2: bool,
#[structopt(
long,
required_if("http2", "true"),
default_value = "",
env = "SERVER_HTTP2_TLS_CERT"
)]
pub http2_tls_cert: String,
#[structopt(
long,
required_if("http2", "true"),
default_value = "",
env = "SERVER_HTTP2_TLS_KEY"
)]
pub http2_tls_key: String,
#[structopt(
long,
short = "x",
parse(try_from_str),
default_value = "true",
env = "SERVER_COMPRESSION"
)]
pub compression: bool,
#[structopt(
long,
short = "z",
parse(try_from_str),
default_value = "false",
env = "SERVER_DIRECTORY_LISTING"
)]
pub directory_listing: bool,
#[structopt(
long,
required_if("directory_listing", "true"),
default_value = "6",
env = "SERVER_DIRECTORY_LISTING_ORDER"
)]
pub directory_listing_order: u8,
#[structopt(
long,
parse(try_from_str),
required_if("http2", "true"),
default_value_if("http2", Some("true"), "true"),
default_value = "false",
env = "SERVER_SECURITY_HEADERS"
)]
pub security_headers: bool,
#[structopt(
long,
short = "e",
parse(try_from_str),
default_value = "true",
env = "SERVER_CACHE_CONTROL_HEADERS"
)]
pub cache_control_headers: bool,
#[structopt(long, default_value = "", env = "SERVER_BASIC_AUTH")]
pub basic_auth: String,
#[structopt(long, short = "q", default_value = "0", env = "SERVER_GRACE_PERIOD")]
pub grace_period: u8,
}
@@ -0,0 +1,22 @@
use hyper::{Body, Response};
use crate::settings::Headers;
pub fn append_headers(
uri: &str,
headers_opts_vec: &Option<Vec<Headers>>,
resp: &mut Response<Body>,
) {
if let Some(headers_vec) = headers_opts_vec {
for headers_entry in headers_vec.iter() {
if headers_entry.source.is_match(uri) {
for (name, value) in &headers_entry.headers {
resp.headers_mut().insert(name, value.to_owned());
}
}
}
}
}
@@ -2,13 +2,13 @@ use hyper::{header::WWW_AUTHENTICATE, Body, Method, Request, Response, StatusCod
use std::{future::Future, path::PathBuf, sync::Arc};
use crate::{
basic_auth, compression, control_headers, cors, error_page, fallback_page, security_headers,
static_files,
basic_auth, compression, control_headers, cors, custom_headers, error_page, fallback_page,
security_headers, settings::Advanced, static_files, Error, Result,
};
use crate::{Error, Result};
pub struct RequestHandlerOpts {
pub root_dir: PathBuf,
pub compression: bool,
pub dir_listing: bool,
@@ -20,6 +20,9 @@ pub struct RequestHandlerOpts {
pub page50x: String,
pub page_fallback: String,
pub basic_auth: String,
pub advanced_opts: Option<Advanced>,
}
@@ -154,6 +157,11 @@ impl RequestHandler {
security_headers::append_headers(&mut resp);
}
if let Some(advanced) = &self.opts.advanced_opts {
custom_headers::append_headers(uri_path, &advanced.headers, &mut resp)
}
Ok(resp)
}
Err(status) => {
@@ -1,7 +1,7 @@
use std::fs;
use std::path::{Path, PathBuf};
use crate::Result;
use crate::{Context, Result};
pub fn get_valid_dirpath<P: AsRef<Path>>(path: P) -> Result<PathBuf>
@@ -37,3 +37,41 @@ pub fn read_file_content(p: &str) -> String {
}
String::new()
}
pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
}
pub fn read_file(path: &Path) -> Result<String> {
match String::from_utf8(read_bytes(path)?) {
Ok(s) => Ok(s),
Err(_) => bail!("path at `{}` was not valid utf-8", path.display()),
}
}
pub fn stringify(dst: &mut String, path: &serde_ignored::Path<'_>) {
use serde_ignored::Path;
match *path {
Path::Root => {}
Path::Seq { parent, index } => {
stringify(dst, parent);
if !dst.is_empty() {
dst.push('.');
}
dst.push_str(&index.to_string());
}
Path::Map { parent, ref key } => {
stringify(dst, parent);
if !dst.is_empty() {
dst.push('.');
}
dst.push_str(key);
}
Path::Some { parent }
| Path::NewtypeVariant { parent }
| Path::NewtypeStruct { parent } => stringify(dst, parent),
}
}
@@ -6,11 +6,14 @@
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate serde;
pub mod basic_auth;
pub mod compression;
pub mod config;
pub mod control_headers;
pub mod cors;
pub mod custom_headers;
pub mod error_page;
pub mod fallback_page;
pub mod handler;
@@ -19,6 +22,7 @@ pub mod logger;
pub mod security_headers;
pub mod server;
pub mod service;
pub mod settings;
pub mod signals;
pub mod static_files;
pub mod tls;
@@ -27,6 +31,6 @@ pub mod transport;
#[macro_use]
pub mod error;
pub use config::Config;
pub use error::*;
pub use server::Server;
pub use settings::Settings;
@@ -3,33 +3,32 @@ use hyper::server::Server as HyperServer;
use listenfd::ListenFd;
use std::net::{IpAddr, SocketAddr, TcpListener};
use std::sync::Arc;
use structopt::StructOpt;
use crate::handler::{RequestHandler, RequestHandlerOpts};
use crate::tls::{TlsAcceptor, TlsConfigBuilder};
use crate::{config::Config, service::RouterService, Context, Result};
use crate::{cors, helpers, logger, signals};
use crate::{cors, helpers, logger, signals, Settings};
use crate::{service::RouterService, Context, Result};
pub struct Server {
opts: Config,
opts: Settings,
threads: usize,
}
impl Server {
pub fn new() -> Server {
pub fn new() -> Result<Server> {
let opts = Config::from_args();
let opts = Settings::get()?;
let cpus = num_cpus::get();
let threads = match opts.threads_multiplier {
let threads = match opts.general.threads_multiplier {
0 | 1 => cpus,
n => cpus * n,
};
Server { opts, threads }
Ok(Server { opts, threads })
}
@@ -53,33 +52,41 @@ impl Server {
async fn start_server(self) -> Result {
let opts = &self.opts;
let general = self.opts.general;
logger::init(&opts.log_level)
.with_context(|| "failed to initialize logging".to_string())?;
let advanced_opts = self.opts.advanced;
let log_level = &general.log_level.to_lowercase();
logger::init(log_level).with_context(|| "failed to initialize logging")?;
tracing::info!("logging level: {}", log_level.to_lowercase());
if general.config_file.is_some() && general.config_file.is_some() {
tracing::info!("config file: {}", general.config_file.unwrap().display());
}
let (tcp_listener, addr_str);
match opts.fd {
match general.fd {
Some(fd) => {
addr_str = format!("@FD({})", fd);
tcp_listener = ListenFd::from_env()
.take_tcp_listener(fd)?
.with_context(|| {
"failed to convert inherited FD into a TCP listener".to_string()
})?;
.with_context(|| "failed to convert inherited FD into a TCP listener")?;
tracing::info!(
"converted inherited file descriptor {} to a TCP listener",
fd
);
}
None => {
let ip = opts
let ip = general
.host
.parse::<IpAddr>()
.with_context(|| format!("failed to parse {} address", opts.host))?;
let addr = SocketAddr::from((ip, opts.port));
.with_context(|| format!("failed to parse {} address", general.host))?;
let addr = SocketAddr::from((ip, general.port));
tcp_listener = TcpListener::bind(addr)
.with_context(|| format!("failed to bind to {} address", addr))?;
addr_str = addr.to_string();
@@ -88,55 +95,55 @@ impl Server {
}
let root_dir = helpers::get_valid_dirpath(&opts.root)
.with_context(|| "root directory was not found or inaccessible".to_string())?;
let root_dir = helpers::get_valid_dirpath(&general.root)
.with_context(|| "root directory was not found or inaccessible")?;
let page404 = helpers::read_file_content(&opts.page404);
let page50x = helpers::read_file_content(&opts.page50x);
let page404 = helpers::read_file_content(&general.page404);
let page50x = helpers::read_file_content(&general.page50x);
let page_fallback = helpers::read_file_content(&opts.page_fallback);
let page_fallback = helpers::read_file_content(&general.page_fallback);
let threads = self.threads;
tracing::info!("runtime worker threads: {}", self.threads);
let security_headers = opts.security_headers;
let security_headers = general.security_headers;
tracing::info!("security headers: enabled={}", security_headers);
let compression = opts.compression;
let compression = general.compression;
tracing::info!("auto compression: enabled={}", compression);
let dir_listing = opts.directory_listing;
let dir_listing = general.directory_listing;
tracing::info!("directory listing: enabled={}", dir_listing);
let dir_listing_order = opts.directory_listing_order;
let dir_listing_order = general.directory_listing_order;
tracing::info!("directory listing order code: {}", dir_listing_order);
let cache_control_headers = opts.cache_control_headers;
let cache_control_headers = general.cache_control_headers;
tracing::info!("cache control headers: enabled={}", cache_control_headers);
let cors = cors::new(
opts.cors_allow_origins.trim(),
opts.cors_allow_headers.trim(),
general.cors_allow_origins.trim(),
general.cors_allow_headers.trim(),
);
let basic_auth = opts.basic_auth.trim().to_owned();
let basic_auth = general.basic_auth.trim().to_owned();
tracing::info!(
"basic authentication: enabled={}",
!self.opts.basic_auth.is_empty()
!general.basic_auth.is_empty()
);
let grace_period = opts.grace_period;
let grace_period = general.grace_period;
tracing::info!("grace period before graceful shutdown: {}s", grace_period);
@@ -153,36 +160,36 @@ impl Server {
page50x,
page_fallback,
basic_auth,
advanced_opts,
}),
});
if opts.http2 {
if general.http2 {
tcp_listener
.set_nonblocking(true)
.expect("cannot set non-blocking");
let listener = tokio::net::TcpListener::from_std(tcp_listener)
.with_context(|| "failed to create tokio::net::TcpListener".to_string())?;
.with_context(|| "failed to create tokio::net::TcpListener")?;
let mut incoming = AddrIncoming::from_listener(listener).with_context(|| {
"failed to create an AddrIncoming from the current tokio::net::TcpListener"
.to_string()
})?;
incoming.set_nodelay(true);
let tls = TlsConfigBuilder::new()
.cert_path(&opts.http2_tls_cert)
.key_path(&opts.http2_tls_key)
.cert_path(&general.http2_tls_cert)
.key_path(&general.http2_tls_key)
.build()
.with_context(|| {
"failed to initialize TLS, probably wrong cert/key or file missing".to_string()
"failed to initialize TLS, probably wrong cert/key or file missing"
})?;
#[cfg(unix)]
let signals = signals::create_signals()
.with_context(|| "failed to register termination signals".to_string())?;
.with_context(|| "failed to register termination signals")?;
#[cfg(unix)]
let handle = signals.handle();
@@ -212,7 +219,7 @@ impl Server {
#[cfg(unix)]
let signals = signals::create_signals()
.with_context(|| "failed to register termination signals".to_string())?;
.with_context(|| "failed to register termination signals")?;
#[cfg(unix)]
let handle = signals.handle();
@@ -246,9 +253,3 @@ impl Server {
Ok(())
}
}
impl Default for Server {
fn default() -> Self {
Self::new()
}
}
@@ -0,0 +1,182 @@
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(about, author)]
pub struct General {
#[structopt(long, short = "a", default_value = "::", env = "SERVER_HOST")]
pub host: String,
#[structopt(long, short = "p", default_value = "80", env = "SERVER_PORT")]
pub port: u16,
#[structopt(
long,
short = "f",
env = "SERVER_LISTEN_FD",
conflicts_with_all(&["host", "port"])
)]
pub fd: Option<usize>,
#[structopt(
long,
short = "n",
default_value = "1",
env = "SERVER_THREADS_MULTIPLIER"
)]
pub threads_multiplier: usize,
#[structopt(long, short = "d", default_value = "./public", env = "SERVER_ROOT")]
pub root: String,
#[structopt(
long,
default_value = "./public/50x.html",
env = "SERVER_ERROR_PAGE_50X"
)]
pub page50x: String,
#[structopt(
long,
default_value = "./public/404.html",
env = "SERVER_ERROR_PAGE_404"
)]
pub page404: String,
#[structopt(long, default_value = "", env = "SERVER_FALLBACK_PAGE")]
pub page_fallback: String,
#[structopt(long, short = "g", default_value = "error", env = "SERVER_LOG_LEVEL")]
pub log_level: String,
#[structopt(
long,
short = "c",
default_value = "",
env = "SERVER_CORS_ALLOW_ORIGINS"
)]
pub cors_allow_origins: String,
#[structopt(
long,
short = "j",
default_value = "origin, content-type",
env = "SERVER_CORS_ALLOW_HEADERS"
)]
pub cors_allow_headers: String,
#[structopt(
long,
short = "t",
parse(try_from_str),
default_value = "false",
env = "SERVER_HTTP2_TLS"
)]
pub http2: bool,
#[structopt(
long,
required_if("http2", "true"),
default_value = "",
env = "SERVER_HTTP2_TLS_CERT"
)]
pub http2_tls_cert: String,
#[structopt(
long,
required_if("http2", "true"),
default_value = "",
env = "SERVER_HTTP2_TLS_KEY"
)]
pub http2_tls_key: String,
#[structopt(
long,
short = "x",
parse(try_from_str),
default_value = "true",
env = "SERVER_COMPRESSION"
)]
pub compression: bool,
#[structopt(
long,
short = "z",
parse(try_from_str),
default_value = "false",
env = "SERVER_DIRECTORY_LISTING"
)]
pub directory_listing: bool,
#[structopt(
long,
required_if("directory_listing", "true"),
default_value = "6",
env = "SERVER_DIRECTORY_LISTING_ORDER"
)]
pub directory_listing_order: u8,
#[structopt(
long,
parse(try_from_str),
required_if("http2", "true"),
default_value_if("http2", Some("true"), "true"),
default_value = "false",
env = "SERVER_SECURITY_HEADERS"
)]
pub security_headers: bool,
#[structopt(
long,
short = "e",
parse(try_from_str),
default_value = "true",
env = "SERVER_CACHE_CONTROL_HEADERS"
)]
pub cache_control_headers: bool,
#[structopt(long, default_value = "", env = "SERVER_BASIC_AUTH")]
pub basic_auth: String,
#[structopt(long, short = "q", default_value = "0", env = "SERVER_GRACE_PERIOD")]
pub grace_period: u8,
#[structopt(long, short = "w", env = "SERVER_CONFIG_FILE")]
pub config_file: Option<PathBuf>,
}
@@ -0,0 +1,173 @@
use headers::HeaderMap;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::path::Path;
use crate::{helpers, Context, Result};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
pub fn name(&self) -> &'static str {
match self {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Headers {
pub source: String,
#[serde(rename(deserialize = "headers"), with = "http_serde::header_map")]
pub headers: HeaderMap,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Advanced {
pub headers: Option<Vec<Headers>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct General {
pub host: Option<String>,
pub port: Option<u16>,
pub root: Option<String>,
pub log_level: Option<LogLevel>,
pub cache_control_headers: Option<bool>,
pub compression: Option<bool>,
pub page404: Option<String>,
pub page50x: Option<String>,
pub http2: Option<bool>,
pub http2_tls_cert: Option<String>,
pub http2_tls_key: Option<String>,
pub security_headers: Option<bool>,
pub cors_allow_origins: Option<String>,
pub cors_allow_headers: Option<String>,
pub directory_listing: Option<bool>,
pub directory_listing_order: Option<u8>,
pub basic_auth: Option<String>,
pub fd: Option<usize>,
pub threads_multiplier: Option<usize>,
pub grace_period: Option<u8>,
pub page_fallback: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Settings {
pub general: Option<General>,
pub advanced: Option<Advanced>,
}
impl Settings {
pub fn read(config_file: &Path) -> Result<Settings> {
let ext = config_file.extension();
if ext.is_none() || ext.unwrap().is_empty() || ext.unwrap().ne("toml") {
bail!("configuration file should be in toml format. E.g `config.toml`");
}
let toml =
read_toml_file(config_file).with_context(|| "error reading toml configuration file")?;
let mut unused = BTreeSet::new();
let manifest: Settings = serde_ignored::deserialize(toml, |path| {
let mut key = String::new();
helpers::stringify(&mut key, &path);
unused.insert(key);
})
.with_context(|| "error during toml configuration file deserialization")?;
for key in unused {
println!(
"Warning: unused configuration manifest key \"{}\" or unsuported",
key
);
}
Ok(manifest)
}
}
fn read_toml_file(path: &Path) -> Result<toml::Value> {
let toml_str = helpers::read_file(path).with_context(|| {
format!(
"error trying to deserialize toml configuration file at \"{}\"",
path.display()
)
})?;
let first_error = match toml_str.parse() {
Ok(res) => return Ok(res),
Err(err) => err,
};
let mut second_parser = toml::de::Deserializer::new(&toml_str);
second_parser.set_require_newline_after_table(false);
if let Ok(res) = toml::Value::deserialize(&mut second_parser) {
let msg = format!(
"\
TOML file found which contains invalid syntax and will soon not parse
at `{}`.
The TOML spec requires newlines after table definitions (e.g., `[a] b = 1` is
invalid), but this file has a table header which does not have a newline after
it. A newline needs to be added and this warning will soon become a hard error
in the future.",
path.display()
);
println!("{}", &msg);
return Ok(res);
}
let first_error = anyhow::Error::from(first_error);
Err(first_error.context("could not parse data input as toml format"))
}
@@ -0,0 +1,210 @@
use globset::{Glob, GlobMatcher};
use headers::HeaderMap;
use structopt::StructOpt;
use crate::{Context, Result};
mod cli;
pub mod file;
use cli::General;
pub struct Headers {
pub source: GlobMatcher,
pub headers: HeaderMap,
}
pub struct Advanced {
pub headers: Option<Vec<Headers>>,
}
pub struct Settings {
pub general: General,
pub advanced: Option<Advanced>,
}
impl Settings {
pub fn get() -> Result<Settings> {
let opts = General::from_args();
let mut host = opts.host.to_owned();
let mut port = opts.port;
let mut root = opts.root.to_owned();
let mut log_level = opts.log_level.to_owned();
let mut config_file = opts.config_file.clone();
let mut cache_control_headers = opts.cache_control_headers;
let mut compression = opts.compression;
let mut page404 = opts.page404.to_owned();
let mut page50x = opts.page50x.to_owned();
let mut http2 = opts.http2;
let mut http2_tls_cert = opts.http2_tls_cert.to_owned();
let mut http2_tls_key = opts.http2_tls_key.to_owned();
let mut security_headers = opts.security_headers;
let mut cors_allow_origins = opts.cors_allow_origins.to_owned();
let mut cors_allow_headers = opts.cors_allow_headers.to_owned();
let mut directory_listing = opts.directory_listing;
let mut directory_listing_order = opts.directory_listing_order;
let mut basic_auth = opts.basic_auth.to_owned();
let mut fd = opts.fd;
let mut threads_multiplier = opts.threads_multiplier;
let mut grace_period = opts.grace_period;
let mut page_fallback = opts.page_fallback.to_owned();
let mut settings_advanced: Option<Advanced> = None;
if let Some(ref p) = opts.config_file {
if p.is_file() {
let path_resolved = p
.canonicalize()
.with_context(|| "error resolving toml config file path")?;
let settings = file::Settings::read(&path_resolved)
.with_context(|| {
"can not read toml config file because has invalid or unsupported format/options"
})?;
config_file = Some(path_resolved);
if let Some(general) = settings.general {
if let Some(ref v) = general.host {
host = v.to_owned()
}
if let Some(v) = general.port {
port = v
}
if let Some(ref v) = general.root {
root = v.to_owned()
}
if let Some(ref v) = general.log_level {
log_level = v.name().to_lowercase();
}
if let Some(v) = general.cache_control_headers {
cache_control_headers = v
}
if let Some(v) = general.compression {
compression = v
}
if let Some(ref v) = general.page404 {
page404 = v.to_owned()
}
if let Some(ref v) = general.page50x {
page50x = v.to_owned()
}
if let Some(v) = general.http2 {
http2 = v
}
if let Some(ref v) = general.http2_tls_cert {
http2_tls_cert = v.to_owned()
}
if let Some(ref v) = general.http2_tls_key {
http2_tls_key = v.to_owned()
}
if let Some(v) = general.security_headers {
security_headers = v
}
if let Some(ref v) = general.cors_allow_origins {
cors_allow_origins = v.to_owned()
}
if let Some(ref v) = general.cors_allow_headers {
cors_allow_headers = v.to_owned()
}
if let Some(v) = general.directory_listing {
directory_listing = v
}
if let Some(v) = general.directory_listing_order {
directory_listing_order = v
}
if let Some(ref v) = general.basic_auth {
basic_auth = v.to_owned()
}
if let Some(v) = general.fd {
fd = Some(v)
}
if let Some(v) = general.threads_multiplier {
threads_multiplier = v
}
if let Some(v) = general.grace_period {
grace_period = v
}
if let Some(ref v) = general.page_fallback {
page_fallback = v.to_owned()
}
}
if let Some(advanced) = settings.advanced {
let headers_entries = match advanced.headers {
Some(headers_entries) => {
let mut headers_vec: Vec<Headers> = Vec::new();
for headers_entry in headers_entries.iter() {
let source = Glob::new(&headers_entry.source)
.with_context(|| {
format!(
"can not compile glob pattern for header source: {}",
&headers_entry.source
)
})?
.compile_matcher();
headers_vec.push(Headers {
source,
headers: headers_entry.headers.to_owned(),
});
}
Some(headers_vec)
}
_ => None,
};
settings_advanced = Some(Advanced {
headers: headers_entries,
});
}
}
}
Ok(Settings {
general: General {
host,
port,
root,
log_level,
config_file,
cache_control_headers,
compression,
page404,
page50x,
http2,
http2_tls_cert,
http2_tls_key,
security_headers,
cors_allow_origins,
cors_allow_headers,
directory_listing,
directory_listing_order,
basic_auth,
fd,
threads_multiplier,
grace_period,
page_fallback,
},
advanced: settings_advanced,
})
}
}
@@ -0,0 +1,68 @@
[general]
host = "::"
port = 8087
root = "docker/public"
log-level = "trace"
cache-control-headers = true
compression = true
page404 = "docker/public/404.html"
page50x = "docker/public/50x.html"
http2 = false
http2-tls-cert = ""
http2-tls-key = ""
security-headers = true
cors-allow-origins = ""
directory-listing = false
basic-auth = ""
threads-multiplier = 1
grace-period = 0
page-fallback = ""
[advanced]
[[advanced.headers]]
source = "**/*.{js,css}"
headers = { Access-Control-Allow-Origin = "*", X-XSS-PROTECTION = "1; mode=block" }
[[advanced.headers]]
source = "index.html"
[advanced.headers.headers]
Cache-Control = "public, max-age=36000"
Content-Security-Policy = "frame-ancestors 'self'"
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
[[advanced.headers]]
source = "**/*.{jpg,jpeg,png,ico,gif}"
headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"