use core::fmt;
use reqwest::Url;
use std::string::String;
#[cfg(test)]
use std::string::ToString;
use cloud_sdk::transport::RequestTarget;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EndpointError {
InvalidUrl,
HttpsRequired,
MissingHost,
CredentialsForbidden,
QueryForbidden,
FragmentForbidden,
TrailingSlash,
InvalidTargetEncoding,
TargetNormalized,
AllocationFailed,
}
#[derive(Clone)]
pub struct HttpsEndpoint {
base: Url,
prefix: String,
}
impl fmt::Debug for HttpsEndpoint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("HttpsEndpoint([redacted])")
}
}
impl HttpsEndpoint {
pub fn new(value: &str) -> Result<Self, EndpointError> {
Self::new_inner(value, true)
}
fn new_inner(value: &str, require_https: bool) -> Result<Self, EndpointError> {
let base = Url::parse(value).map_err(|_| EndpointError::InvalidUrl)?;
if require_https && base.scheme() != "https" {
return Err(EndpointError::HttpsRequired);
}
if base.host_str().is_none() {
return Err(EndpointError::MissingHost);
}
if !base.username().is_empty() || base.password().is_some() {
return Err(EndpointError::CredentialsForbidden);
}
if base.query().is_some() {
return Err(EndpointError::QueryForbidden);
}
if base.fragment().is_some() {
return Err(EndpointError::FragmentForbidden);
}
if base.path() != "/" && base.path().ends_with('/') {
return Err(EndpointError::TrailingSlash);
}
let mut prefix = String::from(base.as_str());
if base.path() == "/" {
prefix.pop();
}
Ok(Self { base, prefix })
}
pub(crate) fn compose(&self, target: RequestTarget<'_>) -> Result<Url, EndpointError> {
validate_target_encoding(target.as_str())?;
let mut absolute = self.prefix.clone();
absolute
.try_reserve_exact(target.as_str().len())
.map_err(|_| EndpointError::AllocationFailed)?;
absolute.push_str(target.as_str());
let url = Url::parse(&absolute).map_err(|_| EndpointError::InvalidUrl)?;
if url.as_str() != absolute {
return Err(EndpointError::TargetNormalized);
}
self.verify_origin(&url)?;
Ok(url)
}
pub(crate) fn verify_origin(&self, url: &Url) -> Result<(), EndpointError> {
if url.scheme() != self.base.scheme()
|| url.host_str() != self.base.host_str()
|| url.port_or_known_default() != self.base.port_or_known_default()
|| !url.username().is_empty()
|| url.password().is_some()
{
return Err(EndpointError::TargetNormalized);
}
Ok(())
}
#[cfg(test)]
pub(crate) fn local_http(value: &str) -> Result<Self, EndpointError> {
let endpoint = Self::new_inner(value, false)?;
if endpoint.base.scheme() != "http"
|| !endpoint.base.host().is_some_and(|host| {
host.to_string()
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
})
{
return Err(EndpointError::HttpsRequired);
}
Ok(endpoint)
}
}
fn validate_target_encoding(target: &str) -> Result<(), EndpointError> {
let path_end = target.find('?').unwrap_or(target.len());
let bytes = target.as_bytes();
let mut index = 0_usize;
while index < bytes.len() {
if bytes.get(index) != Some(&b'%') {
index = index
.checked_add(1)
.ok_or(EndpointError::InvalidTargetEncoding)?;
continue;
}
let high = bytes
.get(
index
.checked_add(1)
.ok_or(EndpointError::InvalidTargetEncoding)?,
)
.and_then(|byte| decode_hex(*byte));
let low = bytes
.get(
index
.checked_add(2)
.ok_or(EndpointError::InvalidTargetEncoding)?,
)
.and_then(|byte| decode_hex(*byte));
let (Some(high), Some(low)) = (high, low) else {
return Err(EndpointError::InvalidTargetEncoding);
};
let decoded = high
.checked_mul(16)
.and_then(|value| value.checked_add(low))
.ok_or(EndpointError::InvalidTargetEncoding)?;
if index < path_end
&& (decoded <= b' '
|| decoded >= 0x7f
|| matches!(decoded, b'.' | b'/' | b'\\' | b'?' | b'#' | b'%'))
{
return Err(EndpointError::InvalidTargetEncoding);
}
index = index
.checked_add(3)
.ok_or(EndpointError::InvalidTargetEncoding)?;
}
Ok(())
}
const fn decode_hex(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => byte.checked_sub(b'0'),
b'a'..=b'f' => match byte.checked_sub(b'a') {
Some(value) => value.checked_add(10),
None => None,
},
b'A'..=b'F' => match byte.checked_sub(b'A') {
Some(value) => value.checked_add(10),
None => None,
},
_ => None,
}
}