liminal-server 0.24.9

Standalone server for the liminal messaging bus
Documentation
//! CON-014 R2: the console served beside this exchange, admitted at every
//! address the machine is reached by. An Origin-bearing upgrade passes when
//! the Origin is listed byte-exact (F6, as before), or when it is an
//! `http://host[:port]` or `https://host[:port]` origin whose port is one the
//! configuration lists and whose host is the host of the request's own `Host`
//! header: the browser reached this machine at some address and fetched the
//! console from the same address on the console's port. A site at another
//! host on a listed port is refused as before; no wildcard exists.

use tungstenite::handshake::server::Request;

use super::AcceptorSettings;

/// Whether `origin` (the request's one `Origin` header, as sent) is admitted
/// under `settings` for `request`.
pub(super) fn origin_admitted(
    origin: &str,
    request: &Request,
    settings: &AcceptorSettings,
) -> bool {
    if settings
        .allowed_origins
        .iter()
        .any(|allowed| allowed == origin)
    {
        return true;
    }
    if settings.allowed_origin_ports.is_empty() {
        return false;
    }
    let Some((origin_host, origin_port)) = host_and_port(origin) else {
        return false;
    };
    // exactly one Host header, read as host[:port]; its port is the port the
    // upgrade arrived on and plays no part here
    let mut hosts = request.headers().get_all("Host").iter();
    let (Some(host), None) = (hosts.next(), hosts.next()) else {
        return false;
    };
    let Ok(host) = host.to_str() else {
        return false;
    };
    let request_host = without_port(host);
    settings.allowed_origin_ports.contains(&origin_port)
        && origin_host.eq_ignore_ascii_case(request_host)
}

/// `scheme://host[:port]` for http and https: the host as written (an IPv6
/// host keeps its brackets) and the port, the scheme's own when none is
/// written. Anything else, `null` included, is `None`.
fn host_and_port(origin: &str) -> Option<(&str, u16)> {
    let (scheme, rest) = origin.split_once("://")?;
    let own_port = match scheme {
        "http" => 80,
        "https" => 443,
        _ => return None,
    };
    if rest.is_empty() || rest.contains('/') {
        return None;
    }
    let host = without_port(rest);
    if host.is_empty() {
        return None;
    }
    let port = if host.len() == rest.len() {
        own_port
    } else {
        rest[host.len() + 1..].parse().ok()?
    };
    Some((host, port))
}

/// `host[:port]` without its port: a bracketed IPv6 host keeps its brackets,
/// and a bare IPv6 address (no brackets, several colons) is taken whole.
fn without_port(authority: &str) -> &str {
    if authority.starts_with('[') {
        return authority
            .find(']')
            .map_or(authority, |end| &authority[..=end]);
    }
    match authority.rsplit_once(':') {
        Some((host, port)) if !host.contains(':') && port.bytes().all(|b| b.is_ascii_digit()) => {
            host
        }
        _ => authority,
    }
}