Add support for SOCKS proxy

This commit is contained in:
silverpill 2022-10-19 18:39:47 +00:00
parent 36730be03b
commit f92428e509
12 changed files with 85 additions and 16 deletions

19
Cargo.lock generated
View file

@ -792,6 +792,12 @@ dependencies = [
"signature", "signature",
] ]
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]] [[package]]
name = "elliptic-curve" name = "elliptic-curve"
version = "0.10.6" version = "0.10.6"
@ -2674,6 +2680,7 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-socks",
"url 2.2.2", "url 2.2.2",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@ -3280,6 +3287,18 @@ dependencies = [
"tokio-util", "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]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.1" version = "0.7.1"

View file

@ -51,7 +51,7 @@ rand = "0.8.4"
# Used for managing database migrations # Used for managing database migrations
refinery = { version = "0.8.4", features = ["tokio-postgres"] } refinery = { version = "0.8.4", features = ["tokio-postgres"] }
# Used for making async HTTP requests # 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 # Used for working with RSA keys
rsa = "0.5.0" rsa = "0.5.0"
pem = "1.0.2" pem = "1.0.2"

View file

@ -23,6 +23,9 @@ registrations_open: false
#post_character_limit: 2000 #post_character_limit: 2000
# Proxy for outgoing requests
#proxy_url: 'socks5h://127.0.0.1:9050'
# List of blocked domains # List of blocked domains
#blocked_instances: [] #blocked_instances: []

View file

@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap};
use std::time::Duration; use std::time::Duration;
use actix_web::http::Method; use actix_web::http::Method;
use reqwest::{Client, Proxy};
use rsa::RsaPrivateKey; use rsa::RsaPrivateKey;
use serde::Serialize; use serde::Serialize;
use tokio::time::sleep; use tokio::time::sleep;
@ -33,6 +34,15 @@ pub enum DelivererError {
HttpError(reqwest::StatusCode), HttpError(reqwest::StatusCode),
} }
fn build_client(instance: &Instance) -> reqwest::Result<Client> {
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( async fn send_activity(
instance: &Instance, instance: &Instance,
actor_key: &RsaPrivateKey, actor_key: &RsaPrivateKey,
@ -48,7 +58,7 @@ async fn send_activity(
actor_key_id, actor_key_id,
)?; )?;
let client = reqwest::Client::new(); let client = build_client(instance)?;
let request = client.post(inbox_url) let request = client.post(inbox_url)
.header("Host", headers.host) .header("Host", headers.host)
.header("Date", headers.date) .header("Date", headers.date)

View file

@ -1,7 +1,7 @@
use std::path::Path; use std::path::Path;
use std::time::Duration; use std::time::Duration;
use reqwest::{Client, Method}; use reqwest::{Client, Method, Proxy};
use serde_json::Value; use serde_json::Value;
use crate::activitypub::activity::Object; use crate::activitypub::activity::Object;
@ -33,9 +33,14 @@ pub enum FetchError {
OtherError(&'static str), OtherError(&'static str),
} }
fn build_client() -> reqwest::Result<Client> { fn build_client(instance: &Instance) -> reqwest::Result<Client> {
let mut client_builder = Client::builder();
let connect_timeout = Duration::from_secs(FETCHER_CONNECTION_TIMEOUT); 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) .connect_timeout(connect_timeout)
.build() .build()
} }
@ -46,7 +51,7 @@ async fn send_request(
url: &str, url: &str,
query_params: &[(&str, &str)], query_params: &[(&str, &str)],
) -> Result<String, FetchError> { ) -> Result<String, FetchError> {
let client = build_client()?; let client = build_client(instance)?;
let mut request_builder = client.get(url); let mut request_builder = client.get(url);
if !query_params.is_empty() { if !query_params.is_empty() {
request_builder = request_builder.query(query_params); request_builder = request_builder.query(query_params);
@ -83,10 +88,11 @@ async fn send_request(
const FILE_MAX_SIZE: u64 = 1024 * 1024 * 20; const FILE_MAX_SIZE: u64 = 1024 * 1024 * 20;
pub async fn fetch_file( pub async fn fetch_file(
instance: &Instance,
url: &str, url: &str,
output_dir: &Path, output_dir: &Path,
) -> Result<(String, Option<String>), FetchError> { ) -> Result<(String, Option<String>), FetchError> {
let client = build_client()?; let client = build_client(instance)?;
let response = client.get(url).send().await?; let response = client.get(url).send().await?;
if let Some(file_size) = response.content_length() { if let Some(file_size) = response.content_length() {
if file_size > FILE_MAX_SIZE { if file_size > FILE_MAX_SIZE {
@ -111,7 +117,7 @@ pub async fn perform_webfinger_query(
guess_protocol(&actor_address.hostname), guess_protocol(&actor_address.hostname),
actor_address.hostname, actor_address.hostname,
); );
let client = build_client()?; let client = build_client(instance)?;
let mut request_builder = client.get(&webfinger_url); let mut request_builder = client.get(&webfinger_url);
if !instance.is_private { if !instance.is_private {
// Public instance should set User-Agent header // Public instance should set User-Agent header
@ -145,13 +151,14 @@ pub async fn fetch_actor(
} }
pub async fn fetch_actor_images( pub async fn fetch_actor_images(
instance: &Instance,
actor: &Actor, actor: &Actor,
media_dir: &Path, media_dir: &Path,
default_avatar: Option<String>, default_avatar: Option<String>,
default_banner: Option<String>, default_banner: Option<String>,
) -> (Option<String>, Option<String>) { ) -> (Option<String>, Option<String>) {
let maybe_avatar = if let Some(icon) = &actor.icon { 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), Ok((file_name, _)) => Some(file_name),
Err(error) => { Err(error) => {
log::warn!("failed to fetch avatar ({})", error); log::warn!("failed to fetch avatar ({})", error);
@ -162,7 +169,7 @@ pub async fn fetch_actor_images(
None None
}; };
let maybe_banner = if let Some(image) = &actor.image { 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), Ok((file_name, _)) => Some(file_name),
Err(error) => { Err(error) => {
log::warn!("failed to fetch banner ({})", error); log::warn!("failed to fetch banner ({})", error);

View file

@ -70,6 +70,7 @@ async fn create_remote_profile(
return Err(ImportError::LocalObject); return Err(ImportError::LocalObject);
}; };
let (maybe_avatar, maybe_banner) = fetch_actor_images( let (maybe_avatar, maybe_banner) = fetch_actor_images(
instance,
&actor, &actor,
media_dir, media_dir,
None, None,
@ -115,6 +116,7 @@ pub async fn get_or_import_profile_by_actor_id(
log::info!("re-fetched profile {}", profile.acct); log::info!("re-fetched profile {}", profile.acct);
let profile_updated = update_remote_profile( let profile_updated = update_remote_profile(
db_client, db_client,
instance,
media_dir, media_dir,
profile, profile,
actor, actor,
@ -143,6 +145,7 @@ pub async fn get_or_import_profile_by_actor_id(
log::info!("re-fetched profile {}", profile.acct); log::info!("re-fetched profile {}", profile.acct);
let profile_updated = update_remote_profile( let profile_updated = update_remote_profile(
db_client, db_client,
instance,
media_dir, media_dir,
profile, profile,
actor, actor,

View file

@ -161,7 +161,11 @@ pub async fn handle_note(
}; };
let attachment_url = attachment.url let attachment_url = attachment.url
.ok_or(ValidationError("attachment URL is missing"))?; .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| { .map_err(|err| {
log::warn!("{}", err); log::warn!("{}", err);
ValidationError("failed to fetch attachment") ValidationError("failed to fetch attachment")

View file

@ -9,6 +9,7 @@ use crate::activitypub::{
fetcher::helpers::ImportError, fetcher::helpers::ImportError,
vocabulary::PERSON, vocabulary::PERSON,
}; };
use crate::config::{Config, Instance};
use crate::errors::ValidationError; use crate::errors::ValidationError;
use crate::models::profiles::queries::{ use crate::models::profiles::queries::{
get_profile_by_remote_actor_id, get_profile_by_remote_actor_id,
@ -18,8 +19,8 @@ use crate::models::profiles::types::{DbActorProfile, ProfileUpdateData};
use super::HandlerResult; use super::HandlerResult;
pub async fn handle_update_person( pub async fn handle_update_person(
config: &Config,
db_client: &impl GenericClient, db_client: &impl GenericClient,
media_dir: &Path,
activity: Activity, activity: Activity,
) -> HandlerResult { ) -> HandlerResult {
let actor: Actor = serde_json::from_value(activity.object) let actor: Actor = serde_json::from_value(activity.object)
@ -31,13 +32,20 @@ pub async fn handle_update_person(
db_client, db_client,
&actor.id, &actor.id,
).await?; ).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)) Ok(Some(PERSON))
} }
/// Updates remote actor's profile /// Updates remote actor's profile
pub async fn update_remote_profile( pub async fn update_remote_profile(
db_client: &impl GenericClient, db_client: &impl GenericClient,
instance: &Instance,
media_dir: &Path, media_dir: &Path,
profile: DbActorProfile, profile: DbActorProfile,
actor: Actor, actor: Actor,
@ -58,6 +66,7 @@ pub async fn update_remote_profile(
); );
}; };
let (maybe_avatar, maybe_banner) = fetch_actor_images( let (maybe_avatar, maybe_banner) = fetch_actor_images(
instance,
&actor, &actor,
media_dir, media_dir,
profile.avatar_file_name, profile.avatar_file_name,

View file

@ -195,7 +195,7 @@ pub async fn receive_activity(
}, },
(UPDATE, PERSON) => { (UPDATE, PERSON) => {
require_actor_signature(&activity.actor, &signer_id)?; 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, _) => { (ADD, _) => {
require_actor_signature(&activity.actor, &signer_id)?; require_actor_signature(&activity.actor, &signer_id)?;

View file

@ -135,7 +135,13 @@ impl RefetchActor {
&self.id, &self.id,
).await?; ).await?;
let actor = fetch_actor(&config.instance(), &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"); println!("profile updated");
Ok(()) Ok(())
} }

View file

@ -98,6 +98,8 @@ pub struct Config {
#[serde(default = "default_post_character_limit")] #[serde(default = "default_post_character_limit")]
pub post_character_limit: usize, pub post_character_limit: usize,
proxy_url: Option<String>,
#[serde(default)] #[serde(default)]
pub blocked_instances: Vec<String>, pub blocked_instances: Vec<String>,
@ -129,6 +131,7 @@ impl Config {
_url: self.try_instance_url().unwrap(), _url: self.try_instance_url().unwrap(),
_version: self.version.clone(), _version: self.version.clone(),
actor_key: self.instance_rsa_key.clone().unwrap(), actor_key: self.instance_rsa_key.clone().unwrap(),
proxy_url: self.proxy_url.clone(),
is_private: matches!(self.environment, Environment::Development), is_private: matches!(self.environment, Environment::Development),
} }
} }
@ -160,6 +163,8 @@ pub struct Instance {
_version: String, _version: String,
// Instance actor // Instance actor
pub actor_key: RsaPrivateKey, pub actor_key: RsaPrivateKey,
// Proxy for outgoing requests
pub proxy_url: Option<String>,
// Private instance won't send signed HTTP requests // Private instance won't send signed HTTP requests
pub is_private: bool, pub is_private: bool,
} }
@ -171,6 +176,7 @@ impl Instance {
_url: url, _url: url,
_version: "0.0.0".to_string(), _version: "0.0.0".to_string(),
actor_key, actor_key,
proxy_url: None,
is_private: true, is_private: true,
} }
} }
@ -289,6 +295,7 @@ mod tests {
_url: instance_url, _url: instance_url,
_version: "1.0.0".to_string(), _version: "1.0.0".to_string(),
actor_key: instance_rsa_key, actor_key: instance_rsa_key,
proxy_url: None,
is_private: true, is_private: true,
}; };
@ -305,6 +312,7 @@ mod tests {
_url: instance_url, _url: instance_url,
_version: "1.0.0".to_string(), _version: "1.0.0".to_string(),
actor_key: instance_rsa_key, actor_key: instance_rsa_key,
proxy_url: None,
is_private: true, is_private: true,
}; };

View file

@ -17,7 +17,7 @@ pub fn guess_protocol(hostname: &str) -> &'static str {
let maybe_ipv6_address = hostname.parse::<Ipv6Addr>(); let maybe_ipv6_address = hostname.parse::<Ipv6Addr>();
if let Ok(ipv6_address) = maybe_ipv6_address { if let Ok(ipv6_address) = maybe_ipv6_address {
let prefix = ipv6_address.segments()[0]; let prefix = ipv6_address.segments()[0];
if prefix >= 0x0200 && prefix <= 0x03ff { if (0x0200..=0x03ff).contains(&prefix) {
// Yggdrasil // Yggdrasil
return "http"; return "http";
}; };