use reqwest::Url;
use super::{DEFAULT_OAUTH_ISSUER, LoginError, is_loopback_host};
use crate::config::{CliConfig, DEFAULT_ACCOUNT_ENDPOINT, DEFAULT_BASIN_ENDPOINT};
fn is_production_issuer(issuer: &str) -> bool {
match (Url::parse(issuer), Url::parse(DEFAULT_OAUTH_ISSUER)) {
(Ok(issuer), Ok(production)) => issuer == production,
_ => issuer == DEFAULT_OAUTH_ISSUER,
}
}
pub(crate) fn effective_endpoints(config: &CliConfig) -> (String, String) {
match (&config.account_endpoint, &config.basin_endpoint) {
(Some(account), Some(basin)) => (normalize_endpoint(account), normalize_endpoint(basin)),
_ => (
DEFAULT_ACCOUNT_ENDPOINT.to_owned(),
DEFAULT_BASIN_ENDPOINT.to_owned(),
),
}
}
pub(crate) fn uses_loopback_endpoints(config: &CliConfig) -> bool {
let (account, basin) = effective_endpoints(config);
endpoint_environment(&account, &basin) == Some(EndpointEnvironment::Loopback)
}
fn normalize_endpoint(endpoint: &str) -> String {
endpoint.trim_end_matches('/').to_owned()
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EndpointEnvironment {
Production,
Staging,
Sandbox,
Loopback,
}
pub(super) fn validate_endpoint_binding(
issuer: &str,
account_endpoint: &str,
basin_endpoint: &str,
tls_verification_disabled: bool,
) -> Result<(), LoginError> {
let environment = endpoint_environment(account_endpoint, basin_endpoint)
.ok_or(LoginError::UnsafeBrowserDestination)?;
if tls_verification_disabled && environment != EndpointEnvironment::Loopback {
return Err(LoginError::UnsafeBrowserDestination);
}
let issuer_matches = if is_production_issuer(issuer) {
environment == EndpointEnvironment::Production
} else {
matches!(
environment,
EndpointEnvironment::Staging
| EndpointEnvironment::Sandbox
| EndpointEnvironment::Loopback
)
};
issuer_matches
.then_some(())
.ok_or(LoginError::UnsafeBrowserDestination)
}
fn endpoint_environment(account: &str, basin: &str) -> Option<EndpointEnvironment> {
if account == DEFAULT_ACCOUNT_ENDPOINT && basin == DEFAULT_BASIN_ENDPOINT {
return Some(EndpointEnvironment::Production);
}
let account_url = trusted_endpoint_url(account, false)?;
let basin_url = trusted_endpoint_url(basin, true)?;
if is_loopback_host(account_url.host_str()?) && is_loopback_host(basin_url.host_str()?) {
return Some(EndpointEnvironment::Loopback);
}
if account_url.scheme() != "https" || basin_url.scheme() != "https" {
return None;
}
let account_environment = account_endpoint_environment(account_url.host_str()?)?;
let basin_environment = basin_endpoint_environment(basin_url.host_str()?)?;
(account_environment == basin_environment).then_some(account_environment)
}
fn trusted_endpoint_url(endpoint: &str, basin: bool) -> Option<Url> {
let basin_placeholders = endpoint.matches("{basin}.").count();
if (!basin && basin_placeholders != 0) || basin_placeholders > 1 {
return None;
}
let raw = endpoint.replace("{basin}.", "");
let url = Url::parse(&raw).ok()?;
if !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
|| url.path() != "/"
{
return None;
}
let loopback = url.host_str().is_some_and(is_loopback_host);
if !matches!(url.scheme(), "http" | "https")
|| (!loopback && url.scheme() != "https")
|| (basin && !loopback && basin_placeholders != 1)
{
return None;
}
Some(url)
}
fn account_endpoint_environment(host: &str) -> Option<EndpointEnvironment> {
let labels = host.split('.').collect::<Vec<_>>();
match labels.as_slice() {
[_, "o-staging", "s2", "dev"] | [_, "o-staging", _, "s2", "dev"] => {
Some(EndpointEnvironment::Staging)
}
[_, "o-sandbox", "s2", "dev"] | [_, "o-sandbox", _, "s2", "dev"] => {
Some(EndpointEnvironment::Sandbox)
}
_ => None,
}
}
fn basin_endpoint_environment(host: &str) -> Option<EndpointEnvironment> {
let labels = host.split('.').collect::<Vec<_>>();
match labels.as_slice() {
["b-staging", "s2", "dev"] | ["b-staging", _, "s2", "dev"] => {
Some(EndpointEnvironment::Staging)
}
["b-sandbox", "s2", "dev"] | ["b-sandbox", _, "s2", "dev"] => {
Some(EndpointEnvironment::Sandbox)
}
_ => None,
}
}