mirror of
https://github.com/chatmail/core.git
synced 2026-04-20 15:06:30 +03:00
feat: shadowsocks support
This change introduces new config options `proxy_enabled` and `proxy_url` that replace `socks5_*`. Tested with deltachat-repl by starting it with `cargo run --locked -p deltachat-repl -- deltachat-db` and running ``` > set proxy_enabled 1 > set proxy_url ss://... > setqr dcaccount:https://chatmail.example.org/new > configure ```
This commit is contained in:
@@ -91,21 +91,43 @@ pub enum Config {
|
||||
/// Should not be extended in the future, create new config keys instead.
|
||||
ServerFlags,
|
||||
|
||||
/// True if proxy is enabled.
|
||||
///
|
||||
/// Can be used to disable proxy without erasing known URLs.
|
||||
ProxyEnabled,
|
||||
|
||||
/// Proxy URL.
|
||||
///
|
||||
/// Supported URLs schemes are `socks5://` (SOCKS5) and `ss://` (Shadowsocks).
|
||||
///
|
||||
/// May contain multiple URLs separated by newline, in which case the first one is used.
|
||||
ProxyUrl,
|
||||
|
||||
/// True if SOCKS5 is enabled.
|
||||
///
|
||||
/// Can be used to disable SOCKS5 without erasing SOCKS5 configuration.
|
||||
///
|
||||
/// Deprecated in favor of `ProxyEnabled`.
|
||||
Socks5Enabled,
|
||||
|
||||
/// SOCKS5 proxy server hostname or address.
|
||||
///
|
||||
/// Deprecated in favor of `ProxyUrl`.
|
||||
Socks5Host,
|
||||
|
||||
/// SOCKS5 proxy server port.
|
||||
///
|
||||
/// Deprecated in favor of `ProxyUrl`.
|
||||
Socks5Port,
|
||||
|
||||
/// SOCKS5 proxy server username.
|
||||
///
|
||||
/// Deprecated in favor of `ProxyUrl`.
|
||||
Socks5User,
|
||||
|
||||
/// SOCKS5 proxy server password.
|
||||
///
|
||||
/// Deprecated in favor of `ProxyUrl`.
|
||||
Socks5Password,
|
||||
|
||||
/// Own name to use in the `From:` field when sending messages.
|
||||
@@ -638,6 +660,7 @@ impl Context {
|
||||
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
|
||||
match key {
|
||||
Config::Socks5Enabled
|
||||
| Config::ProxyEnabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
| Config::MdnsEnabled
|
||||
|
||||
@@ -196,8 +196,8 @@ async fn get_configured_param(
|
||||
param.smtp.password.clone()
|
||||
};
|
||||
|
||||
let socks5_config = param.socks5_config.clone();
|
||||
let socks5_enabled = socks5_config.is_some();
|
||||
let proxy_config = param.proxy_config.clone();
|
||||
let proxy_enabled = proxy_config.is_some();
|
||||
|
||||
let mut addr = param.addr.clone();
|
||||
if param.oauth2 {
|
||||
@@ -240,7 +240,7 @@ async fn get_configured_param(
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
provider = provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await;
|
||||
provider = provider::get_provider_info(ctx, ¶m_domain, proxy_enabled).await;
|
||||
if let Some(provider) = provider {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "Offline autoconfig found, but no servers defined.");
|
||||
@@ -356,7 +356,7 @@ async fn get_configured_param(
|
||||
.collect(),
|
||||
smtp_user: param.smtp.user.clone(),
|
||||
smtp_password,
|
||||
socks5_config: param.socks5_config.clone(),
|
||||
proxy_config: param.proxy_config.clone(),
|
||||
provider,
|
||||
certificate_checks: match param.certificate_checks {
|
||||
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
|
||||
@@ -388,7 +388,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let smtp_param = configured_param.smtp.clone();
|
||||
let smtp_password = configured_param.smtp_password.clone();
|
||||
let smtp_addr = configured_param.addr.clone();
|
||||
let smtp_socks5 = configured_param.socks5_config.clone();
|
||||
let proxy_config = configured_param.proxy_config.clone();
|
||||
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp = Smtp::new();
|
||||
@@ -396,7 +396,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&smtp_password,
|
||||
&smtp_socks5,
|
||||
&proxy_config,
|
||||
&smtp_addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
@@ -414,7 +414,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let mut imap = Imap::new(
|
||||
configured_param.imap.clone(),
|
||||
configured_param.imap_password.clone(),
|
||||
configured_param.socks5_config.clone(),
|
||||
configured_param.proxy_config.clone(),
|
||||
&configured_param.addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
|
||||
@@ -726,7 +726,7 @@ impl Context {
|
||||
let request_msgs = message::get_request_msg_cnt(self).await;
|
||||
let contacts = Contact::get_real_cnt(self).await?;
|
||||
let is_configured = self.get_config_int(Config::Configured).await?;
|
||||
let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?;
|
||||
let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?;
|
||||
let dbversion = self
|
||||
.sql
|
||||
.get_raw_config_int("dbversion")
|
||||
@@ -807,7 +807,7 @@ impl Context {
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
res.insert("socks5_enabled", socks5_enabled.to_string());
|
||||
res.insert("proxy_enabled", proxy_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2);
|
||||
|
||||
@@ -1693,6 +1693,8 @@ mod tests {
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
"socks5_user",
|
||||
|
||||
15
src/imap.rs
15
src/imap.rs
@@ -37,12 +37,12 @@ use crate::login_param::{
|
||||
};
|
||||
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
|
||||
use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::oauth2::get_oauth2_access_token;
|
||||
use crate::receive_imf::{
|
||||
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
|
||||
};
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{self, create_id, duration_to_str};
|
||||
@@ -80,8 +80,9 @@ pub(crate) struct Imap {
|
||||
/// Password.
|
||||
password: String,
|
||||
|
||||
/// SOCKS 5 configuration.
|
||||
socks5_config: Option<Socks5Config>,
|
||||
/// Proxy configuration.
|
||||
proxy_config: Option<ProxyConfig>,
|
||||
|
||||
strict_tls: bool,
|
||||
|
||||
oauth2: bool,
|
||||
@@ -237,7 +238,7 @@ impl Imap {
|
||||
pub fn new(
|
||||
lp: Vec<ConfiguredServerLoginParam>,
|
||||
password: String,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
proxy_config: Option<ProxyConfig>,
|
||||
addr: &str,
|
||||
strict_tls: bool,
|
||||
oauth2: bool,
|
||||
@@ -248,7 +249,7 @@ impl Imap {
|
||||
addr: addr.to_string(),
|
||||
lp,
|
||||
password,
|
||||
socks5_config,
|
||||
proxy_config,
|
||||
strict_tls,
|
||||
oauth2,
|
||||
login_failed_once: false,
|
||||
@@ -271,7 +272,7 @@ impl Imap {
|
||||
let imap = Self::new(
|
||||
param.imap.clone(),
|
||||
param.imap_password.clone(),
|
||||
param.socks5_config.clone(),
|
||||
param.proxy_config.clone(),
|
||||
¶m.addr,
|
||||
param.strict_tls(),
|
||||
param.oauth2,
|
||||
@@ -336,7 +337,7 @@ impl Imap {
|
||||
let connection_candidate = lp.connection.clone();
|
||||
let client = match Client::connect(
|
||||
context,
|
||||
self.socks5_config.clone(),
|
||||
self.proxy_config.clone(),
|
||||
self.strict_tls,
|
||||
connection_candidate,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::ops::{Deref, DerefMut};
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_imap::Client as ImapClient;
|
||||
use async_imap::Session as ImapSession;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
@@ -12,12 +11,12 @@ use super::session::Session;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::net::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::tools::time;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -157,25 +156,25 @@ impl Client {
|
||||
|
||||
pub async fn connect(
|
||||
context: &Context,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
proxy_config: Option<ProxyConfig>,
|
||||
strict_tls: bool,
|
||||
candidate: ConnectionCandidate,
|
||||
) -> Result<Self> {
|
||||
let host = &candidate.host;
|
||||
let port = candidate.port;
|
||||
let security = candidate.security;
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
if let Some(proxy_config) = proxy_config {
|
||||
let client = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config)
|
||||
Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config)
|
||||
.await?
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls)
|
||||
Client::connect_starttls_proxy(context, host, port, proxy_config, strict_tls)
|
||||
.await?
|
||||
}
|
||||
ConnectionSecurity::Plain => {
|
||||
Client::connect_insecure_socks5(context, host, port, socks5_config).await?
|
||||
Client::connect_insecure_proxy(context, host, port, proxy_config).await?
|
||||
}
|
||||
};
|
||||
Ok(client)
|
||||
@@ -249,17 +248,17 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_secure_socks5(
|
||||
async fn connect_secure_proxy(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
socks5_config: Socks5Config,
|
||||
proxy_config: ProxyConfig,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config
|
||||
let proxy_stream = proxy_config
|
||||
.connect(context, domain, port, strict_tls)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), socks5_stream).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
@@ -270,14 +269,14 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_insecure_socks5(
|
||||
async fn connect_insecure_proxy(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
socks5_config: Socks5Config,
|
||||
proxy_config: ProxyConfig,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config.connect(context, domain, port, false).await?;
|
||||
let buffered_stream = BufWriter::new(socks5_stream);
|
||||
let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
|
||||
let buffered_stream = BufWriter::new(proxy_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
@@ -287,20 +286,20 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_starttls_socks5(
|
||||
async fn connect_starttls_proxy(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
socks5_config: Socks5Config,
|
||||
proxy_config: ProxyConfig,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config
|
||||
let proxy_stream = proxy_config
|
||||
.connect(context, hostname, port, strict_tls)
|
||||
.await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let buffered_socks5_stream = BufWriter::new(socks5_stream);
|
||||
let mut client = ImapClient::new(buffered_socks5_stream);
|
||||
let buffered_proxy_stream = BufWriter::new(proxy_stream);
|
||||
let mut client = ImapClient::new(buffered_proxy_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
@@ -309,10 +308,10 @@ impl Client {
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
.context("STARTTLS command failed")?;
|
||||
let buffered_socks5_stream = client.into_inner();
|
||||
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
|
||||
let buffered_proxy_stream = client.into_inner();
|
||||
let proxy_stream = buffered_proxy_stream.into_inner();
|
||||
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, &[], socks5_stream)
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
|
||||
@@ -84,7 +84,6 @@ mod scheduler;
|
||||
pub mod securejoin;
|
||||
mod simplify;
|
||||
mod smtp;
|
||||
mod socks;
|
||||
pub mod stock_str;
|
||||
mod sync;
|
||||
mod timesmearing;
|
||||
|
||||
@@ -11,8 +11,8 @@ use crate::configure::server_params::{expand_param_vector, ServerParams};
|
||||
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
|
||||
use crate::context::Context;
|
||||
use crate::net::load_connection_timestamp;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::sql::Sql;
|
||||
|
||||
/// User-entered setting for certificate checks.
|
||||
@@ -116,7 +116,8 @@ pub struct EnteredLoginParam {
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: EnteredCertificateChecks,
|
||||
|
||||
pub socks5_config: Option<Socks5Config>,
|
||||
/// Proxy configuration.
|
||||
pub proxy_config: Option<ProxyConfig>,
|
||||
|
||||
pub oauth2: bool,
|
||||
}
|
||||
@@ -195,7 +196,7 @@ impl EnteredLoginParam {
|
||||
.unwrap_or_default();
|
||||
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
|
||||
|
||||
let socks5_config = Socks5Config::from_database(&context.sql).await?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
|
||||
Ok(EnteredLoginParam {
|
||||
addr,
|
||||
@@ -214,7 +215,7 @@ impl EnteredLoginParam {
|
||||
password: send_pw,
|
||||
},
|
||||
certificate_checks,
|
||||
socks5_config,
|
||||
proxy_config,
|
||||
oauth2,
|
||||
})
|
||||
}
|
||||
@@ -380,7 +381,8 @@ pub struct ConfiguredLoginParam {
|
||||
|
||||
pub smtp_password: String,
|
||||
|
||||
pub socks5_config: Option<Socks5Config>,
|
||||
/// Proxy configuration.
|
||||
pub proxy_config: Option<ProxyConfig>,
|
||||
|
||||
pub provider: Option<&'static Provider>,
|
||||
|
||||
@@ -679,7 +681,7 @@ impl ConfiguredLoginParam {
|
||||
}];
|
||||
}
|
||||
|
||||
let socks5_config = Socks5Config::from_database(&context.sql).await?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
|
||||
Ok(Some(ConfiguredLoginParam {
|
||||
addr,
|
||||
@@ -691,7 +693,7 @@ impl ConfiguredLoginParam {
|
||||
smtp_password: send_pw,
|
||||
certificate_checks,
|
||||
provider,
|
||||
socks5_config,
|
||||
proxy_config,
|
||||
oauth2,
|
||||
}))
|
||||
}
|
||||
@@ -778,7 +780,7 @@ impl ConfiguredLoginParam {
|
||||
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
|
||||
match self.certificate_checks {
|
||||
ConfiguredCertificateChecks::OldAutomatic => {
|
||||
provider_strict_tls.unwrap_or(self.socks5_config.is_some())
|
||||
provider_strict_tls.unwrap_or(self.proxy_config.is_some())
|
||||
}
|
||||
ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
|
||||
ConfiguredCertificateChecks::Strict => true,
|
||||
@@ -863,8 +865,8 @@ mod tests {
|
||||
}],
|
||||
smtp_user: "".to_string(),
|
||||
smtp_password: "bar".to_string(),
|
||||
// socks5_config is not saved by `save_to_database`, using default value
|
||||
socks5_config: None,
|
||||
// proxy_config is not saved by `save_to_database`, using default value
|
||||
proxy_config: None,
|
||||
provider: None,
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
@@ -967,7 +969,7 @@ mod tests {
|
||||
],
|
||||
smtp_user: "alice@posteo.de".to_string(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
socks5_config: None,
|
||||
proxy_config: None,
|
||||
provider: get_provider_by_id("posteo"),
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::tools::time;
|
||||
|
||||
pub(crate) mod dns;
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod proxy;
|
||||
pub(crate) mod session;
|
||||
pub(crate) mod tls;
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ use mime::Mime;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug)]
|
||||
@@ -43,7 +43,7 @@ where
|
||||
{
|
||||
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
|
||||
let host = parsed_url.host().context("URL has no host")?;
|
||||
let socks5_config_opt = Socks5Config::from_database(&context.sql).await?;
|
||||
let proxy_config_opt = ProxyConfig::load(context).await?;
|
||||
|
||||
let stream: Box<dyn SessionStream> = match scheme {
|
||||
"http" => {
|
||||
@@ -54,11 +54,11 @@ where
|
||||
// better resolve from scratch each time to prevent
|
||||
// cache poisoning attacks from having lasting effects.
|
||||
let load_cache = false;
|
||||
if let Some(socks5_config) = socks5_config_opt {
|
||||
let socks5_stream = socks5_config
|
||||
if let Some(proxy_config) = proxy_config_opt {
|
||||
let proxy_stream = proxy_config
|
||||
.connect(context, host, port, load_cache)
|
||||
.await?;
|
||||
Box::new(socks5_stream)
|
||||
Box::new(proxy_stream)
|
||||
} else {
|
||||
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
|
||||
Box::new(tcp_stream)
|
||||
@@ -69,11 +69,11 @@ where
|
||||
let load_cache = true;
|
||||
let strict_tls = true;
|
||||
|
||||
if let Some(socks5_config) = socks5_config_opt {
|
||||
let socks5_stream = socks5_config
|
||||
if let Some(proxy_config) = proxy_config_opt {
|
||||
let proxy_stream = proxy_config
|
||||
.connect(context, host, port, load_cache)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], socks5_stream).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], proxy_stream).await?;
|
||||
Box::new(tls_stream)
|
||||
} else {
|
||||
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
|
||||
|
||||
435
src/net/proxy.rs
Normal file
435
src/net/proxy.rs
Normal file
@@ -0,0 +1,435 @@
|
||||
//! # Proxy support.
|
||||
//!
|
||||
//! Delta Chat supports SOCKS5 and Shadowsocks protocols.
|
||||
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use fast_socks5::util::target_addr::ToTargetAddr;
|
||||
use fast_socks5::AuthenticationMethod;
|
||||
use fast_socks5::Socks5Command;
|
||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||
use pin_project::pin_project;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::sql::Sql;
|
||||
|
||||
/// Default SOCKS5 port according to [RFC 1928](https://tools.ietf.org/html/rfc1928).
|
||||
pub const DEFAULT_SOCKS_PORT: u16 = 1080;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShadowsocksConfig {
|
||||
pub server_config: shadowsocks::config::ServerConfig,
|
||||
}
|
||||
|
||||
impl PartialEq for ShadowsocksConfig {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.server_config.to_url() == other.server_config.to_url()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ShadowsocksConfig {}
|
||||
|
||||
/// Wrapper for Shadowsocks stream implementing
|
||||
/// `Debug` and `SessionStream`.
|
||||
///
|
||||
/// Passes `AsyncRead` and `AsyncWrite` traits through.
|
||||
#[pin_project]
|
||||
pub(crate) struct ShadowsocksStream<S> {
|
||||
#[pin]
|
||||
pub(crate) stream: shadowsocks::ProxyClientStream<S>,
|
||||
}
|
||||
|
||||
impl<S> std::fmt::Debug for ShadowsocksStream<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ShadowsocksStream")
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsyncRead for ShadowsocksStream<S>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
self.project().stream.poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsyncWrite for ShadowsocksStream<S>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, std::io::Error>> {
|
||||
self.project().stream.poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
self.project().stream.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
self.project().stream.poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Socks5Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub user_password: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl Socks5Config {
|
||||
async fn connect(
|
||||
&self,
|
||||
context: &Context,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
load_dns_cache: bool,
|
||||
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
|
||||
let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache)
|
||||
.await
|
||||
.context("Failed to connect to SOCKS5 proxy")?;
|
||||
|
||||
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
|
||||
{
|
||||
Some(AuthenticationMethod::Password {
|
||||
username: username.into(),
|
||||
password: password.into(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut socks_stream =
|
||||
Socks5Stream::use_stream(tcp_stream, authentication_method, Default::default()).await?;
|
||||
let target_addr = (target_host, target_port).to_target_addr()?;
|
||||
socks_stream
|
||||
.request(Socks5Command::TCPConnect, target_addr)
|
||||
.await?;
|
||||
|
||||
Ok(socks_stream)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ProxyConfig {
|
||||
Socks5(Socks5Config),
|
||||
|
||||
Shadowsocks(ShadowsocksConfig),
|
||||
}
|
||||
|
||||
impl ProxyConfig {
|
||||
/// Creates a new proxy configuration by parsing given proxy URL.
|
||||
fn from_url(url: &str) -> Result<Self> {
|
||||
let url = url::Url::parse(url).context("Cannot parse proxy URL")?;
|
||||
match url.scheme() {
|
||||
"ss" => {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(url.as_str())?;
|
||||
let shadowsocks_config = ShadowsocksConfig { server_config };
|
||||
Ok(Self::Shadowsocks(shadowsocks_config))
|
||||
}
|
||||
|
||||
// Because of `curl` convention,
|
||||
// `socks5` URL scheme may be expected to resolve domain names locally
|
||||
// with `socks5h` URL scheme meaning that hostnames are passed to the proxy.
|
||||
// Resolving hostnames locally is not supported
|
||||
// in Delta Chat when using a proxy
|
||||
// to prevent DNS leaks.
|
||||
// Because of this we do not distinguish
|
||||
// between `socks5` and `socks5h`.
|
||||
"socks5" => {
|
||||
let host = url
|
||||
.host_str()
|
||||
.context("socks5 URL has no host")?
|
||||
.to_string();
|
||||
let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
|
||||
let user_password = if let Some(password) = url.password() {
|
||||
let username = percent_encoding::percent_decode_str(url.username())
|
||||
.decode_utf8()
|
||||
.context("SOCKS5 username is not a valid UTF-8")?
|
||||
.to_string();
|
||||
let password = percent_encoding::percent_decode_str(password)
|
||||
.decode_utf8()
|
||||
.context("SOCKS5 password is not a valid UTF-8")?
|
||||
.to_string();
|
||||
Some((username, password))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let socks5_config = Socks5Config {
|
||||
host,
|
||||
port,
|
||||
user_password,
|
||||
};
|
||||
Ok(Self::Socks5(socks5_config))
|
||||
}
|
||||
scheme => Err(format_err!("Unknown URL scheme {scheme:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password`
|
||||
/// config into `proxy_url` if `proxy_url` is unset or empty.
|
||||
///
|
||||
/// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case.
|
||||
async fn migrate_socks_config(sql: &Sql) -> Result<()> {
|
||||
if sql.get_raw_config("proxy_url").await?.is_none() {
|
||||
// Load legacy SOCKS5 settings.
|
||||
if let Some(host) = sql
|
||||
.get_raw_config("socks5_host")
|
||||
.await?
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
let port: u16 = sql
|
||||
.get_raw_config_int("socks5_port")
|
||||
.await?
|
||||
.unwrap_or(DEFAULT_SOCKS_PORT.into()) as u16;
|
||||
let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
|
||||
let pass = sql
|
||||
.get_raw_config("socks5_password")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut proxy_url = "socks5://".to_string();
|
||||
if !pass.is_empty() {
|
||||
proxy_url += &percent_encode(user.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
proxy_url += ":";
|
||||
proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
proxy_url += "@";
|
||||
};
|
||||
proxy_url += &host;
|
||||
proxy_url += ":";
|
||||
proxy_url += &port.to_string();
|
||||
|
||||
sql.set_raw_config("proxy_url", Some(&proxy_url)).await?;
|
||||
} else {
|
||||
sql.set_raw_config("proxy_url", Some("")).await?;
|
||||
}
|
||||
|
||||
let socks5_enabled = sql.get_raw_config("socks5_enabled").await?;
|
||||
sql.set_raw_config("proxy_enabled", socks5_enabled.as_deref())
|
||||
.await?;
|
||||
}
|
||||
|
||||
sql.set_raw_config("socks5_enabled", None).await?;
|
||||
sql.set_raw_config("socks5_host", None).await?;
|
||||
sql.set_raw_config("socks5_port", None).await?;
|
||||
sql.set_raw_config("socks5_user", None).await?;
|
||||
sql.set_raw_config("socks5_password", None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads proxy configuration from the database.
|
||||
pub async fn load(context: &Context) -> Result<Option<Self>> {
|
||||
Self::migrate_socks_config(&context.sql)
|
||||
.await
|
||||
.context("Failed to migrate legacy SOCKS config")?;
|
||||
|
||||
let enabled = context.get_config_bool(Config::ProxyEnabled).await?;
|
||||
if !enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let proxy_url = context
|
||||
.get_config(Config::ProxyUrl)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let proxy_url = proxy_url
|
||||
.split_once('\n')
|
||||
.map_or(proxy_url.clone(), |(first_url, _rest)| {
|
||||
first_url.to_string()
|
||||
});
|
||||
let proxy_config = Self::from_url(&proxy_url).context("Failed to parse proxy URL")?;
|
||||
Ok(Some(proxy_config))
|
||||
}
|
||||
|
||||
/// If `load_dns_cache` is true, loads cached DNS resolution results.
|
||||
/// Use this only if the connection is going to be protected with TLS checks.
|
||||
pub async fn connect(
|
||||
&self,
|
||||
context: &Context,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
load_dns_cache: bool,
|
||||
) -> Result<Box<dyn SessionStream>> {
|
||||
match self {
|
||||
ProxyConfig::Socks5(socks5_config) => {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, target_host, target_port, load_dns_cache)
|
||||
.await?;
|
||||
Ok(Box::new(socks5_stream))
|
||||
}
|
||||
ProxyConfig::Shadowsocks(ShadowsocksConfig { server_config }) => {
|
||||
let shadowsocks_context = shadowsocks::context::Context::new_shared(
|
||||
shadowsocks::config::ServerType::Local,
|
||||
);
|
||||
|
||||
let tcp_stream = {
|
||||
let server_addr = server_config.addr();
|
||||
let host = server_addr.host();
|
||||
let port = server_addr.port();
|
||||
connect_tcp(context, &host, port, load_dns_cache)
|
||||
.await
|
||||
.context("Failed to connect to Shadowsocks proxy")?
|
||||
};
|
||||
|
||||
let proxy_client_stream = shadowsocks::ProxyClientStream::from_stream(
|
||||
shadowsocks_context,
|
||||
tcp_stream,
|
||||
server_config,
|
||||
(target_host.to_string(), target_port),
|
||||
);
|
||||
let shadowsocks_stream = ShadowsocksStream {
|
||||
stream: proxy_client_stream,
|
||||
};
|
||||
|
||||
Ok(Box::new(shadowsocks_stream))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Socks5Config {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"host:{},port:{},user_password:{}",
|
||||
self.host,
|
||||
self.port,
|
||||
if let Some(user_password) = self.user_password.clone() {
|
||||
format!("user: {}, password: ***", user_password.0)
|
||||
} else {
|
||||
"user: None".to_string()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_socks5_url() {
|
||||
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:9050").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Socks5(Socks5Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9050,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Socks5(Socks5Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9150,
|
||||
user_password: Some(("foo".to_string(), "bar".to_string()))
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("socks5://%66oo:b%61r@127.0.0.1:9150").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Socks5(Socks5Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9150,
|
||||
user_password: Some(("foo".to_string(), "bar".to_string()))
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shadowsocks_url() {
|
||||
// Example URL from <https://shadowsocks.org/doc/sip002.html>.
|
||||
let proxy_config =
|
||||
ProxyConfig::from_url("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1")
|
||||
.unwrap();
|
||||
assert!(matches!(proxy_config, ProxyConfig::Shadowsocks(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_proxy_url() {
|
||||
assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
|
||||
assert!(ProxyConfig::from_url("abc").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_socks5_migration() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Test that config is migrated on attempt to load even if disabled.
|
||||
t.set_config(Config::Socks5Host, Some("127.0.0.1")).await?;
|
||||
t.set_config(Config::Socks5Port, Some("9050")).await?;
|
||||
|
||||
let proxy_config = ProxyConfig::load(&t).await?;
|
||||
// Even though proxy is not enabled, config should be migrated.
|
||||
assert_eq!(proxy_config, None);
|
||||
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?.unwrap(),
|
||||
"socks5://127.0.0.1:9050"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Test SOCKS5 setting migration if proxy was never configured.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_socks5_migration_unconfigured() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Try to load config to trigger migration.
|
||||
assert_eq!(ProxyConfig::load(&t).await?, None);
|
||||
|
||||
assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?.unwrap(),
|
||||
String::new()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Test SOCKS5 setting migration if SOCKS5 host is empty.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_socks5_migration_empty() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
t.set_config(Config::Socks5Host, Some("")).await?;
|
||||
|
||||
// Try to load config to trigger migration.
|
||||
assert_eq!(ProxyConfig::load(&t).await?, None);
|
||||
|
||||
assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?.unwrap(),
|
||||
String::new()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::net::proxy::ShadowsocksStream;
|
||||
use async_native_tls::TlsStream;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use std::pin::Pin;
|
||||
@@ -44,6 +45,11 @@ impl<T: SessionStream> SessionStream for Socks5Stream<T> {
|
||||
self.get_socket_mut().set_read_timeout(timeout)
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for ShadowsocksStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.stream.get_mut().set_read_timeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// Session stream with a read buffer.
|
||||
pub(crate) trait SessionBufStream: SessionStream + AsyncBufRead {}
|
||||
|
||||
@@ -62,8 +62,8 @@ pub async fn get_oauth2_url(
|
||||
addr: &str,
|
||||
redirect_uri: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, proxy_enabled).await {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
|
||||
@@ -83,8 +83,8 @@ pub(crate) async fn get_oauth2_access_token(
|
||||
code: &str,
|
||||
regenerate: bool,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, proxy_enabled).await {
|
||||
let lock = context.oauth2_mutex.lock().await;
|
||||
|
||||
// read generated token
|
||||
@@ -232,8 +232,8 @@ pub(crate) async fn get_oauth2_addr(
|
||||
addr: &str,
|
||||
code: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?;
|
||||
let oauth2 = match Oauth2::from_address(context, addr, proxy_enabled).await {
|
||||
Some(o) => o,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
95
src/qr.rs
95
src/qr.rs
@@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::Deserialize;
|
||||
|
||||
use self::dclogin_scheme::configure_from_login_qr;
|
||||
@@ -20,6 +20,7 @@ use crate::events::EventType;
|
||||
use crate::key::Fingerprint;
|
||||
use crate::message::Message;
|
||||
use crate::net::http::post_empty;
|
||||
use crate::net::proxy::DEFAULT_SOCKS_PORT;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::token;
|
||||
use crate::tools::validate_id;
|
||||
@@ -541,16 +542,15 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
|
||||
|
||||
const SOCKS5_DEFAULT_PORT: u16 = 1080;
|
||||
let mut host: Option<String> = None;
|
||||
let mut port: u16 = SOCKS5_DEFAULT_PORT;
|
||||
let mut port: u16 = DEFAULT_SOCKS_PORT;
|
||||
let mut user: Option<String> = None;
|
||||
let mut pass: Option<String> = None;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "server" {
|
||||
host = Some(value.to_string());
|
||||
} else if key == "port" {
|
||||
port = value.parse().unwrap_or(SOCKS5_DEFAULT_PORT);
|
||||
port = value.parse().unwrap_or(DEFAULT_SOCKS_PORT);
|
||||
} else if key == "user" {
|
||||
user = Some(value.to_string());
|
||||
} else if key == "pass" {
|
||||
@@ -661,22 +661,33 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
user,
|
||||
pass,
|
||||
} => {
|
||||
// disable proxy before changing settings to not use a combination of old and new
|
||||
context
|
||||
.set_config_bool(Config::Socks5Enabled, false)
|
||||
.await?;
|
||||
let mut proxy_url = "socks5://".to_string();
|
||||
if let Some(pass) = pass {
|
||||
proxy_url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC)
|
||||
.to_string();
|
||||
proxy_url += ":";
|
||||
proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
proxy_url += "@";
|
||||
};
|
||||
proxy_url += &host;
|
||||
proxy_url += ":";
|
||||
proxy_url += &port.to_string();
|
||||
|
||||
context.set_config(Config::Socks5Host, Some(&host)).await?;
|
||||
let old_proxy_url_value = context
|
||||
.get_config(Config::ProxyUrl)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let proxy_urls: Vec<&str> = std::iter::once(proxy_url.as_str())
|
||||
.chain(
|
||||
old_proxy_url_value
|
||||
.split('\n')
|
||||
.filter(|s| !s.is_empty() && *s != proxy_url),
|
||||
)
|
||||
.collect();
|
||||
context
|
||||
.set_config_u32(Config::Socks5Port, u32::from(port))
|
||||
.set_config(Config::ProxyUrl, Some(&proxy_urls.join("\n")))
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::Socks5User, user.as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::Socks5Password, pass.as_deref())
|
||||
.await?;
|
||||
context.set_config_bool(Config::Socks5Enabled, true).await?;
|
||||
context.set_config_bool(Config::ProxyEnabled, true).await?;
|
||||
}
|
||||
Qr::WithdrawVerifyContact {
|
||||
invitenumber,
|
||||
@@ -1630,50 +1641,48 @@ mod tests {
|
||||
async fn test_set_socks5_proxy_config_from_qr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, false);
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
|
||||
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Host).await?,
|
||||
Some("foo".to_string())
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some("socks5://foo:666".to_string())
|
||||
);
|
||||
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 666);
|
||||
assert_eq!(t.get_config(Config::Socks5User).await?, None);
|
||||
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
|
||||
|
||||
// make sure, user&password are reset when not specified in the URL
|
||||
t.set_config(Config::Socks5User, Some("alice")).await?;
|
||||
t.set_config(Config::Socks5Password, Some("secret")).await?;
|
||||
// Test URL without port.
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Host).await?,
|
||||
Some("1.2.3.4".to_string())
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string())
|
||||
);
|
||||
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
|
||||
assert_eq!(t.get_config(Config::Socks5User).await?, None);
|
||||
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
|
||||
|
||||
// make sure, user&password are set when specified in the URL
|
||||
// Password is an URL-encoded "x&%$X".
|
||||
let res =
|
||||
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Host).await?,
|
||||
Some("jau".to_string())
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
|
||||
|
||||
// Scanning existing proxy brings it to the top in the list.
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5User).await?,
|
||||
Some("Da".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Password).await?,
|
||||
Some("x&%$X".to_string())
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -18,9 +18,9 @@ use crate::login_param::{ConfiguredLoginParam, ConfiguredServerLoginParam};
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::sql;
|
||||
use crate::stock_str::unencrypted_email;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
@@ -95,7 +95,7 @@ impl Smtp {
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.smtp_password,
|
||||
&lp.socks5_config,
|
||||
&lp.proxy_config,
|
||||
&lp.addr,
|
||||
lp.strict_tls(),
|
||||
lp.oauth2,
|
||||
@@ -110,7 +110,7 @@ impl Smtp {
|
||||
context: &Context,
|
||||
login_params: &[ConfiguredServerLoginParam],
|
||||
password: &str,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
proxy_config: &Option<ProxyConfig>,
|
||||
addr: &str,
|
||||
strict_tls: bool,
|
||||
oauth2: bool,
|
||||
@@ -130,7 +130,7 @@ impl Smtp {
|
||||
info!(context, "SMTP trying to connect to {}.", &lp.connection);
|
||||
let transport = match connect::connect_and_auth(
|
||||
context,
|
||||
socks5_config,
|
||||
proxy_config,
|
||||
strict_tls,
|
||||
lp.connection.clone(),
|
||||
oauth2,
|
||||
|
||||
@@ -9,13 +9,13 @@ use tokio::io::BufStream;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::net::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
use crate::oauth2::get_oauth2_access_token;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::tools::time;
|
||||
|
||||
/// Converts port number to ALPN list.
|
||||
@@ -31,7 +31,7 @@ fn alpn(port: u16) -> &'static [&'static str] {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn connect_and_auth(
|
||||
context: &Context,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
proxy_config: &Option<ProxyConfig>,
|
||||
strict_tls: bool,
|
||||
candidate: ConnectionCandidate,
|
||||
oauth2: bool,
|
||||
@@ -40,7 +40,7 @@ pub(crate) async fn connect_and_auth(
|
||||
password: &str,
|
||||
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
|
||||
let session_stream =
|
||||
connect_stream(context, socks5_config.clone(), strict_tls, candidate).await?;
|
||||
connect_stream(context, proxy_config.clone(), strict_tls, candidate).await?;
|
||||
let client = async_smtp::SmtpClient::new()
|
||||
.smtp_utf8(true)
|
||||
.without_greeting();
|
||||
@@ -127,7 +127,7 @@ async fn connection_attempt(
|
||||
/// to unify the result regardless of whether TLS or STARTTLS is used.
|
||||
async fn connect_stream(
|
||||
context: &Context,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
proxy_config: Option<ProxyConfig>,
|
||||
strict_tls: bool,
|
||||
candidate: ConnectionCandidate,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
@@ -135,18 +135,17 @@ async fn connect_stream(
|
||||
let port = candidate.port;
|
||||
let security = candidate.security;
|
||||
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
if let Some(proxy_config) = proxy_config {
|
||||
let stream = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
connect_secure_socks5(context, host, port, strict_tls, socks5_config.clone())
|
||||
.await?
|
||||
connect_secure_proxy(context, host, port, strict_tls, proxy_config.clone()).await?
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
connect_starttls_socks5(context, host, port, strict_tls, socks5_config.clone())
|
||||
connect_starttls_proxy(context, host, port, strict_tls, proxy_config.clone())
|
||||
.await?
|
||||
}
|
||||
ConnectionSecurity::Plain => {
|
||||
connect_insecure_socks5(context, host, port, socks5_config.clone()).await?
|
||||
connect_insecure_proxy(context, host, port, proxy_config.clone()).await?
|
||||
}
|
||||
};
|
||||
Ok(stream)
|
||||
@@ -192,37 +191,37 @@ async fn skip_smtp_greeting<R: tokio::io::AsyncBufReadExt + Unpin>(stream: &mut
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_secure_socks5(
|
||||
async fn connect_secure_proxy(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
socks5_config: Socks5Config,
|
||||
proxy_config: ProxyConfig,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let socks5_stream = socks5_config
|
||||
let proxy_stream = proxy_config
|
||||
.connect(context, hostname, port, strict_tls)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), socks5_stream).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), proxy_stream).await?;
|
||||
let mut buffered_stream = BufStream::new(tls_stream);
|
||||
skip_smtp_greeting(&mut buffered_stream).await?;
|
||||
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
|
||||
Ok(session_stream)
|
||||
}
|
||||
|
||||
async fn connect_starttls_socks5(
|
||||
async fn connect_starttls_proxy(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
socks5_config: Socks5Config,
|
||||
proxy_config: ProxyConfig,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let socks5_stream = socks5_config
|
||||
let proxy_stream = proxy_config
|
||||
.connect(context, hostname, port, strict_tls)
|
||||
.await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let client = SmtpClient::new().smtp_utf8(true);
|
||||
let transport = SmtpTransport::new(client, BufStream::new(socks5_stream)).await?;
|
||||
let transport = SmtpTransport::new(client, BufStream::new(proxy_stream)).await?;
|
||||
let tcp_stream = transport.starttls().await?.into_inner();
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream)
|
||||
.await
|
||||
@@ -232,16 +231,14 @@ async fn connect_starttls_socks5(
|
||||
Ok(session_stream)
|
||||
}
|
||||
|
||||
async fn connect_insecure_socks5(
|
||||
async fn connect_insecure_proxy(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
socks5_config: Socks5Config,
|
||||
proxy_config: ProxyConfig,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, hostname, port, false)
|
||||
.await?;
|
||||
let mut buffered_stream = BufStream::new(socks5_stream);
|
||||
let proxy_stream = proxy_config.connect(context, hostname, port, false).await?;
|
||||
let mut buffered_stream = BufStream::new(proxy_stream);
|
||||
skip_smtp_greeting(&mut buffered_stream).await?;
|
||||
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
|
||||
Ok(session_stream)
|
||||
|
||||
101
src/socks.rs
101
src/socks.rs
@@ -1,101 +0,0 @@
|
||||
//! # SOCKS5 support.
|
||||
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use fast_socks5::client::{Config, Socks5Stream};
|
||||
use fast_socks5::util::target_addr::ToTargetAddr;
|
||||
use fast_socks5::AuthenticationMethod;
|
||||
use fast_socks5::Socks5Command;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::sql::Sql;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Socks5Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub user_password: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl Socks5Config {
|
||||
/// Reads SOCKS5 configuration from the database.
|
||||
pub async fn from_database(sql: &Sql) -> Result<Option<Self>> {
|
||||
let enabled = sql.get_raw_config_bool("socks5_enabled").await?;
|
||||
if enabled {
|
||||
let host = sql.get_raw_config("socks5_host").await?.unwrap_or_default();
|
||||
let port: u16 = sql
|
||||
.get_raw_config_int("socks5_port")
|
||||
.await?
|
||||
.unwrap_or_default() as u16;
|
||||
let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
|
||||
let password = sql
|
||||
.get_raw_config("socks5_password")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let socks5_config = Self {
|
||||
host,
|
||||
port,
|
||||
user_password: if !user.is_empty() {
|
||||
Some((user, password))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
Ok(Some(socks5_config))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// If `load_dns_cache` is true, loads cached DNS resolution results.
|
||||
/// Use this only if the connection is going to be protected with TLS checks.
|
||||
pub async fn connect(
|
||||
&self,
|
||||
context: &Context,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
load_dns_cache: bool,
|
||||
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
|
||||
let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache).await?;
|
||||
|
||||
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
|
||||
{
|
||||
Some(AuthenticationMethod::Password {
|
||||
username: username.into(),
|
||||
password: password.into(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut socks_stream =
|
||||
Socks5Stream::use_stream(tcp_stream, authentication_method, Config::default()).await?;
|
||||
let target_addr = (target_host, target_port).to_target_addr()?;
|
||||
socks_stream
|
||||
.request(Socks5Command::TCPConnect, target_addr)
|
||||
.await?;
|
||||
|
||||
Ok(socks_stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Socks5Config {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"host:{},port:{},user_password:{}",
|
||||
self.host,
|
||||
self.port,
|
||||
if let Some(user_password) = self.user_password.clone() {
|
||||
format!("user: {}, password: ***", user_password.0)
|
||||
} else {
|
||||
"user: None".to_string()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user