Compare commits

...

22 commits

Author SHA1 Message Date
ee1eb9f470
Add README listing crates 2024-11-16 07:30:35 +01:00
8ca9d01688
webfinger: Don't link self 2024-11-16 07:29:09 +01:00
7319a3fe71
webfinger: Add basic CSS to the page 2024-11-16 07:24:14 +01:00
0498e45b59
webfinger: Handle not found and redirects 2024-11-16 06:46:25 +01:00
7f392bef36
core: Fix schema 2024-11-16 06:45:58 +01:00
c83fdc43e6
webfinger: text/html version of the document 2024-11-16 06:16:52 +01:00
dfee4e510e
core: Add meta_subjects to handle redirects 2024-11-16 06:16:34 +01:00
d80888c9db
webfinger: Add link properties and titles 2024-11-16 02:58:45 +01:00
5b8b166323
Obliterate .idea/misc.xml from existence 2024-11-16 02:56:24 +01:00
86f9aa27f2
hostmeta: Schema improvements 2024-11-16 02:55:08 +01:00
a5b6485d11
core: Allow querying meta objects grouped by parent 2024-11-16 02:54:55 +01:00
a5b9918c43
webfinger: Property.r#type to Property.rel 2024-11-16 01:42:04 +01:00
8b5408f21e
hostmeta: Property.r#type to Property.rel 2024-11-16 01:41:57 +01:00
72f3669ef1
webfinger: yield to type_ 2024-11-16 01:35:30 +01:00
8a49903eca
core: Support meta titles 2024-11-15 20:53:59 +01:00
f2d73c1798
core: Further schema improvements 2024-11-15 20:48:51 +01:00
d1dcbc1fc6
core: Give up on renaming type_ 2024-11-15 20:44:06 +01:00
e5412d85ba
core: Update schema with more details 2024-11-15 20:43:41 +01:00
c36b6edbfb
webfinger: Serve XRD and JRD responses 2024-11-15 18:12:13 +01:00
49b1b8ba65
hostmeta: Fix value being wrapped in a tag 2024-11-15 18:10:39 +01:00
32f756f800
hostmeta: Fix type not being an attribute 2024-11-15 18:10:02 +01:00
a020861e33
hostmeta: Make the root element use the XRD tag 2024-11-15 17:57:31 +01:00
13 changed files with 540 additions and 57 deletions

3
.gitignore vendored
View file

@ -15,3 +15,6 @@ Cargo.lock
# Environment # Environment
.env.local .env.local
# IntelliJ IDEA thing, mostly broken versioning
.idea/misc.xml

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

View file

@ -11,3 +11,15 @@ Federation database
> [!Caution] > [!Caution]
> >
> This software suite is in active development! > 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

View file

@ -1,4 +1,6 @@
DROP TABLE IF EXISTS meta_properties CASCADE; DROP TABLE IF EXISTS meta_properties CASCADE;
DROP TABLE IF EXISTS meta_link_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_links CASCADE;
DROP TABLE IF EXISTS meta_aliases CASCADE; DROP TABLE IF EXISTS meta_aliases CASCADE;
DROP TABLE IF EXISTS meta_subjects CASCADE;

View file

