liminal-server 0.27.3

Standalone server for the liminal messaging bus
Documentation
use std::net::SocketAddr;

use crate::config::types::{Reach, ServerConfig};

/// ACCESS-010 R1: an exchange with no `[auth]` is open mode, and open mode
/// listens on loopback only. Every connection to it is admitted, so the one
/// boundary it has is the machine: `listen_address`, each
/// `extra_listen_addresses` entry and the `[websocket]` listener must name a
/// loopback IP. The health listener is a probe and is not covered. With
/// `[auth]` present nothing here applies but one rule: `[open]` beside it is
/// refused (ACCESS-011 R1). With `[open] reach = "network"` the loopback
/// bound is lifted by the operator's written word.
pub(super) fn validate_open_mode(config: &ServerConfig, errors: &mut Vec<String>) {
    if config.auth.is_some() {
        if config.open.is_some() {
            errors.push(
                "[open] names an exchange with no [auth]; this configuration has [auth], remove one"
                    .to_owned(),
            );
        }
        return;
    }
    // ACCESS-011 R1: the operator's written choice to reach the network.
    if config.open_reach() == Reach::Network {
        return;
    }
    refuse_off_loopback("listen_address", config.listen_address, errors);
    for (index, address) in config.extra_listen_addresses.iter().enumerate() {
        refuse_off_loopback(
            &format!("extra_listen_addresses[{index}]"),
            *address,
            errors,
        );
    }
    if let Some(websocket) = config.websocket.as_ref() {
        refuse_off_loopback("websocket.listen_address", websocket.listen_address, errors);
    }
}

fn refuse_off_loopback(field: &str, address: SocketAddr, errors: &mut Vec<String>) {
    if address.ip().is_loopback() {
        return;
    }
    errors.push(format!(
        "open mode (no [auth]): {field} {address} is not loopback; an exchange with no [auth] \
         listens on loopback only, give [auth] to listen elsewhere"
    ));
}