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#[derive(Debug, thiserror::Error)]
11pub enum UpstreamError {
12 #[error("dns lookup failed")]
14 LookupFailed(#[source] io::Error),
15 #[error("dns lookup returned no results")]
18 EmptyLookupResult,
19}
20
21#[derive(Debug, Eq, Hash, PartialEq, thiserror::Error)]
23pub enum UpstreamParseError {
24 #[error("invalid upstream URL: bad URL format")]
26 BadUrl,
27 #[error("invalid upstream URL: non root URL given")]
29 NonOriginUrl,
30 #[error("invalid upstream URL: unknown or unsupported URL scheme")]
32 UnknownScheme,
33 #[error("invalid upstream URL: no host")]
35 NoHost,
36}
37
38#[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 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 pub fn from_dsn(dsn: &Dsn) -> Self {
63 Self::new(dsn.host(), dsn.port(), dsn.scheme())
64 }
65
66 pub fn host(&self) -> &str {
68 &self.host
69 }
70
71 pub fn port(&self) -> u16 {
73 self.port
74 }
75
76 pub fn get_url(&self, path: &str) -> Url {
78 format!("{self}{}", path.trim_start_matches(&['/'][..]))
79 .parse()
80 .unwrap()
81 }
82
83 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 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}