#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EndpointOverrides {
pds: Vec<(String, String)>,
web: Vec<(String, String)>,
}
pub const VAR: &str = "DID_ENDPOINT_OVERRIDES";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OverrideError(String);
impl std::fmt::Display for OverrideError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DID_ENDPOINT_OVERRIDES: {}", self.0)
}
}
impl std::error::Error for OverrideError {}
fn origin(s: &str) -> Option<String> {
let lower = s.to_ascii_lowercase();
let rest = lower.strip_prefix("https://").or_else(|| lower.strip_prefix("http://"))?;
(!rest.is_empty() && !rest.contains(['/', '?', '#', '@', '='])).then_some(lower)
}
pub fn overrides_or_fail() -> Result<EndpointOverrides, OverrideError> {
let overrides = EndpointOverrides::from_env()?;
#[cfg(feature = "net")]
if !overrides.is_empty() {
tracing::info!(overrides = %overrides.describe(), "reading these endpoints in-network ({VAR})");
}
Ok(overrides)
}
impl EndpointOverrides {
pub fn none() -> Self {
Self::default()
}
pub fn parse(raw: &str) -> Result<Self, OverrideError> {
let mut out = Self::default();
for pair in raw.split_whitespace() {
let (from, to) = pair
.split_once('=')
.ok_or_else(|| OverrideError(format!("`{pair}` is not `from=to`")))?;
let to = to.trim_end_matches('/');
let to = origin(to).ok_or_else(|| OverrideError(format!("`{to}` must be a bare http(s) origin")))?;
if let Some(host) = from.strip_prefix("did:web:") {
if host.is_empty() || host.contains(':') || host.contains('/') {
return Err(OverrideError(format!("`{from}`: only a host-only did:web is supported")));
}
let host = host.to_ascii_lowercase();
if out.web.iter().any(|(h, _)| *h == host) {
return Err(OverrideError(format!("`{from}` is named twice")));
}
out.web.push((host, to));
} else {
let trimmed = from.trim_end_matches('/');
let from = origin(trimmed)
.ok_or_else(|| OverrideError(format!("`{trimmed}` must be a bare http(s) origin or a did:web")))?;
if out.pds.iter().any(|(f, _)| *f == from) {
return Err(OverrideError(format!("`{from}` is named twice")));
}
out.pds.push((from, to));
}
}
Ok(out)
}
pub fn from_env() -> Result<Self, OverrideError> {
Self::from_var(std::env::var(VAR))
}
fn from_var(var: Result<String, std::env::VarError>) -> Result<Self, OverrideError> {
match var {
Ok(v) => Self::parse(&v),
Err(std::env::VarError::NotPresent) => Ok(Self::none()),
Err(std::env::VarError::NotUnicode(_)) => Err(OverrideError("the value is not UTF-8".into())),
}
}
pub fn is_empty(&self) -> bool {
self.pds.is_empty() && self.web.is_empty()
}
pub fn pds(&self, endpoint: &str) -> String {
let endpoint = endpoint.trim_end_matches('/');
self.pds
.iter()
.find(|(from, _)| from.eq_ignore_ascii_case(endpoint))
.map(|(_, to)| to.clone())
.unwrap_or_else(|| endpoint.to_string())
}
pub fn did_web_doc_url(&self, did: &str) -> Option<String> {
let host = did.strip_prefix("did:web:")?;
self.web
.iter()
.find(|(h, _)| h.eq_ignore_ascii_case(host))
.map(|(_, base)| format!("{base}/.well-known/did.json"))
}
pub fn describe(&self) -> String {
let mut parts: Vec<String> = self.pds.iter().map(|(f, t)| format!("{f} -> {t}")).collect();
parts.extend(self.web.iter().map(|(h, t)| format!("did:web:{h} -> {t}")));
parts.join(", ")
}
}
#[cfg(test)]
mod tests {
use super::*;
const IN_NETWORK: &str = "https://pds.app.example=http://pds:3000 \
did:web:app.example=http://appview:8080/";
#[test]
fn unset_and_empty_rewrite_nothing() {
let o = EndpointOverrides::parse(" ").unwrap();
assert!(o.is_empty());
assert_eq!(o.pds("https://pds.example/"), "https://pds.example");
assert_eq!(o.did_web_doc_url("did:web:example.com"), None);
}
#[test]
fn a_named_pds_origin_goes_in_network_and_others_are_untouched() {
let o = EndpointOverrides::parse(IN_NETWORK).unwrap();
assert_eq!(o.pds("https://pds.app.example"), "http://pds:3000");
assert_eq!(o.pds("https://pds.app.example/"), "http://pds:3000");
assert_eq!(o.pds("https://PDS.app.example"), "http://pds:3000");
assert_eq!(o.pds("https://bsky.social"), "https://bsky.social");
assert_eq!(o.pds("https://pds.scenius.social"), "https://pds.scenius.social");
}
#[test]
fn a_named_did_web_is_fetched_in_network_and_only_that_one() {
let o = EndpointOverrides::parse(IN_NETWORK).unwrap();
assert_eq!(
o.did_web_doc_url("did:web:app.example").as_deref(),
Some("http://appview:8080/.well-known/did.json")
);
assert_eq!(o.did_web_doc_url("did:web:rules.app.example"), None);
assert_eq!(o.did_web_doc_url("did:plc:abc"), None);
assert!(o.describe().contains("did:web:app.example -> http://appview:8080"));
}
#[test]
fn malformed_values_are_refused_not_ignored() {
for bad in [
"https://pds.x", "https://pds.x=pds:3000", "https://pds.x/xrpc=http://pds:3000", "pds.x=http://pds:3000", "did:web:a.example:8080=http://x:1", "did:web:=http://x:1",
"https://a=http://b https://a=http://c", "https://pds.x=http://pds:3000=x", "https://pds.x=http://user@pds:3000", ] {
assert!(EndpointOverrides::parse(bad).is_err(), "{bad} must be refused");
}
}
#[test]
fn names_differing_only_in_case_are_one_name() {
for twice in [
"https://PDS.x=http://a:1 https://pds.x=http://b:2",
"HTTPS://pds.x=http://a:1 https://pds.x/=http://b:2",
"did:web:Example.com=http://a:1 did:web:example.com=http://b:2",
] {
let err = EndpointOverrides::parse(twice).unwrap_err();
assert!(err.to_string().contains("named twice"), "{twice}: {err}");
}
let o = EndpointOverrides::parse("HTTPS://PDS.X=HTTP://Pds:3000 did:web:Example.COM=http://AppView:8080").unwrap();
assert_eq!(o.describe(), "https://pds.x -> http://pds:3000, did:web:example.com -> http://appview:8080");
assert_eq!(o.pds("https://pds.x"), "http://pds:3000");
assert_eq!(o.pds("https://Pds.X/"), "http://pds:3000");
assert_eq!(o.did_web_doc_url("did:web:EXAMPLE.com").as_deref(), Some("http://appview:8080/.well-known/did.json"));
}
#[test]
fn only_an_absent_variable_means_none() {
assert_eq!(EndpointOverrides::from_var(Err(std::env::VarError::NotPresent)), Ok(EndpointOverrides::none()));
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
let bad = std::ffi::OsString::from_vec(vec![0x66, 0xff]);
assert!(EndpointOverrides::from_var(Err(std::env::VarError::NotUnicode(bad))).is_err(), "not UTF-8 is refused");
}
assert!(EndpointOverrides::from_var(Ok("nonsense".into())).is_err());
}
}