Handle webfinger directly

This commit is contained in:
asonix 2025-08-10 12:47:31 -05:00
parent 6ff7b59778
commit 6cf19bd4c9
8 changed files with 173 additions and 85 deletions

15
Cargo.lock generated
View File

@ -224,19 +224,6 @@ dependencies = [
"url",
]
[[package]]
name = "actix-webfinger"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74a22b44deff50693521b489885151fd65a2a596f7aef6d8c0753485b8915082"
dependencies = [
"actix-rt",
"actix-web",
"serde",
"serde_derive",
"thiserror 1.0.69",
]
[[package]]
name = "addr2line"
version = "0.21.0"
@ -396,7 +383,6 @@ dependencies = [
"activitystreams",
"activitystreams-ext",
"actix-web",
"actix-webfinger",
"ammonia",
"async-cpupool",
"background-jobs",
@ -448,6 +434,7 @@ dependencies = [
"tracing-log",
"tracing-opentelemetry",
"tracing-subscriber",
"url",
"uuid",
]

View File

@ -25,7 +25,6 @@ default = []
[dependencies]
actix-web = { version = "4.4.0", default-features = false, features = ["compress-brotli", "compress-gzip", "rustls-0_23"] }
actix-webfinger = { version = "0.5.0", default-features = false }
activitystreams = "0.7.0-alpha.25"
activitystreams-ext = "0.1.0-alpha.3"
ammonia = "4.0.0"
@ -84,6 +83,7 @@ tracing-subscriber = { version = "0.3", features = [
"fmt",
] }
tokio = { version = "1", features = ["full", "tracing"] }
url = { version = "2.5.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
[dependencies.background-jobs]

View File

@ -50,8 +50,8 @@ use self::{
data::{ActorCache, MediaCache, State},
db::Db,
jobs::create_workers,
middleware::{DebugPayload, MyVerify, RelayResolver, Timings},
routes::{actor, healthz, inbox, index, nodeinfo, nodeinfo_meta, statics},
middleware::{DebugPayload, MyVerify, Timings},
routes::{actor, healthz, inbox, index, nodeinfo, nodeinfo_meta, statics, webfinger},
spawner::Spawner,
};
@ -377,7 +377,7 @@ async fn server_main(
.service(web::resource("/nodeinfo/2.0.json").route(web::get().to(nodeinfo)))
.service(
web::scope("/.well-known")
.service(actix_webfinger::scoped::<RelayResolver>())
.service(web::resource("/webfinger").route(web::get().to(webfinger)))
.service(web::resource("/nodeinfo").route(web::get().to(nodeinfo_meta))),
)
.service(web::resource("/static/{filename}").route(web::get().to(statics)))

View File

@ -1,9 +1,7 @@
mod payload;
mod timings;
mod verifier;
mod webfinger;
pub(crate) use payload::DebugPayload;
pub(crate) use timings::Timings;
pub(crate) use verifier::MyVerify;
pub(crate) use webfinger::RelayResolver;

View File

@ -1,57 +0,0 @@
use crate::{
config::{Config, UrlKind},
data::State,
future::LocalBoxFuture,
};
use actix_web::web::Data;
use actix_webfinger::{Resolver, Webfinger};
use rsa_magic_public_key::AsMagicPublicKey;
pub(crate) struct RelayResolver;
#[derive(Clone, Debug, thiserror::Error)]
#[error("Error resolving webfinger data")]
pub(crate) struct RelayError;
impl Resolver for RelayResolver {
type State = (Data<State>, Data<Config>);
type Error = RelayError;
fn find(
scheme: Option<&str>,
account: &str,
domain: &str,
(state, config): Self::State,
) -> LocalBoxFuture<'static, Result<Option<Webfinger>, Self::Error>> {
let domain = domain.to_owned();
let account = account.to_owned();
let scheme = scheme.map(|scheme| scheme.to_owned());
let fut = async move {
if let Some(scheme) = scheme {
if scheme != "acct:" {
return Ok(None);
}
}
if domain != config.hostname() {
return Ok(None);
}
if account != "relay" {
return Ok(None);
}
let mut wf = Webfinger::new(config.generate_resource().as_str());
wf.add_alias(config.generate_url(UrlKind::Actor).as_str())
.add_activitypub(config.generate_url(UrlKind::Actor).as_str())
.add_magic_public_key(&state.public_key.as_magic_public_key());
Ok(Some(wf))
};
Box::pin(fut)
}
}
impl actix_web::error::ResponseError for RelayError {}

View File

@ -5,6 +5,7 @@ mod index;
mod media;
mod nodeinfo;
mod statics;
mod webfinger;
pub(crate) use self::{
actor::route as actor,
@ -14,6 +15,7 @@ pub(crate) use self::{
media::route as media,
nodeinfo::{route as nodeinfo, well_known as nodeinfo_meta},
statics::route as statics,
webfinger::resolve as webfinger,
};
use actix_web::HttpResponse;

View File

@ -3,17 +3,15 @@ use crate::{
data::State,
};
use actix_web::{web, Responder};
use actix_webfinger::Link;
use serde_json::Value;
#[tracing::instrument(name = "Well Known NodeInfo", skip(config))]
pub(crate) async fn well_known(config: web::Data<Config>) -> impl Responder {
web::Json(Links {
links: vec![Link {
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_owned(),
href: Some(config.generate_url(UrlKind::NodeInfo).to_string()),
template: None,
kind: None,
}],
links: [serde_json::json!({
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": config.generate_url(UrlKind::NodeInfo),
})],
})
.customize()
.insert_header(("Content-Type", "application/jrd+json"))
@ -21,7 +19,7 @@ pub(crate) async fn well_known(config: web::Data<Config>) -> impl Responder {
#[derive(serde::Serialize)]
struct Links {
links: Vec<Link>,
links: [Value; 1],
}
#[tracing::instrument(name = "NodeInfo", skip_all)]

160
src/routes/webfinger.rs Normal file
View File

@ -0,0 +1,160 @@
use crate::{
config::{Config, UrlKind},
data::State,
};
use actix_web::{
dev::Payload,
http::{header::ACCEPT, StatusCode},
web::{Data, Query},
FromRequest, HttpRequest, HttpResponse, ResponseError,
};
use rsa_magic_public_key::AsMagicPublicKey;
use std::future::{ready, Ready};
use url::Url;
#[derive(Debug, thiserror::Error)]
pub(crate) enum ErrorKind {
#[error("Accept Header is required")]
MissingAccept,
#[error("Unsupported accept type")]
InvalidAccept,
#[error("Query is malformed")]
InvalidQuery,
#[error("No records match the provided resource")]
NotFound,
}
impl ResponseError for ErrorKind {
fn status_code(&self) -> StatusCode {
match self {
Self::MissingAccept | Self::InvalidAccept | Self::InvalidQuery => {
StatusCode::BAD_REQUEST
}
Self::NotFound => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
HttpResponse::build(self.status_code()).finish()
}
}
#[derive(serde::Deserialize)]
struct Resource {
resource: String,
}
pub(crate) enum WebfingerResource {
Url(Url),
Unknown(String),
}
fn is_supported_json(m: &mime::Mime) -> bool {
matches!(
(
m.type_().as_str(),
m.subtype().as_str(),
m.suffix().map(|s| s.as_str()),
),
("*", "*", None)
| ("application", "*", None)
| ("application", "json", None)
| ("application", "jrd", Some("json"))
)
}
impl WebfingerResource {
fn parse_request(req: &HttpRequest) -> Result<Self, ErrorKind> {
let Some(accept) = req.headers().get(ACCEPT) else {
return Err(ErrorKind::MissingAccept);
};
let accept = accept.to_str().map_err(|_| ErrorKind::InvalidAccept)?;
let accept = accept
.parse::<mime::Mime>()
.map_err(|_| ErrorKind::InvalidAccept)?;
if !is_supported_json(&accept) {
return Err(ErrorKind::InvalidAccept);
}
let Resource { resource } = Query::<Resource>::from_query(req.query_string())
.map_err(|_| ErrorKind::InvalidQuery)?
.into_inner();
let wr = match Url::parse(&resource) {
Ok(url) => WebfingerResource::Url(url),
Err(_) => WebfingerResource::Unknown(resource),
};
Ok(wr)
}
}
impl FromRequest for WebfingerResource {
type Error = ErrorKind;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(Self::parse_request(req))
}
}
pub(crate) async fn resolve(
config: Data<Config>,
state: Data<State>,
resource: WebfingerResource,
) -> Result<HttpResponse, ErrorKind> {
match resource {
WebfingerResource::Unknown(handle) => {
if handle.trim_start_matches('@') == config.generate_resource() {
return Ok(respond(&config, &state));
}
}
WebfingerResource::Url(url) => match url.scheme() {
"acct" => {
if url.path().trim_start_matches('@') == config.generate_resource() {
return Ok(respond(&config, &state));
}
}
"http" | "https" => {
if url.as_str() == config.generate_url(UrlKind::Actor).as_str() {
return Ok(respond(&config, &state));
}
}
_ => return Err(ErrorKind::NotFound),
},
}
Err(ErrorKind::NotFound)
}
fn respond(config: &Config, state: &State) -> HttpResponse {
HttpResponse::Ok()
.content_type("application/jrd+json")
.json(serde_json::json!({
"subject": format!("acct:{}", config.generate_resource()),
"aliases": [
config.generate_url(UrlKind::Actor),
],
"links": [
{
"rel": "self",
"href": config.generate_url(UrlKind::Actor),
"type": "application/activity+json"
},
{
"rel": "self",
"href": config.generate_url(UrlKind::Actor),
"type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
},
{
"rel": "magic-public-key",
"href": state.public_key.as_magic_public_key()
}
]
}))
}