Skip to main content

relay_config/
upstream.rs

1use std::net::{SocketAddr, ToSocketAddrs};
2use std::str::FromStr;
3use std::sync::Arc;
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 {
42    host: Arc<str>,
43    port: u16,
44    scheme: Scheme,
45}
46
47impl UpstreamDescriptor {
48    /// Manually constructs an upstream descriptor.
49    pub fn new<T>(host: T, port: u16, scheme: Scheme) -> Self
50    where
51        T: Into<Arc<str>>,
52    {
53        UpstreamDescriptor {
54            host: host.into(),
55            port,
56            scheme,
57        }
58    }
59
60    /// Given a DSN this returns an upstream descriptor that
61    /// describes it.
62    pub fn from_dsn(dsn: &Dsn) -> Self {
63        Self::new(dsn.host(), dsn.port(), dsn.scheme())
64    }
65
66    /// Returns the host as a string.
67    pub fn host(&self) -> &str {
68        &self.host
69    }
70
71    /// Returns the upstream port
72    pub fn port(&self) -> u16 {
73        self.port
74    }
75
76    /// Returns a URL relative to the upstream.
77    pub fn get_url(&self, path: &str) -> Url {
78        format!("{self}{}", path.trim_start_matches(&['/'][..]))
79            .parse()
80            .unwrap()
81    }
82
83    /// Returns the socket address of the upstream.
84    ///
85    /// This might perform a DSN lookup and could fail.  Callers are
86    /// encouraged this call this regularly as DNS might be used for
87    /// load balancing purposes and results might expire.
88    pub fn socket_addr(self) -> Result<SocketAddr, UpstreamError> {
89        (self.host(), self.port())
90            .to_socket_addrs()
91            .map_err(UpstreamError::LookupFailed)?
92            .next()
93            .ok_or(UpstreamError::EmptyLookupResult)
94    }
95
96    /// Returns the upstream's connection scheme.
97    pub fn scheme(&self) -> Scheme {
98        self.scheme
99    }
100}
101
102impl Default for UpstreamDescriptor {
103    fn default() -> Self {
104        Self::new("sentry.io", 443, Scheme::Https)
105    }
106}
107
108impl fmt::Display for UpstreamDescriptor {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        write!(f, "{}://{}", &self.scheme, &self.host)?;
111        if self.port() != self.scheme.default_port() {
112            write!(f, ":{}", self.port())?;
113        }
114        write!(f, "/")
115    }
116}
117
118impl FromStr for UpstreamDescriptor {
119    type Err = UpstreamParseError;
120
121    fn from_str(s: &str) -> Result<Self, UpstreamParseError> {
122        let url = Url::parse(s).map_err(|_| UpstreamParseError::BadUrl)?;
123        if url.path() != "/" || !(url.query().is_none() || url.query() == Some("")) {
124            return Err(UpstreamParseError::NonOriginUrl);
125        }
126
127        let scheme = match url.scheme() {
128            "http" => Scheme::Http,
129            "https" => Scheme::Https,
130            _ => return Err(UpstreamParseError::UnknownScheme),
131        };
132
133        Ok(UpstreamDescriptor {
134            host: match url.host_str() {
135                Some(host) => host.into(),
136                None => return Err(UpstreamParseError::NoHost),
137            },
138            port: url.port().unwrap_or_else(|| scheme.default_port()),
139            scheme,
140        })
141    }
142}
143
144relay_common::impl_str_serde!(UpstreamDescriptor, "a sentry upstream URL");
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_basic_parsing() {
152        let desc: UpstreamDescriptor = "https://sentry.io/".parse().unwrap();
153        assert_eq!(desc.host(), "sentry.io");
154        assert_eq!(desc.port(), 443);
155        assert_eq!(desc.scheme(), Scheme::Https);
156    }
157
158    #[test]
159    fn test_from_dsn() {
160        let dsn: Dsn = "https://username:password@domain:8888/42".parse().unwrap();
161        let desc = UpstreamDescriptor::from_dsn(&dsn);
162        assert_eq!(desc.host(), "domain");
163        assert_eq!(desc.port(), 8888);
164        assert_eq!(desc.scheme(), Scheme::Https);
165    }
166}