1use std::net::SocketAddr;
20
21use frame_host::FrameConfig;
22
23#[must_use]
33pub fn coherence_violations(config: &FrameConfig) -> Vec<String> {
34 if !config.ports_explicit {
35 return Vec::new();
36 }
37 let (Some(bind), Some(websocket)) = (config.frame.bind, config.bus.websocket.as_ref()) else {
38 return Vec::new();
41 };
42 port_coherence_violations(
43 bind,
44 config.bus.listen_address,
45 config.bus.health_listen_address,
46 websocket.listen_address,
47 &websocket.allowed_origins,
48 )
49}
50
51fn port_coherence_violations(
54 bind: SocketAddr,
55 bus: SocketAddr,
56 health: SocketAddr,
57 websocket: SocketAddr,
58 allowed_origins: &[String],
59) -> Vec<String> {
60 let mut violations = Vec::new();
61
62 let origin = format!("http://{bind}");
63 if !allowed_origins.iter().any(|allowed| allowed == &origin) {
64 violations.push(format!(
65 "[bus.websocket].allowed_origins {allowed_origins:?} does not contain the page origin \
66 \"{origin}\" formed from [frame].bind: the served page would be Origin-refused by the \
67 bus. If you change [frame].bind you must also add its origin to \
68 [bus.websocket].allowed_origins."
69 ));
70 }
71
72 let labelled = [
73 ("[frame].bind", bind.port()),
74 ("[bus].listen_address", bus.port()),
75 ("[bus].health_listen_address", health.port()),
76 ("[bus.websocket].listen_address", websocket.port()),
77 ];
78 for (index, (key_a, port_a)) in labelled.iter().enumerate() {
79 for (key_b, port_b) in &labelled[index + 1..] {
80 if port_a == port_b {
81 violations.push(format!(
82 "{key_a} and {key_b} both use port {port_a}: the four frame.toml ports \
83 ([frame].bind, [bus].listen_address, [bus].health_listen_address, \
84 [bus.websocket].listen_address) must be pairwise distinct or the host cannot \
85 bind them all."
86 ));
87 }
88 }
89 }
90
91 violations
92}
93
94#[cfg(test)]
95mod tests {
96 use super::port_coherence_violations;
97 use std::net::SocketAddr;
98
99 fn addr(port: u16) -> SocketAddr {
100 SocketAddr::from(([127, 0, 0, 1], port))
101 }
102
103 #[test]
104 fn a_coherent_default_family_has_no_violations() {
105 let violations = port_coherence_violations(
106 addr(4190),
107 addr(9460),
108 addr(9461),
109 addr(9463),
110 &["http://127.0.0.1:4190".to_owned()],
111 );
112 assert!(
113 violations.is_empty(),
114 "unexpected violations: {violations:?}"
115 );
116 }
117
118 #[test]
119 fn missing_origin_names_the_allowed_origins_key() {
120 let violations = port_coherence_violations(
123 addr(4191),
124 addr(9460),
125 addr(9461),
126 addr(9463),
127 &["http://127.0.0.1:4190".to_owned()],
128 );
129 assert_eq!(
130 violations.len(),
131 1,
132 "one violation expected: {violations:?}"
133 );
134 let message = &violations[0];
135 assert!(message.contains("[bus.websocket].allowed_origins"));
136 assert!(message.contains("[frame].bind"));
137 assert!(message.contains("http://127.0.0.1:4191"));
138 }
139
140 #[test]
141 fn a_duplicated_port_names_both_keys() {
142 let violations = port_coherence_violations(
144 addr(4190),
145 addr(9460),
146 addr(9460),
147 addr(9463),
148 &["http://127.0.0.1:4190".to_owned()],
149 );
150 assert_eq!(
151 violations.len(),
152 1,
153 "one violation expected: {violations:?}"
154 );
155 let message = &violations[0];
156 assert!(message.contains("[bus].listen_address"));
157 assert!(message.contains("[bus].health_listen_address"));
158 assert!(message.contains("9460"));
159 }
160
161 #[test]
162 fn both_kinds_of_violation_surface_together() {
163 let violations = port_coherence_violations(
165 addr(4191),
166 addr(9460),
167 addr(9461),
168 addr(9460),
169 &["http://127.0.0.1:4190".to_owned()],
170 );
171 assert_eq!(
172 violations.len(),
173 2,
174 "origin + duplicate expected: {violations:?}"
175 );
176 }
177}