rust-web-server 18.4.0

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 std::env;
use crate::entry_point::command_line_args::{CommandLineArgument, override_environment_variables_from_command_line_args};
use crate::entry_point::Config;

#[test]
fn command_line_arg_port() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(0).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "p");
    assert_eq!(argument.long_form, "port");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_PORT.to_string());
    assert_eq!(hint, "Port");

    CommandLineArgument::set_environment_variable(argument, "7888".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "7888");
}

#[test]
fn command_line_arg_ip() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(1).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "i");
    assert_eq!(argument.long_form, "ip");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_IP.to_string());
    assert_eq!(hint, "IP or domain");

    CommandLineArgument::set_environment_variable(argument, "127.0.0.1".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "127.0.0.1");
}

#[test]
fn command_line_arg_thread_count() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(2).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "t");
    assert_eq!(argument.long_form, "thread-count");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_THREAD_COUNT.to_string());
    assert_eq!(hint, "Number of threads");

    CommandLineArgument::set_environment_variable(argument, "200".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "200");
}

#[test]
fn command_line_arg_request_allocation_size() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(10).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "r");
    assert_eq!(argument.long_form, "request-allocation-size-in-bytes");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES.to_string());
    assert_eq!(hint, "In bytes, how much memory to allocate for each request");

    CommandLineArgument::set_environment_variable(argument, "10000".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "10000");
}

#[test]
fn command_line_arg_thread_cors_allow_all() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(3).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "a");
    assert_eq!(argument.long_form, "cors-allow-all");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_CORS_ALLOW_ALL.to_string());
    assert_eq!(hint, "If set to true, will allow all CORS requests, other CORS properties will be ignored");

    CommandLineArgument::set_environment_variable(argument, "true".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "true");
}

#[test]
fn command_line_arg_thread_cors_allow_origins() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(4).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "o");
    assert_eq!(argument.long_form, "cors-allow-origins");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_CORS_ALLOW_ORIGINS.to_string());
    assert_eq!(hint, "Comma separated list of allowed origins, example: https://foo.example,https://bar.example");

    CommandLineArgument::set_environment_variable(argument, "https://foo.example,https://bar.example".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "https://foo.example,https://bar.example");
}

#[test]
fn command_line_arg_thread_cors_allow_methods() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(5).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "m");
    assert_eq!(argument.long_form, "cors-allow-methods");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_CORS_ALLOW_METHODS.to_string());
    assert_eq!(hint, "Comma separated list of allowed methods, example: POST,PUT");

    CommandLineArgument::set_environment_variable(argument, "POST,PUT".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "POST,PUT");
}

#[test]
fn command_line_arg_thread_cors_allow_headers() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(6).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "h");
    assert_eq!(argument.long_form, "cors-allow-headers");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_CORS_ALLOW_HEADERS.to_string());
    assert_eq!(hint, "Comma separated list of allowed request headers, in lowercase, example: content-type,x-custom-header");

    CommandLineArgument::set_environment_variable(argument, "content-type,x-custom-header".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "content-type,x-custom-header");
}

#[test]
fn command_line_arg_thread_cors_allow_credentials() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(7).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "c");
    assert_eq!(argument.long_form, "cors-allow-credentials");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS.to_string());
    assert_eq!(hint, "If set to true, will allow to transmit credentials via CORS requests");

    CommandLineArgument::set_environment_variable(argument, "true".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "true");
}

#[test]
fn command_line_arg_thread_cors_expose_headers() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(8).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "e");
    assert_eq!(argument.long_form, "cors-expose-headers");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_CORS_EXPOSE_HEADERS.to_string());
    assert_eq!(hint, "Comma separated list of allowed response headers, in lowercase, example: content-type,x-custom-header");

    CommandLineArgument::set_environment_variable(argument, "content-type,x-custom-header".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "content-type,x-custom-header");
}

