diff --git a/Cargo.lock b/Cargo.lock index ee90018..0ef7265 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -792,6 +792,12 @@ dependencies = [ "signature", ] +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "elliptic-curve" version = "0.10.6" @@ -2674,6 +2680,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-socks", "url 2.2.2", "wasm-bindgen", "wasm-bindgen-futures", @@ -3280,6 +3287,18 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 2497bb5..00eeb2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ rand = "0.8.4" # Used for managing database migrations refinery = { version = "0.8.4", features = ["tokio-postgres"] } # Used for making async HTTP requests -reqwest = { version = "0.11.10", features = ["json", "multipart"] } +reqwest = { version = "0.11.10", features = ["json", "multipart", "socks"] } # Used for working with RSA keys rsa = "0.5.0" pem = "1.0.2" diff --git a/contrib/mitra_config.yaml b/contrib/mitra_config.yaml index 7f21438..7e68aa1 100644 --- a/contrib/mitra_config.yaml +++ b/contrib/mitra_config.yaml @@ -23,6 +23,9 @@ registrations_open: false #post_character_limit: 2000 +# Proxy for outgoing requests +#proxy_url: 'socks5h://127.0.0.1:9050' + # List of blocked domains #blocked_instances: [] diff --git a/src/activitypub/deliverer.rs b/src/activitypub/deliverer.rs index 370fc46..af7d7ee 100644 --- a/src/activitypub/deliverer.rs +++ b/src/activitypub/deliverer.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap}; use std::time::Duration; use actix_web::http::Method; +use reqwest::{Client, Proxy}; use rsa::RsaPrivateKey; use serde::Serialize; use tokio::time::sleep; @@ -33,6 +34,15 @@ pub enum DelivererError { HttpError(reqwest::StatusCode), } +fn build_client(instance: &Instance) -> reqwest::Result { + let mut client_builder = Client::builder(); + if let Some(ref proxy_url) = instance.proxy_url { + let proxy = Proxy::all(proxy_url)?; + client_builder = client_builder.proxy(proxy); + }; + client_builder.build() +} + async fn send_activity( instance: &Instance, actor_key: &RsaPrivateKey, @@ -48,7 +58,7 @@ async fn send_activity( actor_key_id, )?; - let client = reqwest::Client::new(); + let client = build_client(instance)?; let request = client.post(inbox_url) .header("Host", headers.host) .header("Date", headers.date) diff --git a/src/activitypub/fetcher/fetchers.rs b/src/activitypub/fetcher/fetchers.rs index 332d91e..0545040 100644 --- a/src/activitypub/fetcher/fetchers.rs +++ b/src/activitypub/fetcher/fetchers.rs @@ -1,7 +1,7 @@ use std::path::Path; use std::time::Duration; -use reqwest::{Client, Method}; +use reqwest::{Client, Method, Proxy}; use serde_json::Value; use crate::activitypub::activity::Object; @@ -33,9 +33,14 @@ pub enum FetchError { OtherError(&'static str), } -fn build_client() -> reqwest::Result { +fn build_client(instance: &Instance) -> reqwest::Result { + let mut client_builder = Client::builder(); let connect_timeout = Duration::from_secs(FETCHER_CONNECTION_TIMEOUT); - Client::builder() + if let Some(ref proxy_url) = instance.proxy_url { + let proxy = Proxy::all(proxy_url)?; + client_builder = client_builder.proxy(proxy); + }; + client_builder .connect_timeout(connect_timeout) .build() } @@ -46,7 +51,7 @@ async fn send_request( url: &str, query_params: &[(&str, &str)], ) -> Result { - let client = build_client()?; + let client = build_client(instance)?; let mut request_builder = client.get(url); if !query_params.is_empty() { request_builder = request_builder.query(query_params); @@ -83,10 +88,11 @@ async fn send_request( const FILE_MAX_SIZE: u64 = 1024 * 1024 * 20; pub async fn fetch_file( + instance: &Instance, url: &str, output_dir: &Path, ) -> Result<(String, Option), FetchError> { - let client = build_client()?; + let client = build_client(instance)?; let response = client.get(url).send().await?; if let Some(file_size) = response.content_length() { if file_size > FILE_MAX_SIZE { @@ -111,7 +117,7 @@ pub async fn perform_webfinger_query( guess_protocol(&actor_address.hostname), actor_address.hostname, ); - let client = build_client()?; + let client = build_client(instance)?; let mut request_builder = client.get(&webfinger_url); if !instance.is_private { // Public instance should set User-Agent header @@ -145,13 +151,14 @@ pub async fn fetch_actor( } pub async fn fetch_actor_images( + instance: &Instance, actor: &Actor, media_dir: &Path, default_avatar: Option, default_banner: Option, ) -> (Option, Option) { let maybe_avatar = if let Some(icon) = &actor.icon { - match fetch_file(&icon.url, media_dir).await { + match fetch_file(instance, &icon.url, media_dir).await { Ok((file_name, _)) => Some(file_name), Err(error) => { log::warn!("failed to fetch avatar ({})", error); @@ -162,7 +169,7 @@ pub async fn fetch_actor_images( None }; let maybe_banner = if let Some(image) = &actor.image { - match fetch_file(&image.url, media_dir).await { + match fetch_file(instance, &image.url, media_dir).await { Ok((file_name, _)) => Some(file_name), Err(error) => { log::warn!("failed to fetch banner ({})", error); diff --git a/src/activitypub/fetcher/helpers.rs b/src/activitypub/fetcher/helpers.rs index 7085ceb..4c3a86f 100644 --- a/src/activitypub/fetcher/helpers.rs +++ b/src/activitypub/fetcher/helpers.rs @@ -70,6 +70,7 @@ async fn create_remote_profile( return Err(ImportError::LocalObject); }; let (maybe_avatar, maybe_banner) = fetch_actor_images( + instance, &actor, media_dir, None, @@ -115,6 +116,7 @@ pub async fn get_or_import_profile_by_actor_id( log::info!("re-fetched profile {}", profile.acct); let profile_updated = update_remote_profile( db_client, + instance, media_dir, profile, actor, @@ -143,6 +145,7 @@ pub async fn get_or_import_profile_by_actor_id( log::info!("re-fetched profile {}", profile.acct); let profile_updated = update_remote_profile( db_client, + instance, media_dir, profile, actor, diff --git a/src/activitypub/handlers/create_note.rs b/src/activitypub/handlers/create_note.rs index bea3798..f66b22f 100644 --- a/src/activitypub/handlers/create_note.rs +++ b/src/activitypub/handlers/create_note.rs @@ -161,7 +161,11 @@ pub async fn handle_note( }; let attachment_url = attachment.url .ok_or(ValidationError("attachment URL is missing"))?; - let (file_name, media_type) = fetch_file(&attachment_url, media_dir).await + let (file_name, media_type) = fetch_file( + instance, + &attachment_url, + media_dir, + ).await .map_err(|err| { log::warn!("{}", err); ValidationError("failed to fetch attachment") diff --git a/src/activitypub/handlers/update_person.rs b/src/activitypub/handlers/update_person.rs index 99f9d61..7d03593 100644 --- a/src/activitypub/handlers/update_person.rs +++ b/src/activitypub/handlers/update_person.rs @@ -9,6 +9,7 @@ use crate::activitypub::{ fetcher::helpers::ImportError, vocabulary::PERSON, }; +use crate::config::{Config, Instance}; use crate::errors::ValidationError; use crate::models::profiles::queries::{ get_profile_by_remote_actor_id, @@ -18,8 +19,8 @@ use crate::models::profiles::types::{DbActorProfile, ProfileUpdateData}; use super::HandlerResult; pub async fn handle_update_person( + config: &Config, db_client: &impl GenericClient, - media_dir: &Path, activity: Activity, ) -> HandlerResult { let actor: Actor = serde_json::from_value(activity.object) @@ -31,13 +32,20 @@ pub async fn handle_update_person( db_client, &actor.id, ).await?; - update_remote_profile(db_client, media_dir, profile, actor).await?; + update_remote_profile( + db_client, + &config.instance(), + &config.media_dir(), + profile, + actor, + ).await?; Ok(Some(PERSON)) } /// Updates remote actor's profile pub async fn update_remote_profile( db_client: &impl GenericClient, + instance: &Instance, media_dir: &Path, profile: DbActorProfile, actor: Actor, @@ -58,6 +66,7 @@ pub async fn update_remote_profile( ); }; let (maybe_avatar, maybe_banner) = fetch_actor_images( + instance, &actor, media_dir, profile.avatar_file_name, diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index af8b54a..c820188 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -195,7 +195,7 @@ pub async fn receive_activity( }, (UPDATE, PERSON) => { require_actor_signature(&activity.actor, &signer_id)?; - handle_update_person(db_client, &config.media_dir(), activity).await? + handle_update_person(config, db_client, activity).await? }, (ADD, _) => { require_actor_signature(&activity.actor, &signer_id)?; diff --git a/src/cli.rs b/src/cli.rs index e871798..6e1b7a4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -135,7 +135,13 @@ impl RefetchActor { &self.id, ).await?; let actor = fetch_actor(&config.instance(), &self.id).await?; - update_remote_profile(db_client, &config.media_dir(), profile, actor).await?; + update_remote_profile( + db_client, + &config.instance(), + &config.media_dir(), + profile, + actor, + ).await?; println!("profile updated"); Ok(()) } diff --git a/src/config/main.rs b/src/config/main.rs index edec225..96501af 100644 --- a/src/config/main.rs +++ b/src/config/main.rs @@ -98,6 +98,8 @@ pub struct Config { #[serde(default = "default_post_character_limit")] pub post_character_limit: usize, + proxy_url: Option, + #[serde(default)] pub blocked_instances: Vec, @@ -129,6 +131,7 @@ impl Config { _url: self.try_instance_url().unwrap(), _version: self.version.clone(), actor_key: self.instance_rsa_key.clone().unwrap(), + proxy_url: self.proxy_url.clone(), is_private: matches!(self.environment, Environment::Development), } } @@ -160,6 +163,8 @@ pub struct Instance { _version: String, // Instance actor pub actor_key: RsaPrivateKey, + // Proxy for outgoing requests + pub proxy_url: Option, // Private instance won't send signed HTTP requests pub is_private: bool, } @@ -171,6 +176,7 @@ impl Instance { _url: url, _version: "0.0.0".to_string(), actor_key, + proxy_url: None, is_private: true, } } @@ -289,6 +295,7 @@ mod tests { _url: instance_url, _version: "1.0.0".to_string(), actor_key: instance_rsa_key, + proxy_url: None, is_private: true, }; @@ -305,6 +312,7 @@ mod tests { _url: instance_url, _version: "1.0.0".to_string(), actor_key: instance_rsa_key, + proxy_url: None, is_private: true, }; diff --git a/src/utils/urls.rs b/src/utils/urls.rs index 9c5c7ed..5b1cf5d 100644 --- a/src/utils/urls.rs +++ b/src/utils/urls.rs @@ -17,7 +17,7 @@ pub fn guess_protocol(hostname: &str) -> &'static str { let maybe_ipv6_address = hostname.parse::(); if let Ok(ipv6_address) = maybe_ipv6_address { let prefix = ipv6_address.segments()[0]; - if prefix >= 0x0200 && prefix <= 0x03ff { + if (0x0200..=0x03ff).contains(&prefix) { // Yggdrasil return "http"; };