Compare commits

...

3 commits

Author SHA1 Message Date
Rafael Caricio 60a27b5b11
Make generic errors carry more details 2023-04-26 12:55:42 +02:00
Rafael Caricio 0d77557ad6
Simplify deployment workflow 2023-04-26 12:54:48 +02:00
Rafael Caricio 1e40a42524
Support database connection via SSL
This is required to use managed Postgres databases. It is necessary to
use SSL connection to the remote host as the connection goes through the
open internet.
2023-04-26 12:07:36 +02:00
55 changed files with 472 additions and 222 deletions

21
.dockerignore Normal file
View file

@ -0,0 +1,21 @@
# flyctl launch added from .gitignore
**/.env.local
**/config.yaml
target
# other things
docs/*
fedimovies-*
scripts/*
src/*
# flyctl launch added from .idea/.gitignore
# Default ignored files
.idea/shelf
.idea/workspace.xml
# Editor-based HTTP Client requests
.idea/httpRequests
# Datasource local storage ignored files
.idea/dataSources
.idea/dataSources.local.xml
fly.toml

4
.gitignore vendored
View file

@ -1,5 +1,9 @@
.env.local
config.yaml
/secret/*
/files/*
!/files/.gitkeep
/build/*
!/build/.gitkeep
/target
fly.toml

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"

0
build/.gitkeep Normal file
View file

16
contrib/Dockerfile Normal file
View file

@ -0,0 +1,16 @@
FROM ubuntu:23.04
RUN apt-get update && apt-get install -y \
curl \
wget \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/lib/data
COPY build/fedimovies /usr/local/bin
COPY build/fedimoviesctl /usr/local/bin
COPY secret/fedimovies.conf /etc/fedimovies.conf
COPY files /www/frontend/
CMD ["/usr/local/bin/fedimovies"]

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).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

@ -33,6 +33,8 @@ pub struct Config {
// Core settings
pub database_url: String,
#[serde(default)]
pub tls_ca_file: Option<PathBuf>,
pub storage_dir: PathBuf,
pub web_client_dir: Option<PathBuf>,

View file

@ -27,6 +27,8 @@ thiserror = "1.0.37"
# Async runtime
tokio = { version = "1.20.4", features = [] }
# Used for working with Postgresql database
openssl = { version = "0.10", features = ["vendored"] }
postgres-openssl = "0.5.0"
tokio-postgres = { version = "0.7.6", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
postgres-types = { version = "0.2.3", features = ["derive", "with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
postgres-protocol = "0.6.4"
@ -38,7 +40,6 @@ uuid = { version = "1.1.2", features = ["serde", "v4"] }
[dev-dependencies]
fedimovies-utils = { path = "../fedimovies-utils", features = ["test-utils"] }
serial_test = "0.7.0"
[features]

View file

@ -1,3 +1,7 @@
use deadpool_postgres::SslMode;
use openssl::ssl::{SslConnector, SslMethod};
use postgres_openssl::MakeTlsConnector;
use std::path::Path;
use tokio_postgres::config::Config as DatabaseConfig;
use tokio_postgres::error::{Error as PgError, SqlState};
@ -11,6 +15,7 @@ pub mod test_utils;
pub type DbPool = deadpool_postgres::Pool;
pub use tokio_postgres::GenericClient as DatabaseClient;
use tokio_postgres::NoTls;
#[derive(thiserror::Error, Debug)]
#[error("database type error")]
@ -37,21 +42,49 @@ pub enum DatabaseError {
AlreadyExists(&'static str), // object type
}
pub async fn create_database_client(db_config: &DatabaseConfig) -> tokio_postgres::Client {
let (client, connection) = db_config.connect(tokio_postgres::NoTls).await.unwrap();
tokio::spawn(async move {
if let Err(err) = connection.await {
log::error!("connection error: {}", err);
};
});
pub async fn create_database_client(
db_config: &DatabaseConfig,
ca_file_path: Option<&Path>,
) -> tokio_postgres::Client {
let client = if let Some(ca_file_path) = ca_file_path {
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
log::debug!("Using TLS CA file: {}", ca_file_path.display());
builder.set_ca_file(ca_file_path).unwrap();
let connector = MakeTlsConnector::new(builder.build());
let (client, connection) = db_config.connect(connector).await.unwrap();
tokio::spawn(async move {
if let Err(err) = connection.await {
log::error!("connection with tls error: {}", err);
};
});
client
} else {
let (client, connection) = db_config.connect(tokio_postgres::NoTls).await.unwrap();
tokio::spawn(async move {
if let Err(err) = connection.await {
log::error!("connection error: {}", err);
};
});
client
};
client
}
pub fn create_pool(database_url: &str, pool_size: usize) -> DbPool {
let manager = deadpool_postgres::Manager::new(
database_url.parse().expect("invalid database URL"),
tokio_postgres::NoTls,
);
pub fn create_pool(database_url: &str, ca_file_path: Option<&Path>, pool_size: usize) -> DbPool {
let manager = if let Some(ca_file_path) = ca_file_path {
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
log::info!("Using TLS CA file: {}", ca_file_path.display());
builder.set_ca_file(ca_file_path).unwrap();
let connector = MakeTlsConnector::new(builder.build());
deadpool_postgres::Manager::new(
database_url.parse().expect("invalid database URL"),
connector,
)
} else {
deadpool_postgres::Manager::new(database_url.parse().expect("invalid database URL"), NoTls)
};
DbPool::builder(manager)
.max_size(pool_size)
.build()

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

9
justfile Normal file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env -S just --justfile
build-release:
cargo build --release --target x86_64-unknown-linux-gnu
cp target/x86_64-unknown-linux-gnu/release/fedimovies build/fedimovies
cp target/x86_64-unknown-linux-gnu/release/fedimoviesctl build/fedimoviesctl
deploy: build-release
fly deploy

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

@ -44,7 +44,11 @@ async fn main() -> std::io::Result<()> {
// https://wiki.postgresql.org/wiki/Number_Of_Database_Connections
let db_pool_size = num_cpus::get() * 2;
let db_pool = create_pool(&config.database_url, db_pool_size);
let db_pool = create_pool(
&config.database_url,
config.tls_ca_file.as_ref().map(|s| s.as_path()),
db_pool_size,
);
let mut db_client = get_database_client(&db_pool).await.unwrap();
apply_migrations(&mut db_client).await;

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