aauth_protocol/client/
connect.rs1use url::Url;
6
7use crate::error::{Error, Result};
8use crate::identifier::authority;
9
10#[derive(Clone, Debug, Default)]
13pub struct ConnectTo(Vec<(String, String)>);
14
15#[derive(Debug, PartialEq, Eq)]
17pub struct Route {
18 pub url: String,
20 pub host: Option<String>,
22}
23
24impl ConnectTo {
25 pub fn parse(raw: &str) -> Result<Self> {
27 raw.split(',')
28 .map(str::trim)
29 .filter(|entry| !entry.is_empty())
30 .map(|entry| {
31 entry
32 .split_once('=')
33 .map(|(from, to)| (from.trim().to_ascii_lowercase(), to.trim().to_string()))
34 .filter(|(from, to)| !from.is_empty() && !to.is_empty())
35 .ok_or_else(|| {
36 Error::Invalid(format!("connect-to entry {entry:?} is not from=to"))
37 })
38 })
39 .collect::<Result<_, _>>()
40 .map(Self)
41 }
42
43 pub fn is_empty(&self) -> bool {
45 self.0.is_empty()
46 }
47
48 pub fn route(&self, url: &str) -> Result<Route> {
50 let parsed = Url::parse(url).map_err(|e| Error::Url(format!("{url}: {e}")))?;
51 let from = authority(&parsed).ok_or_else(|| Error::Url(format!("{url} has no host")))?;
52 let Some((_, to)) = self.0.iter().find(|(candidate, _)| *candidate == from) else {
53 return Ok(Route {
54 url: url.to_string(),
55 host: None,
56 });
57 };
58 let mut rest = parsed.path().to_string();
59 if let Some(query) = parsed.query() {
60 rest = format!("{rest}?{query}");
61 }
62 Ok(Route {
63 url: format!("{}{rest}", to.trim_end_matches('/')),
64 host: Some(from),
65 })
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn a_mapped_authority_dials_elsewhere_and_keeps_its_host() {
75 let connect = ConnectTo::parse(
76 "ps.acme.localhost=http://host.docker.internal:3400, localhost:3400=http://host.docker.internal:3400/",
77 )
78 .unwrap();
79 assert_eq!(
80 connect
81 .route("https://ps.acme.localhost/ps/token?x=1")
82 .unwrap(),
83 Route {
84 url: "http://host.docker.internal:3400/ps/token?x=1".into(),
85 host: Some("ps.acme.localhost".into()),
86 }
87 );
88 assert_eq!(
89 connect.route("https://ps.example/token").unwrap(),
90 Route {
91 url: "https://ps.example/token".into(),
92 host: None,
93 }
94 );
95 }
96
97 #[test]
98 fn a_malformed_entry_is_refused() {
99 assert!(ConnectTo::parse("localhost:3400").is_err());
100 assert!(ConnectTo::parse("").unwrap().is_empty());
101 }
102}