use std::collections::{BTreeSet, HashMap};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use axum::body::Bytes;
use axum::error_handling::HandleErrorLayer;
use axum::extract::{DefaultBodyLimit, State};
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::post;
use axum::Router;
use serde::Serialize;
use serde_json::Value;
use tower::limit::ConcurrencyLimitLayer;
use tower::timeout::TimeoutLayer;
use tower::ServiceBuilder;
use trust_tasks_rs::{
discovery::DiscoveryRegistry, document_digest, erase_verifier,
specs::trust_task_discovery::v0_1 as discovery, DocumentDigest, DynProofVerifier, ErrorPayload,
ErrorResponse, FreshnessPolicy, InMemoryReplayGuard, Payload, ProofVerifier, RejectReason,
ReplayGuard, ReplayVerdict, RequestPayload, ResolvedParties, StaleReason, StandardCode,
TransportHandler, TrustTask, PROOF_NOT_ACCEPTED_BY_POLICY,
};
use uuid::Uuid;
use crate::auth::{Auth, BearerAuth};
use crate::handler::HttpsHandler;
use crate::status::status_for_code;
pub const MAX_BODY_BYTES: usize = 256 * 1024;
pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 512;
#[derive(Debug, Clone)]
pub struct RequestContext {
pub authenticated_sender: Option<String>,
pub local: Option<String>,
pub resolved: ResolvedParties,
}
type DispatchFn = Box<
dyn Fn(TrustTask<Value>, &RequestContext) -> Result<Option<Value>, RejectReason> + Send + Sync,
>;
struct Route {
dispatch: DispatchFn,
}
struct ServerState {
local_vid: Option<String>,
auth: Box<dyn Auth>,
routes: HashMap<String, Route>,
verifier: Option<Arc<dyn DynProofVerifier>>,
require_attribution: bool,
allowed_did_methods: Option<BTreeSet<String>>,
replay_guard: Option<Arc<dyn ReplayGuard>>,
freshness: FreshnessPolicy,
}
pub struct HttpsServerBuilder {
local_vid: Option<String>,
auth: Option<Box<dyn Auth>>,
routes: HashMap<String, Route>,
verifier: Option<Arc<dyn DynProofVerifier>>,
require_attribution: bool,
allowed_did_methods: Option<BTreeSet<String>>,
replay_protection: bool,
replay_guard: Option<Arc<dyn ReplayGuard>>,
freshness: FreshnessPolicy,
public_discovery: Arc<AtomicBool>,
request_timeout: Option<Duration>,
max_concurrent_requests: Option<usize>,
}
impl HttpsServerBuilder {
pub fn local_vid(mut self, vid: impl Into<String>) -> Self {
self.local_vid = Some(vid.into());
self
}
pub fn with_auth(mut self, auth: impl Auth) -> Self {
self.auth = Some(Box::new(auth));
self
}
pub fn with_verifier<V>(mut self, verifier: V) -> Self
where
V: ProofVerifier + Send + Sync + 'static,
{
self.verifier = Some(erase_verifier(verifier));
self
}
pub fn on<P, F>(mut self, handler: F) -> Self
where
P: RequestPayload + 'static,
P::Response: Serialize + 'static,
F: Fn(&TrustTask<P>, &RequestContext) -> Result<P::Response, RejectReason>
+ Send
+ Sync
+ 'static,
{
let dispatch: DispatchFn = Box::new(move |doc: TrustTask<Value>, ctx: &RequestContext| {
let typed = downcast::<P>(doc)?;
typed.enforce_spec_policy()?;
let response_payload = handler(&typed, ctx)?;
let new_id = format!("urn:uuid:{}", Uuid::new_v4());
let response_doc = typed.respond_with(new_id, response_payload);
Ok(Some(
serde_json::to_value(&response_doc).expect("response serialises (typed structs)"),
))
});
let key = P::type_uri().for_routing().to_string();
self.routes.insert(key, Route { dispatch });
self
}
pub fn on_ack<P, F>(mut self, handler: F) -> Self
where
P: Payload + 'static,
F: Fn(&TrustTask<P>, &RequestContext) -> Result<(), RejectReason> + Send + Sync + 'static,
{
let dispatch: DispatchFn = Box::new(move |doc: TrustTask<Value>, ctx: &RequestContext| {
let typed = downcast::<P>(doc)?;
typed.enforce_spec_policy()?;
handler(&typed, ctx)?;
Ok(None)
});
let key = P::type_uri().for_routing().to_string();
self.routes.insert(key, Route { dispatch });
self
}
pub fn require_attribution(mut self, require: bool) -> Self {
self.require_attribution = require;
self
}
pub fn replay_protection(mut self, keep_record: bool) -> Self {
self.replay_protection = keep_record;
self
}
pub fn with_replay_guard<G>(mut self, guard: G) -> Self
where
G: ReplayGuard + 'static,
{
self.replay_guard = Some(Arc::new(guard));
self
}
pub fn freshness(mut self, policy: FreshnessPolicy) -> Self {
self.freshness = policy;
self
}
pub fn allowed_did_methods<I, S>(mut self, methods: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_did_methods = Some(methods.into_iter().map(Into::into).collect());
self
}
pub fn public_discovery(self) -> Self {
self.public_discovery.store(true, Ordering::Relaxed);
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = Some(timeout);
self
}
pub fn max_concurrent_requests(mut self, limit: usize) -> Self {
self.max_concurrent_requests = Some(limit);
self
}
pub fn with_discovery(self, registry: DiscoveryRegistry) -> Self {
let public = Arc::clone(&self.public_discovery);
self.on::<discovery::Payload, _>(move |req, ctx| {
if !public.load(Ordering::Relaxed) && ctx.authenticated_sender.is_none() {
return Err(RejectReason::PermissionDenied {
reason: "discovery requires an authenticated sender".into(),
});
}
Ok(registry.respond_to(&req.payload))
})
}
pub fn enable_discovery(self) -> Self {
let mut registry: DiscoveryRegistry = self.routes.keys().cloned().collect();
registry.register_payload::<discovery::Payload>();
self.with_discovery(registry)
}
pub fn build(self) -> HttpsServer {
let auth = self.auth.unwrap_or_else(|| Box::new(BearerAuth::new()));
HttpsServer {
state: Arc::new(ServerState {
local_vid: self.local_vid,
auth,
routes: self.routes,
verifier: self.verifier,
require_attribution: self.require_attribution,
allowed_did_methods: self.allowed_did_methods,
replay_guard: self.replay_protection.then(|| {
self.replay_guard
.unwrap_or_else(|| Arc::new(InMemoryReplayGuard::default()))
}),
freshness: self.freshness,
}),
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
max_concurrent_requests: self
.max_concurrent_requests
.unwrap_or(DEFAULT_MAX_CONCURRENT_REQUESTS),
}
}
}
pub struct HttpsServer {
state: Arc<ServerState>,
request_timeout: Duration,
max_concurrent_requests: usize,
}
impl HttpsServer {
pub fn builder() -> HttpsServerBuilder {
HttpsServerBuilder {
local_vid: None,
auth: None,
routes: HashMap::new(),
verifier: None,
require_attribution: true,
allowed_did_methods: None,
replay_protection: true,
replay_guard: None,
freshness: FreshnessPolicy::consequential(),
public_discovery: Arc::new(AtomicBool::new(false)),
request_timeout: None,
max_concurrent_requests: None,
}
}
pub fn into_router(self) -> Router {
let timeout = self.request_timeout;
let concurrency = self.max_concurrent_requests;
Router::new()
.route("/trust-tasks", post(dispatch_handler))
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(|err: tower::BoxError| async move {
if err.is::<tower::timeout::error::Elapsed>() {
StatusCode::REQUEST_TIMEOUT
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}))
.layer(ConcurrencyLimitLayer::new(concurrency))
.layer(TimeoutLayer::new(timeout)),
)
.with_state(self.state)
}
pub async fn serve(self, addr: impl tokio::net::ToSocketAddrs) -> std::io::Result<()> {
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, self.into_router()).await
}
}
async fn dispatch_handler(
State(state): State<Arc<ServerState>>,
headers: HeaderMap,
body: Bytes,
) -> Response {
if !is_json_content_type(&headers) {
return StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response();
}
let doc: TrustTask<Value> = match serde_json::from_slice(&body) {
Ok(d) => d,
Err(e) => {
return reject_response(None, None, RejectReason::malformed_from_serde(&e));
}
};
let peer_vid = extract_bearer(&headers).and_then(|tok| state.auth.resolve(tok));
let handler = HttpsHandler::new(state.local_vid.clone(), peer_vid);
let routing_key = doc.type_uri.for_routing().to_string();
let Some(route) = state.routes.get(&routing_key) else {
return reject_response(
Some(&handler),
Some(&doc),
RejectReason::UnsupportedType {
type_uri: routing_key,
},
);
};
let resolved = match handler.resolve_parties(&doc) {
Ok(r) => r,
Err(consistency) => {
let reason: RejectReason = consistency.into();
return reject_response(Some(&handler), Some(&doc), reason);
}
};
let now = chrono::Utc::now();
let my_vid = state.local_vid.as_deref().unwrap_or("");
if let Err(reason) = doc.validate_basic(now, my_vid) {
return reject_response(Some(&handler), Some(&doc), reason);
}
if let Err(reason) = doc.validate_freshness(now, &state.freshness) {
return reject_response(Some(&handler), Some(&doc), reason);
}
if state.require_attribution && handler.peer().is_none() && doc.proof.is_none() {
return reject_response(Some(&handler), Some(&doc), RejectReason::ProofRequired);
}
if let (Some(allowed), Some(proof)) = (&state.allowed_did_methods, &doc.proof) {
if !did_method_allowed(&proof.verification_method, allowed) {
return reject_response(
Some(&handler),
Some(&doc),
RejectReason::ProofInvalid {
reason: "verification method is not acceptable under this consumer's \
proof policy"
.to_string(),
},
);
}
}
if doc.proof.is_some() {
match &state.verifier {
Some(v) => {
if let Err(err) = v.verify_json(&doc).await {
return reject_response(
Some(&handler),
Some(&doc),
RejectReason::ProofInvalid {
reason: err.to_string(),
},
);
}
}
None => {
return reject_response(
Some(&handler),
Some(&doc),
RejectReason::MalformedRequest {
reason: PROOF_NOT_ACCEPTED_BY_POLICY.to_string(),
},
);
}
}
}
let mut claim: Option<DocumentDigest> = None;
if let Some(guard) = &state.replay_guard {
let digest = match document_digest(&doc) {
Ok(digest) => digest,
Err(e) => {
return reject_response(
Some(&handler),
Some(&doc),
RejectReason::malformed_from_serde(&e),
);
}
};
let Some(retain_until) = state.freshness.record_expiry(&doc, now) else {
return reject_response(
Some(&handler),
Some(&doc),
RejectReason::Stale {
detail: StaleReason::Unboundable,
},
);
};
match guard.claim(&doc.id, &digest, Some(retain_until), now).await {
Ok(ReplayVerdict::Fresh) => claim = Some(digest),
Ok(ReplayVerdict::Duplicate {
prior_response: Some(prior),
..
}) => return success_response(prior),
Ok(ReplayVerdict::Duplicate {
in_flight: true, ..
}) => return StatusCode::ACCEPTED.into_response(),
Ok(ReplayVerdict::Duplicate { .. }) => return StatusCode::NO_CONTENT.into_response(),
Ok(ReplayVerdict::Conflict) => {
return reject_response(Some(&handler), Some(&doc), RejectReason::IdConflict);
}
Ok(_) => {
return reject_response(
Some(&handler),
Some(&doc),
RejectReason::Unavailable { retry_after: None },
);
}
Err(e) => return reject_response(Some(&handler), Some(&doc), e.into()),
}
}
let ctx = RequestContext {
authenticated_sender: handler.peer().map(str::to_string),
local: handler.local().map(str::to_string),
resolved,
};
let dispatched = doc.clone();
let dispatch_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(route.dispatch)(dispatched, &ctx)
}));
let dispatch_result = match dispatch_result {
Ok(result) => result,
Err(panic) => {
release_claim(&state, &doc.id, claim.as_ref()).await;
std::panic::resume_unwind(panic);
}
};
match dispatch_result {
Ok(success_body) => {
if claim.is_some() {
if let Some(guard) = &state.replay_guard {
let _ = guard.record_response(&doc.id, success_body.as_ref()).await;
}
}
match success_body {
Some(body) => success_response(body),
None => StatusCode::NO_CONTENT.into_response(),
}
}
Err(reason) => {
release_claim(&state, &doc.id, claim.as_ref()).await;
reject_response(Some(&handler), Some(&doc), reason)
}
}
}
async fn release_claim(state: &ServerState, id: &str, digest: Option<&DocumentDigest>) {
if let (Some(guard), Some(digest)) = (&state.replay_guard, digest) {
let _ = guard.release(id, digest).await;
}
}
fn is_json_content_type(headers: &HeaderMap) -> bool {
let Some(value) = headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
else {
return false;
};
let media_type = value.split(';').next().unwrap_or("").trim();
media_type.eq_ignore_ascii_case("application/json")
}
fn did_method_allowed(verification_method: &str, allowed: &BTreeSet<String>) -> bool {
let Some(rest) = verification_method.strip_prefix("did:") else {
return false;
};
let Some((method, remainder)) = rest.split_once(':') else {
return false;
};
if method.is_empty() || remainder.is_empty() {
return false;
}
allowed.contains(method)
}
fn extract_bearer(headers: &HeaderMap) -> Option<&str> {
let value = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
let token = value
.strip_prefix("Bearer ")
.or_else(|| value.strip_prefix("bearer "))?;
Some(token.trim())
}
fn downcast<P: Payload>(doc: TrustTask<Value>) -> Result<TrustTask<P>, RejectReason> {
let TrustTask {
id,
thread_id,
parent_thread_id,
ceremony,
type_uri,
issuer,
recipient,
issued_at,
expires_at,
payload,
context,
proof,
extra,
} = doc;
let payload: P =
serde_json::from_value(payload).map_err(|e| RejectReason::malformed_from_serde(&e))?;
Ok(TrustTask {
id,
thread_id,
parent_thread_id,
ceremony,
type_uri,
issuer,
recipient,
issued_at,
expires_at,
payload,
context,
proof,
extra,
})
}
fn success_response(body: Value) -> Response {
let bytes = serde_json::to_vec(&body).expect("serialise success body");
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
bytes,
)
.into_response()
}
fn reject_response(
handler: Option<&HttpsHandler>,
request: Option<&TrustTask<Value>>,
reason: RejectReason,
) -> Response {
let error_doc = build_error_response(handler, request, reason);
let status = status_for_code(&error_doc.payload.code);
let status = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let body = serde_json::to_vec(&error_doc).expect("serialise error response");
(status, [(header::CONTENT_TYPE, "application/json")], body).into_response()
}
fn build_error_response(
handler: Option<&HttpsHandler>,
request: Option<&TrustTask<Value>>,
reason: RejectReason,
) -> ErrorResponse {
let new_id = format!("urn:uuid:{}", Uuid::new_v4());
match (handler, request) {
(Some(h), Some(req)) => {
match h.reject(req, new_id.clone(), reason.clone()) {
Some(resp) => resp,
None => suppressed_error_response(&new_id),
}
}
(_, Some(req)) => req.reject_with(new_id, reason),
_ => {
let mut doc = TrustTask::new(
new_id,
trust_tasks_rs::trust_task_error_type_uri(),
ErrorPayload::from(reason),
);
doc.issued_at = Some(chrono::Utc::now());
doc
}
}
}
fn suppressed_error_response(new_id: &str) -> ErrorResponse {
let mut doc = TrustTask::new(
new_id.to_string(),
trust_tasks_rs::trust_task_error_type_uri(),
ErrorPayload::from(RejectReason::MalformedRequest {
reason: String::new(),
}),
);
doc.issued_at = Some(chrono::Utc::now());
doc
}
fn _verify_standard_code_into() {
let _: trust_tasks_rs::TrustTaskCode = StandardCode::Expired.into();
}