rust-web-server 18.10.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
#[cfg(test)]
mod tests;

use crate::header::Header;
use crate::request::{METHOD, Request};
use crate::response::Error;
use crate::server_config::ServerConfig;

#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Cors {
    pub allow_all: bool,
    pub allow_origins: Vec<String>,
    pub allow_methods: Vec<String>,
    pub allow_headers: Vec<String>,
    pub allow_credentials: bool,
    pub expose_headers: Vec<String>,
    pub max_age: String,
}

impl Cors {
    pub const MAX_AGE: &'static str = "86400";

    /// `true` when `origin` is one of the comma-separated `allowed` origins —
    /// the whole origin, not a substring of the list (#58): with
    /// `https://app.example.com` allowed, `https://app.example.co` and
    /// `https://app.example.com.evil` used to match too. ASCII case is
    /// ignored (scheme and host are case-insensitive).
    pub fn origin_is_allowed(allowed: &str, origin: &str) -> bool {
        let origin = origin.trim();
        !origin.is_empty() && allowed.split(',').any(|candidate| candidate.trim().eq_ignore_ascii_case(origin))
    }

    pub fn get_vary_header_value() -> String {
        Header::_ORIGIN.to_string()
    }

    pub fn allow_all(request: &Request) -> Result<Vec<Header>, Error> {
        let mut headers : Vec<Header> = vec![];
        let origin = request.header(Header::_ORIGIN);
        if origin.is_some() {
            let allow_origin = Header {
                name: Header::_ACCESS_CONTROL_ALLOW_ORIGIN.into(),
                value: origin.unwrap().value.to_string().into()
            };
            headers.push(allow_origin);

            let allow_credentials = Header {
                name: Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.into(),
                value: "true".into()
            };
            headers.push(allow_credentials);

            let is_options = request.method == METHOD.options;
            if is_options {
                let method = request.header(Header::_ACCESS_CONTROL_REQUEST_METHOD);
                if method.is_some() {
                    let allow_method = Header {
                        name: Header::_ACCESS_CONTROL_ALLOW_METHODS.into(),
                        value: method.unwrap().value.to_string().into()
                    };
                    headers.push(allow_method);
                }

                let access_control_request_headers = request.header(Header::_ACCESS_CONTROL_REQUEST_HEADERS);
                if access_control_request_headers.is_some() {
                    let request_headers = access_control_request_headers.unwrap();
                    let allow_headers = Header {
                        name: Header::_ACCESS_CONTROL_ALLOW_HEADERS.into(),
                        value: request_headers.value.to_lowercase().into(),
                    };
                    headers.push(allow_headers);

                    let expose_headers = Header {
                        name: Header::_ACCESS_CONTROL_EXPOSE_HEADERS.into(),
                        value: request_headers.value.to_lowercase().into(),
                    };
                    headers.push(expose_headers);
                }

                let max_age = Header {
                    name: Header::_ACCESS_CONTROL_MAX_AGE.into(),
                    value: Cors::MAX_AGE.into()
                };
                headers.push(max_age);
            }

        }

        Ok(headers)
    }

    pub fn _process(request: &Request, cors: &Cors) -> Result<Vec<Header>, Error> {
        let mut headers : Vec<Header> = vec![];

        let allow_origins = cors.allow_origins.join(",");
        let boxed_origin = request.header(Header::_ORIGIN);

        if boxed_origin.is_none() {
            return Ok(headers)
        }

        let origin = boxed_origin.unwrap();
        let origin_value = format!("{}", origin.value);

        let is_valid_origin = Cors::origin_is_allowed(&allow_origins, &origin_value);
        if !is_valid_origin {
            return Ok(headers)
        }

        let allow_origin = Header {
            name: Header::_ACCESS_CONTROL_ALLOW_ORIGIN.into(),
            value: origin_value.into()
        };
        headers.push(allow_origin);

        if cors.allow_credentials {
            let allow_credentials = Header {
                name: Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.into(),
                value: cors.allow_credentials.to_string().into()
            };
            headers.push(allow_credentials);
        }

        // On the actual response, where the browser reads it (#58).
        if !cors.expose_headers.is_empty() {
            headers.push(Header {
                name: Header::_ACCESS_CONTROL_EXPOSE_HEADERS.into(),
                value: cors.expose_headers.join(",").to_lowercase().into(),
            });
        }

        let is_options = request.method == METHOD.options;
        if is_options {
            let methods = cors.allow_methods.join(",");
            let allow_methods = Header {
                name: Header::_ACCESS_CONTROL_ALLOW_METHODS.into(),
                value: methods.into()
            };
            headers.push(allow_methods);

            let allow_headers_value = cors.allow_headers.join(",");
            let allow_headers = Header {
                name: Header::_ACCESS_CONTROL_ALLOW_HEADERS.into(),
                value: allow_headers_value.to_lowercase().into()
            };
            headers.push(allow_headers);

            let max_age = Header {
                name: Header::_ACCESS_CONTROL_MAX_AGE.into(),
                value: cors.max_age.to_string().into()
            };
            headers.push(max_age);
        }

        Ok(headers)
    }

