use std::net::SocketAddr;
use crate::{Error, Scheme};
#[derive(
Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct Origin {
pub scheme: Scheme,
pub host: url::Host<String>,
pub port: u16,
}
impl Origin {
pub fn from_scheme_and_socket_addr(scheme: Scheme, socket_addr: SocketAddr) -> Self {
Self {
scheme,
host: match socket_addr.ip() {
std::net::IpAddr::V4(ipv4_addr) => url::Host::Ipv4(ipv4_addr),
std::net::IpAddr::V6(ipv6_addr) => url::Host::Ipv6(ipv6_addr),
},
port: socket_addr.port(),
}
}
pub fn as_url(&self) -> String {
let Self { scheme, host, port } = self;
let host = format_host(host);
format!("{}://{host}:{port}", scheme.as_http_scheme())
}
pub fn coerce_http_url(&self) -> String {
let Self {
scheme: _,
host,
port,
} = self;
let host = format_host(host);
format!("http://{host}:{port}")
}
pub(crate) fn replace_and_parse(
input: &str,
default_localhost_port: Option<u16>,
) -> Result<(Self, url::Url), Error> {
let scheme: Scheme;
let rewritten;
if !input.contains("://") && (input.contains("localhost") || input.contains("127.0.0.1")) {
scheme = Scheme::RerunHttp;
rewritten = format!("http://{input}");
} else {
scheme = input.parse()?;
rewritten = scheme.canonical_url(input);
}
let mut http_url = url::Url::parse(&rewritten)?;
let default_port = if is_origin_localhost(&http_url.origin()) {
default_localhost_port
} else if rewritten.starts_with("https://") {
Some(443)
} else if rewritten.starts_with("http://") {
Some(80)
} else {
None
};
if let Some(default_port) = default_port {
let has_port = if let Some(rest) = http_url.to_string().strip_prefix("http://") {
url::Url::parse(&format!("foobarbaz://{rest}"))?
.port()
.is_some()
} else if let Some(rest) = http_url.to_string().strip_prefix("https://") {
url::Url::parse(&format!("foobarbaz://{rest}"))?
.port()
.is_some()
} else {
true };
if !has_port {
http_url.set_port(Some(default_port)).ok();
}
}
let url::Origin::Tuple(_, host, port) = http_url.origin() else {
return Err(Error::UnexpectedOpaqueOrigin(input.to_owned()));
};
let origin = Self { scheme, host, port };
Ok((origin, http_url))
}
}
impl std::str::FromStr for Origin {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::replace_and_parse(value, None).map(|(origin, _)| origin)
}
}
impl std::fmt::Display for Origin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { scheme, host, port } = self;
let host = format_host(host);
write!(f, "{scheme}://{host}:{port}")
}
}
fn format_host(host: &url::Host<String>) -> String {
if is_host_unspecified(host) {
"127.0.0.1".to_owned()
} else {
host.to_string()
}
}
fn is_host_unspecified(host: &url::Host) -> bool {
match host {
url::Host::Domain(_domain) => false,
url::Host::Ipv4(ip) => ip.is_unspecified(),
url::Host::Ipv6(ip) => ip.is_unspecified(),
}
}
fn is_origin_localhost(origin: &url::Origin) -> bool {
match origin {
url::Origin::Opaque(_) => false,
url::Origin::Tuple(_, host, _) => match host {
url::Host::Domain(domain) => domain == "localhost",
url::Host::Ipv4(ip) => ip.is_loopback() || ip.is_unspecified(),
url::Host::Ipv6(ip) => ip.is_loopback() || ip.is_unspecified(),
},
}
}
#[test]
fn test_origin_format() {
assert_eq!(
Origin::from_scheme_and_socket_addr(Scheme::Rerun, "192.168.0.2:1234".parse().unwrap())
.to_string(),
"rerun://192.168.0.2:1234"
);
assert_eq!(
Origin::from_scheme_and_socket_addr(Scheme::Rerun, "0.0.0.0:1234".parse().unwrap())
.to_string(),
"rerun://127.0.0.1:1234"
);
}