#[test]
fn command_line_arg_thread_cors_max_age() {
    let _g = crate::test_env::lock();
    let command_line_arg_list = CommandLineArgument::get_command_line_arg_list();

    let argument = command_line_arg_list.get(9).unwrap();
    let hint = argument._hint.as_ref().unwrap();
    assert_eq!(argument.short_form, "g");
    assert_eq!(argument.long_form, "cors-max-age");
    assert_eq!(argument.environment_variable, Config::RWS_CONFIG_CORS_MAX_AGE.to_string());
    assert_eq!(hint, "How long results of preflight requests can be cached (in seconds)");

    CommandLineArgument::set_environment_variable(argument, "86400".to_string());

    let env_var = env::var(&argument.environment_variable).unwrap();
    assert_eq!(env_var, "86400");
}

#[test]
fn parse() {
    let _g = crate::test_env::lock();
    let args_vec_as_str : Vec<&str> = "-i=127.0.0.1 -p=7777 -t=100 -a=false -o=https://foo.example,https://bar.example -m=GET,POST,PUT,DELETE -h=content-type,x-custom-header -c=true -e=content-type,x-custom-header -g=86400"
        .split_whitespace()
        .collect::<Vec<&str>>();

    let _args_vec_as_string : Vec<String> = args_vec_as_str.iter().map(|str| str.to_string()).collect::<Vec<String>>();

    let predefined_arguments_list = CommandLineArgument::get_command_line_arg_list();
    let _args_list : Vec<CommandLineArgument> = CommandLineArgument::_parse(_args_vec_as_string, predefined_arguments_list);

    let env_var = env::var(Config::RWS_CONFIG_CORS_MAX_AGE).unwrap();
    assert_eq!(env_var, "86400");

    let env_var = env::var(Config::RWS_CONFIG_CORS_EXPOSE_HEADERS).unwrap();
    assert_eq!(env_var, "content-type,x-custom-header");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS).unwrap();
    assert_eq!(env_var, "true");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_HEADERS).unwrap();
    assert_eq!(env_var, "content-type,x-custom-header");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_METHODS).unwrap();
    assert_eq!(env_var, "GET,POST,PUT,DELETE");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_ORIGINS).unwrap();
    assert_eq!(env_var, "https://foo.example,https://bar.example");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_ALL).unwrap();
    assert_eq!(env_var, "false");

    let env_var = env::var(Config::RWS_CONFIG_THREAD_COUNT).unwrap();
    assert_eq!(env_var, "100");

    let env_var = env::var(Config::RWS_CONFIG_PORT).unwrap();
    assert_eq!(env_var, "7777");

    let env_var = env::var(Config::RWS_CONFIG_IP).unwrap();
    assert_eq!(env_var, "127.0.0.1");
}

#[test]
fn parse_long_form() {
    let _g = crate::test_env::lock();
    let args_vec_as_str : Vec<&str> = "--ip=127.0.0.1 --port=7777 --thread-count=100 --cors-allow-all=false --cors-allow-origins=https://foo.example,https://bar.example --cors-allow-methods=GET,POST,PUT,DELETE --cors-allow-headers=content-type,x-custom-header --cors-allow-credentials=true --cors-expose-headers=content-type,x-custom-header --cors-max-age=86400"
        .split_whitespace()
        .collect::<Vec<&str>>();

    let _args_vec_as_string : Vec<String> = args_vec_as_str.iter().map(|str| str.to_string()).collect::<Vec<String>>();

    let predefined_arguments_list = CommandLineArgument::get_command_line_arg_list();
    let _args_list : Vec<CommandLineArgument> = CommandLineArgument::_parse(_args_vec_as_string, predefined_arguments_list);

    let env_var = env::var(Config::RWS_CONFIG_CORS_MAX_AGE).unwrap();
    assert_eq!(env_var, "86400");

    let env_var = env::var(Config::RWS_CONFIG_CORS_EXPOSE_HEADERS).unwrap();
    assert_eq!(env_var, "content-type,x-custom-header");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS).unwrap();
    assert_eq!(env_var, "true");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_HEADERS).unwrap();
    assert_eq!(env_var, "content-type,x-custom-header");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_METHODS).unwrap();
    assert_eq!(env_var, "GET,POST,PUT,DELETE");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_ORIGINS).unwrap();
    assert_eq!(env_var, "https://foo.example,https://bar.example");

    let env_var = env::var(Config::RWS_CONFIG_CORS_ALLOW_ALL).unwrap();
    assert_eq!(env_var, "false");

    let env_var = env::var(Config::RWS_CONFIG_THREAD_COUNT).unwrap();
    assert_eq!(env_var, "100");

    let env_var = env::var(Config::RWS_CONFIG_PORT).unwrap();
    assert_eq!(env_var, "7777");

    let env_var = env::var(Config::RWS_CONFIG_IP).unwrap();
    assert_eq!(env_var, "127.0.0.1");
}