    /// Build CORS headers using the provided [`ServerConfig`].
    ///
    /// This is the primary implementation. [`get_headers`] is the legacy
    /// entry point that reads from environment variables; all new call-sites
    /// should prefer this version.
    pub fn get_headers_from_config(request: &Request, config: &ServerConfig) -> Vec<Header> {
        if config.cors_allow_all {
            return Cors::allow_all(request).unwrap_or_default();
        }
        Cors::process_using_config(request, config).unwrap_or_default()
    }

    fn process_using_config(request: &Request, config: &ServerConfig) -> Result<Vec<Header>, Error> {
        let mut headers: Vec<Header> = vec![];

        let boxed_origin = request.header(Header::_ORIGIN);
        if boxed_origin.is_none() {
            return Ok(headers);
        }
        let origin_value = boxed_origin.unwrap().value.clone();

        if !Cors::origin_is_allowed(&config.cors_allow_origins, &origin_value) {
            return Ok(headers);
        }

        headers.push(Header {
            name: Header::_ACCESS_CONTROL_ALLOW_ORIGIN.into(),
            value: origin_value,
        });

        let credentials_str = &config.cors_allow_credentials;
        if credentials_str.eq_ignore_ascii_case("true") {
            headers.push(Header {
                name: Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.into(),
                value: "true".into(),
            });
        }

        // On the actual response, where the browser reads it — it was sent
        // only on the preflight, which ignores it (#58).
        if !config.cors_expose_headers.is_empty() {
            headers.push(Header {
                name: Header::_ACCESS_CONTROL_EXPOSE_HEADERS.into(),
                value: config.cors_expose_headers.to_lowercase().into(),
            });
        }

        if request.method == METHOD.options {
            if !config.cors_allow_methods.is_empty() {
                headers.push(Header {
                    name: Header::_ACCESS_CONTROL_ALLOW_METHODS.into(),
                    value: config.cors_allow_methods.clone().into(),
                });
            }
            if !config.cors_allow_headers.is_empty() {
                headers.push(Header {
                    name: Header::_ACCESS_CONTROL_ALLOW_HEADERS.into(),
                    value: config.cors_allow_headers.to_lowercase().into(),
                });
            }
            if !config.cors_max_age.is_empty() {
                headers.push(Header {
                    name: Header::_ACCESS_CONTROL_MAX_AGE.into(),
                    value: config.cors_max_age.clone().into(),
                });
            }
        }

        Ok(headers)
    }

    /// Legacy entry point that reads CORS settings from environment variables.
    ///
    /// Prefer [`get_headers_from_config`] when a [`ServerConfig`] is available
    /// (e.g. inside [`App::execute`]). This variant is kept for call-sites that
    /// do not yet have an `App`-level config reference.
    pub fn process_using_default_config(request: &Request) -> Result<Vec<Header>, Error> {
        let config = ServerConfig::current();
        Self::process_using_config(request, &config)
    }

    /// Legacy entry point that reads CORS settings from environment variables.
    ///
    /// Prefer [`get_headers_from_config`] when a [`ServerConfig`] is available.
    pub fn get_headers(request: &Request) -> Vec<Header> {
        let config = ServerConfig::current();
        Cors::get_headers_from_config(request, &config)
    }
}