relay_config/
upstream.rs

1use std::borrow::Cow;
2use std::net::{SocketAddr, ToSocketAddrs};
3use std::str::FromStr;
4use std::{fmt, io};
5
6use relay_common::{Dsn, Scheme};
7use url::Url;
8
9/// Indicates failures in the upstream error api.
10#[derive(Debug, thiserror::Error)]
11pub enum UpstreamError {
12    /// Raised if the DNS lookup for an upstream host failed.
13    #[error("dns lookup failed")]
14    LookupFailed(#[source] io::Error),
15    /// Raised if the DNS lookup succeeded but an empty result was
16    /// returned.
17    #[error("dns lookup returned no results")]
18    EmptyLookupResult,
19}
20
21/// Raised if a URL cannot be parsed into an upstream descriptor.
22#[derive(Debug, Eq, Hash, PartialEq, thiserror::Error)]
23pub enum UpstreamParseError {
24    /// Raised if an upstream could not be parsed as URL.
25    #[error("invalid upstream URL: bad URL format")]
26    BadUrl,
27    /// Raised if a path was added to a URL.
28    #[error("invalid upstream URL: non root URL given")]
29    NonOriginUrl,
30    /// Raised if an unknown or unsupported scheme is encountered.
31    #[error("invalid upstream URL: unknown or unsupported URL scheme")]
32    UnknownScheme,
33    /// Raised if no host was provided.
34    #[error("invalid upstream URL: no host")]
35    NoHost,
36}
37
38/// The upstream target is a type that holds all the information
39/// to uniquely identify an upstream target.
40#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
41pub struct UpstreamDescriptor<'a> {
42    host: Cow<'a, str>,
43    port: u16,
44    scheme: Scheme,
45}
46
47impl<'a> UpstreamDescriptor<'a> {
48    /// Manually constructs an upstream descriptor.
49    pub fn new(host: &'a str, port: u16, scheme: Scheme) -> UpstreamDescriptor<'a> {
50        UpstreamDescriptor {
51            host: Cow::Borrowed(host),
52            port,
53            scheme,
54        }
55    }
56
57    /// Given a DSN this returns an upstream descriptor that
58    /// describes it.
59    pub fn from_dsn(dsn: &'a Dsn) -> UpstreamDescriptor<'a> {
60        UpstreamDescriptor {
61            host: Cow::Borrowed(dsn.host()),
62            port: dsn.port(),
63            scheme: dsn.scheme(),
64        }
65    }
66
67    /// Returns the host as a string.
68    pub fn host(&self) -> &str {
69        &self.host
70    }
71
72    /// Returns the upstream port
73    pub fn port(&self) -> u16 {
74        self.port
75    }
76
77    /// Returns a URL relative to the upstream.
78    pub fn get_url(&self, path: &str) -> Url {
79        format!("{self}{}", path.trim_start_matches(&['/'][..]))
80            .parse()
81            .unwrap()
82    }
83
84    /// Returns the socket address of the upstream.
85    ///
86    /// This might perform a DSN lookup and could fail.  Callers are
87    /// encouraged this call this regularly as DNS might be used for
88    /// load balancing purposes and results might expire.
89    pub fn socket_addr(self) -> Result<SocketAddr, UpstreamError> {
90        (self.host(), self.port())
91            .to_socket_addrs()
92            .map_err(UpstreamError::LookupFailed)?
93            .next()
94            .ok_or(UpstreamError::EmptyLookupResult)
95    }
96
97    /// Returns the upstream's connection scheme.
98    pub fn scheme(&self) -> Scheme {
99        self.scheme
100    }
101
102    /// Returns a version of the upstream descriptor that is static.
103    pub fn into_owned(self) -> UpstreamDescriptor<'static> {
104        UpstreamDescriptor {
105            host: Cow::Owned(self.host.into_owned()),
106            port: self.port,
107            scheme: self.scheme,
108        }
109    }
110}
111
112impl Default for UpstreamDescriptor<'static> {
113    fn default() -> UpstreamDescriptor<'static> {
114        UpstreamDescriptor {
115            host: Cow::Borrowed("sentry.io"),
116            port: 443,
117            scheme: Scheme::Https,
118        }
119    }
120}
121
122impl fmt::Display for UpstreamDescriptor<'_> {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "{}://{}", &self.scheme, &self.host)?;
125        if self.port() != self.scheme.default_port() {
126            write!(f, ":{}", self.port())?;
127        }
128        write!(f, "/")
129    }
130}
131
132impl FromStr for UpstreamDescriptor<'static> {
133    type Err = UpstreamParseError;
134
135    fn from_str(s: &str) -> Result<UpstreamDescriptor<'static>, UpstreamParseError> {
136        let url = Url::parse(s).map_err(|_| UpstreamParseError::BadUrl)?;
137        if url.path() != "/" || !(url.query().is_none() || url.query() == Some("")) {
138            return Err(UpstreamParseError::NonOriginUrl);
139        }
140
141        let scheme = match url.scheme() {
142            "http" => Scheme::Http,
143            "https" => Scheme::Https,
144            _ => return Err(UpstreamParseError::UnknownScheme),
145        };
146
147        Ok(UpstreamDescriptor {
148            host: match url.host_str() {
149                Some(host) => Cow::Owned(host.to_string()),
150                None => return Err(UpstreamParseError::NoHost),
151            },
152            port: url.port().unwrap_or_else(|| scheme.default_port()),
153            scheme,
154        })
155    }
156}
157
158relay_common::impl_str_serde!(UpstreamDescriptor<'static>, "a sentry upstream URL");
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_basic_parsing() {
166        let desc: UpstreamDescriptor<'_> = "https://sentry.io/".parse().unwrap();
167        assert_eq!(desc.host(), "sentry.io");
168        assert_eq!(desc.port(), 443);
169        assert_eq!(desc.scheme(), Scheme::Https);
170    }
171
172    #[test]
173    fn test_from_dsn() {
174        let dsn: Dsn = "https://username:password@domain:8888/42".parse().unwrap();
175        let desc = UpstreamDescriptor::from_dsn(&dsn);
176        assert_eq!(desc.host(), "domain");
177        assert_eq!(desc.port(), 8888);
178        assert_eq!(desc.scheme(), Scheme::Https);
179    }
180}