From 7aa5a6738cebd450bb6909bd2d8a392eb0443206 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Fri, 3 Apr 2026 19:30:26 +0200 Subject: [PATCH] feat: add host-meta endpoint with domain-aware XRD response --- src/handler/host_meta.rs | 45 +++++++++++++++++++++++++++++++++ src/handler/mod.rs | 2 ++ tests/test_host_meta.rs | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/handler/host_meta.rs create mode 100644 tests/test_host_meta.rs diff --git a/src/handler/host_meta.rs b/src/handler/host_meta.rs new file mode 100644 index 0000000..07f4b9f --- /dev/null +++ b/src/handler/host_meta.rs @@ -0,0 +1,45 @@ +use axum::extract::State; +use axum_extra::extract::Host; +use axum::http::header; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::Router; +use sea_orm::*; + +use crate::entity::domains; +use crate::error::{AppError, AppResult}; +use crate::state::AppState; + +async fn host_meta( + State(state): State, + Host(hostname): Host, +) -> AppResult { + // Strip port if present + let domain = hostname.split(':').next().unwrap_or(&hostname); + + // Check this domain is registered and verified + let _domain = domains::Entity::find() + .filter(domains::Column::Domain.eq(domain)) + .filter(domains::Column::Verified.eq(true)) + .one(&state.db) + .await? + .ok_or(AppError::NotFound)?; + + let base_url = &state.settings.server.base_url; + let xrd = format!( + r#" + + +"# + ); + + Ok(( + [(header::CONTENT_TYPE, "application/xrd+xml; charset=utf-8")], + xrd, + ) + .into_response()) +} + +pub fn router() -> Router { + Router::new().route("/.well-known/host-meta", get(host_meta)) +} diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 1c09ae5..cf46fd5 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,4 +1,5 @@ mod health; +mod host_meta; mod webfinger; use axum::Router; @@ -7,6 +8,7 @@ use crate::state::AppState; pub fn router(state: AppState) -> Router { Router::new() .merge(webfinger::router()) + .merge(host_meta::router()) .merge(health::router()) .with_state(state) } diff --git a/tests/test_host_meta.rs b/tests/test_host_meta.rs new file mode 100644 index 0000000..15831e7 --- /dev/null +++ b/tests/test_host_meta.rs @@ -0,0 +1,54 @@ +mod common; + +use axum_test::TestServer; +use webfingerd::handler; + +#[tokio::test] +async fn test_host_meta_returns_xrd_for_known_domain() { + let state = common::test_state().await; + + // Seed a verified domain in DB + use sea_orm::ActiveModelTrait; + use sea_orm::Set; + use webfingerd::entity::domains; + + let domain = domains::ActiveModel { + id: Set(uuid::Uuid::new_v4().to_string()), + domain: Set("example.com".into()), + owner_token_hash: Set("hash".into()), + registration_secret: Set("secret".into()), + challenge_type: Set("dns-01".into()), + challenge_token: Set(None), + verified: Set(true), + created_at: Set(chrono::Utc::now().naive_utc()), + verified_at: Set(Some(chrono::Utc::now().naive_utc())), + }; + domain.insert(&state.db).await.unwrap(); + + let app = handler::router(state); + let server = TestServer::new(app); + + let response = server + .get("/.well-known/host-meta") + .add_header("Host", "example.com") + .await; + + response.assert_status_ok(); + let body = response.text(); + assert!(body.contains("application/jrd+json") || body.contains("XRD")); + assert!(body.contains("/.well-known/webfinger")); +} + +#[tokio::test] +async fn test_host_meta_returns_404_for_unknown_domain() { + let state = common::test_state().await; + let app = handler::router(state); + let server = TestServer::new(app); + + let response = server + .get("/.well-known/host-meta") + .add_header("Host", "unknown.example.com") + .await; + + response.assert_status_not_found(); +}