use super::{actor::ID, util, CLIENT, DOMAIN}; use anyhow::anyhow; use axum::{ extract::{Form, Query, RequestParts}, response::{IntoResponse, Redirect, Response}, Json, }; use hyper::{header::ACCEPT, Body, Request, StatusCode}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; static ACCT: Lazy = Lazy::new(|| format!("blog@{}", *DOMAIN)); const OIDC_SUBJECT: &'static str = "me@shadowfacts.net"; pub async fn handle(req: Request) -> Response { let resource = resource(req).await; if resource.as_deref() == Some(&*ACCT) { Json(WebfingerResponse::blog()).into_response() } else if resource.as_deref() == Some(&OIDC_SUBJECT) { // used for tailscale OIDC Json(WebfingerResponse::oidc()).into_response() } else { // mastodon responds with 400 and an empty body, so we do too StatusCode::BAD_REQUEST.into_response() } } pub async fn handle_interact( Form(InteractParams { acct, permalink }): Form, ) -> Response { if let Ok(response) = query(&acct).await { for link in response.links { if let Link { payload: LinkPayload::OStatusSubscribe { template }, .. } = link { let url = template.replace("{uri}", &format!("https://{}{}", &*DOMAIN, permalink)); return Redirect::to(&url).into_response(); } } } ( StatusCode::BAD_REQUEST, "Unable to find remote subscribe URL", ) .into_response() } async fn resource(req: Request) -> Option { let mut parts = RequestParts::new(req); let params = parts.extract::>().await; params.ok().map(|Query(params)| { if params.resource.starts_with("acct:") { params.resource[5..].into() } else { params.resource } }) } async fn query(acct: &str) -> anyhow::Result { let (_, domain) = util::parse_acct(acct).ok_or(anyhow!("invalid acct"))?; let response = CLIENT .get(format!( "https://{}/.well-known/webfinger?resource={}", domain, acct )) .header(ACCEPT, "application/json") .send() .await?; Ok(response.json().await?) } #[derive(Deserialize)] pub struct InteractParams { permalink: String, #[serde(rename = "remote_follow[acct]")] acct: String, } #[derive(Deserialize)] pub struct Params { resource: String, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct WebfingerResponse { subject: String, links: Vec, } impl WebfingerResponse { fn blog() -> Self { Self { subject: ACCT.to_owned(), links: vec![Link { rel: "self".to_owned(), payload: LinkPayload::Webfinger { r#type: "application/activity+json".to_owned(), href: ID.to_string(), }, }], } } fn oidc() -> Self { Self { subject: OIDC_SUBJECT.to_owned(), links: vec![Link { rel: "http://openid.net/specs/connect/1.0/issuer".to_owned(), payload: LinkPayload::Href { href: "https://auth.shadowfacts.net/application/o/tailscale/".to_owned(), }, }], } } } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Link { rel: String, #[serde(flatten)] payload: LinkPayload, } #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum LinkPayload { Webfinger { r#type: String, href: String }, OStatusSubscribe { template: String }, Href { href: String }, } #[cfg(test)] mod tests { use super::*; #[test] fn test_deserialize_link_payloads() { let json = r#" { "subject": "acct:Gargron@mastodon.social", "aliases": [ "https://mastodon.social/@Gargron", "https://mastodon.social/users/Gargron" ], "links": [ { "rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://mastodon.social/@Gargron" }, { "rel": "self", "type": "application/activity+json", "href": "https://mastodon.social/users/Gargron" }, { "rel": "http://ostatus.org/schema/1.0/subscribe", "template": "https://mastodon.social/authorize_interaction?uri={uri}" } ] } "#; let response: WebfingerResponse = serde_json::from_str(&json).unwrap(); assert_eq!( response, WebfingerResponse { subject: "acct:Gargron@mastodon.social".to_owned(), links: vec![ Link { rel: "http://webfinger.net/rel/profile-page".to_owned(), payload: LinkPayload::Webfinger { r#type: "text/html".to_owned(), href: "https://mastodon.social/@Gargron".to_owned() } }, Link { rel: "self".to_owned(), payload: LinkPayload::Webfinger { r#type: "application/activity+json".to_owned(), href: "https://mastodon.social/users/Gargron".to_owned() } }, Link { rel: "http://ostatus.org/schema/1.0/subscribe".to_owned(), payload: LinkPayload::OStatusSubscribe { template: "https://mastodon.social/authorize_interaction?uri={uri}" .to_owned() } }, ] } ); } }