rust-web-server 18.8.2

Single-binary web server and reverse proxy: automatic HTTPS (Let's Encrypt), HTTP/1.1, HTTP/2 and HTTP/3, load balancing, auth and rate limiting, configured in one TOML file. Also a Rust library: routing, middleware, MCP server, ORM, background jobs. No hyper, no Tower; HTTP/2 and HTTP/3 framing via h2 and quinn.
Documentation
use crate::header::Header;

#[test]
fn parse() {
    let raw_header = "Some-Header: some value";
    let header = Header::parse_header(raw_header).unwrap();

    assert_eq!(header.name, "Some-Header".to_string());
    assert_eq!(header.value, "some value".to_string());
}

#[test]
fn parse_not_valid_header() {
    let raw_header = "some random characters";
    let boxed_header = Header::parse_header(raw_header);

    assert!(boxed_header.is_err());
    let err_msg = boxed_header.err().unwrap();
    assert_eq!("Unable to parse header: some random characters", err_msg);
}

#[test]
fn header_test() {
    let header = Header { name: Header::_ORIGIN.into(), value: "some string".into() };

    assert_eq!(header.name, Header::_ORIGIN.to_string());
    assert_eq!(header.value, "some string".to_string());
}

#[test]
fn header_constants() {
    let header = Header { name: Header::_CONTENT_TYPE.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_CONTENT_TYPE.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_CONTENT_LENGTH.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_CONTENT_LENGTH.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_X_CONTENT_TYPE_OPTIONS.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_X_CONTENT_TYPE_OPTIONS.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_RANGE.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_RANGE.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCEPT_RANGES.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCEPT_RANGES.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_CONTENT_RANGE.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_CONTENT_RANGE.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_HOST.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_HOST.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_VARY.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_VARY.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ORIGIN.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ORIGIN.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_REQUEST_METHOD.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_REQUEST_METHOD.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_REQUEST_HEADERS.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_REQUEST_HEADERS.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_ALLOW_ORIGIN.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_ALLOW_ORIGIN.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_ALLOW_METHODS.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_ALLOW_METHODS.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_ALLOW_HEADERS.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_ALLOW_HEADERS.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_MAX_AGE.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_MAX_AGE.to_string());
    assert_eq!(header.value, "some string".to_string());

    let header = Header { name: Header::_ACCESS_CONTROL_EXPOSE_HEADERS.into(), value: "some string".into() };
    assert_eq!(header.name, Header::_ACCESS_CONTROL_EXPOSE_HEADERS.to_string());
    assert_eq!(header.value, "some string".to_string());

    assert_eq!(Header::_ACCEPT, "Accept");
    assert_eq!(Header::_ACCEPT_CH, "Accept-CH");
    assert_eq!(Header::_ACCEPT_ENCODING, "Accept-Encoding");
    assert_eq!(Header::_ACCEPT_LANGUAGE, "Accept-Language");
    assert_eq!(Header::_ACCEPT_PATCH, "Accept-Patch");
    assert_eq!(Header::_AGE, "Age");
    assert_eq!(Header::_DATE_UNIX_EPOCH_NANOS, "Date-Unix-Epoch-Nanos");
    assert_eq!(Header::_ALT_SVC, "Alt-Svc");
    assert_eq!(Header::_AUTHORIZATION, "Authorization");
    assert_eq!(Header::_CACHE_CONTROL, "Cache-Control");
    assert_eq!(Header::_CLEAR_SITE_DATA, "Clear-Site-Data");
    assert_eq!(Header::_CONTENT_DISPOSITION, "Content-Disposition");
    assert_eq!(Header::_CONTENT_LANGUAGE, "Content-Language");
    assert_eq!(Header::_CONTENT_LENGTH, "Content-Length");
    assert_eq!(Header::_CONTENT_LOCATION, "Content-Location");
    assert_eq!(Header::_CONTENT_SECURITY_POLICY, "Content-Security-Policy");
    assert_eq!(Header::_CONTENT_SECURITY_POLICY_REPORT_ONLY, "Content-Security-Policy-Report-Only");
    assert_eq!(Header::_COOKIE, "Cookie");
    assert_eq!(Header::_CROSS_ORIGIN_EMBEDDER_POLICY, "Cross-Origin-Embedder-Policy");
    assert_eq!(Header::_CROSS_ORIGIN_OPENER_POLICY, "Cross-Origin-Opener-Policy");
    assert_eq!(Header::_CROSS_ORIGIN_RESOURCE_POLICY, "Cross-Origin-Resource-Policy");
    assert_eq!(Header::_DATE, "Date");
    assert_eq!(Header::_DEVICE_MEMORY, "Device-Memory");
    assert_eq!(Header::_DIGEST, "Digest");
    assert_eq!(Header::_DOWNLINK, "Downlink");
    assert_eq!(Header::_EARLY_DATA, "Early-Data");
    assert_eq!(Header::_ECT, "ECT");
    assert_eq!(Header::_ETAG, "ETag");
    assert_eq!(Header::_EXPECT, "Expect");
    assert_eq!(Header::_EXPIRES, "Expires");
    assert_eq!(Header::_FEATURE_POLICY, "Feature-Policy");
    assert_eq!(Header::_PERMISSIONS_POLICY, "Permissions-Policy");
    assert_eq!(Header::_FROM, "From");
    assert_eq!(Header::_IF_MODIFIED_SINCE, "If-Modified-Since");
    assert_eq!(Header::_IF_NONE_MATCH, "If-None-Match");
    assert_eq!(Header::_IF_RANGE, "If-Range");
    assert_eq!(Header::_IF_UNMODIFIED_SINCE, "If-Unmodified-Since");
    assert_eq!(Header::_LINK, "Link");
    assert_eq!(Header::_LOCATION, "Location");
    assert_eq!(Header::_MAX_FORWARDS, "Max-Forwards");
    assert_eq!(Header::_NEL, "NEL");
    assert_eq!(Header::_PROXY_AUTHENTICATE, "Proxy-Authenticate");
    assert_eq!(Header::_PROXY_AUTHORIZATION, "Proxy-Authorization");
    assert_eq!(Header::_REFERER, "Referer");
    assert_eq!(Header::_REFERRER_POLICY, "Referrer-Policy");
    assert_eq!(Header::_RTT, "RTT");
    assert_eq!(Header::_SAVE_DATA, "Save-Data");
    assert_eq!(Header::_SEC_CH_UA, "Sec-CH-UA");
    assert_eq!(Header::_SEC_CH_UA_ARCH, "Sec-CH-UA-Arch");
    assert_eq!(Header::_SEC_CH_UA_BITNESS, "Sec-CH-UA-Bitness");
    assert_eq!(Header::_SEC_CH_UA_FULL_VERSION_LIST, "Sec-CH-UA-Full-Version-List");
    assert_eq!(Header::_SEC_CH_UA_MOBILE, "Sec-CH-UA-Mobile");
    assert_eq!(Header::_SEC_CH_UA_MODEL, "Sec-CH-UA-Model");
    assert_eq!(Header::_SEC_CH_UA_PLATFORM, "Sec-CH-UA-Platform");
    assert_eq!(Header::_SEC_CH_UA_PLATFORM_VERSION, "Sec-CH-UA-Platform-Version");
    assert_eq!(Header::_SEC_FETCH_DEST, "Sec-Fetch-Dest");
    assert_eq!(Header::_SEC_FETCH_MODE, "Sec-Fetch-Mode");
    assert_eq!(Header::_SEC_FETCH_SITE, "Sec-Fetch-Site");
    assert_eq!(Header::_SEC_FETCH_USER, "Sec-Fetch-User");
    assert_eq!(Header::_SERVER, "Server");
    assert_eq!(Header::_SET_COOKIE, "Set-Cookie");
    assert_eq!(Header::_SET_COOKIE, "Set-Cookie");
    assert_eq!(Header::_SOURCE_MAP, "SourceMap");
    assert_eq!(Header::_STRICT_TRANSPORT_SECURITY, "Strict-Transport-Security");
    assert_eq!(Header::_TE, "TE");
    assert_eq!(Header::_TIMING_ALLOW_ORIGIN, "Timing-Allow-Origin");
    assert_eq!(Header::_TRAILER, "Trailer");
    assert_eq!(Header::_TRANSFER_ENCODING, "Transfer-Encoding");
    assert_eq!(Header::_UPGRADE, "Upgrade");
    assert_eq!(Header::_USER_AGENT, "User-Agent");
    assert_eq!(Header::_VARY, "Vary");
    assert_eq!(Header::_VIA, "Via");
    assert_eq!(Header::_WANT_DIGEST, "Want-Digest");
    assert_eq!(Header::_WWW_AUTHENTICATE, "WWW-Authenticate");
    assert_eq!(Header::_X_CONTENT_TYPE_OPTIONS, "X-Content-Type-Options");
    assert_eq!(Header::_X_CONTENT_TYPE_OPTIONS_VALUE_NOSNIFF, "nosniff");
    assert_eq!(Header::_X_FRAME_OPTIONS, "X-Frame-Options");
    assert_eq!(Header::_X_FRAME_OPTIONS_VALUE_DENY, "DENY");
    assert_eq!(Header::_X_FRAME_OPTIONS_VALUE_SAME_ORIGIN, "SAMEORIGIN");
    assert_eq!(Header::_LAST_MODIFIED, "Last-Modified");
    assert_eq!(Header::_LAST_MODIFIED_UNIX_EPOCH_NANOS, "Last-Modified-Unix-Epoch-Nanos");

    assert_eq!(Header::_STRICT_TRANSPORT_SECURITY_VALUE_DEFAULT, "max-age=31536000; includeSubDomains");
    assert_eq!(Header::_REFERRER_POLICY_VALUE_DEFAULT, "strict-origin-when-cross-origin");
    assert_eq!(Header::_PERMISSIONS_POLICY_VALUE_DEFAULT, "geolocation=(), microphone=(), camera=()");
    assert_eq!(Header::_CONTENT_SECURITY_POLICY_VALUE_DEFAULT, "default-src 'self'");
}

#[test]
fn hsts_header() {
    let h = Header::get_hsts_header();
    assert_eq!(h.name, Header::_STRICT_TRANSPORT_SECURITY);
    assert_eq!(h.value, Header::_STRICT_TRANSPORT_SECURITY_VALUE_DEFAULT);
}

#[test]
fn security_headers_are_opt_in() {
    use crate::server_config::ServerConfig;
    let request = crate::request::Request { method: "GET".to_string(), request_uri: "/".to_string(), http_version: "HTTP/1.1".to_string(), headers: vec![], body: Default::default() };
    let opt_in = [Header::_X_FRAME_OPTIONS, Header::_REFERRER_POLICY, Header::_PERMISSIONS_POLICY, Header::_CONTENT_SECURITY_POLICY, Header::_DATE_UNIX_EPOCH_NANOS];

    // Default (18.0.0): none of them; nosniff and Date always.
    let defaults = Header::get_header_list_with_config(&request, &ServerConfig::default());
    for name in opt_in {
        assert!(!defaults.iter().any(|h| h.name == name), "{name} sent by default");
    }
    assert!(defaults.iter().any(|h| h.name == Header::_X_CONTENT_TYPE_OPTIONS));
    assert!(defaults.iter().any(|h| h.name == Header::_DATE));

    let config = ServerConfig {
        security_headers: true,
        nanos_headers: true,
        csp: Header::_CONTENT_SECURITY_POLICY_VALUE_DEFAULT.to_string(),
        ..ServerConfig::default()
    };
    let headers = Header::get_header_list_with_config(&request, &config);
    let value = |name: &str| headers.iter().find(|h| h.name == name).map(|h| h.value.to_string());
    assert_eq!(Some(Header::_X_FRAME_OPTIONS_VALUE_SAME_ORIGIN.to_string()), value(Header::_X_FRAME_OPTIONS));
    assert_eq!(Some(Header::_REFERRER_POLICY_VALUE_DEFAULT.to_string()), value(Header::_REFERRER_POLICY));
    assert_eq!(Some(Header::_PERMISSIONS_POLICY_VALUE_DEFAULT.to_string()), value(Header::_PERMISSIONS_POLICY));
    assert_eq!(Some(Header::_CONTENT_SECURITY_POLICY_VALUE_DEFAULT.to_string()), value(Header::_CONTENT_SECURITY_POLICY));
    assert!(value(Header::_DATE_UNIX_EPOCH_NANOS).is_some_and(|v| v.parse::<u128>().is_ok()));
}

#[test]
fn security_headers_setting_brings_the_default_csp_unless_csp_is_set() {
    let _g = crate::test_env::lock();
    use crate::server_config::ServerConfig;
    let saved: Vec<_> = ["RWS_CONFIG_SECURITY_HEADERS", "RWS_CONFIG_CSP", "RWS_CONFIG_NANOS_HEADERS"].iter().map(|k| (*k, std::env::var(k).ok())).collect();

    std::env::remove_var("RWS_CONFIG_CSP");
    std::env::remove_var("RWS_CONFIG_SECURITY_HEADERS");
    std::env::remove_var("RWS_CONFIG_NANOS_HEADERS");
    let off = ServerConfig::from_env();
    assert!(!off.security_headers && !off.nanos_headers && off.csp.is_empty());

    std::env::set_var("RWS_CONFIG_SECURITY_HEADERS", "true");
    std::env::set_var("RWS_CONFIG_NANOS_HEADERS", "TRUE");
    let on = ServerConfig::from_env();
    assert!(on.security_headers && on.nanos_headers);
    assert_eq!(Header::_CONTENT_SECURITY_POLICY_VALUE_DEFAULT, on.csp);

    std::env::set_var("RWS_CONFIG_CSP", "");
    assert_eq!("", ServerConfig::from_env().csp, "an explicit empty CSP still suppresses it");
    std::env::set_var("RWS_CONFIG_SECURITY_HEADERS", "false");
    std::env::set_var("RWS_CONFIG_CSP", "default-src 'none'");
    assert_eq!("default-src 'none'", ServerConfig::from_env().csp, "an explicit CSP is sent without the setting");

    for (k, v) in saved {
        match v { Some(v) => std::env::set_var(k, v), None => std::env::remove_var(k) }
    }
    crate::server_config::invalidate();
}

#[test]
fn header_new_borrows_static_strings_and_owns_strings() {
    use std::borrow::Cow;
    let h = Header::new(Header::_CONTENT_TYPE, "text/plain");
    assert!(matches!(h.name, Cow::Borrowed(_)) && matches!(h.value, Cow::Borrowed(_)));
    let h = Header::new("X-Id", format!("{}-{}", 1, 2));
    assert!(matches!(h.value, Cow::Owned(_)));
    assert_eq!("1-2", h.value);
    assert_eq!("X-Id: 1-2", h.as_string());
}

// The standard headers are cloned onto every response: all but the
// per-request Date values must be borrowed, so that clone allocates nothing.
#[test]
fn standard_header_list_is_borrowed_except_date() {
    use std::borrow::Cow;
    use crate::server_config::ServerConfig;
    let request = crate::request::Request { method: "GET".to_string(), request_uri: "/".to_string(), http_version: "HTTP/1.1".to_string(), headers: vec![], body: Default::default() };
    for csp in ["", "default-src 'self'", "default-src 'none'"] {
        let config = ServerConfig { csp: csp.to_string(), security_headers: true, nanos_headers: true, ..ServerConfig::default() };
        let list = Header::get_header_list_with_config(&request, &config);
        assert!(list.len() >= 9);
        for h in &list {
            assert!(matches!(h.name, Cow::Borrowed(_)), "{:?}", h.name);
            if h.name != Header::_DATE_UNIX_EPOCH_NANOS && h.name != Header::_DATE {
                assert!(matches!(h.value, Cow::Borrowed(_)), "{}: {:?}", h.name, h.value);
            }
        }
        assert_eq!(!csp.is_empty(), list.iter().any(|h| h.name == Header::_CONTENT_SECURITY_POLICY && h.value == csp));
    }
}

#[test]
fn name_from_borrows_common_names_in_their_exact_spelling() {
    use std::borrow::Cow;
    for name in ["Host", "host", "Content-Type", "content-type", "ETag", "X-Forwarded-For"] {
        let n = Header::name_from(name);
        assert!(matches!(n, Cow::Borrowed(_)), "{name}");
        assert_eq!(name, n);
    }
    // Other spellings and unknown names are kept exactly, owned.
    for name in ["HOST", "Content-type", "X-Custom", "", "host "] {
        let n = Header::name_from(name);
        assert!(matches!(n, Cow::Owned(_)), "{name:?}");
        assert_eq!(name, n);
    }
}