use std::borrow::Cow;
use std::net::{SocketAddr, ToSocketAddrs};
use std::str::FromStr;
use std::{fmt, io};
use relay_common::{Dsn, Scheme};
use url::Url;
#[derive(Debug, thiserror::Error)]
pub enum UpstreamError {
#[error("dns lookup failed")]
LookupFailed(#[source] io::Error),
#[error("dns lookup returned no results")]
EmptyLookupResult,
}
#[derive(Debug, Eq, Hash, PartialEq, thiserror::Error)]
pub enum UpstreamParseError {
#[error("invalid upstream URL: bad URL format")]
BadUrl,
#[error("invalid upstream URL: non root URL given")]
NonOriginUrl,
#[error("invalid upstream URL: unknown or unsupported URL scheme")]
UnknownScheme,
#[error("invalid upstream URL: no host")]
NoHost,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct UpstreamDescriptor<'a> {
host: Cow<'a, str>,
port: u16,
scheme: Scheme,
}
impl<'a> UpstreamDescriptor<'a> {
pub fn new(host: &'a str, port: u16, scheme: Scheme) -> UpstreamDescriptor<'a> {
UpstreamDescriptor {
host: Cow::Borrowed(host),
port,
scheme,
}
}
pub fn from_dsn(dsn: &'a Dsn) -> UpstreamDescriptor<'a> {
UpstreamDescriptor {
host: Cow::Borrowed(dsn.host()),
port: dsn.port(),
scheme: dsn.scheme(),
}
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> u16 {
self.port
}
pub fn get_url(&self, path: &str) -> Url {
format!("{self}{}", path.trim_start_matches(&['/'][..]))
.parse()
.unwrap()
}
pub fn socket_addr(self) -> Result<SocketAddr, UpstreamError> {
(self.host(), self.port())
.to_socket_addrs()
.map_err(UpstreamError::LookupFailed)?
.next()
.ok_or(UpstreamError::EmptyLookupResult)
}
pub fn scheme(&self) -> Scheme {
self.scheme
}
pub fn into_owned(self) -> UpstreamDescriptor<'static> {
UpstreamDescriptor {
host: Cow::Owned(self.host.into_owned()),
port: self.port,
scheme: self.scheme,
}
}
}
impl Default for UpstreamDescriptor<'static> {
fn default() -> UpstreamDescriptor<'static> {
UpstreamDescriptor {
host: Cow::Borrowed("sentry.io"),
port: 443,
scheme: Scheme::Https,
}
}
}
impl fmt::Display for UpstreamDescriptor<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}://{}", &self.scheme, &self.host)?;
if self.port() != self.scheme.default_port() {
write!(f, ":{}", self.port())?;
}
write!(f, "/")
}
}
impl FromStr for UpstreamDescriptor<'static> {
type Err = UpstreamParseError;
fn from_str(s: &str) -> Result<UpstreamDescriptor<'static>, UpstreamParseError> {
let url = Url::parse(s).map_err(|_| UpstreamParseError::BadUrl)?;
if url.path() != "/" || !(url.query().is_none() || url.query() == Some("")) {
return Err(UpstreamParseError::NonOriginUrl);
}
let scheme = match url.scheme() {
"http" => Scheme::Http,
"https" => Scheme::Https,
_ => return Err(UpstreamParseError::UnknownScheme),
};
Ok(UpstreamDescriptor {
host: match url.host_str() {
Some(host) => Cow::Owned(host.to_string()),
None => return Err(UpstreamParseError::NoHost),
},
port: url.port().unwrap_or_else(|| scheme.default_port()),
scheme,
})
}
}
relay_common::impl_str_serde!(UpstreamDescriptor<'static>, "a sentry upstream URL");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_parsing() {
let desc: UpstreamDescriptor<'_> = "https://sentry.io/".parse().unwrap();
assert_eq!(desc.host(), "sentry.io");
assert_eq!(desc.port(), 443);
assert_eq!(desc.scheme(), Scheme::Https);
}
#[test]
fn test_from_dsn() {
let dsn: Dsn = "https://username:password@domain:8888/42".parse().unwrap();
let desc = UpstreamDescriptor::from_dsn(&dsn);
assert_eq!(desc.host(), "domain");
assert_eq!(desc.port(), 8888);
assert_eq!(desc.scheme(), Scheme::Https);
}
}