use std::net::SocketAddr;
use frame_host::FrameConfig;
#[must_use]
pub fn coherence_violations(config: &FrameConfig) -> Vec<String> {
if !config.ports_explicit {
return Vec::new();
}
let (Some(bind), Some(websocket)) = (config.frame.bind, config.bus.websocket.as_ref()) else {
return Vec::new();
};
port_coherence_violations(
bind,
config.bus.listen_address,
config.bus.health_listen_address,
websocket.listen_address,
&websocket.allowed_origins,
)
}
fn port_coherence_violations(
bind: SocketAddr,
bus: SocketAddr,
health: SocketAddr,
websocket: SocketAddr,
allowed_origins: &[String],
) -> Vec<String> {
let mut violations = Vec::new();
let origin = format!("http://{bind}");
if !allowed_origins.iter().any(|allowed| allowed == &origin) {
violations.push(format!(
"[bus.websocket].allowed_origins {allowed_origins:?} does not contain the page origin \
\"{origin}\" formed from [frame].bind: the served page would be Origin-refused by the \
bus. If you change [frame].bind you must also add its origin to \
[bus.websocket].allowed_origins."
));
}
let labelled = [
("[frame].bind", bind.port()),
("[bus].listen_address", bus.port()),
("[bus].health_listen_address", health.port()),
("[bus.websocket].listen_address", websocket.port()),
];
for (index, (key_a, port_a)) in labelled.iter().enumerate() {
for (key_b, port_b) in &labelled[index + 1..] {
if port_a == port_b {
violations.push(format!(
"{key_a} and {key_b} both use port {port_a}: the four frame.toml ports \
([frame].bind, [bus].listen_address, [bus].health_listen_address, \
[bus.websocket].listen_address) must be pairwise distinct or the host cannot \
bind them all."
));
}
}
}
violations
}
#[cfg(test)]
mod tests {
use super::port_coherence_violations;
use std::net::SocketAddr;
fn addr(port: u16) -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], port))
}
#[test]
fn a_coherent_default_family_has_no_violations() {
let violations = port_coherence_violations(
addr(4190),
addr(9460),
addr(9461),
addr(9463),
&["http://127.0.0.1:4190".to_owned()],
);
assert!(
violations.is_empty(),
"unexpected violations: {violations:?}"
);
}
#[test]
fn missing_origin_names_the_allowed_origins_key() {
let violations = port_coherence_violations(
addr(4191),
addr(9460),
addr(9461),
addr(9463),
&["http://127.0.0.1:4190".to_owned()],
);
assert_eq!(
violations.len(),
1,
"one violation expected: {violations:?}"
);
let message = &violations[0];
assert!(message.contains("[bus.websocket].allowed_origins"));
assert!(message.contains("[frame].bind"));
assert!(message.contains("http://127.0.0.1:4191"));
}
#[test]
fn a_duplicated_port_names_both_keys() {
let violations = port_coherence_violations(
addr(4190),
addr(9460),
addr(9460),
addr(9463),
&["http://127.0.0.1:4190".to_owned()],
);
assert_eq!(
violations.len(),
1,
"one violation expected: {violations:?}"
);
let message = &violations[0];
assert!(message.contains("[bus].listen_address"));
assert!(message.contains("[bus].health_listen_address"));
assert!(message.contains("9460"));
}
#[test]
fn both_kinds_of_violation_surface_together() {
let violations = port_coherence_violations(
addr(4191),
addr(9460),
addr(9461),
addr(9460),
&["http://127.0.0.1:4190".to_owned()],
);
assert_eq!(
violations.len(),
2,
"origin + duplicate expected: {violations:?}"
);
}
}