@ -1,25 +1,39 @@
CREATE TABLE meta_aliases ( 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(), id UUID DEFAULT gen_random_uuid(),
document BPCHAR NOT NULL, document BPCHAR NOT NULL,
pattern BPCHAR NOT NULL, pattern BPCHAR NOT NULL,
alias BPCHAR NOT NULL, alias BPCHAR NOT NULL,
CONSTRAINT unique_aliases UNIQUE (alias),
PRIMARY KEY (id) PRIMARY KEY (id)
); );
CREATE TABLE meta_links ( CREATE TABLE IF NOT EXISTS meta_links (
id UUID DEFAULT gen_random_uuid(), id UUID DEFAULT gen_random_uuid(),
document BPCHAR NOT NULL, document BPCHAR NOT NULL,
pattern BPCHAR NOT NULL, pattern BPCHAR NOT NULL,
rel BPCHAR NOT NULL, rel BPCHAR NOT NULL,
type BPCHAR, type BPCHAR,
href 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) PRIMARY KEY (id)
); );
CREATE TABLE meta_link_properties ( CREATE TABLE IF NOT EXISTS meta_link_properties (
id UUID DEFAULT gen_random_uuid(), id UUID DEFAULT gen_random_uuid(),
meta_link_id UUID REFERENCES meta_links (id) NOT NULL, meta_link_id UUID REFERENCES meta_links (id) NOT NULL,
rel BPCHAR NOT NULL, rel BPCHAR NOT NULL,
@ -28,7 +42,7 @@ CREATE TABLE meta_link_properties (
PRIMARY KEY (id) PRIMARY KEY (id)
); );
CREATE TABLE meta_properties ( CREATE TABLE IF NOT EXISTS meta_properties (
id UUID DEFAULT gen_random_uuid(), id UUID DEFAULT gen_random_uuid(),
document BPCHAR NOT NULL, document BPCHAR NOT NULL,
pattern BPCHAR NOT NULL, pattern BPCHAR NOT NULL,
@ -37,3 +51,13 @@ CREATE TABLE meta_properties (
PRIMARY KEY (id) 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)
);

View file

@ -1,11 +1,22 @@
use diesel::{Associations, Identifiable, Insertable, QueryResult, Queryable, QueryableByName, Selectable}; use diesel::{Associations, Identifiable, Insertable, QueryResult, Queryable, QueryableByName, Selectable, pg::Pg};
use diesel_async::AsyncPgConnection; use diesel_async::AsyncPgConnection;
use uuid::Uuid; use uuid::Uuid;
use super::schema; 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<String>,
}
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] #[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
#[diesel(table_name = schema::meta_aliases)] #[diesel(table_name = schema::meta_aliases)]
#[diesel(check_for_backend(Pg))]
pub struct MetaAlias { pub struct MetaAlias {
pub id: Uuid, pub id: Uuid,
pub document: String, pub document: String,
@ -15,18 +26,21 @@ pub struct MetaAlias {
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] #[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
#[diesel(table_name = schema::meta_links)] #[diesel(table_name = schema::meta_links)]
#[diesel(check_for_backend(Pg))]
pub struct MetaLink { pub struct MetaLink {
pub id: Uuid, pub id: Uuid,
pub document: String, pub document: String,
pub pattern: String, pub pattern: String,
pub rel: String, pub rel: String,
pub r#type: Option<String>, pub type_: Option<String>,
pub href: Option<String>, pub href: Option<String>,
pub template: Option<String>,
} }
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)] #[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(MetaLink))] #[diesel(belongs_to(MetaLink))]
#[diesel(table_name = schema::meta_link_properties)] #[diesel(table_name = schema::meta_link_properties)]
#[diesel(check_for_backend(Pg))]
pub struct MetaLinkProperty { pub struct MetaLinkProperty {
pub id: Uuid, pub id: Uuid,
pub meta_link_id: Uuid, pub meta_link_id: Uuid,
@ -34,8 +48,20 @@ pub struct MetaLinkProperty {
pub value: Option<String>, pub value: Option<String>,
} }
#[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)] #[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
#[diesel(table_name = schema::meta_properties)] #[diesel(table_name = schema::meta_properties)]
#[diesel(check_for_backend(Pg))]
pub struct MetaProperty { pub struct MetaProperty {
pub id: Uuid, pub id: Uuid,
pub document: String, pub document: String,
@ -44,9 +70,26 @@ pub struct MetaProperty {
pub value: Option<String>, pub value: Option<String>,
} }
impl MetaSubject {
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
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::<diesel::sql_types::Text>().ilike(pattern);
meta_subjects
.filter(document_is_equal)
.filter(subject_matches_pattern)
.select(Self::as_select())
.load(conn)
.await
}
}
impl MetaAlias { impl MetaAlias {
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<MetaAlias>> { pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
use diesel::prelude::*; use diesel::prelude::*;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use schema::meta_aliases::dsl::*; use schema::meta_aliases::dsl::*;
@ -64,7 +107,7 @@ impl MetaAlias {
} }
impl MetaLink { impl MetaLink {
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<MetaLink>> { pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
use diesel::prelude::*; use diesel::prelude::*;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use schema::meta_links::dsl::*; use schema::meta_links::dsl::*;
@ -79,19 +122,32 @@ impl MetaLink {
.load(conn) .load(conn)
.await .await
} }
}
pub async fn query_properties(&self, conn: &mut AsyncPgConnection) -> QueryResult<Vec<MetaLinkProperty>> {
impl MetaLinkProperty {
pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
use diesel::prelude::*; use diesel::prelude::*;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
MetaLinkProperty::belonging_to(self) Self::belonging_to(links)
.load(conn)
.await
}
}
impl MetaLinkTitle {
pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
Self::belonging_to(links)
.load(conn) .load(conn)
.await .await
} }
} }
impl MetaProperty { impl MetaProperty {
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<MetaProperty>> { pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
use diesel::prelude::*; use diesel::prelude::*;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use schema::meta_properties::dsl::*; use schema::meta_properties::dsl::*;

View file

@ -18,14 +18,25 @@ diesel::table! {
} }
} }
diesel::table! {
meta_link_titles (id) {
id -> Uuid,
meta_link_id -> Uuid,
language -> Bpchar,
value -> Bpchar,
}
}
diesel::table! { diesel::table! {
meta_links (id) { meta_links (id) {
id -> Uuid, id -> Uuid,
document -> Bpchar, document -> Bpchar,
pattern -> Bpchar, pattern -> Bpchar,
rel -> Bpchar, rel -> Bpchar,
r#type -> Nullable<Bpchar>, #[sql_name = "type"]
type_ -> Nullable<Bpchar>,
href -> Nullable<Bpchar>, href -> Nullable<Bpchar>,
template -> Nullable<Bpchar>,
} }
} }
@ -39,11 +50,23 @@ diesel::table! {
} }
} }
diesel::table! {
meta_subjects (id) {
id -> Uuid,
document -> Bpchar,
pattern -> Bpchar,
redirect -> Nullable<Bpchar>,
}
}
diesel::joinable!(meta_link_properties -> meta_links (meta_link_id)); 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!( diesel::allow_tables_to_appear_in_same_query!(
meta_aliases, meta_aliases,
meta_link_properties, meta_link_properties,
meta_link_titles,
meta_links, meta_links,
meta_properties, meta_properties,
meta_subjects,
); );

