Make generic errors carry more details

This commit is contained in:
Rafael Caricio 2023-04-26 12:55:42 +02:00
parent 0d77557ad6
commit 60a27b5b11
Signed by: rafaelcaricio
GPG key ID: 3C86DBCE8E93C947
46 changed files with 368 additions and 208 deletions

37
Cargo.lock generated
View file

@ -1005,6 +1005,8 @@ dependencies = [
"fedimovies-utils", "fedimovies-utils",
"hex", "hex",
"log", "log",
"openssl",
"postgres-openssl",
"postgres-protocol", "postgres-protocol",
"postgres-types", "postgres-types",
"postgres_query", "postgres_query",
@ -1860,6 +1862,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 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]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.86" version = "0.9.86"
@ -1868,6 +1879,7 @@ checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@ -2046,6 +2058,19 @@ dependencies = [
"syn 2.0.15", "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]] [[package]]
name = "postgres-protocol" name = "postgres-protocol"
version = "0.6.5" version = "0.6.5"
@ -2928,6 +2953,18 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tokio-postgres" name = "tokio-postgres"
version = "0.7.7" version = "0.7.7"

View file

@ -6,7 +6,7 @@ edition = "2021"
rust-version = "1.68" rust-version = "1.68"
[[bin]] [[bin]]
name = "mitractl" name = "fedimoviesctl"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]

View file

