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

View file

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

View file

@ -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: []

View file

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

View file

@ -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<Client> {
fn build_client(instance: &Instance) -> reqwest::Result<Client> {
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<String, FetchError> {
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<String>), 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<String>,
default_banner: Option<String>,
) -> (Option<String>, Option<String>) {
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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,6 +98,8 @@ pub struct Config {
#[serde(default = "default_post_character_limit")]
pub post_character_limit: usize,
proxy_url: Option<String>,
#[serde(default)]
pub blocked_instances: Vec<String>,
@ -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<String>,
// 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,
};

View file

@ -17,7 +17,7 @@ pub fn guess_protocol(hostname: &str) -> &'static str {
let maybe_ipv6_address = hostname.parse::<Ipv6Addr>();
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";
};