redlib-patches/0002-add-support-for-http-proxy-and-socks-proxy.patch

549 lines
20 KiB
Diff

From a168bb21ab1205c4005092d627c5ac4c50ed29f8 Mon Sep 17 00:00:00 2001
From: Raffael Rehberger <raffael@rtrace.io>
Date: Fri, 1 Aug 2025 11:05:51 +0200
Subject: [PATCH 1/3] install tokio-socks latest
---
Cargo.lock | 19 +++++++++++++++++++
Cargo.toml | 1 +
2 files changed, 20 insertions(+)
diff --git a/Cargo.lock b/Cargo.lock
index 1d881f80..31f4e914 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -508,6 +508,12 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -1397,6 +1403,7 @@ dependencies = [
"tegen",
"time",
"tokio",
+ "tokio-socks",
"toml",
"url",
"uuid",
@@ -2095,6 +2102,18 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "tokio-socks"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
+dependencies = [
+ "either",
+ "futures-util",
+ "thiserror 1.0.69",
+ "tokio",
+]
+
[[package]]
name = "tokio-util"
version = "0.7.13"
diff --git a/Cargo.toml b/Cargo.toml
index 0596bc29..488ee110 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -56,6 +56,7 @@ htmlescape = "0.3.1"
bincode = "1.3.3"
base2048 = "2.0.2"
revision = "0.10.0"
+tokio-socks = "0.5.2"
[dev-dependencies]
From e0800bd6d07094dcd1cdb5156fa81852d6f0055a Mon Sep 17 00:00:00 2001
From: Raffael Rehberger <raffael@rtrace.io>
Date: Fri, 1 Aug 2025 11:06:37 +0200
Subject: [PATCH 2/3] add proxyConnector handling HTTPS_PROXY and SOCKS_PROXY
with (optional) credentials
---
src/client.rs | 14 +++--
src/lib.rs | 1 +
src/proxy.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 175 insertions(+), 4 deletions(-)
create mode 100644 src/proxy.rs
diff --git a/src/client.rs b/src/client.rs
index 76369cad..e71b220a 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -2,7 +2,6 @@ use arc_swap::ArcSwap;
use cached::proc_macro::cached;
use futures_lite::future::block_on;
use futures_lite::{future::Boxed, FutureExt};
-use hyper::client::HttpConnector;
use hyper::header::HeaderValue;
use hyper::{body, body::Buf, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector;
@@ -30,10 +29,16 @@ const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
-pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<HttpConnector>> =
- Lazy::new(|| hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http2().build());
+pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<ProxyConnector>> = Lazy::new(|| {
+ let proxy_connector = ProxyConnector::new();
+ hyper_rustls::HttpsConnectorBuilder::new()
+ .with_native_roots()
+ .https_only()
+ .enable_http2()
+ .wrap_connector(proxy_connector)
+});
-pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
+pub static CLIENT: Lazy<Client<HttpsConnector<ProxyConnector>>> = Lazy::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::new());
@@ -509,6 +514,7 @@ pub async fn rate_limit_check() -> Result<(), String> {
#[cfg(test)]
use {crate::config::get_setting, sealed_test::prelude::*};
+use crate::proxy::ProxyConnector;
#[tokio::test(flavor = "multi_thread")]
async fn test_rate_limit_check() {
diff --git a/src/lib.rs b/src/lib.rs
index b8eb17e7..45425307 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,3 +11,4 @@ pub mod settings;
pub mod subreddit;
pub mod user;
pub mod utils;
+mod proxy;
diff --git a/src/proxy.rs b/src/proxy.rs
new file mode 100644
index 00000000..79d24276
--- /dev/null
+++ b/src/proxy.rs
@@ -0,0 +1,164 @@
+use hyper::client::HttpConnector;
+use hyper::service::Service;
+use hyper::Uri;
+use std::env;
+use std::error::Error;
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+use tokio::net::TcpStream;
+use tokio_socks::tcp::Socks5Stream;
+use log::debug;
+use std::fmt;
+use base64::Engine;
+use base64::engine::general_purpose;
+
+#[derive(Clone)]
+pub enum ProxyConnector {
+ NoProxy(HttpConnector),
+ Socks(String),
+ Http(String),
+}
+
+#[derive(Debug)]
+pub struct ProxyError(String);
+
+impl fmt::Display for ProxyError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "Proxy error: {}", self.0)
+ }
+}
+
+impl Error for ProxyError {}
+
+impl Service<Uri> for ProxyConnector {
+ type Response = TcpStream;
+ type Error = Box<dyn Error + Send + Sync>;
+ type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
+
+ fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+ match self {
+ ProxyConnector::NoProxy(connector) => connector.poll_ready(cx).map_err(Into::into),
+ _ => Poll::Ready(Ok(()))
+ }
+ }
+
+ fn call(&mut self, uri: Uri) -> Self::Future {
+ let this = self.clone();
+ Box::pin(async move {
+ match this {
+ ProxyConnector::NoProxy(mut connector) => {
+ let stream = connector.call(uri).await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
+ Ok(stream)
+ }
+ ProxyConnector::Socks(proxy_addr) => {
+ let (host, port, credentials) = parse_proxy_addr(&proxy_addr)?;
+ let target_addr = get_target_addr(&uri)?;
+
+ let stream = match credentials {
+ Some((username, password)) => {
+ Socks5Stream::connect_with_password(
+ (host.as_str(), port),
+ target_addr,
+ &username,
+ &password
+ ).await
+ },
+ None => {
+ Socks5Stream::connect(
+ (host.as_str(), port),
+ target_addr
+ ).await
+ }
+ }.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
+
+ Ok(stream.into_inner())
+
+ }
+ ProxyConnector::Http(proxy_addr) => {
+ let (host, port, credentials) = parse_proxy_addr(&proxy_addr)?;
+ let proxy_stream = TcpStream::connect((host.as_str(), port)).await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
+
+ let target_addr = get_target_addr(&uri)?;
+ let mut connect_req = format!(
+ "CONNECT {target_addr} HTTP/1.1\r\n\
+ Host: {target_addr}\r\n\
+ Connection: keep-alive\r\n"
+ );
+
+ if let Some((username, password)) = credentials {
+ let auth = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
+ connect_req.push_str(&format!("Proxy-Authorization: Basic {}\r\n", auth));
+ }
+
+ connect_req.push_str("\r\n");
+ proxy_stream.writable().await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
+ proxy_stream.try_write(connect_req.as_bytes()).map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
+
+ let mut response = [0u8; 1024];
+ proxy_stream.readable().await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
+ let n = proxy_stream.try_read(&mut response).map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
+
+ let response = String::from_utf8_lossy(&response[..n]);
+ if !response.starts_with("HTTP/1.1 200") {
+ return Err(Box::new(ProxyError(format!("Proxy CONNECT failed: {}", response))) as Box<dyn Error + Send + Sync>);
+ }
+
+ Ok(proxy_stream)
+ }
+ }
+ })
+ }
+}
+
+impl ProxyConnector {
+ pub fn new() -> Self {
+ if let Ok(socks_proxy) = env::var("SOCKS_PROXY") {
+ debug!("Using SOCKS proxy: {}", socks_proxy);
+ return ProxyConnector::Socks(socks_proxy);
+ }
+
+ if let Ok(http_proxy) = env::var("HTTP_PROXY").or_else(|_| env::var("HTTPS_PROXY")) {
+ debug!("Using HTTP proxy: {}", http_proxy);
+ return ProxyConnector::Http(http_proxy);
+ }
+
+ let mut connector = HttpConnector::new();
+ connector.enforce_http(false);
+ ProxyConnector::NoProxy(connector)
+ }
+}
+
+fn parse_proxy_addr(addr: &str) -> Result<(String, u16, Option<(String, String)>), Box<dyn Error + Send + Sync>> {
+ let uri: Uri = addr.parse()?;
+ let host = uri.host().ok_or("Missing proxy host")?.to_string();
+ let port = uri.port_u16().unwrap_or(if uri.scheme_str() == Some("https") { 443 } else { 80 });
+
+ let credentials = if let Some(authority) = uri.authority() {
+ if let Some(credentials) = authority.as_str().split('@').next() {
+ if credentials != authority.as_str() {
+ let creds: Vec<&str> = credentials.split(':').collect();
+ if creds.len() == 2 {
+ Some((creds[0].to_string(), creds[1].to_string()))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ Ok((host, port, credentials))
+}
+
+
+fn get_target_addr(uri: &Uri) -> Result<String, Box<dyn Error + Send + Sync>> {
+ let host = uri.host().ok_or("Missing target host")?;
+ let port = uri.port_u16().unwrap_or(if uri.scheme_str() == Some("https") { 443 } else { 80 });
+ Ok(format!("{}:{}", host, port))
+}
\ No newline at end of file
From 3dda94cfc7c41eab47d5040e84155ad2b3bc6123 Mon Sep 17 00:00:00 2001
From: Raffael Rehberger <raffael@rtrace.io>
Date: Fri, 1 Aug 2025 11:15:06 +0200
Subject: [PATCH 3/3] refactor proxy to use less inline code
---
src/proxy.rs | 187 +++++++++++++++++++++++++++------------------------
1 file changed, 100 insertions(+), 87 deletions(-)
diff --git a/src/proxy.rs b/src/proxy.rs
index 79d24276..4d242bda 100644
--- a/src/proxy.rs
+++ b/src/proxy.rs
@@ -1,17 +1,21 @@
+use base64::engine::general_purpose;
+use base64::Engine;
use hyper::client::HttpConnector;
use hyper::service::Service;
use hyper::Uri;
+use log::debug;
use std::env;
use std::error::Error;
+use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::net::TcpStream;
use tokio_socks::tcp::Socks5Stream;
-use log::debug;
-use std::fmt;
-use base64::Engine;
-use base64::engine::general_purpose;
+
+type BoxError = Box<dyn Error + Send + Sync>;
+type BoxFuture<T> = Pin<Box<dyn Future<Output = Result<T, BoxError>> + Send>>;
+type Credentials = (String, String);
#[derive(Clone)]
pub enum ProxyConnector {
@@ -33,13 +37,13 @@ impl Error for ProxyError {}
impl Service<Uri> for ProxyConnector {
type Response = TcpStream;
- type Error = Box<dyn Error + Send + Sync>;
- type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
+ type Error = BoxError;
+ type Future = BoxFuture<Self::Response>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self {
ProxyConnector::NoProxy(connector) => connector.poll_ready(cx).map_err(Into::into),
- _ => Poll::Ready(Ok(()))
+ _ => Poll::Ready(Ok(())),
}
}
@@ -48,64 +52,10 @@ impl Service<Uri> for ProxyConnector {
Box::pin(async move {
match this {
ProxyConnector::NoProxy(mut connector) => {
- let stream = connector.call(uri).await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
- Ok(stream)
- }
- ProxyConnector::Socks(proxy_addr) => {
- let (host, port, credentials) = parse_proxy_addr(&proxy_addr)?;
- let target_addr = get_target_addr(&uri)?;
-
- let stream = match credentials {
- Some((username, password)) => {
- Socks5Stream::connect_with_password(
- (host.as_str(), port),
- target_addr,
- &username,
- &password
- ).await
- },
- None => {
- Socks5Stream::connect(
- (host.as_str(), port),
- target_addr
- ).await
- }
- }.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
-
- Ok(stream.into_inner())
-
- }
- ProxyConnector::Http(proxy_addr) => {
- let (host, port, credentials) = parse_proxy_addr(&proxy_addr)?;
- let proxy_stream = TcpStream::connect((host.as_str(), port)).await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
-
- let target_addr = get_target_addr(&uri)?;
- let mut connect_req = format!(
- "CONNECT {target_addr} HTTP/1.1\r\n\
- Host: {target_addr}\r\n\
- Connection: keep-alive\r\n"
- );
-
- if let Some((username, password)) = credentials {
- let auth = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
- connect_req.push_str(&format!("Proxy-Authorization: Basic {}\r\n", auth));
- }
-
- connect_req.push_str("\r\n");
- proxy_stream.writable().await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
- proxy_stream.try_write(connect_req.as_bytes()).map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
-
- let mut response = [0u8; 1024];
- proxy_stream.readable().await.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
- let n = proxy_stream.try_read(&mut response).map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
-
- let response = String::from_utf8_lossy(&response[..n]);
- if !response.starts_with("HTTP/1.1 200") {
- return Err(Box::new(ProxyError(format!("Proxy CONNECT failed: {}", response))) as Box<dyn Error + Send + Sync>);
- }
-
- Ok(proxy_stream)
+ connector.call(uri).await.map_err(Into::into)
}
+ ProxyConnector::Socks(proxy_addr) => handle_socks_connection(&proxy_addr, &uri).await,
+ ProxyConnector::Http(proxy_addr) => handle_http_connection(&proxy_addr, &uri).await,
}
})
}
@@ -129,36 +79,99 @@ impl ProxyConnector {
}
}
-fn parse_proxy_addr(addr: &str) -> Result<(String, u16, Option<(String, String)>), Box<dyn Error + Send + Sync>> {
+async fn handle_socks_connection(proxy_addr: &str, uri: &Uri) -> Result<TcpStream, BoxError> {
+ let (host, port, credentials) = parse_proxy_addr(proxy_addr)?;
+ let target_addr = get_target_addr(uri)?;
+
+ let stream = match credentials {
+ Some((username, password)) => {
+ Socks5Stream::connect_with_password((host.as_str(), port), target_addr, &username, &password).await
+ }
+ None => Socks5Stream::connect((host.as_str(), port), target_addr).await,
+ }?;
+
+ Ok(stream.into_inner())
+}
+
+async fn handle_http_connection(proxy_addr: &str, uri: &Uri) -> Result<TcpStream, BoxError> {
+ let (host, port, credentials) = parse_proxy_addr(proxy_addr)?;
+ let proxy_stream = TcpStream::connect((host.as_str(), port)).await?;
+ let target_addr = get_target_addr(uri)?;
+
+ let connect_req = build_connect_request(&target_addr, credentials)?;
+ write_and_verify_connection(&proxy_stream, &connect_req).await?;
+
+ Ok(proxy_stream)
+}
+
+fn build_connect_request(target_addr: &str, credentials: Option<Credentials>) -> Result<String, BoxError> {
+ let mut req = format!(
+ "CONNECT {target_addr} HTTP/1.1\r\n\
+ Host: {target_addr}\r\n\
+ Connection: keep-alive\r\n"
+ );
+
+ if let Some((username, password)) = credentials {
+ let auth = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
+ req.push_str(&format!("Proxy-Authorization: Basic {}\r\n", auth));
+ }
+
+ req.push_str("\r\n");
+ Ok(req)
+}
+
+async fn write_and_verify_connection(proxy_stream: &TcpStream, connect_req: &str) -> Result<(), BoxError> {
+ proxy_stream.writable().await?;
+ proxy_stream.try_write(connect_req.as_bytes())?;
+
+ let mut response = [0u8; 1024];
+ proxy_stream.readable().await?;
+ let n = proxy_stream.try_read(&mut response)?;
+
+ let response = String::from_utf8_lossy(&response[..n]);
+ if !response.starts_with("HTTP/1.1 200") {
+ return Err(Box::new(ProxyError(format!("Proxy CONNECT failed: {}", response))));
+ }
+
+ Ok(())
+}
+
+fn parse_proxy_addr(addr: &str) -> Result<(String, u16, Option<Credentials>), BoxError> {
let uri: Uri = addr.parse()?;
let host = uri.host().ok_or("Missing proxy host")?.to_string();
- let port = uri.port_u16().unwrap_or(if uri.scheme_str() == Some("https") { 443 } else { 80 });
-
- let credentials = if let Some(authority) = uri.authority() {
- if let Some(credentials) = authority.as_str().split('@').next() {
- if credentials != authority.as_str() {
- let creds: Vec<&str> = credentials.split(':').collect();
- if creds.len() == 2 {
- Some((creds[0].to_string(), creds[1].to_string()))
- } else {
- None
- }
- } else {
- None
- }
- } else {
- None
- }
- } else {
- None
- };
+ let port = uri.port_u16().unwrap_or_else(|| {
+ if uri.scheme_str() == Some("https") { 443 } else { 80 }
+ });
+ let credentials = extract_credentials(uri.authority())?;
Ok((host, port, credentials))
}
+fn extract_credentials(authority: Option<&hyper::http::uri::Authority>) -> Result<Option<Credentials>, BoxError> {
+ let Some(authority) = authority else {
+ return Ok(None);
+ };
+
+ let Some(credentials) = authority.as_str().split('@').next() else {
+ return Ok(None);
+ };
+
+ if credentials == authority.as_str() {
+ return Ok(None);
+ }
+
+ let creds: Vec<&str> = credentials.split(':').collect();
+ if creds.len() == 2 {
+ Ok(Some((creds[0].to_string(), creds[1].to_string())))
+ } else {
+ Ok(None)
+ }
+}
-fn get_target_addr(uri: &Uri) -> Result<String, Box<dyn Error + Send + Sync>> {
+fn get_target_addr(uri: &Uri) -> Result<String, BoxError> {
let host = uri.host().ok_or("Missing target host")?;
- let port = uri.port_u16().unwrap_or(if uri.scheme_str() == Some("https") { 443 } else { 80 });
+ let port = uri.port_u16().unwrap_or_else(|| {
+ if uri.scheme_str() == Some("https") { 443 } else { 80 }
+ });
Ok(format!("{}:{}", host, port))
}
\ No newline at end of file