@ -25,7 +25,11 @@ async fn main() {
} }
let db_config = config.database_url.parse().unwrap(); 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; apply_migrations(db_client).await;
match subcmd { match subcmd {

View file

@ -3,7 +3,8 @@ use super::migrate::apply_migrations;
use tokio_postgres::config::Config; use tokio_postgres::config::Config;
use tokio_postgres::Client; 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 { pub async fn create_test_database() -> Client {
let connection_url = let connection_url =

View file

@ -255,13 +255,7 @@ pub async fn get_mention_notifications(
related_emojis = RELATED_EMOJIS, related_emojis = RELATED_EMOJIS,
); );
let rows = db_client let rows = db_client
.query( .query(&statement, &[&EventType::Mention, &i64::from(limit)])
&statement,
&[
&EventType::Mention,
&i64::from(limit),
],
)
.await?; .await?;
let notifications: Vec<Notification> = rows let notifications: Vec<Notification> = rows
.iter() .iter()

View file

@ -49,6 +49,17 @@ pub fn create_rsa_sha256_signature(
Ok(signature) Ok(signature)
} }
/// RSASSA-PKCS1-v1_5 signature
pub fn create_rsa_signature(
private_key: &RsaPrivateKey,
message: &str,
) -> Result<Vec<u8>, 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 { pub fn get_message_digest(message: &str) -> String {
let digest = Sha256::digest(message.as_bytes()); let digest = Sha256::digest(message.as_bytes());
let digest_b64 = base64::encode(digest); let digest_b64 = base64::encode(digest);

View file

@ -34,39 +34,39 @@ pub fn parse_identity_proof(
attachment: &ActorAttachment, attachment: &ActorAttachment,
) -> Result<IdentityProof, ValidationError> { ) -> Result<IdentityProof, ValidationError> {
if attachment.object_type != IDENTITY_PROOF { 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 let proof_type_str = attachment
.signature_algorithm .signature_algorithm
.as_ref() .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() { let proof_type = match proof_type_str.as_str() {
PROOF_TYPE_ID_EIP191 => IdentityProofType::LegacyEip191IdentityProof, PROOF_TYPE_ID_EIP191 => IdentityProofType::LegacyEip191IdentityProof,
PROOF_TYPE_ID_MINISIGN => IdentityProofType::LegacyMinisignIdentityProof, PROOF_TYPE_ID_MINISIGN => IdentityProofType::LegacyMinisignIdentityProof,
_ => return Err(ValidationError("unsupported proof type")), _ => return Err(ValidationError("unsupported proof type".to_string())),
}; };
let did = attachment let did = attachment
.name .name
.parse::<Did>() .parse::<Did>()
.map_err(|_| ValidationError("invalid DID"))?; .map_err(|_| ValidationError("invalid DID".to_string()))?;
let message = let message = create_identity_claim(actor_id, &did)
create_identity_claim(actor_id, &did).map_err(|_| ValidationError("invalid claim"))?; .map_err(|_| ValidationError("invalid claim".to_string()))?;
let signature = attachment let signature = attachment
.signature_value .signature_value
.as_ref() .as_ref()
.ok_or(ValidationError("missing signature"))?; .ok_or(ValidationError("missing signature".to_string()))?;
match did { match did {
Did::Key(ref did_key) => { Did::Key(ref did_key) => {
if !matches!(proof_type, IdentityProofType::LegacyMinisignIdentityProof) { 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) 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) 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) => { Did::Pkh(ref _did_pkh) => {
return Err(ValidationError("incorrect proof type")); return Err(ValidationError("incorrect proof type".to_string()));
} }
}; };
let proof = IdentityProof { let proof = IdentityProof {
@ -110,12 +110,12 @@ pub fn parse_payment_option(
attachment: &ActorAttachment, attachment: &ActorAttachment,
) -> Result<PaymentOption, ValidationError> { ) -> Result<PaymentOption, ValidationError> {
if attachment.object_type != LINK { if attachment.object_type != LINK {
return Err(ValidationError("invalid attachment type")); return Err(ValidationError("invalid attachment type".to_string()));
}; };
let href = attachment let href = attachment
.href .href
.as_ref() .as_ref()
.ok_or(ValidationError("href attribute is required"))? .ok_or(ValidationError("href attribute is required".to_string()))?
.to_string(); .to_string();
let payment_option = PaymentOption::Link(PaymentLink { let payment_option = PaymentOption::Link(PaymentLink {
name: attachment.name.clone(), name: attachment.name.clone(),
@ -137,12 +137,12 @@ pub fn attach_extra_field(field: ExtraField) -> ActorAttachment {
pub fn parse_extra_field(attachment: &ActorAttachment) -> Result<ExtraField, ValidationError> { pub fn parse_extra_field(attachment: &ActorAttachment) -> Result<ExtraField, ValidationError> {
if attachment.object_type != PROPERTY_VALUE { if attachment.object_type != PROPERTY_VALUE {
return Err(ValidationError("invalid attachment type")); return Err(ValidationError("invalid attachment type".to_string()));
}; };
let property_value = attachment let property_value = attachment
.value .value
.as_ref() .as_ref()
.ok_or(ValidationError("missing property value"))?; .ok_or(ValidationError("missing property value".to_string()))?;
let field = ExtraField { let field = ExtraField {
name: attachment.name.clone(), name: attachment.name.clone(),
value: property_value.to_string(), value: property_value.to_string(),

View file

@ -13,6 +13,7 @@ use fedimovies_utils::{
urls::get_hostname, urls::get_hostname,
}; };
use crate::activitypub::types::build_default_context;
use crate::activitypub::{ use crate::activitypub::{
constants::{ constants::{
AP_CONTEXT, MASTODON_CONTEXT, MITRA_CONTEXT, SCHEMA_ORG_CONTEXT, W3ID_SECURITY_CONTEXT, AP_CONTEXT, MASTODON_CONTEXT, MITRA_CONTEXT, SCHEMA_ORG_CONTEXT, W3ID_SECURITY_CONTEXT,
@ -23,7 +24,6 @@ use crate::activitypub::{
types::deserialize_value_array, types::deserialize_value_array,
vocabulary::{IDENTITY_PROOF, IMAGE, LINK, PERSON, PROPERTY_VALUE, SERVICE}, vocabulary::{IDENTITY_PROOF, IMAGE, LINK, PERSON, PROPERTY_VALUE, SERVICE},
}; };
use crate::activitypub::types::build_default_context;
use crate::errors::ValidationError; use crate::errors::ValidationError;
use crate::media::get_file_url; use crate::media::get_file_url;
use crate::webfinger::types::ActorAddress; use crate::webfinger::types::ActorAddress;
@ -110,6 +110,7 @@ pub struct Actor {
pub inbox: String, pub inbox: String,
pub outbox: String, pub outbox: String,
#[serde(default)]
pub bot: bool, pub bot: bool,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -162,7 +163,8 @@ pub struct Actor {
impl Actor { impl Actor {
pub fn address(&self) -> Result<ActorAddress, ValidationError> { pub fn address(&self) -> Result<ActorAddress, ValidationError> {
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 { let actor_address = ActorAddress {
username: self.preferred_username.clone(), username: self.preferred_username.clone(),
hostname: hostname, hostname: hostname,
@ -203,7 +205,10 @@ impl Actor {
let attachment = match serde_json::from_value(attachment_value.clone()) { let attachment = match serde_json::from_value(attachment_value.clone()) {
Ok(attachment) => attachment, Ok(attachment) => attachment,
Err(_) => { Err(_) => {
log_error(attachment_type, ValidationError("invalid attachment")); log_error(
attachment_type,
ValidationError("invalid attachment".to_string()),
);
continue; continue;
} }
}; };
@ -229,7 +234,7 @@ impl Actor {
_ => { _ => {
log_error( log_error(
attachment_type, attachment_type,
ValidationError("unsupported attachment type"), ValidationError("unsupported attachment type".to_string()),
); );
} }
}; };

View file

@ -7,7 +7,7 @@ use fedimovies_models::{
profiles::queries::get_profile_by_remote_actor_id, profiles::queries::get_profile_by_remote_actor_id,
profiles::types::DbActorProfile, profiles::types::DbActorProfile,
}; };
use fedimovies_utils::{crypto_rsa::deserialize_public_key}; use fedimovies_utils::crypto_rsa::deserialize_public_key;
use crate::http_signatures::verify::{ use crate::http_signatures::verify::{
parse_http_signature, verify_http_signature, parse_http_signature, verify_http_signature,
@ -16,9 +16,8 @@ use crate::http_signatures::verify::{
use crate::json_signatures::{ use crate::json_signatures::{
proofs::ProofType, proofs::ProofType,
verify::{ verify::{
get_json_signature, get_json_signature, verify_rsa_json_signature,
verify_rsa_json_signature, JsonSignatureVerificationError as JsonSignatureError, JsonSignatureVerificationError as JsonSignatureError, JsonSigner,
JsonSigner,
}, },
}; };
use crate::media::MediaStorage; use crate::media::MediaStorage;

View file

@ -214,7 +214,7 @@ pub async fn import_post(
}; };
let object = fetch_object(instance, &object_id).await.map_err(|err| { let object = fetch_object(instance, &object_id).await.map_err(|err| {
log::warn!("{}", err); log::warn!("{}", err);
ValidationError("failed to fetch object") ValidationError("failed to fetch object".into())
})?; })?;
log::info!("fetched object {}", object.id); log::info!("fetched object {}", object.id);
fetch_count += 1; fetch_count += 1;
@ -278,9 +278,9 @@ pub async fn import_from_outbox(
let activities = fetch_outbox(&instance, &actor.outbox, limit).await?; let activities = fetch_outbox(&instance, &actor.outbox, limit).await?;
log::info!("fetched {} activities", activities.len()); log::info!("fetched {} activities", activities.len());
for activity in activities { for activity in activities {
let activity_actor = activity["actor"] let activity_actor = activity["actor"].as_str().ok_or(ValidationError(
.as_str() "actor property is missing from activity".to_string(),
.ok_or(ValidationError("actor property is missing"))?; ))?;
if activity_actor != actor.id { if activity_actor != actor.id {
log::warn!("activity doesn't belong to outbox owner"); log::warn!("activity doesn't belong to outbox owner");
continue; continue;

View file

@ -29,13 +29,17 @@ pub async fn handle_accept(
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
// Accept(Follow) // Accept(Follow)
let activity: Accept = serde_json::from_value(activity) let activity: Accept = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!(
"unexpected Accept activity structure: {}",
activity
))
})?;
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; 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_id = parse_local_object_id(&config.instance_url(), &activity.object)?;
let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?; let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?;
if follow_request.target_id != actor_profile.id { 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) { if matches!(follow_request.request_status, FollowRequestStatus::Accepted) {
// Ignore Accept if follow request already accepted // Ignore Accept if follow request already accepted

View file

@ -23,8 +23,8 @@ pub async fn handle_add(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: Add = serde_json::from_value(activity) let activity: Add = serde_json::from_value(activity.clone())
.map_err(|_| ValidationError("unexpected activity structure"))?; .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_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?; let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?;
if Some(activity.target) == actor.subscribers { if Some(activity.target) == actor.subscribers {

View file

@ -38,8 +38,8 @@ pub async fn handle_announce(
// https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-1b12.md // https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-1b12.md
return Ok(None); return Ok(None);
}; };
let activity: Announce = serde_json::from_value(activity) let activity: Announce = serde_json::from_value(activity.clone())
.map_err(|_| ValidationError("unexpected activity structure"))?; .map_err(|_| ValidationError(format!("unexpected activity structure: {}", activity)))?;
let repost_object_id = activity.id; let repost_object_id = activity.id;
match get_post_by_remote_object_id(db_client, &repost_object_id).await { match get_post_by_remote_object_id(db_client, &repost_object_id).await {
Ok(_) => return Ok(None), // Ignore if repost already exists Ok(_) => return Ok(None), // Ignore if repost already exists

View file

@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use chrono::Utc; use chrono::Utc;
use log::warn;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use uuid::Uuid; use uuid::Uuid;
@ -51,11 +52,11 @@ fn get_object_attributed_to(object: &Object) -> Result<String, ValidationError>
let attributed_to = object let attributed_to = object
.attributed_to .attributed_to
.as_ref() .as_ref()
.ok_or(ValidationError("unattributed note"))?; .ok_or(ValidationError("unattributed note".to_string()))?;
let author_id = parse_array(attributed_to) let author_id = parse_array(attributed_to)
.map_err(|_| ValidationError("invalid attributedTo property"))? .map_err(|_| ValidationError("invalid attributedTo property".to_string()))?
.get(0) .get(0)
.ok_or(ValidationError("invalid attributedTo property"))? .ok_or(ValidationError("invalid attributedTo property".to_string()))?
.to_string(); .to_string();
Ok(author_id) Ok(author_id)
} }
@ -65,7 +66,7 @@ pub fn get_object_url(object: &Object) -> Result<String, ValidationError> {
Some(JsonValue::String(string)) => Some(string.to_owned()), Some(JsonValue::String(string)) => Some(string.to_owned()),
Some(other_value) => { Some(other_value) => {
let links: Vec<Link> = parse_property_value(other_value) let links: Vec<Link> = 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()) links.get(0).map(|link| link.href.clone())
} }
None => None, None => None,
@ -87,7 +88,7 @@ pub fn get_object_content(object: &Object) -> Result<String, ValidationError> {
object.name.as_deref().unwrap_or("").to_string() object.name.as_deref().unwrap_or("").to_string()
}; };
if content.len() > CONTENT_MAX_SIZE { 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()); let content_safe = clean_html(&content, content_allowed_classes());
Ok(content_safe) Ok(content_safe)
@ -122,7 +123,7 @@ pub async fn get_object_attachments(
let mut unprocessed = vec![]; let mut unprocessed = vec![];
if let Some(ref value) = object.attachment { if let Some(ref value) = object.attachment {
let list: Vec<Attachment> = parse_property_value(value) let list: Vec<Attachment> = parse_property_value(value)
.map_err(|_| ValidationError("invalid attachment property"))?; .map_err(|_| ValidationError(format!("invalid attachment property: {value:?}")))?;
let mut downloaded = vec![]; let mut downloaded = vec![];
for attachment in list { for attachment in list {
match attachment.attachment_type.as_str() { match attachment.attachment_type.as_str() {
@ -138,7 +139,7 @@ pub async fn get_object_attachments(
}; };
let attachment_url = attachment let attachment_url = attachment
.url .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( let (file_name, file_size, maybe_media_type) = match fetch_file(
instance, instance,
&attachment_url, &attachment_url,
@ -155,8 +156,12 @@ pub async fn get_object_attachments(
continue; continue;
} }
Err(other_error) => { Err(other_error) => {
log::warn!("{}", other_error); log::warn!("failed to fetch attachment: {}", other_error);
return Err(ValidationError("failed to fetch attachment").into()); return Err(ValidationError(format!(
"failed to fetch attachment: {}",
other_error
))
.into());
} }
}; };
log::info!("downloaded attachment {}", attachment_url); 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 { let emoji = if let Some(emoji_id) = maybe_emoji_id {
update_emoji(db_client, &emoji_id, image, &tag.updated).await? update_emoji(db_client, &emoji_id, image, &tag.updated).await?
} else { } 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( match create_emoji(
db_client, db_client,
emoji_name, emoji_name,
@ -351,9 +357,7 @@ pub async fn get_object_tags(
if let Ok(username) = parse_local_actor_id(&instance.url(), &href) { 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. // 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 { let user = match get_user_by_name(db_client, &username).await {
Ok(user) => { Ok(user) => user,
user
},
Err(DatabaseError::NotFound(_)) => { Err(DatabaseError::NotFound(_)) => {
if let Some(api_key) = &api_key { if let Some(api_key) = &api_key {
log::warn!("failed to find mentioned user by name {}, checking if its a valid movie...", username); 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<Vec<String>, ValidationError> { fn get_audience(object: &Object) -> Result<Vec<String>, ValidationError> {
let primary_audience = match object.to { let primary_audience = match object.to {
Some(ref value) => { Some(ref value) => parse_array(value)
parse_array(value).map_err(|_| ValidationError("invalid 'to' property value"))? .map_err(|_| ValidationError("invalid 'to' property value".to_string()))?,
}
None => vec![], None => vec![],
}; };
let secondary_audience = match object.cc { let secondary_audience = match object.cc {
Some(ref value) => { Some(ref value) => parse_array(value)
parse_array(value).map_err(|_| ValidationError("invalid 'cc' property value"))? .map_err(|_| ValidationError("invalid 'cc' property value".to_string()))?,
}
None => vec![], None => vec![],
}; };
let audience = [primary_audience, secondary_audience].concat(); 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); log::info!("processing object of type {}", object.object_type);
} }
other_type => { other_type => {
log::warn!("discarding object of type {}", other_type); let msg = format!("discarding object of type {}", other_type);
return Err(ValidationError("unsupported object type").into()); log::warn!("{msg}");
return Err(ValidationError(msg).into());
} }
}; };
if object.id.len() > OBJECT_ID_SIZE_MAX { 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)?; let author_id = get_object_attributed_to(&object)?;
@ -571,7 +578,12 @@ pub async fn handle_note(
content += &create_content_link(attachment_url); content += &create_content_link(attachment_url);
} }
if content.is_empty() && attachments.is_empty() { 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( let (mentions, hashtags, links, emojis) = get_object_tags(
@ -652,8 +664,8 @@ pub async fn handle_create(
activity: JsonValue, activity: JsonValue,
mut is_authenticated: bool, mut is_authenticated: bool,
) -> HandlerResult { ) -> HandlerResult {
let activity: CreateNote = let activity: CreateNote = serde_json::from_value(activity.clone())
serde_json::from_value(activity).map_err(|_| ValidationError("invalid object"))?; .map_err(|_| ValidationError(format!("invalid CreateNote activity: {}", activity)))?;
let object = activity.object; let object = activity.object;
// Verify attribution // Verify attribution

View file

@ -29,8 +29,12 @@ pub async fn handle_delete(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: Delete = serde_json::from_value(activity) let activity: Delete = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!(
"unexpected Delete activity structure: {}",
activity
))
})?;
if activity.object == activity.actor { if activity.object == activity.actor {
// Self-delete // Self-delete
let profile = match get_profile_by_remote_actor_id(db_client, &activity.object).await { 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?; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
if post.author.id != actor_profile.id { 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 deletion_queue = delete_post(db_client, &post.id).await?;
let config = config.clone(); let config = config.clone();

View file

@ -31,8 +31,12 @@ pub async fn handle_follow(
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
// Follow(Person) // Follow(Person)
let activity: Follow = serde_json::from_value(activity) let activity: Follow = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!(
"unexpected Follow activity structure: {}",
activity
))
})?;
let source_profile = get_or_import_profile_by_actor_id( let source_profile = get_or_import_profile_by_actor_id(
db_client, db_client,
&config.instance(), &config.instance(),

View file

@ -30,8 +30,9 @@ pub async fn handle_like(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: Like = serde_json::from_value(activity) let activity: Like = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!("unexpected Like activity structure: {}", activity))
})?;
let author = get_or_import_profile_by_actor_id( let author = get_or_import_profile_by_actor_id(
db_client, db_client,
&config.instance(), &config.instance(),

View file

@ -34,12 +34,13 @@ pub async fn handle_move(
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
// Move(Person) // Move(Person)
let activity: Move = serde_json::from_value(activity) let activity: Move = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!("unexpected Move activity structure: {}", activity))
})?;
// Mastodon: actor is old profile (object) // Mastodon: actor is old profile (object)
// Mitra: actor is new profile (target) // Mitra: actor is new profile (target)
if activity.object != activity.actor && activity.target != activity.actor { 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(); let instance = config.instance();
@ -70,7 +71,7 @@ pub async fn handle_move(
// Add aliases reported by server (actor's alsoKnownAs property) // Add aliases reported by server (actor's alsoKnownAs property)
aliases.extend(new_profile.aliases.clone().into_actor_ids()); aliases.extend(new_profile.aliases.clone().into_actor_ids());
if !aliases.contains(&old_actor_id) { 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?; let followers = get_followers(db_client, &old_profile.id).await?;

View file

@ -29,13 +29,17 @@ pub async fn handle_reject(
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
// Reject(Follow) // Reject(Follow)
let activity: Reject = serde_json::from_value(activity) let activity: Reject = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!(
"unexpected Reject activity structure: {}",
activity
))
})?;
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; 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_id = parse_local_object_id(&config.instance_url(), &activity.object)?;
let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?; let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?;
if follow_request.target_id != actor_profile.id { 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) { if matches!(follow_request.request_status, FollowRequestStatus::Rejected) {
// Ignore Reject if follow request already rejected // Ignore Reject if follow request already rejected

View file

@ -27,8 +27,12 @@ pub async fn handle_remove(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: Remove = serde_json::from_value(activity) let activity: Remove = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!(
"unexpected Remove activity structure: {}",
activity
))
})?;
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?; let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?;
if Some(activity.target) == actor.subscribers { if Some(activity.target) == actor.subscribers {

View file

@ -30,8 +30,12 @@ async fn handle_undo_follow(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: UndoFollow = serde_json::from_value(activity) let activity: UndoFollow = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!(
"unexpected UndoFollow activity structure: {}",
activity
))
})?;
let source_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; 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_actor_id = find_object_id(&activity.object["object"])?;
let target_username = parse_local_actor_id(&config.instance_url(), &target_actor_id)?; 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; return handle_undo_follow(config, db_client, activity).await;
}; };
let activity: Undo = serde_json::from_value(activity) let activity: Undo = serde_json::from_value(activity.clone()).map_err(|_| {
.map_err(|_| ValidationError("unexpected activity structure"))?; ValidationError(format!("unexpected Undo activity structure: {}", activity))
})?;
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?; 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 { match get_follow_request_by_activity_id(db_client, &activity.object).await {
Ok(follow_request) => { Ok(follow_request) => {
// Undo(Follow) // Undo(Follow)
if follow_request.source_id != actor_profile.id { 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( unfollow(
db_client, db_client,
@ -89,7 +94,7 @@ pub async fn handle_undo(
Ok(reaction) => { Ok(reaction) => {
// Undo(Like) // Undo(Like)
if reaction.author_id != actor_profile.id { 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?; delete_reaction(db_client, &reaction.author_id, &reaction.post_id).await?;
Ok(Some(LIKE)) Ok(Some(LIKE))
@ -103,13 +108,13 @@ pub async fn handle_undo(
Err(other_error) => return Err(other_error.into()), Err(other_error) => return Err(other_error.into()),
}; };
if post.author.id != actor_profile.id { 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 { match post.repost_of_id {
// Ignore returned data because reposts don't have attached files // Ignore returned data because reposts don't have attached files
Some(_) => delete_post(db_client, &post.id).await?, Some(_) => delete_post(db_client, &post.id).await?,
// Can't undo regular post // 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)) Ok(Some(ANNOUNCE))
} }

View file

@ -38,8 +38,8 @@ async fn handle_update_note(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: UpdateNote = let activity: UpdateNote = serde_json::from_value(activity.clone())
serde_json::from_value(activity).map_err(|_| ValidationError("invalid object"))?; .map_err(|_| ValidationError(format!("invalid UpdateNote object {activity}")))?;
let object = activity.object; let object = activity.object;
let post = match get_post_by_remote_object_id(db_client, &object.id).await { let post = match get_post_by_remote_object_id(db_client, &object.id).await {
Ok(post) => post, Ok(post) => post,
@ -49,7 +49,7 @@ async fn handle_update_note(
}; };
let instance = config.instance(); let instance = config.instance();
if profile_actor_id(&instance.url(), &post.author) != activity.actor { 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)?; let mut content = get_object_content(&object)?;
if object.object_type != NOTE { if object.object_type != NOTE {
@ -65,7 +65,7 @@ async fn handle_update_note(
content += &create_content_link(attachment_url); content += &create_content_link(attachment_url);
} }
if content.is_empty() && attachments.is_empty() { 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(); let tmdb_api_key = config.tmdb_api_key.clone();
@ -106,10 +106,10 @@ async fn handle_update_person(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: UpdatePerson = let activity: UpdatePerson = serde_json::from_value(activity.clone())
serde_json::from_value(activity).map_err(|_| ValidationError("invalid actor data"))?; .map_err(|_| ValidationError(format!("invalid UpdatePerson actor data: {}", activity)))?;
if activity.object.id != activity.actor { 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?; let profile = get_profile_by_remote_actor_id(db_client, &activity.object.id).await?;
update_remote_profile( update_remote_profile(
@ -130,7 +130,7 @@ pub async fn handle_update(
) -> HandlerResult { ) -> HandlerResult {
let object_type = activity["object"]["type"] let object_type = activity["object"]["type"]
.as_str() .as_str()
.ok_or(ValidationError("unknown object type"))?; .ok_or(ValidationError("unknown object type".to_string()))?;
match object_type { match object_type {
NOTE => handle_update_note(config, db_client, activity).await, NOTE => handle_update_note(config, db_client, activity).await,
PERSON => handle_update_person(config, db_client, activity).await, PERSON => handle_update_person(config, db_client, activity).await,

View file

@ -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> { 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(()) Ok(())
} }
@ -89,13 +89,14 @@ pub fn parse_local_actor_id(instance_url: &str, actor_id: &str) -> Result<String
"^{}/users/(?P<username>[0-9a-zA-Z_]+)$", "^{}/users/(?P<username>[0-9a-zA-Z_]+)$",
instance_url.replace('.', r"\."), 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 let url_caps = url_regexp
.captures(actor_id) .captures(actor_id)
.ok_or(ValidationError("invalid actor ID"))?; .ok_or(ValidationError("invalid actor ID".to_string()))?;
let username = url_caps let username = url_caps
.name("username") .name("username")
.ok_or(ValidationError("invalid actor ID"))? .ok_or(ValidationError("invalid actor ID".to_string()))?
.as_str() .as_str()
.to_owned(); .to_owned();
Ok(username) Ok(username)
@ -106,16 +107,17 @@ pub fn parse_local_object_id(instance_url: &str, object_id: &str) -> Result<Uuid
"^{}/objects/(?P<uuid>[0-9a-f-]+)$", "^{}/objects/(?P<uuid>[0-9a-f-]+)$",
instance_url.replace('.', r"\."), 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 let url_caps = url_regexp
.captures(object_id) .captures(object_id)
.ok_or(ValidationError("invalid object ID"))?; .ok_or(ValidationError("invalid object ID".to_string()))?;
let internal_object_id: Uuid = url_caps let internal_object_id: Uuid = url_caps
.name("uuid") .name("uuid")
.ok_or(ValidationError("invalid object ID"))? .ok_or(ValidationError("invalid object ID".to_string()))?
.as_str() .as_str()
.parse() .parse()
.map_err(|_| ValidationError("invalid object ID"))?; .map_err(|_| ValidationError("invalid object ID".to_string()))?;
Ok(internal_object_id) Ok(internal_object_id)
} }

View file

@ -108,7 +108,7 @@ pub fn find_object_id(object: &Value) -> Result<String, ValidationError> {
None => { None => {
let object_id = object["id"] let object_id = object["id"]
.as_str() .as_str()
.ok_or(ValidationError("missing object ID"))? .ok_or(ValidationError("missing object ID".to_string()))?
.to_string(); .to_string();
object_id object_id
} }
@ -133,11 +133,11 @@ pub async fn handle_activity(
) -> Result<(), HandlerError> { ) -> Result<(), HandlerError> {
let activity_type = activity["type"] let activity_type = activity["type"]
.as_str() .as_str()
.ok_or(ValidationError("type property is missing"))? .ok_or(ValidationError("type property is missing".to_string()))?
.to_owned(); .to_owned();
let activity_actor = activity["actor"] let activity_actor = activity["actor"]
.as_str() .as_str()
.ok_or(ValidationError("actor property is missing"))? .ok_or(ValidationError("actor property is missing".to_string()))?
.to_owned(); .to_owned();
let activity = activity.clone(); let activity = activity.clone();
let maybe_object_type = match activity_type.as_str() { let maybe_object_type = match activity_type.as_str() {
@ -177,15 +177,15 @@ pub async fn receive_activity(
) -> Result<(), HandlerError> { ) -> Result<(), HandlerError> {
let activity_type = activity["type"] let activity_type = activity["type"]
.as_str() .as_str()
.ok_or(ValidationError("type property is missing"))?; .ok_or(ValidationError("type property is missing".to_string()))?;
let activity_actor = activity["actor"] let activity_actor = activity["actor"]
.as_str() .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) let actor_hostname = url::Url::parse(activity_actor)
.map_err(|_| ValidationError("invalid actor ID"))? .map_err(|_| ValidationError("invalid actor ID".to_string()))?
.host_str() .host_str()
.ok_or(ValidationError("invalid actor ID"))? .ok_or(ValidationError("invalid actor ID".to_string()))?
.to_string(); .to_string();
if config if config
.blocked_instances .blocked_instances
@ -295,7 +295,7 @@ pub async fn receive_activity(
if activity_type == CREATE { if activity_type == CREATE {
let CreateNote { object, .. } = serde_json::from_value(activity.clone()) 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? { if is_unsolicited_message(db_client, &config.instance_url(), &object).await? {
log::warn!("unsolicited message rejected: {}", object.id); log::warn!("unsolicited message rejected: {}", object.id);
return Ok(()); return Ok(());

View file

@ -9,7 +9,7 @@ pub fn role_from_str(role_str: &str) -> Result<Role, ValidationError> {
"user" => Role::NormalUser, "user" => Role::NormalUser,
"admin" => Role::Admin, "admin" => Role::Admin,
"read_only_user" => Role::ReadOnlyUser, "read_only_user" => Role::ReadOnlyUser,
_ => return Err(ValidationError("unknown role")), _ => return Err(ValidationError("unknown role".to_string())),
}; };
Ok(role) Ok(role)
} }

View file

@ -9,7 +9,7 @@ pub struct ConversionError;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[error("{0}")] #[error("{0}")]
pub struct ValidationError(pub &'static str); pub struct ValidationError(pub String);
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum HttpError { pub enum HttpError {

View file

@ -1,19 +1,19 @@
use anyhow::Error; use anyhow::Error;
use crate::activitypub::builders::announce::prepare_announce;
use fedimovies_config::Config; 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::{ use fedimovies_models::{
database::{get_database_client, DbPool}, database::{get_database_client, DbPool},
emojis::queries::{delete_emoji, find_unused_remote_emojis}, emojis::queries::{delete_emoji, find_unused_remote_emojis},
posts::queries::{delete_post, find_extraneous_posts}, posts::queries::{delete_post, find_extraneous_posts},
profiles::queries::{delete_profile, find_empty_profiles, get_profile_by_id}, 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 fedimovies_utils::datetime::days_before_now;
use crate::activitypub::builders::announce::prepare_announce;
use crate::activitypub::queues::{ use crate::activitypub::queues::{
process_queued_incoming_activities, process_queued_outgoing_activities, 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 // 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?; let db_client = &mut **get_database_client(db_pool).await?;
log::debug!("Reviewing mentions.."); log::debug!("Reviewing mentions..");
// for each mention notification do repost // for each mention notification do repost
@ -103,14 +106,20 @@ pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result
} }
let mut post = post_with_mention.clone(); let mut post = post_with_mention.clone();
let post_id = post.id; 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 // Repost
let repost_data = PostCreateData::repost(post.id, None); let repost_data = PostCreateData::repost(post.id, None);
let mut repost = match create_post(&mut transaction, &current_user.id, repost_data).await { let mut repost =
match create_post(&mut transaction, &current_user.id, repost_data).await {
Ok(repost) => repost, Ok(repost) => repost,
Err(DatabaseError::AlreadyExists(err)) => { Err(DatabaseError::AlreadyExists(err)) => {
log::info!("Review as Mention of {} already reposted the post with id {}", current_user.profile.username, post_id); 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?; delete_notification(&mut transaction, mention_notification.id).await?;
continue; continue;
} }
@ -128,7 +137,11 @@ pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result
// Delete notification to avoid re-processing // Delete notification to avoid re-processing
delete_notification(&mut transaction, mention_notification.id).await?; 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?) Ok(transaction.commit().await?)

View file

@ -83,7 +83,9 @@ pub fn run(config: Config, db_pool: DbPool) -> () {
} }
PeriodicTask::PruneRemoteEmojis => prune_remote_emojis(&config, &db_pool).await, PeriodicTask::PruneRemoteEmojis => prune_remote_emojis(&config, &db_pool).await,
PeriodicTask::SubscriptionExpirationMonitor => Ok(()), 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| { task_result.unwrap_or_else(|err| {
log::error!("{:?}: {}", task, err); log::error!("{:?}: {}", task, err);

View file

@ -3,6 +3,8 @@ use rsa::RsaPrivateKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use crate::json_signatures::proofs::PROOF_TYPE_JCS_RSA;
use fedimovies_utils::crypto_rsa::create_rsa_signature;
use fedimovies_utils::{ use fedimovies_utils::{
canonicalization::{canonicalize_object, CanonicalizationError}, canonicalization::{canonicalize_object, CanonicalizationError},
crypto_rsa::create_rsa_sha256_signature, crypto_rsa::create_rsa_sha256_signature,
@ -29,6 +31,19 @@ pub struct IntegrityProof {
pub proof_value: String, 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)] #[derive(thiserror::Error, Debug)]
pub enum JsonSignatureError { pub enum JsonSignatureError {
#[error(transparent)] #[error(transparent)]
@ -40,16 +55,42 @@ pub enum JsonSignatureError {
#[error("signing error")] #[error("signing error")]
SigningError(#[from] rsa::errors::Error), SigningError(#[from] rsa::errors::Error),
#[error("invalid object")] #[error("invalid JSON signature object")]
InvalidObject, 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( pub fn sign_object(
_object: &Value, object: &Value,
_signer_key: &RsaPrivateKey, signer_key: &RsaPrivateKey,
_signer_key_id: &str, signer_key_id: &str,
) -> Result<Value, JsonSignatureError> { ) -> Result<Value, JsonSignatureError> {
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 { pub fn is_object_signed(object: &Value) -> bool {

View file

@ -9,6 +9,13 @@ pub const PROOF_TYPE_ID_EIP191: &str = "ethereum-eip191-00";
// Identity proof, version 2022A // Identity proof, version 2022A
pub const PROOF_TYPE_ID_MINISIGN: &str = "MitraMinisignSignature2022A"; 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 // https://w3c.github.io/vc-data-integrity/#dataintegrityproof
pub const DATA_INTEGRITY_PROOF: &str = "DataIntegrityProof"; pub const DATA_INTEGRITY_PROOF: &str = "DataIntegrityProof";

View file

@ -32,7 +32,7 @@ pub struct SignatureData {
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum JsonSignatureVerificationError { pub enum JsonSignatureVerificationError {
#[error("invalid object")] #[error("invalid signature object")]
InvalidObject, InvalidObject,
#[error("no proof")] #[error("no proof")]

View file

@ -299,7 +299,7 @@ impl AccountUpdateData {
) -> Result<ProfileUpdateData, MastodonError> { ) -> Result<ProfileUpdateData, MastodonError> {
let maybe_bio = if let Some(ref bio_source) = self.note { let maybe_bio = if let Some(ref bio_source) = self.note {
let bio = markdown_basic_to_html(bio_source) let bio = markdown_basic_to_html(bio_source)
.map_err(|_| ValidationError("invalid markdown"))?; .map_err(|_| ValidationError("invalid markdown".to_string()))?;
Some(bio) Some(bio)
} else { } else {
None None
@ -321,7 +321,7 @@ impl AccountUpdateData {
let mut extra_fields = vec![]; let mut extra_fields = vec![];
for field_source in self.fields_attributes.unwrap_or(vec![]) { for field_source in self.fields_attributes.unwrap_or(vec![]) {
let value = markdown_basic_to_html(&field_source.value) 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 { let extra_field = ExtraField {
name: field_source.name, name: field_source.name,
value: value, value: value,

View file

@ -72,15 +72,15 @@ pub async fn create_account(
let invite_code = account_data let invite_code = account_data
.invite_code .invite_code
.as_ref() .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? { 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)?; validate_local_username(&account_data.username)?;
if account_data.password.is_none() && account_data.message.is_none() { 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 maybe_password_hash = if let Some(password) = account_data.password.as_ref() {
let password_hash = hash_password(password).map_err(|_| MastodonError::InternalError)?; 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 { let user = match create_user(db_client, user_data).await {
Ok(user) => user, Ok(user) => user,
Err(DatabaseError::AlreadyExists(_)) => { 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()), Err(other_error) => return Err(other_error.into()),
}; };
@ -280,7 +280,7 @@ async fn search_by_did(
let did: Did = query_params let did: Did = query_params
.did .did
.parse() .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 profiles = search_profiles_by_did(db_client, &did, false).await?;
let base_url = get_request_base_url(connection_info); let base_url = get_request_base_url(connection_info);
let instance_url = config.instance().url(); let instance_url = config.instance().url();

View file

@ -16,7 +16,7 @@ impl MarkerQueryParams {
let timeline = match self.timeline.as_ref() { let timeline = match self.timeline.as_ref() {
"home" => Timeline::Home, "home" => Timeline::Home,
"notifications" => Timeline::Notifications, "notifications" => Timeline::Notifications,
_ => return Err(ValidationError("invalid timeline name")), _ => return Err(ValidationError("invalid timeline name".to_string())),
}; };
Ok(timeline) Ok(timeline)
} }

View file

@ -42,18 +42,18 @@ async fn authorize_view(
let password_hash = user let password_hash = user
.password_hash .password_hash
.as_ref() .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) let password_correct = verify_password(password_hash, &form_data.password)
.map_err(|_| MastodonError::InternalError)?; .map_err(|_| MastodonError::InternalError)?;
if !password_correct { if !password_correct {
return Err(ValidationError("incorrect password").into()); return Err(ValidationError("incorrect password".to_string()).into());
}; };
if query_params.response_type != "code" { 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?; 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 { 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(); let authorization_code = generate_access_token();
@ -91,36 +91,35 @@ async fn token_view(
let db_client = &**get_database_client(&db_pool).await?; let db_client = &**get_database_client(&db_pool).await?;
let user = match request_data.grant_type.as_str() { let user = match request_data.grant_type.as_str() {
"authorization_code" => { "authorization_code" => {
let authorization_code = request_data let authorization_code = request_data.code.as_ref().ok_or(ValidationError(
.code "authorization code is required".to_string(),
.as_ref() ))?;
.ok_or(ValidationError("authorization code is required"))?;
get_user_by_authorization_code(db_client, authorization_code).await? get_user_by_authorization_code(db_client, authorization_code).await?
} }
"password" => { "password" => {
let username = request_data let username = request_data
.username .username
.as_ref() .as_ref()
.ok_or(ValidationError("username is required"))?; .ok_or(ValidationError("username is required".to_string()))?;
get_user_by_name(db_client, username).await? 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" { if request_data.grant_type == "password" || request_data.grant_type == "ethereum" {
let password = request_data let password = request_data
.password .password
.as_ref() .as_ref()
.ok_or(ValidationError("password is required"))?; .ok_or(ValidationError("password is required".to_string()))?;
let password_hash = user let password_hash = user
.password_hash .password_hash
.as_ref() .as_ref()
.ok_or(ValidationError("password auth is disabled"))?; .ok_or(ValidationError("password auth is disabled".to_string()))?;
let password_correct = let password_correct =
verify_password(password_hash, password).map_err(|_| MastodonError::InternalError)?; verify_password(password_hash, password).map_err(|_| MastodonError::InternalError)?;
if !password_correct { if !password_correct {
return Err(ValidationError("incorrect password").into()); return Err(ValidationError("incorrect password".to_string()).into());
}; };
}; };
let access_token = generate_access_token(); let access_token = generate_access_token();

View file

@ -44,10 +44,10 @@ fn parse_profile_query(query: &str) -> Result<(String, Option<String>), Validati
Regex::new(r"^(@|!)?(?P<username>[\w\.-]+)(@(?P<hostname>[\w\.-]+))?$").unwrap(); Regex::new(r"^(@|!)?(?P<username>[\w\.-]+)(@(?P<hostname>[\w\.-]+))?$").unwrap();
let acct_query_caps = acct_query_re let acct_query_caps = acct_query_re
.captures(query) .captures(query)
.ok_or(ValidationError("invalid profile query"))?; .ok_or(ValidationError("invalid profile query".to_string()))?;
let username = acct_query_caps let username = acct_query_caps
.name("username") .name("username")
.ok_or(ValidationError("invalid profile query"))? .ok_or(ValidationError("invalid profile query".to_string()))?
.as_str() .as_str()
.to_string(); .to_string();
let maybe_hostname = acct_query_caps let maybe_hostname = acct_query_caps
@ -60,10 +60,10 @@ fn parse_tag_query(query: &str) -> Result<String, ValidationError> {
let tag_query_re = Regex::new(r"^#(?P<tag>\w+)$").unwrap(); let tag_query_re = Regex::new(r"^#(?P<tag>\w+)$").unwrap();
let tag_query_caps = tag_query_re let tag_query_caps = tag_query_re
.captures(query) .captures(query)
.ok_or(ValidationError("invalid tag query"))?; .ok_or(ValidationError("invalid tag query".to_string()))?;
let tag = tag_query_caps let tag = tag_query_caps
.name("tag") .name("tag")
.ok_or(ValidationError("invalid tag query"))? .ok_or(ValidationError("invalid tag query".to_string()))?
.as_str() .as_str()
.to_string(); .to_string();
Ok(tag) Ok(tag)

View file

@ -59,7 +59,9 @@ pub fn parse_address_list(csv: &str) -> Result<Vec<ActorAddress>, ValidationErro
addresses.sort(); addresses.sort();
addresses.dedup(); addresses.dedup();
if addresses.len() > 50 { 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) Ok(addresses)
} }

View file

@ -41,7 +41,7 @@ async fn client_config_view(
let db_client = &**get_database_client(&db_pool).await?; let db_client = &**get_database_client(&db_pool).await?;
let mut current_user = get_current_user(db_client, auth.token()).await?; let mut current_user = get_current_user(db_client, auth.token()).await?;
if request_data.len() != 1 { 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 let (client_name, client_config_value) = request_data
.iter() .iter()
@ -172,11 +172,11 @@ async fn move_followers(
let db_client = &mut **get_database_client(&db_pool).await?; let db_client = &mut **get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).await?; let current_user = get_current_user(db_client, auth.token()).await?;
if current_user.profile.identity_proofs.inner().is_empty() { 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(); let instance = config.instance();
if request_data.from_actor_id.starts_with(&instance.url()) { 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 // Existence of actor is not verified because
// the old profile could have been deleted // the old profile could have been deleted
@ -193,7 +193,7 @@ async fn move_followers(
.into_iter() .into_iter()
.map(|profile| profile_actor_id(&instance.url(), &profile)); .map(|profile| profile_actor_id(&instance.url(), &profile));
if !aliases.any(|actor_id| actor_id == request_data.from_actor_id) { 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)?; let address_list = parse_address_list(&request_data.followers_csv)?;

View file

@ -57,14 +57,14 @@ async fn create_status(
Some("direct") => Visibility::Direct, Some("direct") => Visibility::Direct,
Some("private") => Visibility::Followers, Some("private") => Visibility::Followers,
Some("subscribers") => Visibility::Subscribers, 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, None => Visibility::Public,
}; };
let content = match status_data.content_type.as_str() { let content = match status_data.content_type.as_str() {
"text/html" => status_data.status, "text/html" => status_data.status,
"text/markdown" => markdown_lite_to_html(&status_data.status) "text/markdown" => markdown_lite_to_html(&status_data.status)
.map_err(|_| ValidationError("invalid markdown"))?, .map_err(|_| ValidationError("invalid markdown".to_string()))?,
_ => return Err(ValidationError("unsupported content type").into()), _ => return Err(ValidationError("unsupported content type".to_string()).into()),
}; };
// Parse content // Parse content
let PostContent { let PostContent {
@ -95,21 +95,21 @@ async fn create_status(
mentions.sort(); mentions.sort();
mentions.dedup(); mentions.dedup();
if mentions.len() > MENTION_LIMIT { if mentions.len() > MENTION_LIMIT {
return Err(ValidationError("too many mentions").into()); return Err(ValidationError("too many mentions".to_string()).into());
}; };
// Links validation // Links validation
if links.len() > 0 && visibility != Visibility::Public { 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 { if links.len() > LINK_LIMIT {
return Err(ValidationError("too many links").into()); return Err(ValidationError("too many links".to_string()).into());
}; };
// Emoji validation // Emoji validation
let emojis: Vec<_> = emojis.iter().map(|emoji| emoji.id).collect(); let emojis: Vec<_> = emojis.iter().map(|emoji| emoji.id).collect();
if emojis.len() > EMOJI_LIMIT { if emojis.len() > EMOJI_LIMIT {
return Err(ValidationError("too many emojis").into()); return Err(ValidationError("too many emojis".to_string()).into());
}; };
// Reply validation // 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 { let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await {
Ok(post) => post, Ok(post) => post,
Err(DatabaseError::NotFound(_)) => { 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()), Err(other_error) => return Err(other_error.into()),
}; };
if in_reply_to.repost_of_id.is_some() { 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 { 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 { if visibility != Visibility::Public {
let mut in_reply_to_audience: Vec<_> = in_reply_to let mut in_reply_to_audience: Vec<_> = in_reply_to
@ -135,7 +135,7 @@ async fn create_status(
.collect(); .collect();
in_reply_to_audience.push(in_reply_to.author.id); in_reply_to_audience.push(in_reply_to.author.id);
if !mentions.iter().all(|id| in_reply_to_audience.contains(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) Some(in_reply_to)
@ -145,7 +145,7 @@ async fn create_status(
// Validate attachments // Validate attachments
let attachments = status_data.media_ids.unwrap_or(vec![]); let attachments = status_data.media_ids.unwrap_or(vec![]);
if attachments.len() > ATTACHMENT_LIMIT { if attachments.len() > ATTACHMENT_LIMIT {
return Err(ValidationError("too many attachments").into()); return Err(ValidationError("too many attachments".to_string()).into());
}; };
// Create post // Create post
@ -197,8 +197,8 @@ async fn preview_status(
let content = match status_data.content_type.as_str() { let content = match status_data.content_type.as_str() {
"text/html" => status_data.status, "text/html" => status_data.status,
"text/markdown" => markdown_lite_to_html(&status_data.status) "text/markdown" => markdown_lite_to_html(&status_data.status)
.map_err(|_| ValidationError("invalid markdown"))?, .map_err(|_| ValidationError("invalid markdown".to_string()))?,
_ => return Err(ValidationError("unsupported content type").into()), _ => return Err(ValidationError("unsupported content type".to_string()).into()),
}; };
let PostContent { let PostContent {
mut content, mut content,

View file

@ -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> { pub fn validate_emoji_name(emoji_name: &str) -> Result<(), ValidationError> {
let name_re = Regex::new(EMOJI_NAME_RE).unwrap(); let name_re = Regex::new(EMOJI_NAME_RE).unwrap();
if !name_re.is_match(emoji_name) { 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 { 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(()) Ok(())
} }

View file

@ -23,12 +23,12 @@ pub fn clean_content(content: &str) -> Result<String, ValidationError> {
// Check content size to not exceed the hard limit // Check content size to not exceed the hard limit
// Character limit from config is not enforced at the backend // Character limit from config is not enforced at the backend
if content.len() > CONTENT_MAX_SIZE { 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_safe = clean_html_strict(content, &CONTENT_ALLOWED_TAGS, content_allowed_classes());
let content_trimmed = content_safe.trim(); let content_trimmed = content_safe.trim();
if content_trimmed.is_empty() { 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()) Ok(content_trimmed.to_string())
} }

View file

@ -16,21 +16,21 @@ const FIELD_VALUE_MAX_SIZE: usize = 5000;
pub fn validate_username(username: &str) -> Result<(), ValidationError> { pub fn validate_username(username: &str) -> Result<(), ValidationError> {
if username.is_empty() { if username.is_empty() {
return Err(ValidationError("username is empty")); return Err(ValidationError("username is empty".to_string()));
}; };
if username.len() > 100 { 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(); let username_regexp = Regex::new(USERNAME_RE).unwrap();
if !username_regexp.is_match(username) { if !username_regexp.is_match(username) {
return Err(ValidationError("invalid username")); return Err(ValidationError("invalid username".to_string()));
}; };
Ok(()) Ok(())
} }
fn validate_display_name(display_name: &str) -> Result<(), ValidationError> { fn validate_display_name(display_name: &str) -> Result<(), ValidationError> {
if display_name.chars().count() > DISPLAY_NAME_MAX_LENGTH { 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(()) Ok(())
} }
@ -43,7 +43,7 @@ fn clean_bio(bio: &str, is_remote: bool) -> Result<String, ValidationError> {
} else { } else {
// Local profile // Local profile
if bio.chars().count() > BIO_MAX_LENGTH { 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![]) clean_html_strict(bio, &BIO_ALLOWED_TAGS, vec![])
}; };
@ -63,21 +63,23 @@ fn clean_extra_fields(
continue; continue;
}; };
if field.name.len() > FIELD_NAME_MAX_SIZE { 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 { 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); cleaned_extra_fields.push(field);
} }
#[allow(clippy::collapsible_else_if)] #[allow(clippy::collapsible_else_if)]
if is_remote { if is_remote {
if cleaned_extra_fields.len() > 100 { 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 { } else {
if cleaned_extra_fields.len() > 10 { 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) Ok(cleaned_extra_fields)
@ -88,7 +90,9 @@ pub fn clean_profile_create_data(
) -> Result<(), ValidationError> { ) -> Result<(), ValidationError> {
validate_username(&profile_data.username)?; validate_username(&profile_data.username)?;
if profile_data.hostname.is_some() != profile_data.actor_json.is_some() { 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 { if let Some(display_name) = &profile_data.display_name {
validate_display_name(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)?; profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?;
if profile_data.emojis.len() > EMOJI_LIMIT { if profile_data.emojis.len() > EMOJI_LIMIT {
return Err(ValidationError("too many emojis")); return Err(ValidationError("too many emojis".to_string()));
}; };
Ok(()) Ok(())
} }
@ -118,7 +122,7 @@ pub fn clean_profile_update_data(
}; };
profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?; profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?;
if profile_data.emojis.len() > EMOJI_LIMIT { if profile_data.emojis.len() > EMOJI_LIMIT {
return Err(ValidationError("too many emojis")); return Err(ValidationError("too many emojis".to_string()));
}; };
Ok(()) Ok(())
} }

View file

@ -7,7 +7,7 @@ const HASHTAG_NAME_RE: &str = r"^\w+$";
pub fn validate_hashtag(tag_name: &str) -> Result<(), ValidationError> { pub fn validate_hashtag(tag_name: &str) -> Result<(), ValidationError> {
let hashtag_name_re = Regex::new(HASHTAG_NAME_RE).unwrap(); let hashtag_name_re = Regex::new(HASHTAG_NAME_RE).unwrap();
if !hashtag_name_re.is_match(tag_name) { if !hashtag_name_re.is_match(tag_name) {
return Err(ValidationError("invalid tag name")); return Err(ValidationError("invalid tag name".to_string()));
}; };
Ok(()) Ok(())
} }

View file

@ -9,7 +9,7 @@ pub fn validate_local_username(username: &str) -> Result<(), ValidationError> {
// The username regexp should not allow domain names and IP addresses // The username regexp should not allow domain names and IP addresses
let username_regexp = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); let username_regexp = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap();
if !username_regexp.is_match(username) { if !username_regexp.is_match(username) {
return Err(ValidationError("invalid username")); return Err(ValidationError("invalid username".to_string()));
}; };
Ok(()) Ok(())
} }

View file

@ -60,7 +60,7 @@ impl FromStr for ActorAddress {
let actor_address_re = Regex::new(ACTOR_ADDRESS_RE).unwrap(); let actor_address_re = Regex::new(ACTOR_ADDRESS_RE).unwrap();
let caps = actor_address_re let caps = actor_address_re
.captures(value) .captures(value)
.ok_or(ValidationError("invalid actor address"))?; .ok_or(ValidationError("invalid actor address".to_string()))?;
let actor_address = Self { let actor_address = Self {
username: caps["username"].to_string(), username: caps["username"].to_string(),
hostname: caps["hostname"].to_string(), hostname: caps["hostname"].to_string(),

View file

@ -22,7 +22,7 @@ use super::types::{
fn parse_acct_uri(uri: &str) -> Result<ActorAddress, ValidationError> { fn parse_acct_uri(uri: &str) -> Result<ActorAddress, ValidationError> {
let actor_address = uri let actor_address = uri
.strip_prefix("acct:") .strip_prefix("acct:")
.ok_or(ValidationError("invalid query target"))? .ok_or(ValidationError("invalid query target".to_string()))?
.parse()?; .parse()?;
Ok(actor_address) Ok(actor_address)
} }