Skip to main content

pubhubs/servers/config/
host_aliases.rs

1//! Using aliases for hosts
2use std::net::IpAddr;
3use url::Url;
4
5use crate::misc::net_ext;
6
7use indexmap::{IndexMap, IndexSet};
8
9/// A value that can be resolved to an [`IpAddr`].
10#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
11#[serde(rename_all = "snake_case")]
12pub enum HostAlias {
13    /// Literally, this [`IpAddr`].
14    Ip(IpAddr),
15
16    /// The default source IP address when contacting the by [`UrlPwa`] specified host.
17    ///
18    /// The `scheme` must be either `tcp` or `udp`, and a `port` must be specified.
19    /// The url cannot have a `username`, `password`, `path`, `query` or `fragment`.
20    SourceIpFor(UrlPwa),
21}
22
23impl HostAlias {
24    /// If [`HostAlias`] has been resolved to an [`IpAddr`], return that ip address.
25    fn as_ip(&self) -> Option<&IpAddr> {
26        match self {
27            HostAlias::Ip(ip) => Some(ip),
28            _ => None,
29        }
30    }
31}
32
33#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
34#[serde(transparent)]
35pub struct HostAliases {
36    inner: IndexMap<String, HostAlias>,
37}
38
39/// A [`Url`] that perhaps has as host name one of the aliases defined in [`super::Config::host_aliases`].
40#[derive(Debug, Clone)]
41pub enum UrlPwa {
42    /// The host name of the [`Url`] might contain an alias.
43    PerhapsWithAlias(Url),
44
45    /// Any alias has been replaced.
46    WithoutAlias(Url),
47}
48
49impl HostAliases {
50    /// Resolves all host aliases to [`IpAddr`]esses.
51    pub fn resolve_all(&mut self) -> anyhow::Result<()> {
52        Resolver::new(&mut self.inner).resolve_all()?;
53        Ok(())
54    }
55
56    /// Resolves the given [`UrlPwa`] to one without any aliases.
57    ///
58    /// Requires that [`HostAliases::resolve_all`] has been called.
59    pub fn dealias(&self, url_pwa: &mut UrlPwa) {
60        let mut url: Url = match url_pwa {
61            UrlPwa::WithoutAlias(_) => {
62                return; // no alias there
63            }
64            UrlPwa::PerhapsWithAlias(url) => url.clone(),
65        };
66
67        'dealias: {
68            if let Some(host) = url.host() {
69                // get ip address from host
70                let ip: IpAddr = match host {
71                    url::Host::Ipv4(ip) => ip.into(),
72                    url::Host::Ipv6(ip) => ip.into(),
73                    url::Host::Domain(hostname) => {
74                        if let Some(ha) = self.inner.get(hostname) {
75                            *ha.as_ip().unwrap()
76                        } else {
77                            break 'dealias; // host not an alias
78                        }
79                    }
80                };
81
82                url.set_ip_host(ip)
83                    .expect("unexpectedly could not set host of an url that had a host already");
84            }
85        }
86
87        *url_pwa = UrlPwa::WithoutAlias(url);
88    }
89}
90
91/// Helper struct for [`HostAliases::resolve_all`]
92struct Resolver<'a> {
93    aliases: &'a mut IndexMap<String, HostAlias>,
94
95    /// The indices of [`Resolver::aliases`] that still have to be resolved,
96    /// including the alias(es) currently under consideration.
97    todo: std::collections::HashSet<usize>,
98
99    /// The indices of [`Resolver::aliases`] that are currently being resolved.
100    deps_stack: IndexSet<usize>,
101}
102
103impl<'a> Resolver<'a> {
104    fn new(aliases: &'a mut IndexMap<String, HostAlias>) -> Self {
105        let n = aliases.len();
106        Self {
107            aliases,
108            todo: (0..n).collect(),
109            deps_stack: Default::default(),
110        }
111    }
112
113    fn resolve_all(mut self) -> anyhow::Result<()> {
114        while !self.todo.is_empty() {
115            let hai: usize = *self.todo.iter().next().unwrap();
116            self.resolve_new_one(hai)?;
117        }
118        Ok(())
119    }
120
121    fn resolve_new_one(&mut self, hai: usize) -> anyhow::Result<()> {
122        assert_eq!(self.deps_stack.len(), 0);
123
124        self.deps_stack = indexmap::indexset![hai];
125
126        while !self.deps_stack.is_empty() {
127            self.deps_stack_step()?;
128        }
129
130        Ok(())
131    }
132
133    fn deps_stack_step(&mut self) -> anyhow::Result<()> {
134        let latest_ha: usize = *self.deps_stack.last().unwrap();
135
136        if let Err(depi) = self.try_resolve_ha(latest_ha)? {
137            let already_a_dep: bool = !self.deps_stack.insert(depi);
138            anyhow::ensure!(
139                !already_a_dep,
140                "cyclic dependency involving host alias {}",
141                self.aliases.get_index_entry(depi).unwrap().key(),
142            );
143            return Ok(());
144        }
145
146        assert_eq!(self.deps_stack.pop().unwrap(), latest_ha);
147        self.todo.remove(&latest_ha);
148
149        Ok(())
150    }
151
152    /// Tries to resolve the named host alias.  If this host alias
153    /// depends on another unresolved host alias, returns the index of that alias in `Ok(Err(...))`.
154    fn try_resolve_ha(&mut self, hai: usize) -> anyhow::Result<Result<(), usize>> {
155        let ha: &HostAlias = self.aliases.get_index(hai).unwrap().1;
156
157        let ip: IpAddr = match ha {
158            HostAlias::Ip(_) => return Ok(Ok(())), // already resolved
159            HostAlias::SourceIpFor(url_pwa) => {
160                let url_pwa = match self.try_dealias_url_pwa(url_pwa.clone()) {
161                    Ok(url_pwa) => url_pwa,
162                    Err(dep) => return Ok(Err(dep)),
163                };
164                net_ext::source_ip_for(url_pwa.as_ref())?
165            }
166        };
167
168        let ha: &mut HostAlias = self.aliases.get_index_mut(hai).unwrap().1;
169        *ha = HostAlias::Ip(ip);
170
171        Ok(Ok(()))
172    }
173
174    fn try_dealias_url_pwa(&mut self, url: UrlPwa) -> Result<UrlPwa, usize> {
175        let mut url: Url = match url {
176            UrlPwa::WithoutAlias(url) => {
177                return Ok(UrlPwa::WithoutAlias(url));
178            }
179            UrlPwa::PerhapsWithAlias(url) => url,
180        };
181
182        if let Some(host) = url.host() {
183            let ip: IpAddr = match host {
184                url::Host::Domain(hostname) => {
185                    // If `hostname` is an alias, get its index and `HostAlias`.
186                    let (idx, ha): (usize, &HostAlias) =
187                        if let Some((idx, _, ha)) = self.aliases.get_full(hostname) {
188                            (idx, ha)
189                        } else {
190                            return Ok(UrlPwa::WithoutAlias(url));
191                        };
192
193                    if self.todo.contains(&idx) {
194                        // We could already detect a cyclic dependency here, but the return type
195                        // would become less readable.
196                        return Err(idx);
197                    }
198
199                    *ha.as_ip().unwrap()
200                }
201                url::Host::Ipv4(ip) => ip.into(),
202                url::Host::Ipv6(ip) => ip.into(),
203            };
204
205            url.set_ip_host(ip)
206                .expect("unexpectedly could not set host of an url that had a host already");
207        }
208
209        Ok(UrlPwa::WithoutAlias(url))
210    }
211}
212
213impl UrlPwa {
214    /// Returns underlying [`Url`] which may, or may not (anymore) contain an host alias.
215    fn url_perhaps_with_alias(&self) -> &Url {
216        match self {
217            UrlPwa::PerhapsWithAlias(u) | UrlPwa::WithoutAlias(u) => u,
218        }
219    }
220}
221
222impl From<Url> for UrlPwa {
223    fn from(url: Url) -> Self {
224        UrlPwa::WithoutAlias(url)
225    }
226}
227
228impl std::fmt::Display for UrlPwa {
229    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
230        self.url_perhaps_with_alias().fmt(f)
231    }
232}
233
234impl serde::Serialize for UrlPwa {
235    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
236        self.url_perhaps_with_alias().serialize(s)
237    }
238}
239
240impl<'de> serde::Deserialize<'de> for UrlPwa {
241    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
242        Ok(UrlPwa::PerhapsWithAlias(Url::deserialize(d)?))
243    }
244}
245
246impl AsRef<Url> for UrlPwa {
247    fn as_ref(&self) -> &url::Url {
248        if let UrlPwa::WithoutAlias(url) = self {
249            return url;
250        }
251        panic!(
252            "internal error: url {self} is used but might still contain a host alias.  it should have been dealiased during configuration processing."
253        );
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn host_aliases() {
263        let mut has: HostAliases = toml::from_str(
264            r#"
265        [localhost2]
266        source_ip_for = "udp://localhost:1234"
267
268        [localhost]
269        ip = "127.0.0.1"
270
271        [localhost3] 
272        source_ip_for = "udp://localhost2:1234"
273
274        [localhost4]
275        source_ip_for = "udp://[::1]:3"
276
277        "#,
278        )
279        .unwrap();
280
281        has.resolve_all().unwrap();
282
283        let mut url =
284            UrlPwa::PerhapsWithAlias(Url::parse("https://localhost3:1234/dsa?asd#pwa").unwrap());
285
286        has.dealias(&mut url);
287
288        assert_eq!(
289            url.as_ref().to_string(),
290            "https://127.0.0.1:1234/dsa?asd#pwa"
291        );
292
293        let mut url =
294            UrlPwa::PerhapsWithAlias(Url::parse("https://localhost4:1234/dsa?asd#pwa").unwrap());
295
296        has.dealias(&mut url);
297
298        assert_eq!(url.as_ref().to_string(), "https://[::1]:1234/dsa?asd#pwa");
299    }
300}