v6/src/activitypub/webfinger.rs

199 lines
5.7 KiB
Rust

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<String> = Lazy::new(|| format!("blog@{}", *DOMAIN));
const OIDC_SUBJECT: &'static str = "me@shadowfacts.net";
pub async fn handle(req: Request<Body>) -> 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<InteractParams>,
) -> 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<Body>) -> Option<String> {
let mut parts = RequestParts::new(req);
let params = parts.extract::<Query<Params>>().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<WebfingerResponse> {
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<Link>,
}
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()
}
},
]
}
);
}
}