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",
"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"

View file

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

View file

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

View file

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

View file

@ -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<Notification> = rows
.iter()

View file

@ -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<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 {
let digest = Sha256::digest(message.as_bytes());
let digest_b64 = base64::encode(digest);

View file

@ -34,39 +34,39 @@ pub fn parse_identity_proof(
attachment: &ActorAttachment,
) -> Result<IdentityProof, ValidationError> {
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::<Did>()
.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<PaymentOption, ValidationError> {
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<ExtraField, ValidationError> {
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(),

View file

@ -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<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 {
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()),
);
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String, ValidationError>
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<String, ValidationError> {
Some(JsonValue::String(string)) => Some(string.to_owned()),
Some(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())
}
None => None,
@ -87,7 +88,7 @@ pub fn get_object_content(object: &Object) -> Result<String, ValidationError> {
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<Attachment> = 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<Vec<String>, 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

View file

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

View file

@ -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(),

View file

@ -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(),

View file

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

View file

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

View file

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

View file

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

View file

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

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> {
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<String
"^{}/users/(?P<username>[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<Uuid
"^{}/objects/(?P<uuid>[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)
}

View file

@ -108,7 +108,7 @@ pub fn find_object_id(object: &Value) -> Result<String, ValidationError> {
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(());

View file

@ -9,7 +9,7 @@ pub fn role_from_str(role_str: &str) -> Result<Role, ValidationError> {
"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)
}

View file

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

View file

@ -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, &current_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, &current_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?)

View file

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

View file

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

View file

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

View file

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

View file

@ -299,7 +299,7 @@ impl AccountUpdateData {
) -> Result<ProfileUpdateData, MastodonError> {
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,

View file

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

View file

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

View file

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

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();
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<String, ValidationError> {
let tag_query_re = Regex::new(r"^#(?P<tag>\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)

View file

@ -59,7 +59,9 @@ pub fn parse_address_list(csv: &str) -> Result<Vec<ActorAddress>, 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)
}

View file

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

View file

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

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> {
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(())
}

View file

@ -23,12 +23,12 @@ pub fn clean_content(content: &str) -> Result<String, ValidationError> {
// 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())
}

View file

@ -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<String, ValidationError> {
} 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(())
}

View file

@ -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(())
}

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
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(())
}

View file

@ -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(),

View file

@ -22,7 +22,7 @@ use super::types::{
fn parse_acct_uri(uri: &str) -> Result<ActorAddress, ValidationError> {
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)
}