199 lines
5.7 KiB
Rust
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()
|
|
}
|
|
},
|
|
]
|
|
}
|
|
);
|
|
}
|
|
}
|