1use std::net::SocketAddr;
9
10use super::{Refusal, ServerConfig};
11use crate::auth::MIN_TOKEN_LEN;
12
13impl ServerConfig {
14 pub fn validate(&self) -> Result<(), Refusal> {
27 if self.sections.is_empty() {
28 return Err(Refusal::NoSections);
29 }
30 if self.clients.is_empty() {
31 return Err(Refusal::NoClients);
32 }
33
34 let mut seen = Vec::new();
35
36 for section in &self.sections {
37 for (part, value) in [
44 ("application", §ion.application),
45 ("profile", §ion.profile),
46 ] {
47 if !crate::routes::is_name(value) {
48 return Err(Refusal::UnroutableSection {
49 application: section.application.clone(),
50 profile: section.profile.clone(),
51 part,
52 });
53 }
54 }
55
56 let pair = (section.application.as_str(), section.profile.as_str());
57
58 if seen.contains(&pair) {
59 return Err(Refusal::DuplicateSection {
60 application: section.application.clone(),
61 profile: section.profile.clone(),
62 });
63 }
64
65 seen.push(pair);
66 }
67
68 let mut names: Vec<&str> = Vec::new();
69 let mut anonymous = 0;
70
71 for client in &self.clients {
72 if names.contains(&client.name.as_str()) {
73 return Err(Refusal::DuplicateClient {
74 name: client.name.clone(),
75 });
76 }
77
78 names.push(&client.name);
79
80 match &client.token {
81 None => {
82 anonymous += 1;
83
84 if !self.allow_anonymous {
85 return Err(Refusal::AnonymousNotAllowed {
86 client: client.name.clone(),
87 });
88 }
89 if anonymous > 1 {
90 return Err(Refusal::SeveralAnonymousClients);
91 }
92 }
93 Some(token) if token.len() < MIN_TOKEN_LEN => {
94 return Err(Refusal::WeakToken {
95 client: client.name.clone(),
96 });
97 }
98 Some(_) => {}
99 }
100
101 for application in &client.applications {
106 if !self
107 .sections
108 .iter()
109 .any(|section| §ion.application == application)
110 {
111 return Err(Refusal::UnservedGrant {
112 client: client.name.clone(),
113 application: application.clone(),
114 });
115 }
116 }
117 }
118
119 for (index, client) in self.clients.iter().enumerate() {
123 let Some(token) = &client.token else { continue };
124
125 for other in self.clients.iter().skip(index + 1) {
126 if other.token.as_ref().is_some_and(|it| token.same_as(it)) {
127 return Err(Refusal::DuplicateToken);
128 }
129 }
130 }
131
132 let address = self
133 .bind
134 .parse::<SocketAddr>()
135 .map_err(|_| Refusal::UnparsableBind {
136 bind: self.bind.clone(),
137 })?;
138
139 match &self.tls {
151 Some(tls) => {
152 if !cfg!(feature = "tls") {
158 return Err(Refusal::TlsUnsupported);
159 }
160 if tls.crl.is_some() {
165 return Err(Refusal::RevocationUnsupported);
166 }
167 if tls.certificate.trim().is_empty() {
168 return Err(Refusal::TlsPathMissing { key: "certificate" });
169 }
170 if tls.key.trim().is_empty() {
171 return Err(Refusal::TlsPathMissing { key: "key" });
172 }
173 if tls
174 .client_ca
175 .as_ref()
176 .is_some_and(|it| it.trim().is_empty())
177 {
178 return Err(Refusal::TlsPathMissing { key: "client_ca" });
179 }
180 if self.insecure {
181 return Err(Refusal::InsecureWithTls);
182 }
183 }
184 None => {
185 if !address.ip().is_loopback() && !self.insecure {
186 return Err(Refusal::ExposedBind {
187 bind: self.bind.clone(),
188 });
189 }
190 }
191 }
192
193 Ok(())
194 }
195
196 pub fn address(&self) -> Result<SocketAddr, Refusal> {
204 self.bind
205 .parse::<SocketAddr>()
206 .map_err(|_| Refusal::UnparsableBind {
207 bind: self.bind.clone(),
208 })
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::auth::Token;
216 use crate::config::{ClientConfig, SectionConfig, TlsConfig};
217
218 fn section(application: &str, profile: &str) -> SectionConfig {
219 SectionConfig {
220 application: application.to_owned(),
221 profile: profile.to_owned(),
222 files: vec!["config.toml".to_owned()],
223 env_prefix: None,
224 whole_document: false,
225 }
226 }
227
228 fn client(name: &str, token: Option<&str>, applications: &[&str]) -> ClientConfig {
229 ClientConfig {
230 name: name.to_owned(),
231 token: token.map(Token::new),
232 applications: applications.iter().map(|it| (*it).to_owned()).collect(),
233 }
234 }
235
236 const GOOD: &str = "0123456789abcdef0123456789abcdef";
237 const OTHER: &str = "fedcba9876543210fedcba9876543210";
238
239 fn valid() -> ServerConfig {
240 ServerConfig {
241 sections: vec![section("billing", "prod")],
242 clients: vec![client("billing-pod", Some(GOOD), &["billing"])],
243 ..ServerConfig::default()
244 }
245 }
246
247 #[test]
248 fn a_complete_configuration_starts() {
249 assert_eq!(valid().validate(), Ok(()));
250 }
251
252 #[test]
253 fn an_empty_roster_is_refused_at_both_ends() {
254 let mut config = valid();
255 config.sections.clear();
256 assert_eq!(config.validate(), Err(Refusal::NoSections));
257
258 let mut config = valid();
259 config.clients.clear();
260 assert_eq!(config.validate(), Err(Refusal::NoClients));
261 }
262
263 #[test]
264 fn a_duplicate_section_is_refused_but_two_profiles_are_not() {
265 let mut config = valid();
266 config.sections.push(section("billing", "prod"));
267
268 assert_eq!(
269 config.validate(),
270 Err(Refusal::DuplicateSection {
271 application: "billing".to_owned(),
272 profile: "prod".to_owned(),
273 })
274 );
275
276 let mut config = valid();
277 config.sections.push(section("billing", "staging"));
278 assert_eq!(config.validate(), Ok(()));
279 }
280
281 #[test]
285 fn a_section_no_route_could_name_is_refused_at_startup() {
286 for (part, application, profile) in [
287 ("application", "billing api", "prod"),
288 ("profile", "billing", ".hidden"),
289 ("application", "", "prod"),
290 ("profile", "billing", "../etc"),
291 ] {
292 let mut config = valid();
293 config.sections = vec![section(application, profile)];
294 config.clients = vec![client("pod", Some(GOOD), &[application])];
295
296 assert_eq!(
297 config.validate(),
298 Err(Refusal::UnroutableSection {
299 application: application.to_owned(),
300 profile: profile.to_owned(),
301 part,
302 }),
303 "`{application}`/`{profile}` must be refused"
304 );
305 }
306
307 let mut config = valid();
309 config.sections = vec![section("billing-api.v2", "prod_1")];
310 config.clients = vec![client("pod", Some(GOOD), &["billing-api.v2"])];
311 assert_eq!(config.validate(), Ok(()));
312
313 let mut config = valid();
315 let long = "a".repeat(65);
316 config.sections = vec![section(&long, "prod")];
317 config.clients = vec![client("pod", Some(GOOD), &[&long])];
318 assert!(matches!(
319 config.validate(),
320 Err(Refusal::UnroutableSection { .. })
321 ));
322 }
323
324 #[test]
325 fn duplicate_client_names_and_tokens_are_refused() {
326 let mut config = valid();
327 config
328 .clients
329 .push(client("billing-pod", Some(OTHER), &["billing"]));
330
331 assert_eq!(
332 config.validate(),
333 Err(Refusal::DuplicateClient {
334 name: "billing-pod".to_owned()
335 })
336 );
337
338 let mut config = valid();
339 config
340 .clients
341 .push(client("other", Some(GOOD), &["billing"]));
342
343 assert_eq!(config.validate(), Err(Refusal::DuplicateToken));
344 }
345
346 #[test]
347 fn a_short_token_is_refused() {
348 let mut config = valid();
349 config.clients = vec![client("billing-pod", Some("short"), &["billing"])];
350
351 assert_eq!(
352 config.validate(),
353 Err(Refusal::WeakToken {
354 client: "billing-pod".to_owned()
355 })
356 );
357 }
358
359 #[test]
362 fn anonymous_access_needs_an_explicit_opt_in() {
363 let mut config = valid();
364 config.clients = vec![client("anonymous", None, &["billing"])];
365
366 assert_eq!(
367 config.validate(),
368 Err(Refusal::AnonymousNotAllowed {
369 client: "anonymous".to_owned()
370 })
371 );
372
373 config.allow_anonymous = true;
374 assert_eq!(config.validate(), Ok(()));
375
376 config.clients.push(client("also", None, &["billing"]));
377 assert_eq!(config.validate(), Err(Refusal::SeveralAnonymousClients));
378 }
379
380 #[test]
381 fn a_grant_nothing_serves_is_refused() {
382 let mut config = valid();
383 config.clients = vec![client("billing-pod", Some(GOOD), &["biling"])];
384
385 assert_eq!(
386 config.validate(),
387 Err(Refusal::UnservedGrant {
388 client: "billing-pod".to_owned(),
389 application: "biling".to_owned(),
390 })
391 );
392 }
393
394 #[test]
395 fn a_non_loopback_bind_is_refused_without_the_flag() {
396 let mut config = valid();
397 config.bind = "0.0.0.0:8080".to_owned();
398
399 let refusal = config.validate().unwrap_err();
400 assert_eq!(
401 refusal,
402 Refusal::ExposedBind {
403 bind: "0.0.0.0:8080".to_owned()
404 }
405 );
406 assert!(
407 refusal.to_string().contains("insecure"),
408 "the refusal has to name the key that fixes it: {refusal}"
409 );
410
411 config.insecure = true;
412 assert_eq!(config.validate(), Ok(()));
413 }
414
415 fn tls(client_ca: Option<&str>) -> TlsConfig {
416 TlsConfig {
417 certificate: "/etc/tls/server.pem".to_owned(),
418 key: "/etc/tls/server.key".to_owned(),
419 client_ca: client_ca.map(ToOwned::to_owned),
420 crl: None,
421 }
422 }
423
424 #[cfg(feature = "tls")]
428 #[test]
429 fn tls_is_the_acknowledgement_a_non_loopback_bind_needs() {
430 let mut config = valid();
431 config.bind = "0.0.0.0:8443".to_owned();
432 config.tls = Some(tls(None));
433
434 assert_eq!(config.validate(), Ok(()));
435 }
436
437 #[cfg(feature = "tls")]
442 #[test]
443 fn insecure_and_tls_together_are_a_contradiction_rather_than_a_no_op() {
444 let mut config = valid();
445 config.bind = "0.0.0.0:8443".to_owned();
446 config.tls = Some(tls(Some("/etc/tls/ca.pem")));
447 config.insecure = true;
448
449 let refusal = config.validate().unwrap_err();
450
451 assert_eq!(refusal, Refusal::InsecureWithTls);
452 assert!(refusal.to_string().contains("insecure"), "{refusal}");
453 }
454
455 #[cfg(feature = "tls")]
456 #[test]
457 fn a_tls_section_that_names_no_file_is_refused_per_key() {
458 for (key, mut broken) in [
459 ("certificate", tls(None)),
460 ("key", tls(None)),
461 ("client_ca", tls(Some(""))),
462 ] {
463 match key {
464 "certificate" => broken.certificate = String::new(),
465 "key" => broken.key = " ".to_owned(),
466 _ => {}
467 }
468
469 let mut config = valid();
470 config.tls = Some(broken);
471
472 assert_eq!(config.validate(), Err(Refusal::TlsPathMissing { key }));
473 }
474 }
475
476 #[cfg(feature = "tls")]
481 #[test]
482 fn a_crl_is_refused_and_the_refusal_names_the_credential_that_can_be_revoked() {
483 let mut config = valid();
484 let mut with_crl = tls(Some("/etc/tls/ca.pem"));
485 with_crl.crl = Some("/etc/tls/clients.crl".to_owned());
486 config.tls = Some(with_crl);
487
488 let refusal = config.validate().unwrap_err();
489
490 assert_eq!(refusal, Refusal::RevocationUnsupported);
491
492 let rendered = refusal.to_string();
493
494 assert!(rendered.contains("`tls.crl`"), "{rendered}");
495 assert!(rendered.contains("token"), "{rendered}");
496 assert!(rendered.contains("short-lived"), "{rendered}");
497 }
498
499 #[test]
504 fn a_crl_key_parses_so_that_the_refusal_can_explain_rather_than_serde() {
505 let config: ServerConfig = serde_json::from_str(
506 r#"{"sections":[{"application":"a","profile":"p","files":["f"]}],
507 "clients":[{"name":"c","token":"0123456789abcdef0123456789abcdef","applications":["a"]}],
508 "tls":{"certificate":"c.pem","key":"k.pem","crl":"clients.crl"}}"#,
509 )
510 .expect("the key is understood, not unknown");
511
512 assert_eq!(
513 config.tls.expect("the block parsed").crl.as_deref(),
514 Some("clients.crl")
515 );
516 }
517
518 #[cfg(not(feature = "tls"))]
522 #[test]
523 fn a_build_without_the_feature_refuses_a_tls_section() {
524 let mut config = valid();
525 config.tls = Some(tls(None));
526
527 let refusal = config.validate().unwrap_err();
528
529 assert_eq!(refusal, Refusal::TlsUnsupported);
530 assert!(refusal.to_string().contains("--features tls"), "{refusal}");
531 }
532
533 #[test]
537 fn a_tls_section_is_understood_whether_or_not_the_feature_is_on() {
538 let config: ServerConfig = serde_json::from_str(
539 r#"{"sections":[{"application":"a","profile":"p","files":["f"]}],
540 "clients":[{"name":"c","token":"0123456789abcdef0123456789abcdef","applications":["a"]}],
541 "tls":{"certificate":"c.pem","key":"k.pem","client_ca":"ca.pem"}}"#,
542 )
543 .expect("the shape is complete");
544
545 let tls = config.tls.expect("the block is understood");
546
547 assert_eq!(tls.certificate, "c.pem");
548 assert_eq!(tls.client_ca.as_deref(), Some("ca.pem"));
549 }
550
551 #[test]
553 fn without_tls_a_non_loopback_bind_still_needs_the_acknowledgement() {
554 let mut config = valid();
555 config.bind = "0.0.0.0:8080".to_owned();
556
557 assert!(matches!(
558 config.validate(),
559 Err(Refusal::ExposedBind { .. })
560 ));
561
562 config.insecure = true;
563 assert_eq!(config.validate(), Ok(()));
564 }
565
566 #[test]
567 fn ipv6_loopback_counts_as_loopback() {
568 let mut config = valid();
569 config.bind = "[::1]:8080".to_owned();
570
571 assert_eq!(config.validate(), Ok(()));
572 }
573
574 #[test]
575 fn a_hostname_is_refused_rather_than_resolved() {
576 let mut config = valid();
577 config.bind = "localhost:8080".to_owned();
578
579 assert_eq!(
580 config.validate(),
581 Err(Refusal::UnparsableBind {
582 bind: "localhost:8080".to_owned()
583 })
584 );
585 }
586
587 #[test]
590 fn no_refusal_prints_a_token() {
591 let mut config = valid();
592 config
593 .clients
594 .push(client("other", Some(GOOD), &["billing"]));
595
596 let refusal = config.validate().unwrap_err();
597
598 assert!(
599 !refusal.to_string().contains(GOOD) && !format!("{refusal:?}").contains(GOOD),
600 "a credential escaped through a refusal: {refusal}"
601 );
602 }
603
604 #[test]
608 fn the_stream_ceiling_defaults_high_and_zero_is_a_valid_answer() {
609 let config: ServerConfig = serde_json::from_str(
610 r#"{"sections":[{"application":"a","profile":"p","files":["f"]}],
611 "clients":[{"name":"c","token":"0123456789abcdef0123456789abcdef","applications":["a"]}]}"#,
612 )
613 .expect("the shape is complete");
614
615 assert_eq!(config.max_stream_connections, 4096);
616 assert_eq!(config.validate(), Ok(()));
617
618 let mut off = config;
619 off.max_stream_connections = 0;
620 assert_eq!(off.validate(), Ok(()));
621 }
622
623 #[test]
624 fn a_key_the_server_does_not_know_is_refused() {
625 let error = serde_json::from_str::<ServerConfig>(
626 r#"{"sections":[],"clients":[],"allow_anonymou":true}"#,
627 )
628 .unwrap_err();
629
630 assert!(error.to_string().contains("unknown field"), "{error}");
631 }
632}