feat: `static_web_server` crate (#190)
* feat: preliminary `static_web_server` crate setup
* chore: add API docs
resolves #188
Diff
.github/workflows/release.crate.yml | 6 +--
Cargo.toml | 22 ++++++++++-
Makefile | 11 ++++++-
src/basic_auth.rs | 3 ++-
src/compression.rs | 10 +++--
src/compression_static.rs | 6 +++-
src/control_headers.rs | 4 +-
src/cors.rs | 47 +++++++++---------------
src/custom_headers.rs | 3 ++-
src/directory_listing.rs | 5 +++-
src/error.rs | 3 ++-
src/error_page.rs | 3 ++-
src/exts/http.rs | 4 ++-
src/exts/path.rs | 1 +-
src/fallback_page.rs | 3 ++-
src/handler.rs | 22 ++++++++++-
src/helpers.rs | 19 +---------
src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++--
src/logger.rs | 3 ++-
src/redirects.rs | 3 ++-
src/rewrites.rs | 3 ++-
src/security_headers.rs | 3 ++-
src/server.rs | 15 +++++---
src/service.rs | 6 +++-
src/settings/cli.rs | 2 +-
src/settings/file.rs | 68 ++++++++++++++++++++++++++--------
src/settings/mod.rs | 6 +++-
src/signals.rs | 3 ++-
src/static_files.rs | 22 ++++++++---
src/tls.rs | 17 ++++++---
src/transport.rs | 9 +++--
src/winservice.rs | 3 +-
32 files changed, 317 insertions(+), 94 deletions(-)
@@ -9,7 +9,7 @@ on:
jobs:
check-secret:
runs-on: ubuntu-latest
environment: crates-io-libsws
environment: crates-io-static-web-server
outputs:
publish: ${{ steps.check.outputs.publish }}
steps:
@@ -205,7 +205,7 @@ jobs:
needs: test
if: needs.check-secret.outputs.publish == 'true'
runs-on: ubuntu-latest
environment: crates-io-libsws
environment: crates-io-static-web-server
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -217,6 +217,6 @@ jobs:
- name: Publish workspace packages
run: |
cargo publish -p libsws
cargo publish -p static-web-server
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }}
@@ -6,26 +6,44 @@ license = "MIT OR Apache-2.0"
description = "A cross-platform, high-performance and asynchronous web server for static files-serving."
repository = "https://github.com/static-web-server/static-web-server"
readme = "README.md"
homepage = "https://static-web-server.net"
keywords = [
"static-web-server",
"file-server",
"http-server",
"docker-image",
"musl-libc",
"x86",
"x86-64",
"arm",
"arm64",
"linux",
"darwin",
"freebsd",
"windows",
]
categories = ["network-programming", "web-programming::http-server"]
edition = "2021"
include = ["src/**/*", "Cargo.toml", "Cargo.lock"]
include = [
"src/**/*.rs",
"Cargo.toml",
"README.md",
"LICENSE-MIT",
"LICENSE-APACHE"
]
autotests = true
autoexamples = true
[package.metadata.docs.rs]
all-features = true
[lib]
name = "static_web_server"
path = "src/lib.rs"
[[bin]]
name = "static-web-server"
path = "src/bin/server.rs"
doc = false
[features]
default = ["http2"]
@@ -247,6 +247,17 @@ docs-dev:
@docker-compose -f docs/docker-compose.yml up --build
.PHONY: docs-dev
crate-docs:
@cargo doc --no-deps
.PHONY: crate-docs
crate-docs-dev:
@cargo doc --no-deps
@echo "Crate documentation: http://localhost:8787/static_web_server"
@static-web-server -p 8787 -d target/doc/ \
& watchman-make -p 'src/**/*.rs' --run 'cargo doc'
.PHONY: crate-docs-dev
docs-deploy:
@git stash
@rm -rf /tmp/docs
@@ -1,3 +1,6 @@
use bcrypt::verify as bcrypt_verify;
use headers::{authorization::Basic, Authorization, HeaderMapExt};
use hyper::StatusCode;
@@ -1,5 +1,7 @@
use async_compression::tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder};
use bytes::Bytes;
@@ -158,8 +160,8 @@ pub fn create_encoding_header(existing: Option<HeaderValue>, coding: ContentCodi
coding.into()
}
#[pin_project]
#[derive(Debug)]
pub struct CompressableBody<S, E>
@@ -1,3 +1,6 @@
use headers::{ContentCoding, HeaderMap, HeaderValue};
use std::{
ffi::OsStr,
@@ -9,8 +12,11 @@ use crate::{compression, static_files::file_metadata};
pub struct CompressedFileVariant<'a> {
pub file_path: PathBuf,
pub metadata: Metadata,
pub extension: &'a str,
}
@@ -1,4 +1,6 @@
use headers::{CacheControl, HeaderMapExt};
use hyper::{Body, Response};
@@ -1,5 +1,7 @@
use headers::{
AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlExposeHeaders, HeaderMapExt,
@@ -139,13 +141,6 @@ impl Cors {
self
}
pub fn max_age(mut self, seconds: impl Seconds) -> Self {
self.max_age = Some(seconds.seconds());
self
}
@@ -211,6 +206,7 @@ impl Default for Cors {
}
#[derive(Clone, Debug)]
pub struct Configured {
cors: Cors,
allowed_headers: AccessControlAllowHeaders,
@@ -219,16 +215,24 @@ pub struct Configured {
}
#[derive(Debug)]
pub enum Validated {
Preflight(HeaderValue),
Simple(HeaderValue),
NotCors,
}
#[derive(Debug)]
pub enum Forbidden {
Origin,
Method,
Header,
}
@@ -239,6 +243,7 @@ impl Default for Forbidden {
}
impl Configured {
pub fn check_request(
&self,
method: &http::Method,
@@ -299,19 +304,19 @@ impl Configured {
}
}
pub fn is_method_allowed(&self, header: &HeaderValue) -> bool {
fn is_method_allowed(&self, header: &HeaderValue) -> bool {
http::Method::from_bytes(header.as_bytes())
.map(|method| self.cors.allowed_methods.contains(&method))
.unwrap_or(false)
}
pub fn is_header_allowed(&self, header: &str) -> bool {
fn is_header_allowed(&self, header: &str) -> bool {
HeaderName::from_bytes(header.as_bytes())
.map(|header| self.cors.allowed_headers.contains(&header))
.unwrap_or(false)
}
pub fn is_origin_allowed(&self, origin: &HeaderValue) -> bool {
fn is_origin_allowed(&self, origin: &HeaderValue) -> bool {
if let Some(ref allowed) = self.cors.origins {
allowed.contains(origin)
} else {
@@ -330,23 +335,9 @@ impl Configured {
}
}
pub trait Seconds {
fn seconds(self) -> u64;
}
impl Seconds for u32 {
fn seconds(self) -> u64 {
self.into()
}
}
impl Seconds for ::std::time::Duration {
fn seconds(self) -> u64 {
self.as_secs()
}
}
pub trait IntoOrigin {
fn into_origin(self) -> Origin;
}
@@ -1,3 +1,6 @@
use hyper::{Body, Response};
use crate::settings::Headers;
@@ -1,3 +1,8 @@
#![allow(missing_docs)]
use chrono::{DateTime, Local, NaiveDateTime, Utc};
use futures_util::future::Either;
use futures_util::{future, FutureExt};
@@ -1,3 +1,6 @@
pub type Result<T = (), E = anyhow::Error> = anyhow::Result<T, E>;
@@ -1,3 +1,6 @@
use headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt};
use hyper::{Body, Method, Response, StatusCode, Uri};
use mime_guess::mime;
@@ -7,9 +7,13 @@ pub const HTTP_SUPPORTED_METHODS: &[Method; 3] = &[Method::OPTIONS, Method::HEAD
pub trait MethodExt {
fn is_allowed(&self) -> bool;
fn is_get(&self) -> bool;
fn is_head(&self) -> bool;
fn is_options(&self) -> bool;
}
@@ -4,6 +4,7 @@ use std::path::{Component, Path};
pub trait PathExt {
fn is_hidden(&self) -> bool;
}
@@ -1,3 +1,6 @@
use headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt};
use hyper::{Body, Response, StatusCode};
use mime_guess::mime;
@@ -1,3 +1,6 @@
use headers::HeaderValue;
use hyper::{header::WWW_AUTHENTICATE, Body, Request, Response, StatusCode};
use std::{future::Future, net::IpAddr, net::SocketAddr, path::PathBuf, sync::Arc};
@@ -16,29 +19,46 @@ use crate::{
pub struct RequestHandlerOpts {
pub root_dir: PathBuf,
pub compression: bool,
pub compression_static: bool,
pub dir_listing: bool,
pub dir_listing_order: u8,
pub dir_listing_format: DirListFmt,
pub cors: Option<cors::Configured>,
pub security_headers: bool,
pub cache_control_headers: bool,
pub page404: Vec<u8>,
pub page50x: Vec<u8>,
pub page_fallback: Vec<u8>,
pub basic_auth: String,
pub log_remote_address: bool,
pub redirect_trailing_slash: bool,
pub ignore_hidden_files: bool,
pub advanced_opts: Option<Advanced>,
}
pub struct RequestHandler {
pub opts: Arc<RequestHandlerOpts>,
}
@@ -15,21 +15,6 @@ where
}
}
pub fn get_dirname<P: AsRef<Path>>(path: P) -> Result<String>
where
PathBuf: From<P>,
{
let path = get_valid_dirpath(path)?;
match path.iter().last() {
Some(v) => Ok(v.to_str().unwrap().to_owned()),
_ => bail!(
"directory name for path {} was not determined",
path.display()
),
}
}
pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
fs::read(path).with_context(|| format!("failed to read file `{}`", path.display()))
@@ -75,13 +60,13 @@ pub fn stringify(dst: &mut String, path: &serde_ignored::Path<'_>) {
#[cfg(unix)]
pub fn adjust_canonicalization(p: PathBuf) -> String {
pub fn adjust_canonicalization(p: &Path) -> String {
p.display().to_string()
}
#[cfg(windows)]
pub fn adjust_canonicalization(p: PathBuf) -> String {
pub fn adjust_canonicalization(p: &Path) -> String {
const VERBATIM_PREFIX: &str = r#"\\?\"#;
let p = p.to_str().unwrap_or_default();
let p = if p.starts_with(VERBATIM_PREFIX) {
@@ -1,14 +1,81 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(dead_code)]
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate serde;
pub mod basic_auth;
pub mod compression;
pub mod compression_static;
@@ -20,7 +87,6 @@ pub mod error_page;
pub mod exts;
pub mod fallback_page;
pub mod handler;
pub mod helpers;
pub mod logger;
pub mod redirects;
pub mod rewrites;
@@ -34,13 +100,15 @@ pub mod static_files;
#[cfg(feature = "tls")]
pub mod tls;
pub mod transport;
#[cfg(windows)]
pub mod winservice;
#[macro_use]
pub mod error;
mod helpers;
pub use error::*;
pub use server::Server;
pub use settings::Settings;
@@ -1,3 +1,6 @@
use tracing::Level;
use tracing_subscriber::fmt::format::FmtSpan;
@@ -1,3 +1,6 @@
use hyper::StatusCode;
use crate::settings::Redirects;
@@ -1,3 +1,6 @@
use crate::settings::Rewrites;
@@ -1,3 +1,6 @@
use http::header::{
CONTENT_SECURITY_POLICY, STRICT_TRANSPORT_SECURITY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
X_XSS_PROTECTION,
@@ -1,3 +1,6 @@
#[allow(unused_imports)]
use hyper::server::conn::AddrIncoming;
#[allow(unused_imports)]
@@ -43,7 +46,8 @@ impl Server {
})
}
pub fn run_standalone(self) -> Result {
logger::init(&self.opts.general.log_level)?;
@@ -51,7 +55,8 @@ impl Server {
self.run_server_on_rt(None, || {})
}
#[cfg(windows)]
pub fn run_as_service<F>(self, cancel: Option<Receiver<()>>, cancel_fn: F) -> Result
where
@@ -60,8 +65,8 @@ impl Server {
self.run_server_on_rt(cancel, cancel_fn)
}
fn run_server_on_rt<F>(self, cancel_recv: Option<Receiver<()>>, cancel_fn: F) -> Result
pub fn run_server_on_rt<F>(self, cancel_recv: Option<Receiver<()>>, cancel_fn: F) -> Result
where
F: FnOnce(),
{
@@ -100,7 +105,7 @@ impl Server {
if let Some(config_file) = general.config_file {
let config_file = helpers::adjust_canonicalization(config_file);
let config_file = helpers::adjust_canonicalization(&config_file);
tracing::info!("config file: {}", config_file);
}
@@ -1,3 +1,6 @@
use hyper::{service::Service, Body, Request, Response};
use std::convert::Infallible;
use std::future::{ready, Future, Ready};
@@ -14,6 +17,7 @@ pub struct RouterService {
}
impl RouterService {
pub fn new(handler: RequestHandler) -> Self {
Self {
builder: RequestServiceBuilder::new(handler),
@@ -63,12 +67,14 @@ pub struct RequestServiceBuilder {
}
impl RequestServiceBuilder {
pub fn new(handler: RequestHandler) -> Self {
Self {
handler: Arc::new(handler),
}
}
pub fn build(&self, remote_addr: Option<SocketAddr>) -> RequestService {
RequestService {
handler: self.handler.clone(),
@@ -281,11 +281,13 @@ pub struct General {
#[cfg(windows)]
#[structopt(subcommand)]
pub commands: Option<Commands>,
}
#[cfg(windows)]
#[derive(Debug, StructOpt)]
pub enum Commands {
#[structopt(name = "install")]
@@ -11,15 +11,22 @@ use crate::{helpers, Context, Result};
#[derive(Debug, Serialize, Deserialize, Clone)]
#[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",
@@ -33,14 +40,18 @@ impl LogLevel {
#[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_repr, Deserialize_repr, Clone)]
#[repr(u16)]
pub enum RedirectsKind {
Permanent = 301,
@@ -50,16 +61,23 @@ pub enum RedirectsKind {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Redirects {
pub source: String,
pub destination: String,
pub kind: RedirectsKind,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Rewrites {
pub source: String,
pub destination: String,
}
@@ -67,11 +85,11 @@ pub struct Rewrites {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Advanced {
pub headers: Option<Vec<Headers>>,
pub rewrites: Option<Vec<Rewrites>>,
pub redirects: Option<Vec<Redirects>>,
}
@@ -80,70 +98,86 @@ pub struct Advanced {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct General {
pub host: Option<String>,
pub port: Option<u16>,
pub root: Option<PathBuf>,
pub log_level: Option<LogLevel>,
pub cache_control_headers: Option<bool>,
pub compression: Option<bool>,
pub compression_static: Option<bool>,
pub page404: Option<PathBuf>,
pub page50x: Option<PathBuf>,
#[cfg(feature = "http2")]
pub http2: Option<bool>,
#[cfg(feature = "http2")]
pub http2_tls_cert: Option<PathBuf>,
#[cfg(feature = "http2")]
pub http2_tls_key: Option<PathBuf>,
pub security_headers: Option<bool>,
pub cors_allow_origins: Option<String>,
pub cors_allow_headers: Option<String>,
pub cors_expose_headers: Option<String>,
pub directory_listing: Option<bool>,
pub directory_listing_order: Option<u8>,
pub directory_listing_format: Option<DirListFmt>,
pub basic_auth: Option<String>,
pub fd: Option<usize>,
pub threads_multiplier: Option<usize>,
pub max_blocking_threads: Option<usize>,
pub grace_period: Option<u8>,
pub page_fallback: Option<PathBuf>,
pub log_remote_address: Option<bool>,
pub redirect_trailing_slash: Option<bool>,
pub ignore_hidden_files: Option<bool>,
#[cfg(windows)]
pub windows_service: Option<bool>,
}
@@ -151,7 +185,9 @@ pub struct General {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Settings {
pub general: Option<General>,
pub advanced: Option<Advanced>,
}
@@ -1,3 +1,6 @@
use globset::{Glob, GlobMatcher};
use headers::HeaderMap;
use hyper::StatusCode;
@@ -41,8 +44,11 @@ pub struct Redirects {
pub struct Advanced {
pub headers: Option<Vec<Headers>>,
pub rewrites: Option<Vec<Rewrites>>,
pub redirects: Option<Vec<Redirects>>,
}
@@ -1,3 +1,6 @@
use tokio::time::{sleep, Duration};
#[cfg(unix)]
@@ -1,7 +1,8 @@
use bytes::{Bytes, BytesMut};
use futures_util::future::{Either, Future};
@@ -27,20 +28,31 @@ use crate::{compression_static, directory_listing, Result};
pub struct HandleOpts<'a> {
pub method: &'a Method,
pub headers: &'a HeaderMap<HeaderValue>,
pub base_path: &'a PathBuf,
pub uri_path: &'a str,
pub uri_query: Option<&'a str>,
pub dir_listing: bool,
pub dir_listing_order: u8,
pub dir_listing_format: &'a DirListFmt,
pub redirect_trailing_slash: bool,
pub compression_static: bool,
pub ignore_hidden_files: bool,
}
pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response<Body>, bool), StatusCode> {
let method = opts.method;
@@ -472,7 +484,7 @@ const READ_BUF_SIZE: usize = 4_096;
const READ_BUF_SIZE: usize = 8_192;
#[derive(Debug)]
pub struct FileStream<T> {
struct FileStream<T> {
reader: T,
}
@@ -1,5 +1,7 @@
use futures_util::ready;
use hyper::server::accept::Accept;
@@ -23,6 +25,7 @@ use crate::transport::Transport;
#[derive(Debug)]
pub enum TlsConfigError {
Io(io::Error),
CertParseError,
@@ -168,6 +171,7 @@ impl TlsConfigBuilder {
self
}
pub fn build(mut self) -> Result<ServerConfig, TlsConfigError> {
let mut cert_rdr = BufReader::new(self.cert);
let cert = rustls_pemfile::certs(&mut cert_rdr)
@@ -284,9 +288,10 @@ enum State {
Streaming(tokio_rustls::server::TlsStream<AddrStream>),
}
pub struct TlsStream {
state: State,
remote_addr: SocketAddr,
@@ -359,12 +364,14 @@ impl AsyncWrite for TlsStream {
}
}
pub struct TlsAcceptor {
config: Arc<ServerConfig>,
incoming: AddrIncoming,
}
impl TlsAcceptor {
pub fn new(config: ServerConfig, incoming: AddrIncoming) -> TlsAcceptor {
TlsAcceptor {
config: Arc::new(config),
@@ -1,5 +1,7 @@
use std::io;
use std::net::SocketAddr;
@@ -9,7 +11,9 @@ use std::task::{Context, Poll};
use hyper::server::conn::AddrStream;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
pub trait Transport: AsyncRead + AsyncWrite {
fn remote_addr(&self) -> Option<SocketAddr>;
}
@@ -19,6 +23,7 @@ impl Transport for AddrStream {
}
}
pub struct LiftIo<T>(pub T);
impl<T: AsyncRead + Unpin> AsyncRead for LiftIo<T> {
@@ -1,4 +1,5 @@
use std::ffi::OsString;
use std::thread;
@@ -181,7 +182,7 @@ pub fn install_service(config_file: Option<PathBuf>) -> Result {
if let Some(f) = config_file {
let f = helpers::adjust_canonicalization(f);
let f = helpers::adjust_canonicalization(&f);
if !f.is_empty() {
service_binary_arguments.push(OsString::from(["--config-file=", &f].concat()));
}