Compare commits

...

54 commits

Author SHA1 Message Date
0e7aacdd0e
Create webfinger crate' (#5)
Reviewed-on: #5
2024-11-16 06:33:05 +00:00
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
065fa9c80e
Eyes are closing, git fire 2024-11-15 06:50:51 +01:00
f5449cd397
core: Fix r#type again 2024-11-15 06:31:49 +01:00
6315c22ad6
core: Add missing rel attribute to meta_properties 2024-11-15 06:30:42 +01:00
affb063508
webfinger: Add acrate-core dependency 2024-11-15 05:23:21 +01:00
94203cdc7b
core: Filter meta queries on document 2024-11-15 05:23:06 +01:00
d0aa551ef4
core: Add document to the meta types 2024-11-15 05:21:05 +01:00
b7924c8776
core: Fix r#type in schema 2024-11-15 05:20:46 +01:00
3906d03653
core: Add document field to meta tables 2024-11-15 05:19:50 +01:00
69392bf7e5
core: DROP tables only IF EXISTS 2024-11-15 05:19:25 +01:00
153dc7d50f
core: Cascade table DROPs 2024-11-15 05:18:52 +01:00
3e2a7fd110
core: Expose diesel and diesel_async 2024-11-15 04:59:13 +01:00
c235467b06
core: Add queryable meta structs 2024-11-15 03:16:06 +01:00
18b3619785
core: Apparently you still need the postgres feature on diesel 2024-11-15 02:57:39 +01:00
6ebf2ddd6d
core: Use diesel_async dep 2024-11-15 02:40:58 +01:00
b5d907bd07
core: Use r#type instead of type_ 2024-11-15 02:35:17 +01:00
5a3d913933
core: Don't define database functions, implement them in Rust 2024-11-15 02:34:53 +01:00
21842d9f80
core: Add base core::meta structs 2024-11-15 02:16:16 +01:00
2ca4bb3662
core: Rename link to meta_link_id 2024-11-15 02:15:29 +01:00
84f7002338
core: Make link not null 2024-11-15 02:12:26 +01:00
c27af2cf53
core: Use plural in table names 2024-11-15 02:05:49 +01:00
e65c85616d
core: Use plural in table names 2024-11-15 02:04:15 +01:00
956bd17ad0
core: Make core::meta public 2024-11-15 01:57:32 +01:00
c9afa95fd6
core: Remove dependency on workspace
(This will be the database crate)
2024-11-15 01:57:13 +01:00
34e1eff855
core: Add uuid feature to diesel 2024-11-15 01:51:30 +01:00
c09edc9b08
core: Attach core::schema 2024-11-15 01:49:26 +01:00
ad2383c56b
core: Add postgres feature to diesel 2024-11-15 01:48:01 +01:00
03aab44ba4
core: Rename migration 2024-11-15 01:37:26 +01:00
2d3421e2c3
core: Create migration for metadata tables 2024-11-14 06:23:38 +01:00
025311e100
webfinger: Properly handle requests 2024-11-14 04:16:12 +01:00
8145ce57cc
webfinger: Parse query string with serde and axum_extra 2024-11-14 04:02:40 +01:00
646696a49e
webfinger: Create crate by cloning inbox 2024-11-14 03:51:11 +01:00
20 changed files with 916 additions and 17 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

@ -9,6 +9,7 @@
<sourceFolder url="file://$MODULE_DIR$/acrate-inbox/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/acrate-inbox/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/tests" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/acrate-webfinger/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

12
.idea/dataSources.xml Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="acrate" uuid="8258414e-095d-430b-a0a5-b48e72af23a9">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql:///acrate</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

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>

6
.idea/sqldialects.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/acrate-core/migrations/2024-11-14-031744_meta/up.sql" dialect="GenericSQL" />
</component>
</project>

View file

@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo"] members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo", "acrate-webfinger"]

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

@ -4,10 +4,10 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [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" diesel_migrations = "2.2.0"
acrate-hostmeta = { path = "../acrate-hostmeta" } uuid = "1.11.0"
acrate-nodeinfo = { path = "../acrate-nodeinfo" }
[lints.clippy] [lints.clippy]
tabs-in-doc-comments = "allow" tabs-in-doc-comments = "allow"

View file

@ -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;

View file

@ -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)
);

View file

@ -1,4 +1,8 @@
//! Core crate of the `acrate` project. //! Core crate of the `acrate` project.
pub use acrate_nodeinfo as nodeinfo; mod schema;
pub use acrate_hostmeta as hostmeta;
pub mod meta;
pub use diesel;
pub use diesel_async;

165
acrate-core/src/meta.rs Normal file
View file

@ -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<String>,
}
#[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<String>,
pub href: Option<String>,
pub template: Option<String>,
}
#[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<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)]
#[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<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 {
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_aliases::dsl::*;
let document_is_equal = document.eq(doc);
let subject_matches_pattern = subject.into_sql::<diesel::sql_types::Text>().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<Vec<Self>> {
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::<diesel::sql_types::Text>().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<Vec<Self>> {
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<Vec<Self>> {
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<Vec<Self>> {
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::<diesel::sql_types::Text>().ilike(pattern);
meta_properties
.filter(document_is_equal)
.filter(subject_matches_pattern)
.select(Self::as_select())
.load(conn)
.await
}
}

72
acrate-core/src/schema.rs Normal file
View file

@ -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<Bpchar>,
}
}
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<Bpchar>,
href -> Nullable<Bpchar>,
template -> Nullable<Bpchar>,
}
}
diesel::table! {
meta_properties (id) {
id -> Uuid,
document -> Bpchar,
pattern -> Bpchar,
rel -> Bpchar,
value -> Nullable<Bpchar>,
}
}
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_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,
);

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

@ -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"] }

View file

@ -0,0 +1,4 @@
micronfig::config!(
ACRATE_WEBFINGER_DATABASE_URL: String,
ACRATE_WEBFINGER_BIND_ADDRESS: String,
);

View file

@ -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<std::convert::Infallible> {
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");
}

View file

@ -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<String>,
#[serde(default)]
pub rel: Vec<String>,
}
const WEBFINGER_DOC: &str = "/.well-known/webfinger";
#[axum::debug_handler]
pub async fn webfinger_handler(
Query(WebfingerQuery {resource, rel}): Query<WebfingerQuery>,
headers: HeaderMap,
Extension(mj): Extension<Arc<minijinja::Environment<'static>>>,
) -> Result<Response<String>, 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<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 {
"*/*" | "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::<Vec<ResourceDescriptorLinkJRD>>();
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<ResourceDescriptorPropertyXRD> = 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::<Vec<ResourceDescriptorLinkXRD>>();
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#"<?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;
},
}
}
Err(StatusCode::NOT_ACCEPTABLE)
}

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>