use axum::extract::{Request, State};
use axum::http::{StatusCode, request::Parts};
use axum::response::{IntoResponse, Response};
use did_hosting_common::server::domain::assert_resolution_allowed;
use tracing::debug;
use super::resolve_shared::extract_request_host;
use crate::did_ops::{self, DidRecord};
use crate::error::AppError;
use crate::mnemonic::validate_mnemonic;
use crate::server::AppState;
async fn serve_content(
state: &AppState,
mnemonic: &str,
key: &str,
content_type: &str,
track_stats: bool,
request_host: Option<&str>,
) -> Result<Response, AppError> {
match state
.dids_ks
.get::<DidRecord>(did_ops::did_key(mnemonic))
.await?
{
Some(record) => {
if record.disabled || record.deleted_at.is_some() {
return Err(AppError::NotFound(format!("content not found: {mnemonic}")));
}
if let Some(ref did_id) = record.did_id {
assert_resolution_allowed(&state.store, request_host, did_id).await?;
}
}
None => {
return Err(AppError::NotFound(format!("content not found: {mnemonic}")));
}
}
let content = if let Some(cached) = state.did_cache.get(key) {
#[cfg(feature = "metrics")]
did_hosting_common::server::metrics::inc_cache_hit();
cached
} else {
#[cfg(feature = "metrics")]
did_hosting_common::server::metrics::inc_cache_miss();
let data = state
.dids_ks
.get_raw(key)
.await?
.ok_or_else(|| AppError::NotFound(format!("content not found: {mnemonic}")))?;
let data = if content_type == "application/jsonl+json" {
let log_text = String::from_utf8(data)
.map_err(|e| AppError::Internal(format!("invalid log bytes: {e}")))?;
let witness_text = state
.dids_ks
.get_raw(did_ops::content_witness_key(mnemonic))
.await?
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned());
did_ops::servable_did_log(&log_text, witness_text.as_deref())
.ok_or_else(|| AppError::NotFound(format!("content not found: {mnemonic}")))?
.into_bytes()
} else {
data
};
state.did_cache.insert(key.to_string(), data.clone());
std::sync::Arc::new(data)
};
if track_stats && let Some(ref collector) = state.stats_collector {
collector.record_resolve(mnemonic);
#[cfg(feature = "metrics")]
did_hosting_common::server::metrics::inc_resolve();
}
debug!(mnemonic = %mnemonic, size = content.len(), content_type, "content resolved");
Ok((
StatusCode::OK,
[
("content-type", content_type),
("cache-control", "public, max-age=300"),
],
(*content).clone(),
)
.into_response())
}
#[cfg_attr(feature = "openapi", utoipa::path(
get,
path = "/.well-known/did.jsonl",
tag = "resolve",
responses(
(status = 200, description = "Root DID's did:webvh log (JSONL)", content_type = "application/jsonl+json"),
(status = 404, description = "No root DID hosted on this domain"),
),
))]
pub async fn serve_root_did_log(
State(state): State<AppState>,
request: Request,
) -> Result<Response, AppError> {
let (parts, _) = request.into_parts();
let host = extract_request_host(&parts, &state.trusted_proxy_cidrs);
serve_content(
&state,
".well-known",
"content:.well-known:log",
"application/jsonl+json",
true,
host.as_deref(),
)
.await
}
pub async fn serve_root_witness(
State(state): State<AppState>,
request: Request,
) -> Result<Response, AppError> {
let (parts, _) = request.into_parts();
let host = extract_request_host(&parts, &state.trusted_proxy_cidrs);
serve_content(
&state,
".well-known",
"content:.well-known:witness",
"application/json",
false,
host.as_deref(),
)
.await
}
pub async fn dispatch(state: &AppState, parts: &Parts) -> Option<Response> {
let path = parts.uri.path().trim_start_matches('/');
let host = extract_request_host(parts, &state.trusted_proxy_cidrs);
let host = host.as_deref();
if let Some(mnemonic) = path.strip_suffix("/did.jsonl")
&& !mnemonic.is_empty()
{
if let Err(e) = validate_mnemonic(mnemonic) {
return Some(e.into_response());
}
let key = format!("content:{mnemonic}:log");
return Some(
serve_content(state, mnemonic, &key, "application/jsonl+json", true, host)
.await
.unwrap_or_else(|e| e.into_response()),
);
}
if let Some(mnemonic) = path.strip_suffix("/did-witness.json")
&& !mnemonic.is_empty()
{
if let Err(e) = validate_mnemonic(mnemonic) {
return Some(e.into_response());
}
let key = format!("content:{mnemonic}:witness");
return Some(
serve_content(state, mnemonic, &key, "application/json", false, host)
.await
.unwrap_or_else(|e| e.into_response()),
);
}
None
}