View file

@ -17,6 +17,7 @@ pub struct ResourceDescriptorJRD {
/// ///
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1>
/// ///
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>, pub subject: Option<String>,
/// Other names the resource described by this document can be referred to. /// Other names the resource described by this document can be referred to.
@ -70,6 +71,7 @@ pub struct ResourceDescriptorLinkJRD {
/// ///
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2>
/// ///
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>, pub r#type: Option<String>,
/// URI to the resource put in relation. /// URI to the resource put in relation.
@ -78,6 +80,7 @@ pub struct ResourceDescriptorLinkJRD {
/// ///
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3>
/// ///
#[serde(skip_serializing_if = "Option::is_none")]
pub href: Option<String>, pub href: Option<String>,
/// Titles of the resource put in relation in various languages. /// Titles of the resource put in relation in various languages.
@ -104,6 +107,7 @@ pub struct ResourceDescriptorLinkJRD {
/// ///
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2> /// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2>
/// ///
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>, pub template: Option<String>,
} }
@ -219,7 +223,7 @@ impl From<ResourceDescriptorLinkXRD> for ResourceDescriptorLinkJRD {
impl From<ResourceDescriptorPropertyXRD> for (String, Option<String>) { impl From<ResourceDescriptorPropertyXRD> for (String, Option<String>) {
fn from(value: ResourceDescriptorPropertyXRD) -> Self { fn from(value: ResourceDescriptorPropertyXRD) -> Self {
(value.r#type, value.value) (value.rel, value.value)
} }
} }

View file

@ -9,6 +9,7 @@ use crate::jrd::{ResourceDescriptorJRD, ResourceDescriptorLinkJRD};
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3> /// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3>
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "XRD")]
pub struct ResourceDescriptorXRD { pub struct ResourceDescriptorXRD {
/// The resource this document refers to. /// The resource this document refers to.
/// ///
@ -17,6 +18,7 @@ pub struct ResourceDescriptorXRD {
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1>
/// ///
#[serde(rename = "Subject")] #[serde(rename = "Subject")]
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>, pub subject: Option<String>,
/// Other names the resource described by this document can be referred to. /// Other names the resource described by this document can be referred to.
@ -58,6 +60,7 @@ pub struct ResourceDescriptorXRD {
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1> /// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1>
/// ///
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "Link")]
pub struct ResourceDescriptorLinkXRD { pub struct ResourceDescriptorLinkXRD {
/// The kind of relation established by the subject with the attached resource. /// The kind of relation established by the subject with the attached resource.
/// ///
@ -75,6 +78,7 @@ pub struct ResourceDescriptorLinkXRD {
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2>
/// ///
#[serde(rename = "@type")] #[serde(rename = "@type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>, pub r#type: Option<String>,
/// URI to the resource put in relation. /// URI to the resource put in relation.
@ -84,6 +88,7 @@ pub struct ResourceDescriptorLinkXRD {
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3>
/// ///
#[serde(rename = "@href")] #[serde(rename = "@href")]
#[serde(skip_serializing_if = "Option::is_none")]
pub href: Option<String>, pub href: Option<String>,
/// Titles of the resource put in relation in various languages. /// Titles of the resource put in relation in various languages.
@ -93,6 +98,7 @@ pub struct ResourceDescriptorLinkXRD {
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4>
/// ///
#[serde(default)] #[serde(default)]
#[serde(rename = "Title")]
pub titles: Vec<ResourceDescriptorTitleXRD>, pub titles: Vec<ResourceDescriptorTitleXRD>,
/// Additional information about the resource put in relation. /// Additional information about the resource put in relation.
@ -102,6 +108,7 @@ pub struct ResourceDescriptorLinkXRD {
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5>
/// ///
#[serde(default)] #[serde(default)]
#[serde(rename = "Property")]
pub properties: Vec<ResourceDescriptorPropertyXRD>, pub properties: Vec<ResourceDescriptorPropertyXRD>,
/// Template to fill to get the URL to resource-specific information. /// Template to fill to get the URL to resource-specific information.
@ -111,6 +118,7 @@ pub struct ResourceDescriptorLinkXRD {
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2> /// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2>
/// ///
#[serde(rename = "@template")] #[serde(rename = "@template")]
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>, pub template: Option<String>,
} }
@ -120,22 +128,26 @@ pub struct ResourceDescriptorLinkXRD {
/// ///
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4> /// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4>
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "Title")]
pub struct ResourceDescriptorTitleXRD { pub struct ResourceDescriptorTitleXRD {
/// The language of the title. /// The language of the title.
#[serde(rename = "@lang")] #[serde(rename = "@lang")]
pub language: String, pub language: String,
/// The title itself. /// The title itself.
#[serde(rename = "$text")]
pub value: String, pub value: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "Property")]
pub struct ResourceDescriptorPropertyXRD { pub struct ResourceDescriptorPropertyXRD {
/// The property identifier, or type. /// The property identifier, or type.
#[serde(alias = "@type")] #[serde(rename = "@rel")]
pub r#type: String, pub rel: String,
/// The property value. /// The property value.
#[serde(rename = "$text")]
pub value: Option<String>, pub value: Option<String>,
} }
@ -234,7 +246,7 @@ impl From<ResourceDescriptorJRD> for ResourceDescriptorXRD {
impl From<(String, Option<String>)> for ResourceDescriptorPropertyXRD { impl From<(String, Option<String>)> for ResourceDescriptorPropertyXRD {
fn from(value: (String, Option<String>)) -> Self { fn from(value: (String, Option<String>)) -> Self {
Self { Self {
r#type: value.0, rel: value.0,
value: value.1, value: value.1,
} }
} }

View file

@ -11,6 +11,9 @@ axum = { version = "0.7.7", features = ["macros"] }
axum-extra = { version = "0.9.4", features = ["query"] } axum-extra = { version = "0.9.4", features = ["query"] }
log = "0.4.22" log = "0.4.22"
micronfig = "0.3.0" micronfig = "0.3.0"
minijinja = "2.5.0"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
quick-xml = { version = "0.37.0", features = ["serialize"] }
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.132"
tokio = { version = "1.41.1", features = ["macros", "net", "rt-multi-thread"] } tokio = { version = "1.41.1", features = ["macros", "net", "rt-multi-thread"] }

View file

@ -1,4 +1,6 @@
use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use axum::Extension;
mod config; mod config;
mod route; mod route;
@ -8,10 +10,18 @@ mod route;
async fn main() -> anyhow::Result<std::convert::Infallible> { async fn main() -> anyhow::Result<std::convert::Infallible> {
pretty_env_logger::init(); pretty_env_logger::init();
log::debug!("Logging initialized!"); 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..."); log::trace!("Creating Axum router...");
let app = axum::Router::new() let app = axum::Router::new()
.route("/.well-known/webfinger", axum::routing::get(route::webfinger_handler)); .route("/.well-known/webfinger", axum::routing::get(route::webfinger_handler))
.layer(Extension(Arc::new(mj)));
log::trace!("Axum router created successfully!"); log::trace!("Axum router created successfully!");
log::trace!("Creating Tokio listener..."); log::trace!("Creating Tokio listener...");

View file

@ -1,15 +1,18 @@
use axum::body::Body; use std::sync::Arc;
use axum::http::{HeaderMap, HeaderValue, StatusCode}; use axum::Extension;
use axum::http::{HeaderMap, Response, StatusCode};
use axum_extra::extract::Query; use axum_extra::extract::Query;
use serde::Deserialize; use serde::Deserialize;
use acrate_core::diesel::GroupedBy;
use acrate_core::diesel_async::{AsyncConnection, AsyncPgConnection}; 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::jrd::ResourceDescriptorLinkJRD;
use acrate_hostmeta::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD}; use acrate_hostmeta::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD, ResourceDescriptorTitleXRD};
use crate::config; use crate::config;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct WebfingerQuery { pub struct WebfingerQuery {
pub resource: String, pub resource: Option<String>,
#[serde(default)] #[serde(default)]
pub rel: Vec<String>, pub rel: Vec<String>,
@ -21,9 +24,11 @@ const WEBFINGER_DOC: &str = "/.well-known/webfinger";
pub async fn webfinger_handler( pub async fn webfinger_handler(
Query(WebfingerQuery {resource, rel}): Query<WebfingerQuery>, Query(WebfingerQuery {resource, rel}): Query<WebfingerQuery>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<(Body, HeaderMap), StatusCode> { Extension(mj): Extension<Arc<minijinja::Environment<'static>>>,
) -> Result<Response<String>, StatusCode> {
log::info!("Handling a WebFinger request!"); log::info!("Handling a WebFinger request!");
let resource = resource.unwrap_or_else(|| "".to_string());
log::debug!("Resource is: {resource:#?}"); log::debug!("Resource is: {resource:#?}");
log::debug!("Rel is: {rel:#?}"); log::debug!("Rel is: {rel:#?}");
@ -36,29 +41,92 @@ pub async fn webfinger_handler(
.to_string(); .to_string();
log::debug!("Accept is: {accept:#?}"); log::debug!("Accept is: {accept:#?}");
let mut response_headers = HeaderMap::new(); let mut response = Response::new("".to_string());
let mut conn = AsyncPgConnection::establish(config::ACRATE_WEBFINGER_DATABASE_URL()) let mut conn = AsyncPgConnection::establish(config::ACRATE_WEBFINGER_DATABASE_URL())
.await .await
.map_err(|_| StatusCode::BAD_GATEWAY)?; .map_err(|_| StatusCode::BAD_GATEWAY)?;
let aliases = acrate_core::meta::MetaAlias::query_matching(&mut conn, WEBFINGER_DOC, &resource) 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 .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let properties = acrate_core::meta::MetaProperty::query_matching(&mut conn, WEBFINGER_DOC, &resource) let properties = MetaProperty::query_matching(&mut conn, WEBFINGER_DOC, &resource)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let links = acrate_core::meta::MetaLink::query_matching(&mut conn, WEBFINGER_DOC, &resource) let links = MetaLink::query_matching(&mut conn, WEBFINGER_DOC, &resource)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
for mime in accept.split(", ") { let link_properties = MetaLinkProperty::query_by_link(&mut conn, &links)
response_headers.insert("Content-Type", mime.parse().unwrap()); .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<MetaLinkProperty>, Vec<MetaLinkTitle>)> = 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 { match mime {
"application/json" | "application/jrd+json" => { "*/*" | "application/json" | "application/jrd+json" => {
let subject = Some(resource); let subject = Some(resource);
let aliases = aliases let aliases = aliases
@ -71,15 +139,21 @@ pub async fn webfinger_handler(
.map(|prop| (prop.rel, prop.value)) .map(|prop| (prop.rel, prop.value))
.collect(); .collect();
let links = links let links = links_full
.into_iter() .into_iter()
.map(|link| ResourceDescriptorLinkJRD { .map(|(link, properties, titles)| ResourceDescriptorLinkJRD {
rel: link.rel, rel: link.rel,
r#type: link.r#type, r#type: link.type_,
href: link.href, href: link.href,
titles: Default::default(), // TODO: Titles template: link.template,
properties: Default::default(), // TODO: Link properties properties: properties
template: None, // TODO: Template .into_iter()
.map(|property| (property.rel, property.value))
.collect(),
titles: titles
.into_iter()
.map(|title| (title.language, title.value))
.collect(),
}) })
.collect::<Vec<ResourceDescriptorLinkJRD>>(); .collect::<Vec<ResourceDescriptorLinkJRD>>();
@ -90,9 +164,15 @@ pub async fn webfinger_handler(
links, links,
}; };
let body = rd. let json = serde_json::to_string_pretty(&rd)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
return Ok(rd, response_headers) {
let body = response.body_mut();
body.push_str(&json);
}
return Ok(response);
}, },
"application/xml" | "application/xrd+xml" => { "application/xml" | "application/xrd+xml" => {
let subject = Some(resource); let subject = Some(resource);
@ -102,23 +182,35 @@ pub async fn webfinger_handler(
.map(|alias| alias.alias) .map(|alias| alias.alias)
.collect(); .collect();
let properties = properties let properties: Vec<ResourceDescriptorPropertyXRD> = properties
.into_iter() .into_iter()
.map(|prop| ResourceDescriptorPropertyXRD { .map(|prop| ResourceDescriptorPropertyXRD {
r#type: prop.rel, // TODO: Ah si chiama type? rel: prop.rel,
value: prop.value, value: prop.value,
}) })
.collect(); .collect();
let links = links let links = links_full
.into_iter() .into_iter()
.map(|link| ResourceDescriptorLinkXRD { .map(|(link, properties, titles)| ResourceDescriptorLinkXRD {
rel: link.rel, rel: link.rel,
r#type: link.r#type, r#type: link.type_,
href: link.href, href: link.href,
titles: Default::default(), // TODO: Titles template: link.template,
properties: Default::default(), // TODO: Link properties properties: properties
template: None, // TODO: Template .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::<Vec<ResourceDescriptorLinkXRD>>(); .collect::<Vec<ResourceDescriptorLinkXRD>>();
@ -129,11 +221,68 @@ pub async fn webfinger_handler(
links, links,
}; };
return Ok(StatusCode::OK) let xml = quick_xml::se::to_string(&rd)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
{
let body = response.body_mut();
body.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
body.push_str(&xml);
}
return Ok(response);
},
"text/html" => {
let aliases: Vec<String> = aliases.into_iter()
.map(|alias| alias.alias)
.collect();
let properties: Vec<(String, Option<String>)> = properties.into_iter()
.map(|prop| {
(prop.rel, prop.value)
})
.collect();
let links: Vec<(String, Option<String>, Option<String>, Option<String>, Vec<(String, Option<String>)>, 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::<Vec<(String, Option<String>)>>(),
titles.into_iter()
.map(|title| (title.language, title.value))
.collect::<Vec<(String, String)>>()
)
})
.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; continue;
} },
} }
} }

View file

@ -0,0 +1,192 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>{{ subject }} · Acrate Webfinger</title>
<style>
:root {
--wf-yellow: #e2bb03;
--wf-brown: #3b351d;
--link-color: #0094b9;
}
body {
font-family: sans-serif;
}
h1, h2 {
text-align: center;
}
main {
max-width: 800px;
padding: 8px;
margin: 0 auto;
border: 2px solid currentColor;
border-radius: 8px;
}
a {
color: var(--link-color);
}
hr {
margin: 8px -8px;
border: 1px solid currentColor;
opacity: 1;
}
small span {
font-family: monospace;
}
@media screen {
main {
box-shadow: 4px 4px 10px currentColor;
}
}
@media screen and (prefers-color-scheme: dark) {
body {
background-color: black;
color: var(--wf-yellow);
}
main {
background-color: var(--wf-brown);
}
main, hr {
border-color: var(--wf-yellow);
}
}
@media screen and (prefers-color-scheme: light) {
body {
background-color: var(--wf-yellow);
color: var(--wf-brown);
}
main {
background-color: white;
}
main, hr {
border-color: var(--wf-brown);
}
}
</style>
</head>
<body>
<header>
<h1>
Acrate Webfinger
</h1>
</header>
<main>
<h2 id="section-subject">
<span id="subject">{{ subject }}</span>
</h2>
{% if aliases %}
<hr>
<section id="section-aliases">
<h3>
Aliases
</h3>
<ul>
{% for alias in aliases %}
<li><a href="{{ alias }}">{{ alias }}</a></li>
{% endfor %}
</ul>
</section>
{% endif %}
{% if properties %}
<hr>
<section id="section-properties">
<h3>
Properties
</h3>
<dl>
{% for property in properties %}
<dt>
<a href="{{ property[0] }}">{{ property[0] }}</a>
</dt>
<dd>
<span>{{ property[1] }}</span>
</dd>
{% endfor %}
</dl>
</section>
{% endif %}
{% if links %}
<hr>
<section id="section-links">
<h3>
Links
</h3>
<dl>
{% for link in links %}
<dt>
<h4>
{% if link[0] != "self" %}
<a href="{{ link[0] }}">{{ link[0] }}</a>
{% else %}
<span>{{ link[0] }}</span>
{% endif %}
{% if link[1] is not none %}
<small>
(<span>{{ link[1] }}</span>)
</small>
{% endif %}
</h4>
</dt>
<dd>
{% if link[2] is not none %}
<h5>
Link destination
</h5>
<a href="{{ link[2] }}">{{ link[2] }}</a>
{% endif %}
{% if link[3] is not none %}
<h5>
Link template
</h5>
<span>{{ link[3] }}</span>
{% endif %}
{% if link[4] %}
<h5>
Link properties
</h5>
<dl>
{% for property in link[4] %}
<dt>
<a href="{{ property[0] }}">{{ property[0] }}</a>
</dt>
<dd>
<span>{{ property[1] }}</span>
</dd>
{% endfor %}
</dl>
{% endif %}
{% if link[5] %}
<h5>
Link titles
</h5>
<dl>
{% for title in link[5] %}
<dt>
<span>{{ title[0] }}</span>
</dt>
<dd>
<span>{{ title[1] }}</span>
</dd>
{% endfor %}
</dl>
{% endif %}
</dd>
{% endfor %}
</dl>
</section>
{% endif %}
</main>
</body>