diff --git a/Cargo.toml b/Cargo.toml index 3776b1b..d528923 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["acrate_database", "acrate_rd", "acrate_nodeinfo", "acrate_rdserver", "acrate_mime"] +members = ["acrate_database", "acrate_rd", "acrate_nodeinfo", "acrate_rdserver"] diff --git a/README.md b/README.md index 7e2f9c0..cb19056 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,18 @@ Federation database > > This software suite is in active development! +> [!Warning] +> +> **Monorepo!** Make sure to open the root directory in your IDE, or weird things might happen until the packages are published! + ## Crates ### Libraries -- `acrate_database`: Database schema, migrations, and high level database-reliant structures for the [`acrate`] project +- `acrate_database`: Database schema, migrations, and high level database-reliant structures +- `acrate_nodeinfo`: Rust typing and utilities for the NodeInfo format - `acrate_rd`: Rust typing and utilities for the JSON and XML resource descriptior formats ### Binaries -- `acrate-webfinger`: WebFinger server +- `acrate_rdserver`: Resource descriptor web server diff --git a/acrate_database/Cargo.toml b/acrate_database/Cargo.toml index ae7a73a..ba63f83 100644 --- a/acrate_database/Cargo.toml +++ b/acrate_database/Cargo.toml @@ -14,6 +14,7 @@ diesel = { version = "2.2.4", features = ["postgres", "uuid"] } diesel-async = { version = "0.5.1", features = ["postgres"] } diesel_migrations = { version = "2.2.0", optional = true } log = "0.4.22" +mediatype = "0.19.18" micronfig = { version = "0.3.0", optional = true } mime = "0.3.17" pretty_env_logger = { version = "0.5.0", optional = true } diff --git a/acrate_database/src/meta.rs b/acrate_database/src/meta.rs index 17e141a..c527ad8 100644 --- a/acrate_database/src/meta.rs +++ b/acrate_database/src/meta.rs @@ -26,12 +26,12 @@ //! - [`acrate_rdserver`] //! -use std::str::FromStr; use diesel::deserialize::FromSql; use diesel::{AsExpression, Associations, FromSqlRow, Identifiable, Insertable, IntoSql, PgTextExpressionMethods, QueryResult, Queryable, QueryableByName, Selectable, SelectableHelper, ExpressionMethods, BelongingToDsl}; use diesel::pg::{Pg, PgConnection}; use diesel::serialize::{Output, ToSql}; use diesel_async::AsyncPgConnection; +use mediatype::MediaTypeBuf; use uuid::Uuid; use super::schema; @@ -40,7 +40,7 @@ use super::schema; /// Wrapper to use [`mime::Mime`] with [`diesel`]. #[derive(Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression)] #[diesel(sql_type = diesel::sql_types::Text)] -pub struct Mime(pub mime::Mime); +pub struct MediaTypeDatabase(pub MediaTypeBuf); /// A matchable record denoting the existence of a resource descriptor. @@ -173,7 +173,7 @@ pub struct MetaLink { /// The media type of the value of the link. /// /// Can be [`None`] if it shouldn't be specified. - pub type_: Option, + pub type_: Option, /// The URI to the document this property is linking the subject to. /// @@ -206,7 +206,7 @@ pub struct MetaLinkInsert { /// The media type of the value of the link. /// /// Can be [`None`] if it shouldn't be specified. - pub type_: Option, + pub type_: Option, /// The URI to the document this property is linking the subject to. /// @@ -348,8 +348,8 @@ pub struct MetaPropertyInsert { pub value: Option, } -/// Allow [`diesel::sql_types::Text`] values to be parsed as [`Mime`]. -impl FromSql for Mime +/// Allow [`diesel::sql_types::Text`] values to be parsed as [`MediaTypeDatabase`]. +impl FromSql for MediaTypeDatabase where DB: diesel::backend::Backend, String: FromSql, @@ -359,25 +359,25 @@ impl FromSql for Mime let s = >::from_sql(bytes)?; log::trace!("Attempting to parse as a media type: {s:?}"); - let mime = mime::Mime::from_str(&s)?; + let mt: MediaTypeBuf = s.parse()?; - log::trace!("Successfully parsed media type: {mime:?}"); - Ok(Self(mime)) + log::trace!("Successfully parsed media type: {mt:?}"); + Ok(Self(mt)) } } -/// Allow [`diesel::sql_types::Text`] values to be written to with [`Mime`]. -impl ToSql for Mime +/// Allow [`diesel::sql_types::Text`] values to be written to with [`MediaTypeDatabase`]. +impl ToSql for MediaTypeDatabase where DB: diesel::backend::Backend, str: ToSql, { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { log::trace!("Getting the essence of a media type to prepare for serialization..."); - let mime = self.0.essence_str(); + let mt = self.0.as_str(); - log::trace!("Serializing media type as TEXT: {mime:?}"); - >::to_sql(mime, out) + log::trace!("Serializing media type as TEXT: {mt:?}"); + >::to_sql(mt, out) } } diff --git a/acrate_mime/Cargo.toml b/acrate_mime/Cargo.toml deleted file mode 100644 index f178e7a..0000000 --- a/acrate_mime/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "acrate_mime" -version = "0.1.0" -authors = ["Stefano Pigozzi "] -edition = "2021" -description = "Rust typing and utilities for MIME / media types" -repository = "https://forge.steffo.eu/unimore/tirocinio-canali-steffo-acrate" -license = "EUPL-1.2" -keywords = ["mime", "mimetype", "media", "media-type", "mime-type"] -categories = ["web-programming"] - -[dependencies] -mime = "0.3.17" diff --git a/acrate_mime/src/lib.rs b/acrate_mime/src/lib.rs deleted file mode 100644 index b93cf3f..0000000 --- a/acrate_mime/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/acrate_nodeinfo/Cargo.toml b/acrate_nodeinfo/Cargo.toml index 60e55aa..5e7693e 100644 --- a/acrate_nodeinfo/Cargo.toml +++ b/acrate_nodeinfo/Cargo.toml @@ -12,7 +12,7 @@ categories = ["web-programming"] [dependencies] acrate_rd = { path = "../acrate_rd" } log = "0.4.22" -mime = "0.3.17" +mediatype = { version = "0.19.18", features = ["serde"] } reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" diff --git a/acrate_nodeinfo/src/lib.rs b/acrate_nodeinfo/src/lib.rs index 5bc85f0..8b52c46 100644 --- a/acrate_nodeinfo/src/lib.rs +++ b/acrate_nodeinfo/src/lib.rs @@ -8,7 +8,7 @@ //! - //! -use std::str::FromStr; +use mediatype::MediaTypeBuf; use serde::Deserialize; use thiserror::Error; @@ -356,19 +356,20 @@ impl NodeInfo1 { .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)?; - log::trace!("Extracting media type from the `Content-Type` header..."); - let mime_type = content_type.to_str() - .map_err(ContentTypeUnprintable)?; - - log::trace!("Parsing media type: {mime_type:?}"); - let mime_type = mime::Mime::from_str(mime_type) + log::trace!("Extracting media type from the `Content-Type` header: {content_type:?}"); + let media_type: MediaTypeBuf = content_type + .to_str() + .map_err(ContentTypeUnprintable)? + .parse() .map_err(ContentTypeInvalid)?; - log::trace!("Checking if media type is supported: {mime_type:?}"); - let mime_is_json = mime_type == mime::APPLICATION_JSON; - log::trace!("Is media type application/json? {mime_is_json:?}"); + log::trace!("Checking if media type is supported: {media_type:?}"); + + let mime_is_json = media_type.essence().eq(&"application/json".parse::().unwrap()); + log::trace!("Is media type `application/json`? {mime_is_json:?}"); + if !mime_is_json { - log::error!("MIME type `{mime_type}` is not acceptable for JSON parsing."); + log::error!("Media type `{media_type}` is not acceptable for NodeInfo parsing."); return Err(ContentTypeUnsupported); } @@ -434,19 +435,20 @@ impl NodeInfo2 { .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)?; - log::trace!("Extracting media type from the `Content-Type` header..."); - let mime_type = content_type.to_str() - .map_err(ContentTypeUnprintable)?; - - log::trace!("Parsing media type: {mime_type:?}"); - let mime_type = mime::Mime::from_str(mime_type) + log::trace!("Extracting media type from the `Content-Type` header: {content_type:?}"); + let media_type: MediaTypeBuf = content_type + .to_str() + .map_err(ContentTypeUnprintable)? + .parse() .map_err(ContentTypeInvalid)?; - log::trace!("Checking if media type is supported..."); - let mime_is_json = mime_type == mime::APPLICATION_JSON; - log::trace!("Is media type application/json? {mime_is_json:?}"); + log::trace!("Checking if media type is supported: {media_type:?}"); + + let mime_is_json = media_type.essence().eq(&"application/json".parse::().unwrap()); + log::trace!("Is media type `application/json`? {mime_is_json:?}"); + if !mime_is_json { - log::error!("MIME type `{mime_type}` is not acceptable for JSON parsing."); + log::error!("Media type `{media_type}` is not acceptable for NodeInfo parsing."); return Err(ContentTypeUnsupported); } @@ -481,7 +483,7 @@ pub enum NodeInfoGetError { /// The `Content-Type` header of the response is not a valid [`mime::Mime`] type. #[error("the Content-Type header of the response is not a valid media type")] - ContentTypeInvalid(mime::FromStrError), + ContentTypeInvalid(mediatype::MediaTypeError), /// The `Content-Type` header of the response is not a supported [`mime::Mime`] type. #[error("the Content-Type header of the response is not a supported media type")] diff --git a/acrate_rd/Cargo.toml b/acrate_rd/Cargo.toml index 8568dd0..18b1b5d 100644 --- a/acrate_rd/Cargo.toml +++ b/acrate_rd/Cargo.toml @@ -11,7 +11,7 @@ categories = ["web-programming"] [dependencies] log = "0.4.22" -mime = "0.3.17" +mediatype = { version = "0.19.18", features = ["serde"] } quick-xml = { version = "0.37.0", features = ["overlapped-lists", "serialize"] } reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } diff --git a/acrate_rd/src/any.rs b/acrate_rd/src/any.rs index a9c7b37..99acb96 100644 --- a/acrate_rd/src/any.rs +++ b/acrate_rd/src/any.rs @@ -30,7 +30,7 @@ impl ResourceDescriptor { /// /// ``` /// # tokio_test::block_on(async { - /// use acrate_hostmeta::any::ResourceDescriptor; + /// use acrate_rd::any::ResourceDescriptor; /// /// let client = reqwest::Client::new(); /// let url: reqwest::Url = "https://junimo.party/.well-known/host-meta".parse() @@ -185,7 +185,7 @@ impl ResourceDescriptor { /// /// ``` /// # tokio_test::block_on(async { - /// use acrate_hostmeta::any::ResourceDescriptor; + /// use acrate_rd::any::ResourceDescriptor; /// /// let client = reqwest::Client::new(); /// let base: reqwest::Url = "https://junimo.party".parse() diff --git a/acrate_rd/src/jrd.rs b/acrate_rd/src/jrd.rs index 1c08b40..984b1b9 100644 --- a/acrate_rd/src/jrd.rs +++ b/acrate_rd/src/jrd.rs @@ -1,7 +1,7 @@ //! Definition and implementation of [`ResourceDescriptorJRD`]. use std::collections::HashMap; -use std::str::FromStr; +use mediatype::MediaTypeBuf; use serde::{Serialize, Deserialize}; use thiserror::Error; use crate::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD, ResourceDescriptorTitleXRD, ResourceDescriptorXRD}; @@ -76,8 +76,7 @@ pub struct ResourceDescriptorLinkJRD { /// - /// #[serde(skip_serializing_if = "Option::is_none")] - #[serde(with = "crate::utils::serde_mime_opt")] - pub r#type: Option, + pub r#type: Option, /// URI to the resource put in relation. /// @@ -173,24 +172,23 @@ impl ResourceDescriptorJRD { .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)?; - log::trace!("Extracting media type from the `Content-Type` header..."); - let mime_type = content_type.to_str() - .map_err(ContentTypeUnprintable)?; - - log::trace!("Parsing media type: {mime_type:?}"); - let mime_type = mime::Mime::from_str(mime_type) + log::trace!("Extracting media type from the `Content-Type` header: {content_type:?}"); + let media_type: MediaTypeBuf = content_type + .to_str() + .map_err(ContentTypeUnprintable)? + .parse() .map_err(ContentTypeInvalid)?; - log::trace!("Checking if media type is supported: {mime_type:?}"); - let mime_is_json = mime_type == mime::APPLICATION_JSON; - log::trace!("Is media type application/json? {mime_is_json:?}"); - let mime_is_jrd = - mime_type.type_() == mime::APPLICATION - && mime_type.subtype() == "jrd" - && mime_type.suffix() == Some(mime::JSON); - log::trace!("Is media type application/jrd+json? {mime_is_jrd:?}"); + log::trace!("Checking if media type is supported: {media_type:?}"); + + let mime_is_json = media_type.essence().eq(&"application/json".parse::().unwrap()); + log::trace!("Is media type `application/json`? {mime_is_json:?}"); + + let mime_is_jrd = media_type.essence().eq(&"application/jrd+json".parse::().unwrap()); + log::trace!("Is media type `application/jrd+json`? {mime_is_jrd:?}"); + if !(mime_is_json || mime_is_jrd) { - log::error!("MIME type `{mime_type}` is not acceptable for JRD parsing."); + log::error!("Media type `{media_type}` is not acceptable for JRD parsing."); return Err(ContentTypeUnsupported); } @@ -267,7 +265,7 @@ pub enum GetJRDError { /// The `Content-Type` header of the response is not a valid [`mime::Mime`] type. #[error("the Content-Type header of the response is not a valid media type")] - ContentTypeInvalid(mime::FromStrError), + ContentTypeInvalid(mediatype::MediaTypeError), /// The `Content-Type` header of the response is not a supported [`mime::Mime`] type. #[error("the Content-Type header of the response is not a supported media type")] diff --git a/acrate_rd/src/lib.rs b/acrate_rd/src/lib.rs index d1636d5..ce8f07f 100644 --- a/acrate_rd/src/lib.rs +++ b/acrate_rd/src/lib.rs @@ -8,4 +8,3 @@ pub mod jrd; pub mod xrd; pub mod any; -mod utils; diff --git a/acrate_rd/src/utils.rs b/acrate_rd/src/utils.rs deleted file mode 100644 index fecf97b..0000000 --- a/acrate_rd/src/utils.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Various utilities reused in the whole crate. - -/// Module to use in `serde(with = ...)` to [`serde`] a [`mime::Mime`]. -#[allow(dead_code)] -pub mod serde_mime { - use std::fmt::Formatter; - use std::str::FromStr; - use serde::de::{Error, Visitor}; - use serde::{Deserializer, Serializer}; - - pub struct MimeVisitor; - - impl<'de> Visitor<'de> for MimeVisitor { - type Value = mime::Mime; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("a media type (MIME type)") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - mime::Mime::from_str(v) - .map_err(|_| E::custom("failed to parse media type")) - } - } - - pub fn deserialize<'de, De>(deserializer: De) -> Result<>::Value, De::Error> - where - De: Deserializer<'de> - { - let s = deserializer.deserialize_str(MimeVisitor)?; - Ok(s) - } - - pub fn serialize(data: mime::Mime, serializer: Ser) -> Result - where - Ser: Serializer - { - let s = data.essence_str(); - serializer.serialize_str(s) - } -} - -/// Module to use in `serde(with = ...)` to [`serde`] an [`Option`] of [`mime::Mime`]. -#[allow(dead_code)] -pub mod serde_mime_opt { - use std::fmt::Formatter; - use std::str::FromStr; - use serde::de::{Error, Visitor}; - use serde::{Deserializer, Serializer}; - - pub struct MimeVisitor; - - impl<'de> Visitor<'de> for MimeVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("optionally, a media type (MIME type)") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - Ok( - Some( - mime::Mime::from_str(v) - .map_err(|_| E::custom("failed to parse media type"))? - ) - ) - } - - fn visit_none(self) -> Result - where - E: Error, - { - Ok(None) - } - } - - pub fn deserialize<'de, De>(deserializer: De) -> Result<>::Value, De::Error> - where - De: Deserializer<'de> - { - let s = deserializer.deserialize_str(MimeVisitor)?; - Ok(s) - } - - pub fn serialize(data: &Option, serializer: Ser) -> Result - where - Ser: Serializer - { - match data { - None => { - serializer.serialize_none() - } - Some(data) => { - let s = data.essence_str(); - serializer.serialize_str(s) - } - } - } -} diff --git a/acrate_rd/src/xrd.rs b/acrate_rd/src/xrd.rs index 3d6ae6b..a5f5703 100644 --- a/acrate_rd/src/xrd.rs +++ b/acrate_rd/src/xrd.rs @@ -1,6 +1,6 @@ //! Definition and implementation of [`ResourceDescriptorXRD`]. -use std::str::FromStr; +use mediatype::MediaTypeBuf; use serde::{Serialize, Deserialize}; use thiserror::Error; use crate::jrd::{ResourceDescriptorJRD, ResourceDescriptorLinkJRD}; @@ -82,8 +82,7 @@ pub struct ResourceDescriptorLinkXRD { /// #[serde(rename = "@type")] #[serde(skip_serializing_if = "Option::is_none")] - #[serde(with = "crate::utils::serde_mime_opt")] - pub r#type: Option, + pub r#type: Option, /// URI to the resource put in relation. /// @@ -209,22 +208,20 @@ impl ResourceDescriptorXRD { .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)?; - log::trace!("Extracting media type from the `Content-Type` header..."); - let mime_type = content_type.to_str() - .map_err(ContentTypeUnprintable)?; - - log::trace!("Parsing media type: {mime_type:?}"); - let mime_type = mime::Mime::from_str(mime_type) + log::trace!("Extracting media type from the `Content-Type` header: {content_type:?}"); + let media_type: MediaTypeBuf = content_type + .to_str() + .map_err(ContentTypeUnprintable)? + .parse() .map_err(ContentTypeInvalid)?; - log::trace!("Checking if media type is supported: {mime_type:?}"); - let mime_is_xrd = - mime_type.type_().as_str() == mime::APPLICATION - && mime_type.subtype() == "xrd" - && mime_type.suffix() == Some(mime::XML); - log::trace!("Is media type application/xrd+xml? {mime_is_xrd:?}"); + log::trace!("Checking if media type is supported: {media_type:?}"); + + let mime_is_xrd = media_type.essence().eq(&"application/xrd+xml".parse::().unwrap()); + log::trace!("Is media type `application/xrd+xml`? {mime_is_xrd:?}"); + if !mime_is_xrd { - log::error!("MIME type `{mime_type}` is not acceptable for XRD parsing."); + log::error!("MIME type `{media_type}` is not acceptable for XRD parsing."); return Err(ContentTypeUnsupported); } @@ -309,7 +306,7 @@ pub enum GetXRDError { /// The `Content-Type` header of the response is not a valid [`mime::Mime`] type. #[error("the Content-Type header of the response is not a valid media type")] - ContentTypeInvalid(mime::FromStrError), + ContentTypeInvalid(mediatype::MediaTypeError), /// The `Content-Type` header of the response is not a supported [`mime::Mime`] type. #[error("the Content-Type header of the response is not a supported media type")] diff --git a/acrate_rdserver/Cargo.toml b/acrate_rdserver/Cargo.toml index b47b7af..4752f69 100644 --- a/acrate_rdserver/Cargo.toml +++ b/acrate_rdserver/Cargo.toml @@ -23,7 +23,7 @@ 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"] } -mime = "0.3.17" +mediatype = { version = "0.19.18", features = ["serde"] } [lints.clippy] tabs-in-doc-comments = "allow" diff --git a/acrate_rdserver/src/main.rs b/acrate_rdserver/src/main.rs index 7b248b0..3752cbf 100644 --- a/acrate_rdserver/src/main.rs +++ b/acrate_rdserver/src/main.rs @@ -15,12 +15,12 @@ async fn main() -> anyhow::Result { 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"); + mj.add_template("rd.html.j2", include_str!("rd.html.j2")) + .expect("rd.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)) + .route("/*path", axum::routing::get(route::webfinger_handler)) .layer(Extension(Arc::new(mj))); log::trace!("Axum router created successfully!"); diff --git a/acrate_rdserver/src/webfinger.html.j2 b/acrate_rdserver/src/rd.html.j2 similarity index 97% rename from acrate_rdserver/src/webfinger.html.j2 rename to acrate_rdserver/src/rd.html.j2 index 3c4f191..8f9c766 100644 --- a/acrate_rdserver/src/webfinger.html.j2 +++ b/acrate_rdserver/src/rd.html.j2 @@ -1,7 +1,7 @@ - {{ subject }} · Acrate Webfinger + {{ subject }} · Acrate RDServer