diff --git a/Cargo.lock b/Cargo.lock index b21352b..1aa4b4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 9746e74..17c6b80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/src/main.rs b/src/main.rs index 881a94d..ca20d0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::()) + .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))) diff --git a/src/middleware.rs b/src/middleware.rs index 93d6a68..fa73336 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -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; diff --git a/src/middleware/webfinger.rs b/src/middleware/webfinger.rs deleted file mode 100644 index 0590408..0000000 --- a/src/middleware/webfinger.rs +++ /dev/null @@ -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, Data); - type Error = RelayError; - - fn find( - scheme: Option<&str>, - account: &str, - domain: &str, - (state, config): Self::State, - ) -> LocalBoxFuture<'static, Result, 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 {} diff --git a/src/routes.rs b/src/routes.rs index 9afd38b..ef80ebc 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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; diff --git a/src/routes/nodeinfo.rs b/src/routes/nodeinfo.rs index 66785e0..d5d4c32 100644 --- a/src/routes/nodeinfo.rs +++ b/src/routes/nodeinfo.rs @@ -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) -> 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) -> impl Responder { #[derive(serde::Serialize)] struct Links { - links: Vec, + links: [Value; 1], } #[tracing::instrument(name = "NodeInfo", skip_all)] diff --git a/src/routes/webfinger.rs b/src/routes/webfinger.rs new file mode 100644 index 0000000..fd865ba --- /dev/null +++ b/src/routes/webfinger.rs @@ -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 { + 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 { + 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::() + .map_err(|_| ErrorKind::InvalidAccept)?; + + if !is_supported_json(&accept) { + return Err(ErrorKind::InvalidAccept); + } + + let Resource { resource } = Query::::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>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ready(Self::parse_request(req)) + } +} + +pub(crate) async fn resolve( + config: Data, + state: Data, + resource: WebfingerResource, +) -> Result { + 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() + } + ] + })) +}