ap-relay 0.3.123

A simple activitypub relay
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_value = accept.to_str().map_err(|_| ErrorKind::InvalidAccept)?;

        let acceptable = accept_value
            .split(", ")
            .filter_map(|accept| accept.trim().parse::<mime::Mime>().ok())
            .any(|accept| is_supported_json(&accept));

        if !acceptable {
            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()
                }
            ]
        }))
}