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#[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<'a> {
42 host: Cow<'a, str>,
43 port: u16,
44 scheme: Scheme,
45}
46
47impl<'a> UpstreamDescriptor<'a> {
48 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 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 pub fn host(&self) -> &str {
69 &self.host
70 }
71
72 pub fn port(&self) -> u16 {
74 self.port
75 }
76
77 pub fn get_url(&self, path: &str) -> Url {
79 format!("{self}{}", path.trim_start_matches(&['/'][..]))
80 .parse()
81 .unwrap()
82 }
83
84 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 pub fn scheme(&self) -> Scheme {
99 self.scheme
100 }
101
102 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}