diff --git a/.gitignore b/.gitignore index 1469e2f..e3745a3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ Cargo.lock # Environment .env.local + +# IntelliJ IDEA thing, mostly broken versioning +.idea/misc.xml diff --git a/.idea/acrate.iml b/.idea/acrate.iml index 983a32b..32d223d 100644 --- a/.idea/acrate.iml +++ b/.idea/acrate.iml @@ -9,6 +9,7 @@ + diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..6cfc387 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql:///acrate + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 30bab2a..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..4495f93 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index aecded5..357abb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo"] +members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo", "acrate-webfinger"] diff --git a/README.md b/README.md index 814f637..b524419 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,15 @@ Federation database > [!Caution] > > This software suite is in active development! + +## Crates + +### Libraries + +- `acrate-core`: Database stuff +- `acrate-hostmeta`: RFC 6415 serde +- `acrate-nodeinfo`: NodeInfo serde + +### Binaries + +- `acrate-webfinger`: WebFinger server diff --git a/acrate-core/Cargo.toml b/acrate-core/Cargo.toml index 6823787..d5b738a 100644 --- a/acrate-core/Cargo.toml +++ b/acrate-core/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] -diesel = "2.2.4" +diesel = { version = "2.2.4", features = ["postgres", "uuid"] } +diesel-async = { version = "0.5.1", features = ["postgres"] } diesel_migrations = "2.2.0" -acrate-hostmeta = { path = "../acrate-hostmeta" } -acrate-nodeinfo = { path = "../acrate-nodeinfo" } +uuid = "1.11.0" [lints.clippy] tabs-in-doc-comments = "allow" diff --git a/acrate-core/migrations/2024-11-14-031744_meta/down.sql b/acrate-core/migrations/2024-11-14-031744_meta/down.sql new file mode 100644 index 0000000..39d835e --- /dev/null +++ b/acrate-core/migrations/2024-11-14-031744_meta/down.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS meta_properties CASCADE; +DROP TABLE IF EXISTS meta_link_properties CASCADE; +DROP TABLE IF EXISTS meta_link_titles CASCADE; +DROP TABLE IF EXISTS meta_links CASCADE; +DROP TABLE IF EXISTS meta_aliases CASCADE; +DROP TABLE IF EXISTS meta_subjects CASCADE; diff --git a/acrate-core/migrations/2024-11-14-031744_meta/up.sql b/acrate-core/migrations/2024-11-14-031744_meta/up.sql new file mode 100644 index 0000000..8387ac9 --- /dev/null +++ b/acrate-core/migrations/2024-11-14-031744_meta/up.sql @@ -0,0 +1,63 @@ +CREATE TABLE IF NOT EXISTS meta_subjects ( + id UUID DEFAULT gen_random_uuid(), + document BPCHAR NOT NULL, + pattern BPCHAR NOT NULL, + redirect BPCHAR, + + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS meta_aliases ( + id UUID DEFAULT gen_random_uuid(), + document BPCHAR NOT NULL, + pattern BPCHAR NOT NULL, + alias BPCHAR NOT NULL, + + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS meta_links ( + id UUID DEFAULT gen_random_uuid(), + document BPCHAR NOT NULL, + pattern BPCHAR NOT NULL, + rel BPCHAR NOT NULL, + type BPCHAR, + href BPCHAR, + template BPCHAR, + + CONSTRAINT either_href_or_template_not_null CHECK ( + (href IS NOT NULL AND template IS NULL) + OR + (href IS NULL AND template IS NOT NULL) + ), + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS meta_link_properties ( + id UUID DEFAULT gen_random_uuid(), + meta_link_id UUID REFERENCES meta_links (id) NOT NULL, + rel BPCHAR NOT NULL, + value BPCHAR, + + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS meta_properties ( + id UUID DEFAULT gen_random_uuid(), + document BPCHAR NOT NULL, + pattern BPCHAR NOT NULL, + rel BPCHAR NOT NULL, + value BPCHAR, + + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS meta_link_titles ( + id UUID DEFAULT gen_random_uuid(), + meta_link_id UUID REFERENCES meta_links (id) NOT NULL, + language BPCHAR NOT NULL DEFAULT 'und', + value BPCHAR NOT NULL, + + CONSTRAINT unique_languages UNIQUE (meta_link_id, language), + PRIMARY KEY(id) +); diff --git a/acrate-core/src/lib.rs b/acrate-core/src/lib.rs index dae5ee7..d340197 100644 --- a/acrate-core/src/lib.rs +++ b/acrate-core/src/lib.rs @@ -1,4 +1,8 @@ //! Core crate of the `acrate` project. -pub use acrate_nodeinfo as nodeinfo; -pub use acrate_hostmeta as hostmeta; +mod schema; + +pub mod meta; + +pub use diesel; +pub use diesel_async; diff --git a/acrate-core/src/meta.rs b/acrate-core/src/meta.rs new file mode 100644 index 0000000..4fe0343 --- /dev/null +++ b/acrate-core/src/meta.rs @@ -0,0 +1,165 @@ +use diesel::{Associations, Identifiable, Insertable, QueryResult, Queryable, QueryableByName, Selectable, pg::Pg}; +use diesel_async::AsyncPgConnection; +use uuid::Uuid; +use super::schema; + + +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +#[diesel(table_name = schema::meta_subjects)] +#[diesel(check_for_backend(Pg))] +pub struct MetaSubject { + pub id: Uuid, + pub document: String, + pub pattern: String, + pub redirect: Option, +} + +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +#[diesel(table_name = schema::meta_aliases)] +#[diesel(check_for_backend(Pg))] +pub struct MetaAlias { + pub id: Uuid, + pub document: String, + pub pattern: String, + pub alias: String, +} + +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +#[diesel(table_name = schema::meta_links)] +#[diesel(check_for_backend(Pg))] +pub struct MetaLink { + pub id: Uuid, + pub document: String, + pub pattern: String, + pub rel: String, + pub type_: Option, + pub href: Option, + pub template: Option, +} + +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)] +#[diesel(belongs_to(MetaLink))] +#[diesel(table_name = schema::meta_link_properties)] +#[diesel(check_for_backend(Pg))] +pub struct MetaLinkProperty { + pub id: Uuid, + pub meta_link_id: Uuid, + pub rel: String, + pub value: Option, +} + +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)] +#[diesel(belongs_to(MetaLink))] +#[diesel(table_name = schema::meta_link_titles)] +#[diesel(check_for_backend(Pg))] +pub struct MetaLinkTitle { + pub id: Uuid, + pub meta_link_id: Uuid, + pub language: String, + pub value: String, +} + +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +#[diesel(table_name = schema::meta_properties)] +#[diesel(check_for_backend(Pg))] +pub struct MetaProperty { + pub id: Uuid, + pub document: String, + pub pattern: String, + pub rel: String, + pub value: Option, +} + +impl MetaSubject { + pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use schema::meta_subjects::dsl::*; + + let document_is_equal = document.eq(doc); + let subject_matches_pattern = subject.into_sql::().ilike(pattern); + + meta_subjects + .filter(document_is_equal) + .filter(subject_matches_pattern) + .select(Self::as_select()) + .load(conn) + .await + } +} + +impl MetaAlias { + pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use schema::meta_aliases::dsl::*; + + let document_is_equal = document.eq(doc); + let subject_matches_pattern = subject.into_sql::().ilike(pattern); + + meta_aliases + .filter(document_is_equal) + .filter(subject_matches_pattern) + .select(Self::as_select()) + .load(conn) + .await + } +} + +impl MetaLink { + pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use schema::meta_links::dsl::*; + + let document_is_equal = document.eq(doc); + let subject_matches_pattern = subject.into_sql::().ilike(pattern); + + meta_links + .filter(document_is_equal) + .filter(subject_matches_pattern) + .select(Self::as_select()) + .load(conn) + .await + } +} + +impl MetaLinkProperty { + pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult> { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + Self::belonging_to(links) + .load(conn) + .await + } +} + +impl MetaLinkTitle { + pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult> { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + Self::belonging_to(links) + .load(conn) + .await + } +} + +impl MetaProperty { + pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> { + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use schema::meta_properties::dsl::*; + + let document_is_equal = document.eq(doc); + let subject_matches_pattern = subject.into_sql::().ilike(pattern); + + meta_properties + .filter(document_is_equal) + .filter(subject_matches_pattern) + .select(Self::as_select()) + .load(conn) + .await + } +} diff --git a/acrate-core/src/schema.rs b/acrate-core/src/schema.rs new file mode 100644 index 0000000..94b2aa7 --- /dev/null +++ b/acrate-core/src/schema.rs @@ -0,0 +1,72 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + meta_aliases (id) { + id -> Uuid, + document -> Bpchar, + pattern -> Bpchar, + alias -> Bpchar, + } +} + +diesel::table! { + meta_link_properties (id) { + id -> Uuid, + meta_link_id -> Uuid, + rel -> Bpchar, + value -> Nullable, + } +} + +diesel::table! { + meta_link_titles (id) { + id -> Uuid, + meta_link_id -> Uuid, + language -> Bpchar, + value -> Bpchar, + } +} + +diesel::table! { + meta_links (id) { + id -> Uuid, + document -> Bpchar, + pattern -> Bpchar, + rel -> Bpchar, + #[sql_name = "type"] + type_ -> Nullable, + href -> Nullable, + template -> Nullable, + } +} + +diesel::table! { + meta_properties (id) { + id -> Uuid, + document -> Bpchar, + pattern -> Bpchar, + rel -> Bpchar, + value -> Nullable, + } +} + +diesel::table! { + meta_subjects (id) { + id -> Uuid, + document -> Bpchar, + pattern -> Bpchar, + redirect -> Nullable, + } +} + +diesel::joinable!(meta_link_properties -> meta_links (meta_link_id)); +diesel::joinable!(meta_link_titles -> meta_links (meta_link_id)); + +diesel::allow_tables_to_appear_in_same_query!( + meta_aliases, + meta_link_properties, + meta_link_titles, + meta_links, + meta_properties, + meta_subjects, +); diff --git a/acrate-hostmeta/src/jrd.rs b/acrate-hostmeta/src/jrd.rs index 7662b80..24c7f68 100644 --- a/acrate-hostmeta/src/jrd.rs +++ b/acrate-hostmeta/src/jrd.rs @@ -17,6 +17,7 @@ pub struct ResourceDescriptorJRD { /// /// - /// + #[serde(skip_serializing_if = "Option::is_none")] pub subject: Option, /// Other names the resource described by this document can be referred to. @@ -70,6 +71,7 @@ pub struct ResourceDescriptorLinkJRD { /// /// - /// + #[serde(skip_serializing_if = "Option::is_none")] pub r#type: Option, /// URI to the resource put in relation. @@ -78,6 +80,7 @@ pub struct ResourceDescriptorLinkJRD { /// /// - /// + #[serde(skip_serializing_if = "Option::is_none")] pub href: Option, /// Titles of the resource put in relation in various languages. @@ -104,6 +107,7 @@ pub struct ResourceDescriptorLinkJRD { /// /// - /// + #[serde(skip_serializing_if = "Option::is_none")] pub template: Option, } @@ -219,7 +223,7 @@ impl From for ResourceDescriptorLinkJRD { impl From for (String, Option) { fn from(value: ResourceDescriptorPropertyXRD) -> Self { - (value.r#type, value.value) + (value.rel, value.value) } } diff --git a/acrate-hostmeta/src/xrd.rs b/acrate-hostmeta/src/xrd.rs index 0e89c1d..96e3a73 100644 --- a/acrate-hostmeta/src/xrd.rs +++ b/acrate-hostmeta/src/xrd.rs @@ -9,6 +9,7 @@ use crate::jrd::{ResourceDescriptorJRD, ResourceDescriptorLinkJRD}; /// - /// - #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename = "XRD")] pub struct ResourceDescriptorXRD { /// The resource this document refers to. /// @@ -17,6 +18,7 @@ pub struct ResourceDescriptorXRD { /// - /// #[serde(rename = "Subject")] + #[serde(skip_serializing_if = "Option::is_none")] pub subject: Option, /// Other names the resource described by this document can be referred to. @@ -58,6 +60,7 @@ pub struct ResourceDescriptorXRD { /// - /// #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename = "Link")] pub struct ResourceDescriptorLinkXRD { /// The kind of relation established by the subject with the attached resource. /// @@ -75,6 +78,7 @@ pub struct ResourceDescriptorLinkXRD { /// - /// #[serde(rename = "@type")] + #[serde(skip_serializing_if = "Option::is_none")] pub r#type: Option, /// URI to the resource put in relation. @@ -84,6 +88,7 @@ pub struct ResourceDescriptorLinkXRD { /// - /// #[serde(rename = "@href")] + #[serde(skip_serializing_if = "Option::is_none")] pub href: Option, /// Titles of the resource put in relation in various languages. @@ -93,6 +98,7 @@ pub struct ResourceDescriptorLinkXRD { /// - /// #[serde(default)] + #[serde(rename = "Title")] pub titles: Vec, /// Additional information about the resource put in relation. @@ -102,6 +108,7 @@ pub struct ResourceDescriptorLinkXRD { /// - /// #[serde(default)] + #[serde(rename = "Property")] pub properties: Vec, /// Template to fill to get the URL to resource-specific information. @@ -111,6 +118,7 @@ pub struct ResourceDescriptorLinkXRD { /// - /// #[serde(rename = "@template")] + #[serde(skip_serializing_if = "Option::is_none")] pub template: Option, } @@ -120,22 +128,26 @@ pub struct ResourceDescriptorLinkXRD { /// /// - #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename = "Title")] pub struct ResourceDescriptorTitleXRD { /// The language of the title. #[serde(rename = "@lang")] pub language: String, /// The title itself. + #[serde(rename = "$text")] pub value: String, } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename = "Property")] pub struct ResourceDescriptorPropertyXRD { /// The property identifier, or type. - #[serde(alias = "@type")] - pub r#type: String, + #[serde(rename = "@rel")] + pub rel: String, /// The property value. + #[serde(rename = "$text")] pub value: Option, } @@ -234,7 +246,7 @@ impl From for ResourceDescriptorXRD { impl From<(String, Option)> for ResourceDescriptorPropertyXRD { fn from(value: (String, Option)) -> Self { Self { - r#type: value.0, + rel: value.0, value: value.1, } } diff --git a/acrate-webfinger/Cargo.toml b/acrate-webfinger/Cargo.toml new file mode 100644 index 0000000..cfd73d0 --- /dev/null +++ b/acrate-webfinger/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "acrate-webfinger" +version = "0.1.0" +edition = "2021" + +[dependencies] +acrate-core = { path = "../acrate-core" } +acrate-hostmeta = { path = "../acrate-hostmeta" } +anyhow = "1.0.93" +axum = { version = "0.7.7", features = ["macros"] } +axum-extra = { version = "0.9.4", features = ["query"] } +log = "0.4.22" +micronfig = "0.3.0" +minijinja = "2.5.0" +pretty_env_logger = "0.5.0" +quick-xml = { version = "0.37.0", features = ["serialize"] } +serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.132" +tokio = { version = "1.41.1", features = ["macros", "net", "rt-multi-thread"] } diff --git a/acrate-webfinger/src/config.rs b/acrate-webfinger/src/config.rs new file mode 100644 index 0000000..feb7905 --- /dev/null +++ b/acrate-webfinger/src/config.rs @@ -0,0 +1,4 @@ +micronfig::config!( + ACRATE_WEBFINGER_DATABASE_URL: String, + ACRATE_WEBFINGER_BIND_ADDRESS: String, +); diff --git a/acrate-webfinger/src/main.rs b/acrate-webfinger/src/main.rs new file mode 100644 index 0000000..7b248b0 --- /dev/null +++ b/acrate-webfinger/src/main.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; +use anyhow::Context; +use axum::Extension; + +mod config; +mod route; + + +#[tokio::main] +async fn main() -> anyhow::Result { + pretty_env_logger::init(); + log::debug!("Logging initialized!"); + + log::trace!("Creating Minijinja environment..."); + let mut mj = minijinja::Environment::<'static>::new(); + + log::trace!("Adding webfinger page to the Minijinja environment..."); + mj.add_template("webfinger.html.j2", include_str!("webfinger.html.j2")) + .expect("webfinger.html.j2 to be a valid Minijinja template"); + + log::trace!("Creating Axum router..."); + let app = axum::Router::new() + .route("/.well-known/webfinger", axum::routing::get(route::webfinger_handler)) + .layer(Extension(Arc::new(mj))); + log::trace!("Axum router created successfully!"); + + log::trace!("Creating Tokio listener..."); + let bind_address = config::ACRATE_WEBFINGER_BIND_ADDRESS(); + let listener = tokio::net::TcpListener::bind(bind_address) + .await + .context("failed to bind listener to address")?; + log::trace!("Tokio listener bound to: {bind_address}"); + + log::debug!("Starting server..."); + axum::serve(listener, app) + .await + .context("server exited with error")?; + + log::error!("Server exited with no error, panicking."); + panic!("server exited with no error"); +} diff --git a/acrate-webfinger/src/route.rs b/acrate-webfinger/src/route.rs new file mode 100644 index 0000000..13a7d7d --- /dev/null +++ b/acrate-webfinger/src/route.rs @@ -0,0 +1,290 @@ +use std::sync::Arc; +use axum::Extension; +use axum::http::{HeaderMap, Response, StatusCode}; +use axum_extra::extract::Query; +use serde::Deserialize; +use acrate_core::diesel::GroupedBy; +use acrate_core::diesel_async::{AsyncConnection, AsyncPgConnection}; +use acrate_core::meta::{MetaAlias, MetaLink, MetaLinkProperty, MetaLinkTitle, MetaProperty, MetaSubject}; +use acrate_hostmeta::jrd::ResourceDescriptorLinkJRD; +use acrate_hostmeta::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD, ResourceDescriptorTitleXRD}; +use crate::config; + +#[derive(Debug, Clone, Deserialize)] +pub struct WebfingerQuery { + pub resource: Option, + + #[serde(default)] + pub rel: Vec, +} + +const WEBFINGER_DOC: &str = "/.well-known/webfinger"; + +#[axum::debug_handler] +pub async fn webfinger_handler( + Query(WebfingerQuery {resource, rel}): Query, + headers: HeaderMap, + Extension(mj): Extension>>, +) -> Result, StatusCode> { + log::info!("Handling a WebFinger request!"); + + let resource = resource.unwrap_or_else(|| "".to_string()); + log::debug!("Resource is: {resource:#?}"); + + log::debug!("Rel is: {rel:#?}"); + + let accept = headers.get("Accept") + .map(|v| v.to_str()) + .filter(Result::is_ok) + .map(|v| v.unwrap()) + .unwrap_or("application/json") + .to_string(); + log::debug!("Accept is: {accept:#?}"); + + let mut response = Response::new("".to_string()); + + let mut conn = AsyncPgConnection::establish(config::ACRATE_WEBFINGER_DATABASE_URL()) + .await + .map_err(|_| StatusCode::BAD_GATEWAY)?; + + let subjects = MetaSubject::query_matching(&mut conn, WEBFINGER_DOC, &resource) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let subject = subjects.first() + .ok_or(StatusCode::NOT_FOUND)?; + + if subject.redirect.is_some() { + { + let headers = response.headers_mut(); + headers.insert( + "Location", + subject.redirect + .as_ref() + .expect("redirect not to have become suddenly None") + .parse() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + ); + } + { + let status = response.status_mut(); + *status = StatusCode::FOUND; + } + + return Ok(response); + } + + let aliases = MetaAlias::query_matching(&mut conn, WEBFINGER_DOC, &resource) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let properties = MetaProperty::query_matching(&mut conn, WEBFINGER_DOC, &resource) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let links = MetaLink::query_matching(&mut conn, WEBFINGER_DOC, &resource) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let link_properties = MetaLinkProperty::query_by_link(&mut conn, &links) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .grouped_by(&links); + + let link_titles = MetaLinkTitle::query_by_link(&mut conn, &links) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .grouped_by(&links); + + let links_full: Vec<(MetaLink, Vec, Vec)> = links + .into_iter() + .zip(link_properties) + .zip(link_titles) + .map(|((link, properties), titles)| (link, properties, titles)) + .collect(); + + { + let headers = response.headers_mut(); + headers.insert( + "Access-Control-Allow-Origin", + "*".parse().unwrap() + ); + } + + for mime in accept.split(",") { + { + let headers = response.headers_mut(); + headers.insert( + "Content-Type", + mime.parse().map_err(|_| StatusCode::BAD_REQUEST)? + ); + } + + let (mime, _params) = match mime.trim().split_once(";") { + Some((mime, params)) => (mime, Some(params)), + None => (mime, None), + }; + + match mime { + "*/*" | "application/json" | "application/jrd+json" => { + let subject = Some(resource); + + let aliases = aliases + .into_iter() + .map(|alias| alias.alias) + .collect(); + + let properties = properties + .into_iter() + .map(|prop| (prop.rel, prop.value)) + .collect(); + + let links = links_full + .into_iter() + .map(|(link, properties, titles)| ResourceDescriptorLinkJRD { + rel: link.rel, + r#type: link.type_, + href: link.href, + template: link.template, + properties: properties + .into_iter() + .map(|property| (property.rel, property.value)) + .collect(), + titles: titles + .into_iter() + .map(|title| (title.language, title.value)) + .collect(), + }) + .collect::>(); + + let rd = acrate_hostmeta::jrd::ResourceDescriptorJRD { + subject, + aliases, + properties, + links, + }; + + let json = serde_json::to_string_pretty(&rd) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + { + let body = response.body_mut(); + body.push_str(&json); + } + + return Ok(response); + }, + "application/xml" | "application/xrd+xml" => { + let subject = Some(resource); + + let aliases = aliases + .into_iter() + .map(|alias| alias.alias) + .collect(); + + let properties: Vec = properties + .into_iter() + .map(|prop| ResourceDescriptorPropertyXRD { + rel: prop.rel, + value: prop.value, + }) + .collect(); + + let links = links_full + .into_iter() + .map(|(link, properties, titles)| ResourceDescriptorLinkXRD { + rel: link.rel, + r#type: link.type_, + href: link.href, + template: link.template, + properties: properties + .into_iter() + .map(|property| ResourceDescriptorPropertyXRD { + rel: property.rel, + value: property.value, + }) + .collect(), + titles: titles + .into_iter() + .map(|title| ResourceDescriptorTitleXRD { + language: title.language, + value: title.value, + }) + .collect(), + }) + .collect::>(); + + let rd = acrate_hostmeta::xrd::ResourceDescriptorXRD { + subject, + aliases, + properties, + links, + }; + + let xml = quick_xml::se::to_string(&rd) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + { + let body = response.body_mut(); + body.push_str(r#""#); + body.push_str(&xml); + } + + return Ok(response); + }, + "text/html" => { + let aliases: Vec = aliases.into_iter() + .map(|alias| alias.alias) + .collect(); + + let properties: Vec<(String, Option)> = properties.into_iter() + .map(|prop| { + (prop.rel, prop.value) + }) + .collect(); + + let links: Vec<(String, Option, Option, Option, Vec<(String, Option)>, Vec<(String, String)>)> = links_full + .into_iter() + .map(|(link, properties, titles)| { + ( + link.rel, + link.type_, + link.href, + link.template, + properties.into_iter() + .map(|prop| (prop.rel, prop.value)) + .collect::)>>(), + titles.into_iter() + .map(|title| (title.language, title.value)) + .collect::>() + ) + }) + .collect(); + + let html = mj.get_template("webfinger.html.j2") + .expect("webfinger.html.j2 to exist") + .render( + minijinja::context!( + subject => resource, + aliases => aliases, + properties => properties, + links => links, + ) + ) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + { + let body = response.body_mut(); + body.push_str(&html); + } + + return Ok(response); + }, + _ => { + continue; + }, + } + } + + Err(StatusCode::NOT_ACCEPTABLE) +} diff --git a/acrate-webfinger/src/webfinger.html.j2 b/acrate-webfinger/src/webfinger.html.j2 new file mode 100644 index 0000000..3c4f191 --- /dev/null +++ b/acrate-webfinger/src/webfinger.html.j2 @@ -0,0 +1,192 @@ + + + + {{ subject }} ยท Acrate Webfinger + + + +
+

+ Acrate Webfinger +

+
+
+

+ {{ subject }} +

+ {% if aliases %} +
+
+

+ Aliases +

+
    + {% for alias in aliases %} +
  • {{ alias }}
  • + {% endfor %} +
+
+ {% endif %} + {% if properties %} +
+
+

+ Properties +

+
+ {% for property in properties %} +
+ {{ property[0] }} +
+
+ {{ property[1] }} +
+ {% endfor %} +
+
+ {% endif %} + {% if links %} +
+ + {% endif %} +
+