fix: Proxy does not support formats from proxy providers #307

This commit is contained in:
rustmailer
2026-06-26 18:20:19 +08:00
parent cdf27f2dd4
commit 8641bb4b56
6 changed files with 383 additions and 42 deletions
+20 -2
View File
@@ -20,6 +20,7 @@ use crate::error::code::ErrorCode;
use crate::error::BichonResult;
use crate::oauth2::{entity::OAuth2, pending::OAuth2PendingEntity, token::OAuth2AccessToken};
use crate::settings::proxy::Proxy;
use crate::utils::net::parse_proxy_url;
use crate::{decrypt, encrypt, raise_error};
use oauth2::{
basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken,
@@ -265,13 +266,30 @@ impl OAuth2Flow {
fn build_http_client(use_proxy: Option<u64>) -> BichonResult<reqwest::Client> {
if let Some(proxy_id) = use_proxy {
let proxy = Proxy::get(proxy_id)?;
// Normalize the URL: reqwest only understands standard format user:pass@host:port.
// Our parse_proxy_url handles both standard and non-standard (host:port:user:pass).
let proxy_url = match parse_proxy_url(&proxy.url) {
Ok(addr) => {
if let (Some(user), Some(pass)) = (&addr.username, &addr.password) {
format!("socks5://{}:{}@{}:{}", user, pass, addr.host, addr.port)
} else if let Some(user) = &addr.username {
format!("socks5://{}@{}:{}", user, addr.host, addr.port)
} else {
format!("socks5://{}:{}", addr.host, addr.port)
}
}
Err(_) => {
// Fallback: pass through as-is for backward compatibility
proxy.url.clone()
}
};
return oauth2::reqwest::ClientBuilder::new()
.redirect(oauth2::reqwest::redirect::Policy::none())
.proxy(reqwest::Proxy::all(&proxy.url).map_err(|e| {
.proxy(reqwest::Proxy::all(&proxy_url).map_err(|e| {
raise_error!(
format!(
"Failed to configure SOCKS5 proxy ({}): {:#?}. Please check",
&proxy.url, e
&proxy_url, e
),
ErrorCode::InternalError
)
+12 -4
View File
@@ -26,7 +26,7 @@ use crate::{
},
error::{code::ErrorCode, BichonResult},
id, raise_error, utc_now,
utils::net::parse_proxy_addr,
utils::net::parse_proxy_url,
};
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
@@ -98,9 +98,9 @@ impl Proxy {
insert_impl(DB_MANAGER.db(), self.to_owned())
}
/// Validate that the URL is a valid SOCKS5 proxy URL.
/// Validate that the URL is a valid proxy URL.
pub fn validate(&self) -> BichonResult<()> {
parse_proxy_addr(&self.url)?;
parse_proxy_url(&self.url)?;
Ok(())
}
}
@@ -111,7 +111,15 @@ mod tests {
#[test]
fn test_valid_proxy_urls() {
let urls = vec!["socks5://127.0.0.1:1080", "http://127.0.0.1:8080"];
let urls = vec![
"socks5://127.0.0.1:1080",
"http://127.0.0.1:8080",
"socks5://proxy.example.com:1080",
"socks5://user:pass@proxy.example.com:1080",
"socks5://user@proxy.example.com:1080",
// Non-standard format: host:port:user:pass
"socks5://server.nodeprovider.com:8080:username123:passwordhere",
];
for url in urls {
let proxy = Proxy::new(url.to_string());
+199 -19
View File
@@ -32,6 +32,15 @@ use tracing::error;
pub(crate) const TIMEOUT: Duration = Duration::from_secs(30);
/// Parsed proxy address components.
#[derive(Debug, Clone)]
pub struct ProxyAddr {
pub host: String,
pub port: u16,
pub username: Option<String>,
pub password: Option<String>,
}
pub(crate) async fn establish_tcp_connection_with_timeout(
address: SocketAddr,
use_proxy: Option<u64>,
@@ -66,20 +75,27 @@ pub async fn establish_tls_connection(
Ok(tls_stream)
}
pub fn parse_proxy_addr(input: &str) -> BichonResult<SocketAddr> {
// Normalize and check protocol prefix
let (scheme, stripped) = if let Some(rest) = input
/// Parse a proxy URL into its components.
///
/// Supports two formats:
/// - **Standard**: `[scheme://][user:pass@]host:port`
/// - **Non-standard** (some proxy providers): `[scheme://]host:port:username:password`
///
/// The distinguishing feature is the `@` sign in the standard format.
pub fn parse_proxy_url(input: &str) -> BichonResult<ProxyAddr> {
// Normalize and strip scheme prefix
let stripped = if let Some(rest) = input
.strip_prefix("socks5://")
.or_else(|| input.strip_prefix("SOCKS5://"))
.or_else(|| input.strip_prefix("Socks5://"))
{
("socks5", rest)
rest
} else if let Some(rest) = input
.strip_prefix("http://")
.or_else(|| input.strip_prefix("HTTP://"))
.or_else(|| input.strip_prefix("Http://"))
{
("http", rest)
rest
} else {
return Err(raise_error!(
format!(
@@ -90,43 +106,207 @@ pub fn parse_proxy_addr(input: &str) -> BichonResult<SocketAddr> {
));
};
// Parse the remaining address
let addr = stripped.parse::<SocketAddr>().map_err(|e| {
raise_error!(
if stripped.is_empty() {
return Err(raise_error!(
"Proxy URL has empty address after scheme.".into(),
ErrorCode::InvalidParameter
));
}
// Check for standard format: user:pass@host:port
if let Some(at_pos) = stripped.rfind('@') {
let userinfo = &stripped[..at_pos];
let hostport = &stripped[at_pos + 1..];
let (username, password) = split_userinfo(userinfo)?;
let (host, port) = split_hostport(hostport)?;
return Ok(ProxyAddr {
host,
port,
username,
password,
});
}
// No '@' — check for non-standard format: host:port:user:pass
let parts: Vec<&str> = stripped.rsplitn(4, ':').collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>();
match parts.len() {
2 => {
// host:port, no auth
let (host, port) = split_hostport(stripped)?;
Ok(ProxyAddr {
host,
port,
username: None,
password: None,
})
}
4 => {
// Non-standard: host:port:username:password
let host = parts[0].to_string();
let port = parts[1]
.parse::<u16>()
.map_err(|_| {
raise_error!(
format!("Invalid port '{}' in proxy URL.", parts[1]),
ErrorCode::InvalidParameter
)
})?;
let username = parts[2].to_string();
let password = parts[3].to_string();
if host.is_empty() {
return Err(raise_error!(
"Empty hostname in proxy URL.".into(),
ErrorCode::InvalidParameter
));
}
if username.is_empty() {
return Err(raise_error!(
"Empty username in proxy URL.".into(),
ErrorCode::InvalidParameter
));
}
if password.is_empty() {
return Err(raise_error!(
"Empty password in proxy URL.".into(),
ErrorCode::InvalidParameter
));
}
Ok(ProxyAddr {
host,
port,
username: Some(username),
password: Some(password),
})
}
_ => Err(raise_error!(
format!(
"Failed to parse {} proxy address '{}': {}",
scheme, stripped, e
"Invalid proxy URL format '{}'. Expected '[scheme://][user:pass@]host:port' or 'scheme://host:port:user:pass'.",
input
),
ErrorCode::InvalidParameter
)),
}
}
/// Split "user:pass" into (Some(user), Some(pass)), or "user" into (Some(user), None).
fn split_userinfo(userinfo: &str) -> BichonResult<(Option<String>, Option<String>)> {
if userinfo.is_empty() {
return Ok((None, None));
}
if let Some(colon_pos) = userinfo.find(':') {
let user = &userinfo[..colon_pos];
let pass = &userinfo[colon_pos + 1..];
if user.is_empty() {
return Err(raise_error!(
"Empty username in proxy URL credentials.".into(),
ErrorCode::InvalidParameter
));
}
Ok((Some(user.to_string()), Some(pass.to_string())))
} else {
Ok((Some(userinfo.to_string()), None))
}
}
/// Split "host:port" into (host, port). Handles IPv6 addresses in brackets.
fn split_hostport(hostport: &str) -> BichonResult<(String, u16)> {
if hostport.is_empty() {
return Err(raise_error!(
"Empty host:port in proxy URL.".into(),
ErrorCode::InvalidParameter
));
}
// IPv6: [::1]:1080
if hostport.starts_with('[') {
let close_bracket = hostport.find(']').ok_or_else(|| {
raise_error!(
format!("Invalid IPv6 address in proxy URL: '{}'.", hostport),
ErrorCode::InvalidParameter
)
})?;
let host = hostport[1..close_bracket].to_string();
let after_bracket = &hostport[close_bracket + 1..];
if !after_bracket.starts_with(':') {
return Err(raise_error!(
format!("Missing port after IPv6 address in proxy URL: '{}'.", hostport),
ErrorCode::InvalidParameter
));
}
let port = after_bracket[1..].parse::<u16>().map_err(|_| {
raise_error!(
format!("Invalid port in proxy URL: '{}'.", hostport),
ErrorCode::InvalidParameter
)
})?;
return Ok((host, port));
}
// hostname:port or ip:port — split from right
let last_colon = hostport.rfind(':').ok_or_else(|| {
raise_error!(
format!("Missing port in proxy URL: '{}'.", hostport),
ErrorCode::InvalidParameter
)
})?;
let host = hostport[..last_colon].to_string();
let port = hostport[last_colon + 1..].parse::<u16>().map_err(|_| {
raise_error!(
format!("Invalid port in proxy URL: '{}'.", hostport),
ErrorCode::InvalidParameter
)
})?;
Ok(addr)
if host.is_empty() {
return Err(raise_error!(
"Empty hostname in proxy URL.".into(),
ErrorCode::InvalidParameter
));
}
Ok((host, port))
}
/// Try to connect via SOCKS5 proxy or TCP with timeout
/// Try to connect via SOCKS5 proxy or TCP with timeout.
async fn connect_with_optional_proxy(
use_proxy: Option<u64>,
address: SocketAddr,
) -> BichonResult<TcpStream> {
// Try if proxy is enabled
if let Some(proxy_id) = use_proxy {
let proxy = Proxy::get(proxy_id)?;
let proxy = parse_proxy_addr(&proxy.url)?;
return timeout(TIMEOUT, Socks5Stream::connect(proxy, address))
let addr = parse_proxy_url(&proxy.url)?;
let proxy_addr = (addr.host.as_str(), addr.port);
let result = if let (Some(ref user), Some(ref pass)) = (addr.username, addr.password) {
timeout(
TIMEOUT,
Socks5Stream::connect_with_password(proxy_addr, address, user.as_str(), pass.as_str()),
)
.await
} else {
timeout(TIMEOUT, Socks5Stream::connect(proxy_addr, address)).await
};
return result
.map_err(|_| {
error!(
"SOCKS5 proxy connection to {} via {} timed out after {}s",
"SOCKS5 proxy connection to {} via {}:{} timed out after {}s",
address,
proxy,
addr.host,
addr.port,
TIMEOUT.as_secs()
);
raise_error!(
format!(
"SOCKS5 proxy connection to {} via {} timed out after {}s",
"SOCKS5 proxy connection to {} via {}:{} timed out after {}s",
address,
proxy,
addr.host,
addr.port,
TIMEOUT.as_secs()
),
ErrorCode::ConnectionTimeout