Add support for SOCKS proxy
This commit is contained in:
parent
36730be03b
commit
f92428e509
12 changed files with 85 additions and 16 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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: []
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue