use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_did_resolver_cache_sdk::config::{DIDCacheConfig, DIDCacheConfigBuilder};
use affinidi_did_resolver_cache_sdk::network_resolvers::HostPolicy;
pub fn allow_private_did_hosts() -> bool {
#[cfg(feature = "client")]
{
crate::http::EndpointPolicy::process_default().allow_private
}
#[cfg(not(feature = "client"))]
{
std::env::var("VTA_ALLOW_PRIVATE_ENDPOINTS")
.map(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
.unwrap_or(false)
}
}
pub fn webvh_host_policy() -> HostPolicy {
if allow_private_did_hosts() {
HostPolicy::AllowPrivate
} else {
HostPolicy::PublicOnly
}
}
pub fn build_did_cache_config(url: Option<&str>) -> DIDCacheConfig {
let mut builder = DIDCacheConfigBuilder::default().with_host_policy(webvh_host_policy());
if let Some(u) = url {
builder = builder.with_network_mode(u);
}
builder.build()
}
pub fn build_did_cache_config_from_env() -> DIDCacheConfig {
let url = std::env::var("PNM_RESOLVER_URL")
.ok()
.filter(|s| !s.is_empty());
build_did_cache_config(url.as_deref())
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct SharedKey {
runtime: tokio::runtime::Id,
resolver_url: Option<String>,
allow_private: bool,
}
struct SharedEntry {
client: DIDCacheClient,
runtime_alive: std::sync::Weak<()>,
}
static SHARED_RESOLVERS: std::sync::LazyLock<
std::sync::Mutex<std::collections::HashMap<SharedKey, SharedEntry>>,
> = std::sync::LazyLock::new(Default::default);
fn shared_resolvers()
-> std::sync::MutexGuard<'static, std::collections::HashMap<SharedKey, SharedEntry>> {
SHARED_RESOLVERS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn prune_dead(map: &mut std::collections::HashMap<SharedKey, SharedEntry>) {
map.retain(|_, entry| {
let alive = entry.runtime_alive.strong_count() > 0;
if !alive {
entry.client.stop();
}
alive
});
}
pub async fn shared_did_resolver(
url: Option<&str>,
) -> Result<DIDCacheClient, affinidi_did_resolver_cache_sdk::errors::DIDCacheError> {
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return DIDCacheClient::new(build_did_cache_config(url)).await;
};
let key = SharedKey {
runtime: handle.id(),
resolver_url: url.map(str::to_string),
allow_private: allow_private_did_hosts(),
};
{
let mut map = shared_resolvers();
prune_dead(&mut map);
if let Some(entry) = map.get(&key) {
return Ok(entry.client.clone());
}
}
let client = DIDCacheClient::new(build_did_cache_config(url)).await?;
let mut map = shared_resolvers();
if let Some(entry) = map.get(&key) {
client.stop();
return Ok(entry.client.clone());
}
let sentinel = std::sync::Arc::new(());
let runtime_alive = std::sync::Arc::downgrade(&sentinel);
handle.spawn(async move {
let _sentinel = sentinel;
std::future::pending::<()>().await;
});
map.insert(
key,
SharedEntry {
client: client.clone(),
runtime_alive,
},
);
Ok(client)
}
pub async fn shared_did_resolver_from_env()
-> Result<DIDCacheClient, affinidi_did_resolver_cache_sdk::errors::DIDCacheError> {
let url = std::env::var("PNM_RESOLVER_URL")
.ok()
.filter(|s| !s.is_empty());
shared_did_resolver(url.as_deref()).await
}
pub fn shutdown_shared_did_resolvers() {
let mut map = shared_resolvers();
for (_, entry) in map.drain() {
entry.client.stop();
}
}
#[cfg(test)]
mod tests {
use super::*;
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use std::time::Duration;
async fn counting_listener() -> (u16, Arc<AtomicUsize>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let accepted = Arc::new(AtomicUsize::new(0));
let counter = accepted.clone();
tokio::spawn(async move {
while let Ok((socket, _)) = listener.accept().await {
counter.fetch_add(1, Ordering::SeqCst);
drop(socket);
}
});
(port, accepted)
}
#[tokio::test]
async fn default_config_refuses_loopback_webvh_did_without_connecting() {
let (port, accepted) = counting_listener().await;
let client = DIDCacheClient::new(build_did_cache_config(None))
.await
.expect("local-mode DID cache client");
for host in [
"localhost",
"LOCALHOST",
"localhost.",
"svc.localhost",
"printer.local",
"metadata.google.internal",
] {
let did = format!("did:webvh:QmScidNotResolvable:{host}%3A{port}");
let result = client.resolve(&did).await;
assert!(result.is_err(), "{did} resolved but should be refused");
let message = result.unwrap_err().to_string();
assert!(
message.contains("BlockedHost"),
"{did} failed for the wrong reason: {message}"
);
}
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
accepted.load(Ordering::SeqCst),
0,
"a refused did:webvh DID still connected to its host"
);
}
#[test]
fn default_policy_is_public_only() {
assert_eq!(webvh_host_policy(), HostPolicy::PublicOnly);
}
}