#[test]
fn _override() {
    let _g = crate::test_env::lock();
    override_environment_variables_from_command_line_args()
}

fn args(list: &[&str]) -> Vec<String> {
    list.iter().map(|s| s.to_string()).collect()
}

#[test]
fn informational_flag_version_long_and_short() {
    use crate::entry_point::command_line_args::informational_flag_output;
    let expected = format!("rws {}\n", env!("CARGO_PKG_VERSION"));
    assert_eq!(Some(expected.clone()), informational_flag_output(&args(&["rws", "--version"])));
    assert_eq!(Some(expected), informational_flag_output(&args(&["rws", "--port=80", "-V"])));
}

#[test]
fn informational_flag_help_lists_every_option_and_env_var() {
    use crate::entry_point::command_line_args::informational_flag_output;
    let help = informational_flag_output(&args(&["rws", "--help"])).unwrap();
    for argument in CommandLineArgument::get_command_line_arg_list() {
        assert!(help.contains(&format!("--{}=", argument.long_form)), "missing --{}", argument.long_form);
        assert!(help.contains(&argument.environment_variable), "missing {}", argument.environment_variable);
    }
    assert!(help.contains("--version"));
}

#[test]
fn informational_flag_absent_starts_server_normally() {
    use crate::entry_point::command_line_args::informational_flag_output;
    assert_eq!(None, informational_flag_output(&args(&["rws"])));
    assert_eq!(None, informational_flag_output(&args(&["rws", "--port=7979", "-h=content-type"])));
    // The program name itself is never treated as a flag.
    assert_eq!(None, informational_flag_output(&args(&["--version"])));
}

#[test]
fn config_file_acme_section_and_redirect_port_reach_the_environment() {
    let _g = crate::test_env::lock();
    let toml = "http_redirect_port = 80\n\n[acme]\ndomains = [\"example.com\", \"www.example.com\"]\nemail = \"ops@example.com\"\nca_file = \"/etc/rws/ca.pem\"\n";
    crate::entry_point::config_file::read_config_file(std::io::Cursor::new(toml.as_bytes()), String::new()).unwrap();
    let redirect = env::var(Config::RWS_CONFIG_HTTP_REDIRECT_PORT).unwrap();
    let domains = env::var(Config::RWS_CONFIG_ACME_DOMAINS).unwrap();
    let email = env::var(Config::RWS_CONFIG_ACME_EMAIL).unwrap();
    let ca_file = env::var(Config::RWS_CONFIG_ACME_CA_FILE).unwrap();
    for var in [Config::RWS_CONFIG_HTTP_REDIRECT_PORT, Config::RWS_CONFIG_ACME_DOMAINS,
                Config::RWS_CONFIG_ACME_EMAIL, Config::RWS_CONFIG_ACME_CA_FILE] {
        env::set_var(var, "");
    }
    assert_eq!("80", redirect);
    assert_eq!("example.com,www.example.com", domains);
    assert_eq!("ops@example.com", email);
    assert_eq!("/etc/rws/ca.pem", ca_file);
}

#[test]
fn long_only_arguments_are_never_matched_by_a_bare_dash() {
    let _g = crate::test_env::lock();
    env::set_var(Config::RWS_CONFIG_ACME_EMAIL, "unchanged");
    CommandLineArgument::_parse(args(&["-=evil"]), CommandLineArgument::get_command_line_arg_list());
    let email = env::var(Config::RWS_CONFIG_ACME_EMAIL).unwrap();
    env::set_var(Config::RWS_CONFIG_ACME_EMAIL, "");
    assert_eq!("unchanged", email);
}