use crate::frontend::{FrontendError, model::FrontendEnvironmentInput};
use candid::Principal;
use canic_contracts::ids::CanonicalNetworkId;
use std::collections::BTreeSet;
use url::Url;
pub const MAX_ALTERNATIVE_ORIGINS: usize = 100;
pub const MAX_EXPORTED_ROLES: usize = 128;
pub fn validate_origin(origin: &str, local: bool) -> Result<(), FrontendError> {
let parsed = Url::parse(origin).map_err(|_| FrontendError::Origin(origin.to_string()))?;
let canonical = parsed.origin().ascii_serialization() == origin;
let bare = parsed.username().is_empty()
&& parsed.password().is_none()
&& parsed.query().is_none()
&& parsed.fragment().is_none()
&& parsed.path() == "/";
let loopback = parsed.host_str().is_some_and(|host| {
host == "localhost"
|| host.ends_with(".localhost")
|| host == "[::1]"
|| host
.parse::<std::net::Ipv4Addr>()
.is_ok_and(|ip| ip.is_loopback())
});
let scheme = parsed.scheme() == "https" || (local && parsed.scheme() == "http" && loopback);
if !(canonical && bare && scheme) {
return Err(FrontendError::Origin(origin.to_string()));
}
Ok(())
}
fn concrete_principal(principal: Principal) -> Result<(), FrontendError> {
if principal == Principal::anonymous() || principal == Principal::management_canister() {
return Err(FrontendError::Principal);
}
Ok(())
}
pub fn validate_input(
input: &FrontendEnvironmentInput,
environment: &str,
network: CanonicalNetworkId,
admission_nonempty: bool,
admission_origin: Option<&str>,
) -> Result<(), FrontendError> {
if input.schema_version != 1 {
return Err(FrontendError::Schema);
}
if input.environment != environment || input.canonical_network_id != network {
return Err(FrontendError::Environment);
}
let local = network != CanonicalNetworkId::ic_mainnet();
validate_origin(&input.api_origin, local)?;
validate_origin(&input.identity.provider_origin, local)?;
validate_origin(&input.identity.derivation_origin, local)?;
concrete_principal(input.identity.canister_id)?;
if admission_nonempty && admission_origin != Some(input.identity.derivation_origin.as_str()) {
return Err(FrontendError::AdmissionOrigin);
}
if input.identity.alternative_origins.len() > MAX_ALTERNATIVE_ORIGINS {
return Err(FrontendError::Bound("II alternative-origin count"));
}
let mut origins = BTreeSet::new();
for origin in &input.identity.alternative_origins {
validate_origin(origin, local)?;
if !origins.insert(origin) {
return Err(FrontendError::OriginSet);
}
}
if let Some(asset) = &input.asset {
concrete_principal(asset.canister_id)?;
validate_origin(&asset.origin, local)?;
if asset.origin != input.identity.derivation_origin && !origins.contains(&asset.origin) {
return Err(FrontendError::OriginSet);
}
}
if input.roles.is_empty() || input.roles.len() > MAX_EXPORTED_ROLES {
return Err(FrontendError::Bound("selected role count"));
}
let mut identities = BTreeSet::new();
for role in &input.roles {
concrete_principal(role.canister_id)?;
let label = role.role.as_str();
let safe_label = !label.is_empty()
&& label.len() <= 128
&& label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'));
if !safe_label || !identities.insert(role.canister_id) {
return Err(FrontendError::Role(label.to_string()));
}
}
Ok(())
}