From 60a27b5b11fe79194f94248c1023eec598ecfefb Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Wed, 26 Apr 2023 12:55:42 +0200 Subject: [PATCH] Make generic errors carry more details --- Cargo.lock | 37 +++++++++++ fedimovies-cli/Cargo.toml | 2 +- fedimovies-cli/src/main.rs | 6 +- fedimovies-models/src/database/test_utils.rs | 3 +- .../src/notifications/queries.rs | 8 +-- fedimovies-utils/src/crypto_rsa.rs | 11 ++++ src/activitypub/actors/attachments.rs | 30 ++++----- src/activitypub/actors/types.rs | 13 ++-- src/activitypub/authentication.rs | 7 +-- src/activitypub/fetcher/helpers.rs | 8 +-- src/activitypub/handlers/accept.rs | 10 ++- src/activitypub/handlers/add.rs | 4 +- src/activitypub/handlers/announce.rs | 4 +- src/activitypub/handlers/create.rs | 62 +++++++++++-------- src/activitypub/handlers/delete.rs | 10 ++- src/activitypub/handlers/follow.rs | 8 ++- src/activitypub/handlers/like.rs | 5 +- src/activitypub/handlers/move.rs | 9 +-- src/activitypub/handlers/reject.rs | 10 ++- src/activitypub/handlers/remove.rs | 8 ++- src/activitypub/handlers/undo.rs | 21 ++++--- src/activitypub/handlers/update.rs | 16 ++--- src/activitypub/identifiers.rs | 18 +++--- src/activitypub/receiver.rs | 16 ++--- src/admin/roles.rs | 2 +- src/errors.rs | 2 +- src/job_queue/periodic_tasks.rs | 49 +++++++++------ src/job_queue/scheduler.rs | 4 +- src/json_signatures/create.rs | 51 +++++++++++++-- src/json_signatures/proofs.rs | 7 +++ src/json_signatures/verify.rs | 2 +- src/mastodon_api/accounts/types.rs | 4 +- src/mastodon_api/accounts/views.rs | 10 +-- src/mastodon_api/markers/types.rs | 2 +- src/mastodon_api/oauth/views.rs | 25 ++++---- src/mastodon_api/search/helpers.rs | 8 +-- src/mastodon_api/settings/helpers.rs | 4 +- src/mastodon_api/settings/views.rs | 8 +-- src/mastodon_api/statuses/views.rs | 28 ++++----- src/validators/emojis.rs | 4 +- src/validators/posts.rs | 4 +- src/validators/profiles.rs | 28 +++++---- src/validators/tags.rs | 2 +- src/validators/users.rs | 2 +- src/webfinger/types.rs | 2 +- src/webfinger/views.rs | 2 +- 46 files changed, 368 insertions(+), 208 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b43a140..096f373 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1005,6 +1005,8 @@ dependencies = [ "fedimovies-utils", "hex", "log", + "openssl", + "postgres-openssl", "postgres-protocol", "postgres-types", "postgres_query", @@ -1860,6 +1862,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "111.25.3+1.1.1t" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.86" @@ -1868,6 +1879,7 @@ checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -2046,6 +2058,19 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "postgres-openssl" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de0ea6504e07ca78355a6fb88ad0f36cafe9e696cbc6717f16a207f3a60be72" +dependencies = [ + "futures", + "openssl", + "tokio", + "tokio-openssl", + "tokio-postgres", +] + [[package]] name = "postgres-protocol" version = "0.6.5" @@ -2928,6 +2953,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-openssl" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a" +dependencies = [ + "futures-util", + "openssl", + "openssl-sys", + "tokio", +] + [[package]] name = "tokio-postgres" version = "0.7.7" diff --git a/fedimovies-cli/Cargo.toml b/fedimovies-cli/Cargo.toml index b64c03c..f3daa43 100644 --- a/fedimovies-cli/Cargo.toml +++ b/fedimovies-cli/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" rust-version = "1.68" [[bin]] -name = "mitractl" +name = "fedimoviesctl" path = "src/main.rs" [dependencies] diff --git a/fedimovies-cli/src/main.rs b/fedimovies-cli/src/main.rs index ff351fd..3ad2d11 100644 --- a/fedimovies-cli/src/main.rs +++ b/fedimovies-cli/src/main.rs @@ -25,7 +25,11 @@ async fn main() { } let db_config = config.database_url.parse().unwrap(); - let db_client = &mut create_database_client(&db_config, config.tls_ca_file.as_ref().map(|p| p.as_path())).await; + let db_client = &mut create_database_client( + &db_config, + config.tls_ca_file.as_ref().map(|p| p.as_path()), + ) + .await; apply_migrations(db_client).await; match subcmd { diff --git a/fedimovies-models/src/database/test_utils.rs b/fedimovies-models/src/database/test_utils.rs index 90dcd72..a645e5c 100644 --- a/fedimovies-models/src/database/test_utils.rs +++ b/fedimovies-models/src/database/test_utils.rs @@ -3,7 +3,8 @@ use super::migrate::apply_migrations; use tokio_postgres::config::Config; use tokio_postgres::Client; -const DEFAULT_CONNECTION_URL: &str = "postgres://fedimovies:fedimovies@127.0.0.1:55432/fedimovies-test"; +const DEFAULT_CONNECTION_URL: &str = + "postgres://fedimovies:fedimovies@127.0.0.1:55432/fedimovies-test"; pub async fn create_test_database() -> Client { let connection_url = diff --git a/fedimovies-models/src/notifications/queries.rs b/fedimovies-models/src/notifications/queries.rs index 3b94fe5..bce51b7 100644 --- a/fedimovies-models/src/notifications/queries.rs +++ b/fedimovies-models/src/notifications/queries.rs @@ -255,13 +255,7 @@ pub async fn get_mention_notifications( related_emojis = RELATED_EMOJIS, ); let rows = db_client - .query( - &statement, - &[ - &EventType::Mention, - &i64::from(limit), - ], - ) + .query(&statement, &[&EventType::Mention, &i64::from(limit)]) .await?; let notifications: Vec = rows .iter() diff --git a/fedimovies-utils/src/crypto_rsa.rs b/fedimovies-utils/src/crypto_rsa.rs index eebdf41..debf268 100644 --- a/fedimovies-utils/src/crypto_rsa.rs +++ b/fedimovies-utils/src/crypto_rsa.rs @@ -49,6 +49,17 @@ pub fn create_rsa_sha256_signature( Ok(signature) } +/// RSASSA-PKCS1-v1_5 signature +pub fn create_rsa_signature( + private_key: &RsaPrivateKey, + message: &str, +) -> Result, rsa::errors::Error> { + let digest = Sha256::digest(message.as_bytes()); + let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256)); + let signature = private_key.sign(padding, &digest)?; + Ok(signature) +} + pub fn get_message_digest(message: &str) -> String { let digest = Sha256::digest(message.as_bytes()); let digest_b64 = base64::encode(digest); diff --git a/src/activitypub/actors/attachments.rs b/src/activitypub/actors/attachments.rs index a282397..da5f3af 100644 --- a/src/activitypub/actors/attachments.rs +++ b/src/activitypub/actors/attachments.rs @@ -34,39 +34,39 @@ pub fn parse_identity_proof( attachment: &ActorAttachment, ) -> Result { if attachment.object_type != IDENTITY_PROOF { - return Err(ValidationError("invalid attachment type")); + return Err(ValidationError("invalid attachment type".to_string())); }; let proof_type_str = attachment .signature_algorithm .as_ref() - .ok_or(ValidationError("missing proof type"))?; + .ok_or(ValidationError("missing proof type".to_string()))?; let proof_type = match proof_type_str.as_str() { PROOF_TYPE_ID_EIP191 => IdentityProofType::LegacyEip191IdentityProof, PROOF_TYPE_ID_MINISIGN => IdentityProofType::LegacyMinisignIdentityProof, - _ => return Err(ValidationError("unsupported proof type")), + _ => return Err(ValidationError("unsupported proof type".to_string())), }; let did = attachment .name .parse::() - .map_err(|_| ValidationError("invalid DID"))?; - let message = - create_identity_claim(actor_id, &did).map_err(|_| ValidationError("invalid claim"))?; + .map_err(|_| ValidationError("invalid DID".to_string()))?; + let message = create_identity_claim(actor_id, &did) + .map_err(|_| ValidationError("invalid claim".to_string()))?; let signature = attachment .signature_value .as_ref() - .ok_or(ValidationError("missing signature"))?; + .ok_or(ValidationError("missing signature".to_string()))?; match did { Did::Key(ref did_key) => { if !matches!(proof_type, IdentityProofType::LegacyMinisignIdentityProof) { - return Err(ValidationError("incorrect proof type")); + return Err(ValidationError("incorrect proof type".to_string())); }; let signature_bin = parse_minisign_signature(signature) - .map_err(|_| ValidationError("invalid signature encoding"))?; + .map_err(|_| ValidationError("invalid signature encoding".to_string()))?; verify_minisign_signature(did_key, &message, &signature_bin) - .map_err(|_| ValidationError("invalid identity proof"))?; + .map_err(|_| ValidationError("invalid identity proof".to_string()))?; } Did::Pkh(ref _did_pkh) => { - return Err(ValidationError("incorrect proof type")); + return Err(ValidationError("incorrect proof type".to_string())); } }; let proof = IdentityProof { @@ -110,12 +110,12 @@ pub fn parse_payment_option( attachment: &ActorAttachment, ) -> Result { if attachment.object_type != LINK { - return Err(ValidationError("invalid attachment type")); + return Err(ValidationError("invalid attachment type".to_string())); }; let href = attachment .href .as_ref() - .ok_or(ValidationError("href attribute is required"))? + .ok_or(ValidationError("href attribute is required".to_string()))? .to_string(); let payment_option = PaymentOption::Link(PaymentLink { name: attachment.name.clone(), @@ -137,12 +137,12 @@ pub fn attach_extra_field(field: ExtraField) -> ActorAttachment { pub fn parse_extra_field(attachment: &ActorAttachment) -> Result { if attachment.object_type != PROPERTY_VALUE { - return Err(ValidationError("invalid attachment type")); + return Err(ValidationError("invalid attachment type".to_string())); }; let property_value = attachment .value .as_ref() - .ok_or(ValidationError("missing property value"))?; + .ok_or(ValidationError("missing property value".to_string()))?; let field = ExtraField { name: attachment.name.clone(), value: property_value.to_string(), diff --git a/src/activitypub/actors/types.rs b/src/activitypub/actors/types.rs index 20ef924..a2c4b93 100644 --- a/src/activitypub/actors/types.rs +++ b/src/activitypub/actors/types.rs @@ -13,6 +13,7 @@ use fedimovies_utils::{ urls::get_hostname, }; +use crate::activitypub::types::build_default_context; use crate::activitypub::{ constants::{ AP_CONTEXT, MASTODON_CONTEXT, MITRA_CONTEXT, SCHEMA_ORG_CONTEXT, W3ID_SECURITY_CONTEXT, @@ -23,7 +24,6 @@ use crate::activitypub::{ types::deserialize_value_array, vocabulary::{IDENTITY_PROOF, IMAGE, LINK, PERSON, PROPERTY_VALUE, SERVICE}, }; -use crate::activitypub::types::build_default_context; use crate::errors::ValidationError; use crate::media::get_file_url; use crate::webfinger::types::ActorAddress; @@ -110,6 +110,7 @@ pub struct Actor { pub inbox: String, pub outbox: String, + #[serde(default)] pub bot: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -162,7 +163,8 @@ pub struct Actor { impl Actor { pub fn address(&self) -> Result { - let hostname = get_hostname(&self.id).map_err(|_| ValidationError("invalid actor ID"))?; + let hostname = + get_hostname(&self.id).map_err(|_| ValidationError("invalid actor ID".to_string()))?; let actor_address = ActorAddress { username: self.preferred_username.clone(), hostname: hostname, @@ -203,7 +205,10 @@ impl Actor { let attachment = match serde_json::from_value(attachment_value.clone()) { Ok(attachment) => attachment, Err(_) => { - log_error(attachment_type, ValidationError("invalid attachment")); + log_error( + attachment_type, + ValidationError("invalid attachment".to_string()), + ); continue; } }; @@ -229,7 +234,7 @@ impl Actor { _ => { log_error( attachment_type, - ValidationError("unsupported attachment type"), + ValidationError("unsupported attachment type".to_string()), ); } }; diff --git a/src/activitypub/authentication.rs b/src/activitypub/authentication.rs index 04f7681..ff2cf91 100644 --- a/src/activitypub/authentication.rs +++ b/src/activitypub/authentication.rs @@ -7,7 +7,7 @@ use fedimovies_models::{ profiles::queries::get_profile_by_remote_actor_id, profiles::types::DbActorProfile, }; -use fedimovies_utils::{crypto_rsa::deserialize_public_key}; +use fedimovies_utils::crypto_rsa::deserialize_public_key; use crate::http_signatures::verify::{ parse_http_signature, verify_http_signature, @@ -16,9 +16,8 @@ use crate::http_signatures::verify::{ use crate::json_signatures::{ proofs::ProofType, verify::{ - get_json_signature, - verify_rsa_json_signature, JsonSignatureVerificationError as JsonSignatureError, - JsonSigner, + get_json_signature, verify_rsa_json_signature, + JsonSignatureVerificationError as JsonSignatureError, JsonSigner, }, }; use crate::media::MediaStorage; diff --git a/src/activitypub/fetcher/helpers.rs b/src/activitypub/fetcher/helpers.rs index 254caab..15d29f6 100644 --- a/src/activitypub/fetcher/helpers.rs +++ b/src/activitypub/fetcher/helpers.rs @@ -214,7 +214,7 @@ pub async fn import_post( }; let object = fetch_object(instance, &object_id).await.map_err(|err| { log::warn!("{}", err); - ValidationError("failed to fetch object") + ValidationError("failed to fetch object".into()) })?; log::info!("fetched object {}", object.id); fetch_count += 1; @@ -278,9 +278,9 @@ pub async fn import_from_outbox( let activities = fetch_outbox(&instance, &actor.outbox, limit).await?; log::info!("fetched {} activities", activities.len()); for activity in activities { - let activity_actor = activity["actor"] - .as_str() - .ok_or(ValidationError("actor property is missing"))?; + let activity_actor = activity["actor"].as_str().ok_or(ValidationError( + "actor property is missing from activity".to_string(), + ))?; if activity_actor != actor.id { log::warn!("activity doesn't belong to outbox owner"); continue; diff --git a/src/activitypub/handlers/accept.rs b/src/activitypub/handlers/accept.rs index 309b37f..46a9ac8 100644 --- a/src/activitypub/handlers/accept.rs +++ b/src/activitypub/handlers/accept.rs @@ -29,13 +29,17 @@ pub async fn handle_accept( activity: Value, ) -> HandlerResult { // Accept(Follow) - let activity: Accept = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Accept = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!( + "unexpected Accept activity structure: {}", + activity + )) + })?; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; let follow_request_id = parse_local_object_id(&config.instance_url(), &activity.object)?; let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?; if follow_request.target_id != actor_profile.id { - return Err(ValidationError("actor is not a target").into()); + return Err(ValidationError("actor is not a target".to_string()).into()); }; if matches!(follow_request.request_status, FollowRequestStatus::Accepted) { // Ignore Accept if follow request already accepted diff --git a/src/activitypub/handlers/add.rs b/src/activitypub/handlers/add.rs index d4c08f2..92e16d5 100644 --- a/src/activitypub/handlers/add.rs +++ b/src/activitypub/handlers/add.rs @@ -23,8 +23,8 @@ pub async fn handle_add( db_client: &mut impl DatabaseClient, activity: Value, ) -> HandlerResult { - let activity: Add = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Add = serde_json::from_value(activity.clone()) + .map_err(|_| ValidationError(format!("unexpected Add activity structure: {}", activity)))?; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?; if Some(activity.target) == actor.subscribers { diff --git a/src/activitypub/handlers/announce.rs b/src/activitypub/handlers/announce.rs index 407b534..e189e8e 100644 --- a/src/activitypub/handlers/announce.rs +++ b/src/activitypub/handlers/announce.rs @@ -38,8 +38,8 @@ pub async fn handle_announce( // https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-1b12.md return Ok(None); }; - let activity: Announce = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Announce = serde_json::from_value(activity.clone()) + .map_err(|_| ValidationError(format!("unexpected activity structure: {}", activity)))?; let repost_object_id = activity.id; match get_post_by_remote_object_id(db_client, &repost_object_id).await { Ok(_) => return Ok(None), // Ignore if repost already exists diff --git a/src/activitypub/handlers/create.rs b/src/activitypub/handlers/create.rs index e51643f..86daded 100644 --- a/src/activitypub/handlers/create.rs +++ b/src/activitypub/handlers/create.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use chrono::Utc; +use log::warn; use serde::Deserialize; use serde_json::Value as JsonValue; use uuid::Uuid; @@ -51,11 +52,11 @@ fn get_object_attributed_to(object: &Object) -> Result let attributed_to = object .attributed_to .as_ref() - .ok_or(ValidationError("unattributed note"))?; + .ok_or(ValidationError("unattributed note".to_string()))?; let author_id = parse_array(attributed_to) - .map_err(|_| ValidationError("invalid attributedTo property"))? + .map_err(|_| ValidationError("invalid attributedTo property".to_string()))? .get(0) - .ok_or(ValidationError("invalid attributedTo property"))? + .ok_or(ValidationError("invalid attributedTo property".to_string()))? .to_string(); Ok(author_id) } @@ -65,7 +66,7 @@ pub fn get_object_url(object: &Object) -> Result { Some(JsonValue::String(string)) => Some(string.to_owned()), Some(other_value) => { let links: Vec = parse_property_value(other_value) - .map_err(|_| ValidationError("invalid object URL"))?; + .map_err(|_| ValidationError("invalid object URL".to_string()))?; links.get(0).map(|link| link.href.clone()) } None => None, @@ -87,7 +88,7 @@ pub fn get_object_content(object: &Object) -> Result { object.name.as_deref().unwrap_or("").to_string() }; if content.len() > CONTENT_MAX_SIZE { - return Err(ValidationError("content is too long")); + return Err(ValidationError("content is too long".to_string())); }; let content_safe = clean_html(&content, content_allowed_classes()); Ok(content_safe) @@ -122,7 +123,7 @@ pub async fn get_object_attachments( let mut unprocessed = vec![]; if let Some(ref value) = object.attachment { let list: Vec = parse_property_value(value) - .map_err(|_| ValidationError("invalid attachment property"))?; + .map_err(|_| ValidationError(format!("invalid attachment property: {value:?}")))?; let mut downloaded = vec![]; for attachment in list { match attachment.attachment_type.as_str() { @@ -138,7 +139,7 @@ pub async fn get_object_attachments( }; let attachment_url = attachment .url - .ok_or(ValidationError("attachment URL is missing"))?; + .ok_or(ValidationError("attachment URL is missing".to_string()))?; let (file_name, file_size, maybe_media_type) = match fetch_file( instance, &attachment_url, @@ -155,8 +156,12 @@ pub async fn get_object_attachments( continue; } Err(other_error) => { - log::warn!("{}", other_error); - return Err(ValidationError("failed to fetch attachment").into()); + log::warn!("failed to fetch attachment: {}", other_error); + return Err(ValidationError(format!( + "failed to fetch attachment: {}", + other_error + )) + .into()); } }; log::info!("downloaded attachment {}", attachment_url); @@ -281,7 +286,8 @@ pub async fn handle_emoji( let emoji = if let Some(emoji_id) = maybe_emoji_id { update_emoji(db_client, &emoji_id, image, &tag.updated).await? } else { - let hostname = get_hostname(&tag.id).map_err(|_| ValidationError("invalid emoji ID"))?; + let hostname = get_hostname(&tag.id) + .map_err(|_| ValidationError(format!("invalid emoji tag ID: {}", tag.id)))?; match create_emoji( db_client, emoji_name, @@ -351,9 +357,7 @@ pub async fn get_object_tags( if let Ok(username) = parse_local_actor_id(&instance.url(), &href) { // Check if local Movie account exists and if not, create the movie, if valid. let user = match get_user_by_name(db_client, &username).await { - Ok(user) => { - user - }, + Ok(user) => user, Err(DatabaseError::NotFound(_)) => { if let Some(api_key) = &api_key { log::warn!("failed to find mentioned user by name {}, checking if its a valid movie...", username); @@ -484,15 +488,13 @@ pub async fn get_object_tags( fn get_audience(object: &Object) -> Result, ValidationError> { let primary_audience = match object.to { - Some(ref value) => { - parse_array(value).map_err(|_| ValidationError("invalid 'to' property value"))? - } + Some(ref value) => parse_array(value) + .map_err(|_| ValidationError("invalid 'to' property value".to_string()))?, None => vec![], }; let secondary_audience = match object.cc { - Some(ref value) => { - parse_array(value).map_err(|_| ValidationError("invalid 'cc' property value"))? - } + Some(ref value) => parse_array(value) + .map_err(|_| ValidationError("invalid 'cc' property value".to_string()))?, None => vec![], }; let audience = [primary_audience, secondary_audience].concat(); @@ -543,12 +545,17 @@ pub async fn handle_note( log::info!("processing object of type {}", object.object_type); } other_type => { - log::warn!("discarding object of type {}", other_type); - return Err(ValidationError("unsupported object type").into()); + let msg = format!("discarding object of type {}", other_type); + log::warn!("{msg}"); + return Err(ValidationError(msg).into()); } }; if object.id.len() > OBJECT_ID_SIZE_MAX { - return Err(ValidationError("object ID is too long").into()); + return Err(ValidationError(format!( + "object ID is too long, {} of length", + object.id.len() + )) + .into()); }; let author_id = get_object_attributed_to(&object)?; @@ -571,7 +578,12 @@ pub async fn handle_note( content += &create_content_link(attachment_url); } if content.is_empty() && attachments.is_empty() { - return Err(ValidationError("post is empty").into()); + return Err(ValidationError(format!( + "post is empty (content={}, attachments={})", + content.is_empty(), + attachments.is_empty() + )) + .into()); }; let (mentions, hashtags, links, emojis) = get_object_tags( @@ -652,8 +664,8 @@ pub async fn handle_create( activity: JsonValue, mut is_authenticated: bool, ) -> HandlerResult { - let activity: CreateNote = - serde_json::from_value(activity).map_err(|_| ValidationError("invalid object"))?; + let activity: CreateNote = serde_json::from_value(activity.clone()) + .map_err(|_| ValidationError(format!("invalid CreateNote activity: {}", activity)))?; let object = activity.object; // Verify attribution diff --git a/src/activitypub/handlers/delete.rs b/src/activitypub/handlers/delete.rs index 0edfd5d..ea55f25 100644 --- a/src/activitypub/handlers/delete.rs +++ b/src/activitypub/handlers/delete.rs @@ -29,8 +29,12 @@ pub async fn handle_delete( db_client: &mut impl DatabaseClient, activity: Value, ) -> HandlerResult { - let activity: Delete = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Delete = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!( + "unexpected Delete activity structure: {}", + activity + )) + })?; if activity.object == activity.actor { // Self-delete let profile = match get_profile_by_remote_actor_id(db_client, &activity.object).await { @@ -55,7 +59,7 @@ pub async fn handle_delete( }; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; if post.author.id != actor_profile.id { - return Err(ValidationError("actor is not an author").into()); + return Err(ValidationError("actor is not an author".to_string()).into()); }; let deletion_queue = delete_post(db_client, &post.id).await?; let config = config.clone(); diff --git a/src/activitypub/handlers/follow.rs b/src/activitypub/handlers/follow.rs index f4a6b96..5c114ab 100644 --- a/src/activitypub/handlers/follow.rs +++ b/src/activitypub/handlers/follow.rs @@ -31,8 +31,12 @@ pub async fn handle_follow( activity: Value, ) -> HandlerResult { // Follow(Person) - let activity: Follow = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Follow = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!( + "unexpected Follow activity structure: {}", + activity + )) + })?; let source_profile = get_or_import_profile_by_actor_id( db_client, &config.instance(), diff --git a/src/activitypub/handlers/like.rs b/src/activitypub/handlers/like.rs index e650279..b6a9d49 100644 --- a/src/activitypub/handlers/like.rs +++ b/src/activitypub/handlers/like.rs @@ -30,8 +30,9 @@ pub async fn handle_like( db_client: &mut impl DatabaseClient, activity: Value, ) -> HandlerResult { - let activity: Like = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Like = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!("unexpected Like activity structure: {}", activity)) + })?; let author = get_or_import_profile_by_actor_id( db_client, &config.instance(), diff --git a/src/activitypub/handlers/move.rs b/src/activitypub/handlers/move.rs index 598b89c..f9160fb 100644 --- a/src/activitypub/handlers/move.rs +++ b/src/activitypub/handlers/move.rs @@ -34,12 +34,13 @@ pub async fn handle_move( activity: Value, ) -> HandlerResult { // Move(Person) - let activity: Move = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Move = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!("unexpected Move activity structure: {}", activity)) + })?; // Mastodon: actor is old profile (object) // Mitra: actor is new profile (target) if activity.object != activity.actor && activity.target != activity.actor { - return Err(ValidationError("actor ID mismatch").into()); + return Err(ValidationError("actor ID mismatch".to_string()).into()); }; let instance = config.instance(); @@ -70,7 +71,7 @@ pub async fn handle_move( // Add aliases reported by server (actor's alsoKnownAs property) aliases.extend(new_profile.aliases.clone().into_actor_ids()); if !aliases.contains(&old_actor_id) { - return Err(ValidationError("target ID is not an alias").into()); + return Err(ValidationError("target ID is not an alias".to_string()).into()); }; let followers = get_followers(db_client, &old_profile.id).await?; diff --git a/src/activitypub/handlers/reject.rs b/src/activitypub/handlers/reject.rs index cbd99a4..8e6bc05 100644 --- a/src/activitypub/handlers/reject.rs +++ b/src/activitypub/handlers/reject.rs @@ -29,13 +29,17 @@ pub async fn handle_reject( activity: Value, ) -> HandlerResult { // Reject(Follow) - let activity: Reject = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Reject = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!( + "unexpected Reject activity structure: {}", + activity + )) + })?; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; let follow_request_id = parse_local_object_id(&config.instance_url(), &activity.object)?; let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?; if follow_request.target_id != actor_profile.id { - return Err(ValidationError("actor is not a target").into()); + return Err(ValidationError("actor is not a target".to_string()).into()); }; if matches!(follow_request.request_status, FollowRequestStatus::Rejected) { // Ignore Reject if follow request already rejected diff --git a/src/activitypub/handlers/remove.rs b/src/activitypub/handlers/remove.rs index 07ed193..32f0cc4 100644 --- a/src/activitypub/handlers/remove.rs +++ b/src/activitypub/handlers/remove.rs @@ -27,8 +27,12 @@ pub async fn handle_remove( db_client: &mut impl DatabaseClient, activity: Value, ) -> HandlerResult { - let activity: Remove = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Remove = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!( + "unexpected Remove activity structure: {}", + activity + )) + })?; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?; if Some(activity.target) == actor.subscribers { diff --git a/src/activitypub/handlers/undo.rs b/src/activitypub/handlers/undo.rs index d249dad..7f5855b 100644 --- a/src/activitypub/handlers/undo.rs +++ b/src/activitypub/handlers/undo.rs @@ -30,8 +30,12 @@ async fn handle_undo_follow( db_client: &mut impl DatabaseClient, activity: Value, ) -> HandlerResult { - let activity: UndoFollow = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: UndoFollow = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!( + "unexpected UndoFollow activity structure: {}", + activity + )) + })?; let source_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; let target_actor_id = find_object_id(&activity.object["object"])?; let target_username = parse_local_actor_id(&config.instance_url(), &target_actor_id)?; @@ -63,15 +67,16 @@ pub async fn handle_undo( return handle_undo_follow(config, db_client, activity).await; }; - let activity: Undo = serde_json::from_value(activity) - .map_err(|_| ValidationError("unexpected activity structure"))?; + let activity: Undo = serde_json::from_value(activity.clone()).map_err(|_| { + ValidationError(format!("unexpected Undo activity structure: {}", activity)) + })?; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; match get_follow_request_by_activity_id(db_client, &activity.object).await { Ok(follow_request) => { // Undo(Follow) if follow_request.source_id != actor_profile.id { - return Err(ValidationError("actor is not a follower").into()); + return Err(ValidationError("actor is not a follower".to_string()).into()); }; unfollow( db_client, @@ -89,7 +94,7 @@ pub async fn handle_undo( Ok(reaction) => { // Undo(Like) if reaction.author_id != actor_profile.id { - return Err(ValidationError("actor is not an author").into()); + return Err(ValidationError("actor is not an author".to_string()).into()); }; delete_reaction(db_client, &reaction.author_id, &reaction.post_id).await?; Ok(Some(LIKE)) @@ -103,13 +108,13 @@ pub async fn handle_undo( Err(other_error) => return Err(other_error.into()), }; if post.author.id != actor_profile.id { - return Err(ValidationError("actor is not an author").into()); + return Err(ValidationError("actor is not an author".to_string()).into()); }; match post.repost_of_id { // Ignore returned data because reposts don't have attached files Some(_) => delete_post(db_client, &post.id).await?, // Can't undo regular post - None => return Err(ValidationError("object is not a repost").into()), + None => return Err(ValidationError("object is not a repost".to_string()).into()), }; Ok(Some(ANNOUNCE)) } diff --git a/src/activitypub/handlers/update.rs b/src/activitypub/handlers/update.rs index 7e1ca41..16c9aa0 100644 --- a/src/activitypub/handlers/update.rs +++ b/src/activitypub/handlers/update.rs @@ -38,8 +38,8 @@ async fn handle_update_note( db_client: &mut impl DatabaseClient, activity: Value, ) -> HandlerResult { - let activity: UpdateNote = - serde_json::from_value(activity).map_err(|_| ValidationError("invalid object"))?; + let activity: UpdateNote = serde_json::from_value(activity.clone()) + .map_err(|_| ValidationError(format!("invalid UpdateNote object {activity}")))?; let object = activity.object; let post = match get_post_by_remote_object_id(db_client, &object.id).await { Ok(post) => post, @@ -49,7 +49,7 @@ async fn handle_update_note( }; let instance = config.instance(); if profile_actor_id(&instance.url(), &post.author) != activity.actor { - return Err(ValidationError("actor is not an author").into()); + return Err(ValidationError("actor is not an author".to_string()).into()); }; let mut content = get_object_content(&object)?; if object.object_type != NOTE { @@ -65,7 +65,7 @@ async fn handle_update_note( content += &create_content_link(attachment_url); } if content.is_empty() && attachments.is_empty() { - return Err(ValidationError("post is empty").into()); + return Err(ValidationError("post is empty".to_string()).into()); }; let tmdb_api_key = config.tmdb_api_key.clone(); @@ -106,10 +106,10 @@ async fn handle_update_person( db_client: &mut impl DatabaseClient, activity: Value, ) -> HandlerResult { - let activity: UpdatePerson = - serde_json::from_value(activity).map_err(|_| ValidationError("invalid actor data"))?; + let activity: UpdatePerson = serde_json::from_value(activity.clone()) + .map_err(|_| ValidationError(format!("invalid UpdatePerson actor data: {}", activity)))?; if activity.object.id != activity.actor { - return Err(ValidationError("actor ID mismatch").into()); + return Err(ValidationError("actor ID mismatch".to_string()).into()); }; let profile = get_profile_by_remote_actor_id(db_client, &activity.object.id).await?; update_remote_profile( @@ -130,7 +130,7 @@ pub async fn handle_update( ) -> HandlerResult { let object_type = activity["object"]["type"] .as_str() - .ok_or(ValidationError("unknown object type"))?; + .ok_or(ValidationError("unknown object type".to_string()))?; match object_type { NOTE => handle_update_note(config, db_client, activity).await, PERSON => handle_update_person(config, db_client, activity).await, diff --git a/src/activitypub/identifiers.rs b/src/activitypub/identifiers.rs index 699a30b..76e0adb 100644 --- a/src/activitypub/identifiers.rs +++ b/src/activitypub/identifiers.rs @@ -80,7 +80,7 @@ pub fn local_tag_collection(instance_url: &str, tag_name: &str) -> String { } pub fn validate_object_id(object_id: &str) -> Result<(), ValidationError> { - get_hostname(object_id).map_err(|_| ValidationError("invalid object ID"))?; + get_hostname(object_id).map_err(|_| ValidationError("invalid object ID".to_string()))?; Ok(()) } @@ -89,13 +89,14 @@ pub fn parse_local_actor_id(instance_url: &str, actor_id: &str) -> Result[0-9a-zA-Z_]+)$", instance_url.replace('.', r"\."), ); - let url_regexp = Regex::new(&url_regexp_str).map_err(|_| ValidationError("error"))?; + let url_regexp = + Regex::new(&url_regexp_str).map_err(|_| ValidationError("error".to_string()))?; let url_caps = url_regexp .captures(actor_id) - .ok_or(ValidationError("invalid actor ID"))?; + .ok_or(ValidationError("invalid actor ID".to_string()))?; let username = url_caps .name("username") - .ok_or(ValidationError("invalid actor ID"))? + .ok_or(ValidationError("invalid actor ID".to_string()))? .as_str() .to_owned(); Ok(username) @@ -106,16 +107,17 @@ pub fn parse_local_object_id(instance_url: &str, object_id: &str) -> Result[0-9a-f-]+)$", instance_url.replace('.', r"\."), ); - let url_regexp = Regex::new(&url_regexp_str).map_err(|_| ValidationError("error"))?; + let url_regexp = + Regex::new(&url_regexp_str).map_err(|_| ValidationError("error".to_string()))?; let url_caps = url_regexp .captures(object_id) - .ok_or(ValidationError("invalid object ID"))?; + .ok_or(ValidationError("invalid object ID".to_string()))?; let internal_object_id: Uuid = url_caps .name("uuid") - .ok_or(ValidationError("invalid object ID"))? + .ok_or(ValidationError("invalid object ID".to_string()))? .as_str() .parse() - .map_err(|_| ValidationError("invalid object ID"))?; + .map_err(|_| ValidationError("invalid object ID".to_string()))?; Ok(internal_object_id) } diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index d1652fa..db327ca 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -108,7 +108,7 @@ pub fn find_object_id(object: &Value) -> Result { None => { let object_id = object["id"] .as_str() - .ok_or(ValidationError("missing object ID"))? + .ok_or(ValidationError("missing object ID".to_string()))? .to_string(); object_id } @@ -133,11 +133,11 @@ pub async fn handle_activity( ) -> Result<(), HandlerError> { let activity_type = activity["type"] .as_str() - .ok_or(ValidationError("type property is missing"))? + .ok_or(ValidationError("type property is missing".to_string()))? .to_owned(); let activity_actor = activity["actor"] .as_str() - .ok_or(ValidationError("actor property is missing"))? + .ok_or(ValidationError("actor property is missing".to_string()))? .to_owned(); let activity = activity.clone(); let maybe_object_type = match activity_type.as_str() { @@ -177,15 +177,15 @@ pub async fn receive_activity( ) -> Result<(), HandlerError> { let activity_type = activity["type"] .as_str() - .ok_or(ValidationError("type property is missing"))?; + .ok_or(ValidationError("type property is missing".to_string()))?; let activity_actor = activity["actor"] .as_str() - .ok_or(ValidationError("actor property is missing"))?; + .ok_or(ValidationError("actor property is missing".to_string()))?; let actor_hostname = url::Url::parse(activity_actor) - .map_err(|_| ValidationError("invalid actor ID"))? + .map_err(|_| ValidationError("invalid actor ID".to_string()))? .host_str() - .ok_or(ValidationError("invalid actor ID"))? + .ok_or(ValidationError("invalid actor ID".to_string()))? .to_string(); if config .blocked_instances @@ -295,7 +295,7 @@ pub async fn receive_activity( if activity_type == CREATE { let CreateNote { object, .. } = serde_json::from_value(activity.clone()) - .map_err(|_| ValidationError("invalid object"))?; + .map_err(|_| ValidationError(format!("invalid CreateNote object: {}", activity)))?; if is_unsolicited_message(db_client, &config.instance_url(), &object).await? { log::warn!("unsolicited message rejected: {}", object.id); return Ok(()); diff --git a/src/admin/roles.rs b/src/admin/roles.rs index f48dce5..aee84d5 100644 --- a/src/admin/roles.rs +++ b/src/admin/roles.rs @@ -9,7 +9,7 @@ pub fn role_from_str(role_str: &str) -> Result { "user" => Role::NormalUser, "admin" => Role::Admin, "read_only_user" => Role::ReadOnlyUser, - _ => return Err(ValidationError("unknown role")), + _ => return Err(ValidationError("unknown role".to_string())), }; Ok(role) } diff --git a/src/errors.rs b/src/errors.rs index 02c151f..25ddc62 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -9,7 +9,7 @@ pub struct ConversionError; #[derive(thiserror::Error, Debug)] #[error("{0}")] -pub struct ValidationError(pub &'static str); +pub struct ValidationError(pub String); #[derive(thiserror::Error, Debug)] pub enum HttpError { diff --git a/src/job_queue/periodic_tasks.rs b/src/job_queue/periodic_tasks.rs index ff901c4..c58964c 100644 --- a/src/job_queue/periodic_tasks.rs +++ b/src/job_queue/periodic_tasks.rs @@ -1,19 +1,19 @@ use anyhow::Error; +use crate::activitypub::builders::announce::prepare_announce; use fedimovies_config::Config; +use fedimovies_models::database::DatabaseError; +use fedimovies_models::notifications::queries::{delete_notification, get_mention_notifications}; +use fedimovies_models::posts::queries::create_post; +use fedimovies_models::posts::types::PostCreateData; +use fedimovies_models::users::queries::get_user_by_id; use fedimovies_models::{ database::{get_database_client, DbPool}, emojis::queries::{delete_emoji, find_unused_remote_emojis}, posts::queries::{delete_post, find_extraneous_posts}, profiles::queries::{delete_profile, find_empty_profiles, get_profile_by_id}, }; -use fedimovies_models::database::DatabaseError; -use fedimovies_models::notifications::queries::{delete_notification, get_mention_notifications}; -use fedimovies_models::posts::queries::create_post; -use fedimovies_models::posts::types::PostCreateData; -use fedimovies_models::users::queries::get_user_by_id; use fedimovies_utils::datetime::days_before_now; -use crate::activitypub::builders::announce::prepare_announce; use crate::activitypub::queues::{ process_queued_incoming_activities, process_queued_outgoing_activities, @@ -80,7 +80,10 @@ pub async fn prune_remote_emojis(config: &Config, db_pool: &DbPool) -> Result<() } // Finds mention notifications and repost them -pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result<(), anyhow::Error> { +pub async fn handle_movies_mentions( + config: &Config, + db_pool: &DbPool, +) -> Result<(), anyhow::Error> { let db_client = &mut **get_database_client(db_pool).await?; log::debug!("Reviewing mentions.."); // for each mention notification do repost @@ -103,19 +106,25 @@ pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result } let mut post = post_with_mention.clone(); let post_id = post.id; - let current_user = get_user_by_id(&transaction, &mention_notification.recipient.id).await?; + let current_user = + get_user_by_id(&transaction, &mention_notification.recipient.id).await?; // Repost let repost_data = PostCreateData::repost(post.id, None); - let mut repost = match create_post(&mut transaction, ¤t_user.id, repost_data).await { - Ok(repost) => repost, - Err(DatabaseError::AlreadyExists(err)) => { - log::info!("Review as Mention of {} already reposted the post with id {}", current_user.profile.username, post_id); - delete_notification(&mut transaction, mention_notification.id).await?; - continue; - } - Err(err) => return Err(err.into()), - }; + let mut repost = + match create_post(&mut transaction, ¤t_user.id, repost_data).await { + Ok(repost) => repost, + Err(DatabaseError::AlreadyExists(err)) => { + log::info!( + "Review as Mention of {} already reposted the post with id {}", + current_user.profile.username, + post_id + ); + delete_notification(&mut transaction, mention_notification.id).await?; + continue; + } + Err(err) => return Err(err.into()), + }; post.repost_count += 1; repost.repost_of = Some(Box::new(post)); @@ -128,7 +137,11 @@ pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result // Delete notification to avoid re-processing delete_notification(&mut transaction, mention_notification.id).await?; - log::info!("Review as Mention of {} reposted with post id {}", current_user.profile.username, post_id); + log::info!( + "Review as Mention of {} reposted with post id {}", + current_user.profile.username, + post_id + ); } } Ok(transaction.commit().await?) diff --git a/src/job_queue/scheduler.rs b/src/job_queue/scheduler.rs index c1a509f..2810522 100644 --- a/src/job_queue/scheduler.rs +++ b/src/job_queue/scheduler.rs @@ -83,7 +83,9 @@ pub fn run(config: Config, db_pool: DbPool) -> () { } PeriodicTask::PruneRemoteEmojis => prune_remote_emojis(&config, &db_pool).await, PeriodicTask::SubscriptionExpirationMonitor => Ok(()), - PeriodicTask::HandleMoviesMentions => handle_movies_mentions(&config, &db_pool).await, + PeriodicTask::HandleMoviesMentions => { + handle_movies_mentions(&config, &db_pool).await + } }; task_result.unwrap_or_else(|err| { log::error!("{:?}: {}", task, err); diff --git a/src/json_signatures/create.rs b/src/json_signatures/create.rs index 49f9ce8..09eb3a8 100644 --- a/src/json_signatures/create.rs +++ b/src/json_signatures/create.rs @@ -3,6 +3,8 @@ use rsa::RsaPrivateKey; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::json_signatures::proofs::PROOF_TYPE_JCS_RSA; +use fedimovies_utils::crypto_rsa::create_rsa_signature; use fedimovies_utils::{ canonicalization::{canonicalize_object, CanonicalizationError}, crypto_rsa::create_rsa_sha256_signature, @@ -29,6 +31,19 @@ pub struct IntegrityProof { pub proof_value: String, } +impl IntegrityProof { + fn jcs_rsa(signer_key_id: &str, signature: &[u8]) -> Self { + Self { + proof_type: PROOF_TYPE_JCS_RSA.to_string(), + proof_purpose: PROOF_PURPOSE.to_string(), + cryptosuite: None, + verification_method: signer_key_id.to_string(), + created: Utc::now(), + proof_value: encode_multibase_base58btc(signature), + } + } +} + #[derive(thiserror::Error, Debug)] pub enum JsonSignatureError { #[error(transparent)] @@ -40,16 +55,42 @@ pub enum JsonSignatureError { #[error("signing error")] SigningError(#[from] rsa::errors::Error), - #[error("invalid object")] + #[error("invalid JSON signature object")] InvalidObject, + + #[error("already signed")] + AlreadySigned, +} + +pub fn add_integrity_proof( + object_value: &mut Value, + proof: IntegrityProof, +) -> Result<(), JsonSignatureError> { + let object_map = object_value + .as_object_mut() + .ok_or(JsonSignatureError::InvalidObject)?; + if object_map.contains_key(PROOF_KEY) { + return Err(JsonSignatureError::AlreadySigned); + }; + let proof_value = serde_json::to_value(proof)?; + object_map.insert(PROOF_KEY.to_string(), proof_value); + Ok(()) } pub fn sign_object( - _object: &Value, - _signer_key: &RsaPrivateKey, - _signer_key_id: &str, + object: &Value, + signer_key: &RsaPrivateKey, + signer_key_id: &str, ) -> Result { - Err(JsonSignatureError::InvalidObject) + // Canonicalize + let message = canonicalize_object(object)?; + // Sign + let signature = create_rsa_signature(signer_key, &message)?; + // Insert proof + let proof = IntegrityProof::jcs_rsa(signer_key_id, &signature); + let mut object_value = serde_json::to_value(object)?; + add_integrity_proof(&mut object_value, proof)?; + Ok(object_value) } pub fn is_object_signed(object: &Value) -> bool { diff --git a/src/json_signatures/proofs.rs b/src/json_signatures/proofs.rs index 1b176ab..bed229c 100644 --- a/src/json_signatures/proofs.rs +++ b/src/json_signatures/proofs.rs @@ -9,6 +9,13 @@ pub const PROOF_TYPE_ID_EIP191: &str = "ethereum-eip191-00"; // Identity proof, version 2022A pub const PROOF_TYPE_ID_MINISIGN: &str = "MitraMinisignSignature2022A"; +// Similar to https://identity.foundation/JcsEd25519Signature2020/ +// - Canonicalization algorithm: JCS +// - Digest algorithm: SHA-256 +// - Signature algorithm: RSASSA-PKCS1-v1_5 +pub const PROOF_TYPE_JCS_RSA: &str = "MitraJcsRsaSignature2022"; +pub const PROOF_TYPE_JCS_RSA_LEGACY: &str = "JcsRsaSignature2022"; + // https://w3c.github.io/vc-data-integrity/#dataintegrityproof pub const DATA_INTEGRITY_PROOF: &str = "DataIntegrityProof"; diff --git a/src/json_signatures/verify.rs b/src/json_signatures/verify.rs index 6958821..70244ef 100644 --- a/src/json_signatures/verify.rs +++ b/src/json_signatures/verify.rs @@ -32,7 +32,7 @@ pub struct SignatureData { #[derive(thiserror::Error, Debug)] pub enum JsonSignatureVerificationError { - #[error("invalid object")] + #[error("invalid signature object")] InvalidObject, #[error("no proof")] diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index 733594a..765970f 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -299,7 +299,7 @@ impl AccountUpdateData { ) -> Result { let maybe_bio = if let Some(ref bio_source) = self.note { let bio = markdown_basic_to_html(bio_source) - .map_err(|_| ValidationError("invalid markdown"))?; + .map_err(|_| ValidationError("invalid markdown".to_string()))?; Some(bio) } else { None @@ -321,7 +321,7 @@ impl AccountUpdateData { let mut extra_fields = vec![]; for field_source in self.fields_attributes.unwrap_or(vec![]) { let value = markdown_basic_to_html(&field_source.value) - .map_err(|_| ValidationError("invalid markdown"))?; + .map_err(|_| ValidationError("invalid markdown".to_string()))?; let extra_field = ExtraField { name: field_source.name, value: value, diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 469ec9b..6448426 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -72,15 +72,15 @@ pub async fn create_account( let invite_code = account_data .invite_code .as_ref() - .ok_or(ValidationError("invite code is required"))?; + .ok_or(ValidationError("invite code is required".to_string()))?; if !is_valid_invite_code(db_client, invite_code).await? { - return Err(ValidationError("invalid invite code").into()); + return Err(ValidationError("invalid invite code".to_string()).into()); }; }; validate_local_username(&account_data.username)?; if account_data.password.is_none() && account_data.message.is_none() { - return Err(ValidationError("password or EIP-4361 message is required").into()); + return Err(ValidationError("password or EIP-4361 message is required".to_string()).into()); }; let maybe_password_hash = if let Some(password) = account_data.password.as_ref() { let password_hash = hash_password(password).map_err(|_| MastodonError::InternalError)?; @@ -117,7 +117,7 @@ pub async fn create_account( let user = match create_user(db_client, user_data).await { Ok(user) => user, Err(DatabaseError::AlreadyExists(_)) => { - return Err(ValidationError("user already exists").into()) + return Err(ValidationError("user already exists".to_string()).into()) } Err(other_error) => return Err(other_error.into()), }; @@ -280,7 +280,7 @@ async fn search_by_did( let did: Did = query_params .did .parse() - .map_err(|_| ValidationError("invalid DID"))?; + .map_err(|_| ValidationError("invalid DID".to_string()))?; let profiles = search_profiles_by_did(db_client, &did, false).await?; let base_url = get_request_base_url(connection_info); let instance_url = config.instance().url(); diff --git a/src/mastodon_api/markers/types.rs b/src/mastodon_api/markers/types.rs index 0ed9804..e57f0cb 100644 --- a/src/mastodon_api/markers/types.rs +++ b/src/mastodon_api/markers/types.rs @@ -16,7 +16,7 @@ impl MarkerQueryParams { let timeline = match self.timeline.as_ref() { "home" => Timeline::Home, "notifications" => Timeline::Notifications, - _ => return Err(ValidationError("invalid timeline name")), + _ => return Err(ValidationError("invalid timeline name".to_string())), }; Ok(timeline) } diff --git a/src/mastodon_api/oauth/views.rs b/src/mastodon_api/oauth/views.rs index 0d8f54a..0ad98ff 100644 --- a/src/mastodon_api/oauth/views.rs +++ b/src/mastodon_api/oauth/views.rs @@ -42,18 +42,18 @@ async fn authorize_view( let password_hash = user .password_hash .as_ref() - .ok_or(ValidationError("password auth is disabled"))?; + .ok_or(ValidationError("password auth is disabled".to_string()))?; let password_correct = verify_password(password_hash, &form_data.password) .map_err(|_| MastodonError::InternalError)?; if !password_correct { - return Err(ValidationError("incorrect password").into()); + return Err(ValidationError("incorrect password".to_string()).into()); }; if query_params.response_type != "code" { - return Err(ValidationError("invalid response type").into()); + return Err(ValidationError("invalid response type".to_string()).into()); }; let oauth_app = get_oauth_app_by_client_id(db_client, &query_params.client_id).await?; if oauth_app.redirect_uri != query_params.redirect_uri { - return Err(ValidationError("invalid redirect_uri parameter").into()); + return Err(ValidationError("invalid redirect_uri parameter".to_string()).into()); }; let authorization_code = generate_access_token(); @@ -91,36 +91,35 @@ async fn token_view( let db_client = &**get_database_client(&db_pool).await?; let user = match request_data.grant_type.as_str() { "authorization_code" => { - let authorization_code = request_data - .code - .as_ref() - .ok_or(ValidationError("authorization code is required"))?; + let authorization_code = request_data.code.as_ref().ok_or(ValidationError( + "authorization code is required".to_string(), + ))?; get_user_by_authorization_code(db_client, authorization_code).await? } "password" => { let username = request_data .username .as_ref() - .ok_or(ValidationError("username is required"))?; + .ok_or(ValidationError("username is required".to_string()))?; get_user_by_name(db_client, username).await? } _ => { - return Err(ValidationError("unsupported grant type").into()); + return Err(ValidationError("unsupported grant type".to_string()).into()); } }; if request_data.grant_type == "password" || request_data.grant_type == "ethereum" { let password = request_data .password .as_ref() - .ok_or(ValidationError("password is required"))?; + .ok_or(ValidationError("password is required".to_string()))?; let password_hash = user .password_hash .as_ref() - .ok_or(ValidationError("password auth is disabled"))?; + .ok_or(ValidationError("password auth is disabled".to_string()))?; let password_correct = verify_password(password_hash, password).map_err(|_| MastodonError::InternalError)?; if !password_correct { - return Err(ValidationError("incorrect password").into()); + return Err(ValidationError("incorrect password".to_string()).into()); }; }; let access_token = generate_access_token(); diff --git a/src/mastodon_api/search/helpers.rs b/src/mastodon_api/search/helpers.rs index 83ca1e3..213feff 100644 --- a/src/mastodon_api/search/helpers.rs +++ b/src/mastodon_api/search/helpers.rs @@ -44,10 +44,10 @@ fn parse_profile_query(query: &str) -> Result<(String, Option), Validati Regex::new(r"^(@|!)?(?P[\w\.-]+)(@(?P[\w\.-]+))?$").unwrap(); let acct_query_caps = acct_query_re .captures(query) - .ok_or(ValidationError("invalid profile query"))?; + .ok_or(ValidationError("invalid profile query".to_string()))?; let username = acct_query_caps .name("username") - .ok_or(ValidationError("invalid profile query"))? + .ok_or(ValidationError("invalid profile query".to_string()))? .as_str() .to_string(); let maybe_hostname = acct_query_caps @@ -60,10 +60,10 @@ fn parse_tag_query(query: &str) -> Result { let tag_query_re = Regex::new(r"^#(?P\w+)$").unwrap(); let tag_query_caps = tag_query_re .captures(query) - .ok_or(ValidationError("invalid tag query"))?; + .ok_or(ValidationError("invalid tag query".to_string()))?; let tag = tag_query_caps .name("tag") - .ok_or(ValidationError("invalid tag query"))? + .ok_or(ValidationError("invalid tag query".to_string()))? .as_str() .to_string(); Ok(tag) diff --git a/src/mastodon_api/settings/helpers.rs b/src/mastodon_api/settings/helpers.rs index 80d2cd0..3bd6905 100644 --- a/src/mastodon_api/settings/helpers.rs +++ b/src/mastodon_api/settings/helpers.rs @@ -59,7 +59,9 @@ pub fn parse_address_list(csv: &str) -> Result, ValidationErro addresses.sort(); addresses.dedup(); if addresses.len() > 50 { - return Err(ValidationError("can't process more than 50 items at once")); + return Err(ValidationError( + "can't process more than 50 items at once".to_string(), + )); }; Ok(addresses) } diff --git a/src/mastodon_api/settings/views.rs b/src/mastodon_api/settings/views.rs index a9329c7..bf370da 100644 --- a/src/mastodon_api/settings/views.rs +++ b/src/mastodon_api/settings/views.rs @@ -41,7 +41,7 @@ async fn client_config_view( let db_client = &**get_database_client(&db_pool).await?; let mut current_user = get_current_user(db_client, auth.token()).await?; if request_data.len() != 1 { - return Err(ValidationError("can't update more than one config").into()); + return Err(ValidationError("can't update more than one config".to_string()).into()); }; let (client_name, client_config_value) = request_data .iter() @@ -172,11 +172,11 @@ async fn move_followers( let db_client = &mut **get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; if current_user.profile.identity_proofs.inner().is_empty() { - return Err(ValidationError("identity proof is required").into()); + return Err(ValidationError("identity proof is required".to_string()).into()); }; let instance = config.instance(); if request_data.from_actor_id.starts_with(&instance.url()) { - return Err(ValidationError("can't move from local actor").into()); + return Err(ValidationError("can't move from local actor".to_string()).into()); }; // Existence of actor is not verified because // the old profile could have been deleted @@ -193,7 +193,7 @@ async fn move_followers( .into_iter() .map(|profile| profile_actor_id(&instance.url(), &profile)); if !aliases.any(|actor_id| actor_id == request_data.from_actor_id) { - return Err(ValidationError("old profile is not an alias").into()); + return Err(ValidationError("old profile is not an alias".to_string()).into()); }; }; let address_list = parse_address_list(&request_data.followers_csv)?; diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index 2cd2a55..727c71d 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -57,14 +57,14 @@ async fn create_status( Some("direct") => Visibility::Direct, Some("private") => Visibility::Followers, Some("subscribers") => Visibility::Subscribers, - Some(_) => return Err(ValidationError("invalid visibility parameter").into()), + Some(_) => return Err(ValidationError("invalid visibility parameter".to_string()).into()), None => Visibility::Public, }; let content = match status_data.content_type.as_str() { "text/html" => status_data.status, "text/markdown" => markdown_lite_to_html(&status_data.status) - .map_err(|_| ValidationError("invalid markdown"))?, - _ => return Err(ValidationError("unsupported content type").into()), + .map_err(|_| ValidationError("invalid markdown".to_string()))?, + _ => return Err(ValidationError("unsupported content type".to_string()).into()), }; // Parse content let PostContent { @@ -95,21 +95,21 @@ async fn create_status( mentions.sort(); mentions.dedup(); if mentions.len() > MENTION_LIMIT { - return Err(ValidationError("too many mentions").into()); + return Err(ValidationError("too many mentions".to_string()).into()); }; // Links validation if links.len() > 0 && visibility != Visibility::Public { - return Err(ValidationError("can't add links to non-public posts").into()); + return Err(ValidationError("can't add links to non-public posts".to_string()).into()); }; if links.len() > LINK_LIMIT { - return Err(ValidationError("too many links").into()); + return Err(ValidationError("too many links".to_string()).into()); }; // Emoji validation let emojis: Vec<_> = emojis.iter().map(|emoji| emoji.id).collect(); if emojis.len() > EMOJI_LIMIT { - return Err(ValidationError("too many emojis").into()); + return Err(ValidationError("too many emojis".to_string()).into()); }; // Reply validation @@ -117,15 +117,15 @@ async fn create_status( let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await { Ok(post) => post, Err(DatabaseError::NotFound(_)) => { - return Err(ValidationError("parent post does not exist").into()); + return Err(ValidationError("parent post does not exist".to_string()).into()); } Err(other_error) => return Err(other_error.into()), }; if in_reply_to.repost_of_id.is_some() { - return Err(ValidationError("can't reply to repost").into()); + return Err(ValidationError("can't reply to repost".to_string()).into()); }; if in_reply_to.visibility != Visibility::Public && visibility != Visibility::Direct { - return Err(ValidationError("reply must have direct visibility").into()); + return Err(ValidationError("reply must have direct visibility".to_string()).into()); }; if visibility != Visibility::Public { let mut in_reply_to_audience: Vec<_> = in_reply_to @@ -135,7 +135,7 @@ async fn create_status( .collect(); in_reply_to_audience.push(in_reply_to.author.id); if !mentions.iter().all(|id| in_reply_to_audience.contains(id)) { - return Err(ValidationError("audience can't be expanded").into()); + return Err(ValidationError("audience can't be expanded".to_string()).into()); }; }; Some(in_reply_to) @@ -145,7 +145,7 @@ async fn create_status( // Validate attachments let attachments = status_data.media_ids.unwrap_or(vec![]); if attachments.len() > ATTACHMENT_LIMIT { - return Err(ValidationError("too many attachments").into()); + return Err(ValidationError("too many attachments".to_string()).into()); }; // Create post @@ -197,8 +197,8 @@ async fn preview_status( let content = match status_data.content_type.as_str() { "text/html" => status_data.status, "text/markdown" => markdown_lite_to_html(&status_data.status) - .map_err(|_| ValidationError("invalid markdown"))?, - _ => return Err(ValidationError("unsupported content type").into()), + .map_err(|_| ValidationError("invalid markdown".to_string()))?, + _ => return Err(ValidationError("unsupported content type".to_string()).into()), }; let PostContent { mut content, diff --git a/src/validators/emojis.rs b/src/validators/emojis.rs index 33c836c..d7e6576 100644 --- a/src/validators/emojis.rs +++ b/src/validators/emojis.rs @@ -11,10 +11,10 @@ pub const EMOJI_MEDIA_TYPES: [&str; 4] = ["image/apng", "image/gif", "image/png" pub fn validate_emoji_name(emoji_name: &str) -> Result<(), ValidationError> { let name_re = Regex::new(EMOJI_NAME_RE).unwrap(); if !name_re.is_match(emoji_name) { - return Err(ValidationError("invalid emoji name")); + return Err(ValidationError("invalid emoji name".to_string())); }; if emoji_name.len() > EMOJI_NAME_SIZE_MAX { - return Err(ValidationError("emoji name is too long")); + return Err(ValidationError("emoji name is too long".to_string())); }; Ok(()) } diff --git a/src/validators/posts.rs b/src/validators/posts.rs index 4d6cfdc..5d4f7e8 100644 --- a/src/validators/posts.rs +++ b/src/validators/posts.rs @@ -23,12 +23,12 @@ pub fn clean_content(content: &str) -> Result { // Check content size to not exceed the hard limit // Character limit from config is not enforced at the backend if content.len() > CONTENT_MAX_SIZE { - return Err(ValidationError("post is too long")); + return Err(ValidationError("post is too long".to_string())); }; let content_safe = clean_html_strict(content, &CONTENT_ALLOWED_TAGS, content_allowed_classes()); let content_trimmed = content_safe.trim(); if content_trimmed.is_empty() { - return Err(ValidationError("post can not be empty")); + return Err(ValidationError("post can not be empty".to_string())); }; Ok(content_trimmed.to_string()) } diff --git a/src/validators/profiles.rs b/src/validators/profiles.rs index 126a8a1..386fd48 100644 --- a/src/validators/profiles.rs +++ b/src/validators/profiles.rs @@ -16,21 +16,21 @@ const FIELD_VALUE_MAX_SIZE: usize = 5000; pub fn validate_username(username: &str) -> Result<(), ValidationError> { if username.is_empty() { - return Err(ValidationError("username is empty")); + return Err(ValidationError("username is empty".to_string())); }; if username.len() > 100 { - return Err(ValidationError("username is too long")); + return Err(ValidationError("username is too long".to_string())); }; let username_regexp = Regex::new(USERNAME_RE).unwrap(); if !username_regexp.is_match(username) { - return Err(ValidationError("invalid username")); + return Err(ValidationError("invalid username".to_string())); }; Ok(()) } fn validate_display_name(display_name: &str) -> Result<(), ValidationError> { if display_name.chars().count() > DISPLAY_NAME_MAX_LENGTH { - return Err(ValidationError("display name is too long")); + return Err(ValidationError("display name is too long".to_string())); }; Ok(()) } @@ -43,7 +43,7 @@ fn clean_bio(bio: &str, is_remote: bool) -> Result { } else { // Local profile if bio.chars().count() > BIO_MAX_LENGTH { - return Err(ValidationError("bio is too long")); + return Err(ValidationError("bio is too long".to_string())); }; clean_html_strict(bio, &BIO_ALLOWED_TAGS, vec![]) }; @@ -63,21 +63,23 @@ fn clean_extra_fields( continue; }; if field.name.len() > FIELD_NAME_MAX_SIZE { - return Err(ValidationError("field name is too long")); + return Err(ValidationError("field name is too long".to_string())); }; if field.value.len() > FIELD_VALUE_MAX_SIZE { - return Err(ValidationError("field value is too long")); + return Err(ValidationError("field value is too long".to_string())); }; cleaned_extra_fields.push(field); } #[allow(clippy::collapsible_else_if)] if is_remote { if cleaned_extra_fields.len() > 100 { - return Err(ValidationError("at most 100 fields are allowed")); + return Err(ValidationError( + "at most 100 fields are allowed".to_string(), + )); }; } else { if cleaned_extra_fields.len() > 10 { - return Err(ValidationError("at most 10 fields are allowed")); + return Err(ValidationError("at most 10 fields are allowed".to_string())); }; }; Ok(cleaned_extra_fields) @@ -88,7 +90,9 @@ pub fn clean_profile_create_data( ) -> Result<(), ValidationError> { validate_username(&profile_data.username)?; if profile_data.hostname.is_some() != profile_data.actor_json.is_some() { - return Err(ValidationError("hostname and actor_json field mismatch")); + return Err(ValidationError( + "hostname and actor_json field mismatch".to_string(), + )); }; if let Some(display_name) = &profile_data.display_name { validate_display_name(display_name)?; @@ -100,7 +104,7 @@ pub fn clean_profile_create_data( }; profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?; if profile_data.emojis.len() > EMOJI_LIMIT { - return Err(ValidationError("too many emojis")); + return Err(ValidationError("too many emojis".to_string())); }; Ok(()) } @@ -118,7 +122,7 @@ pub fn clean_profile_update_data( }; profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?; if profile_data.emojis.len() > EMOJI_LIMIT { - return Err(ValidationError("too many emojis")); + return Err(ValidationError("too many emojis".to_string())); }; Ok(()) } diff --git a/src/validators/tags.rs b/src/validators/tags.rs index 85e6055..d206a7f 100644 --- a/src/validators/tags.rs +++ b/src/validators/tags.rs @@ -7,7 +7,7 @@ const HASHTAG_NAME_RE: &str = r"^\w+$"; pub fn validate_hashtag(tag_name: &str) -> Result<(), ValidationError> { let hashtag_name_re = Regex::new(HASHTAG_NAME_RE).unwrap(); if !hashtag_name_re.is_match(tag_name) { - return Err(ValidationError("invalid tag name")); + return Err(ValidationError("invalid tag name".to_string())); }; Ok(()) } diff --git a/src/validators/users.rs b/src/validators/users.rs index 0fb1d54..6db4f3c 100644 --- a/src/validators/users.rs +++ b/src/validators/users.rs @@ -9,7 +9,7 @@ pub fn validate_local_username(username: &str) -> Result<(), ValidationError> { // The username regexp should not allow domain names and IP addresses let username_regexp = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); if !username_regexp.is_match(username) { - return Err(ValidationError("invalid username")); + return Err(ValidationError("invalid username".to_string())); }; Ok(()) } diff --git a/src/webfinger/types.rs b/src/webfinger/types.rs index cbc71d4..4e20ae6 100644 --- a/src/webfinger/types.rs +++ b/src/webfinger/types.rs @@ -60,7 +60,7 @@ impl FromStr for ActorAddress { let actor_address_re = Regex::new(ACTOR_ADDRESS_RE).unwrap(); let caps = actor_address_re .captures(value) - .ok_or(ValidationError("invalid actor address"))?; + .ok_or(ValidationError("invalid actor address".to_string()))?; let actor_address = Self { username: caps["username"].to_string(), hostname: caps["hostname"].to_string(), diff --git a/src/webfinger/views.rs b/src/webfinger/views.rs index fcc43b3..41172c7 100644 --- a/src/webfinger/views.rs +++ b/src/webfinger/views.rs @@ -22,7 +22,7 @@ use super::types::{ fn parse_acct_uri(uri: &str) -> Result { let actor_address = uri .strip_prefix("acct:") - .ok_or(ValidationError("invalid query target"))? + .ok_or(ValidationError("invalid query target".to_string()))? .parse()?; Ok(actor_address) }