diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index a2ea9da14e..7a2bee21f9 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2507,6 +2507,7 @@ void dc_stop_ongoing_process (dc_context_t* context); #define DC_QR_BACKUP 251 #define DC_QR_BACKUP2 252 #define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern +#define DC_QR_SOCKS5_PROXY 270 // text1=host, text2=port #define DC_QR_ADDR 320 // id=contact #define DC_QR_TEXT 330 // text1=text #define DC_QR_URL 332 // text1=URL @@ -2560,6 +2561,10 @@ void dc_stop_ongoing_process (dc_context_t* context); * ask the user if they want to use the given service for video chats; * if so, call dc_set_config_from_qr(). * + * - DC_QR_SOCKS5_PROXY with dc_lot_t::text1=host, dc_lot_t::text2=port: + * ask the user if they want to use the given proxy and overwrite the previous one, if any. + * if so, call dc_set_config_from_qr() and restart I/O. + * * - DC_QR_ADDR with dc_lot_t::id=Contact ID: * e-mail address scanned, optionally, a draft message could be set in * dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT; diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index 5c7479a87d..3e3c37c41e 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -52,6 +52,7 @@ impl Lot { Qr::Backup { .. } => None, Qr::Backup2 { .. } => None, Qr::WebrtcInstance { domain, .. } => Some(domain), + Qr::Socks5Proxy { host, .. } => Some(host), Qr::Addr { draft, .. } => draft.as_deref(), Qr::Url { url } => Some(url), Qr::Text { text } => Some(text), @@ -68,7 +69,10 @@ impl Lot { pub fn get_text2(&self) -> Option> { match self { Self::Summary(summary) => Some(summary.truncated_text(160)), - Self::Qr(_) => None, + Self::Qr(qr) => match qr { + Qr::Socks5Proxy { port, .. } => Some(Cow::Owned(format!("{port}"))), + _ => None, + }, Self::Error(_) => None, } } @@ -105,6 +109,7 @@ impl Lot { Qr::Backup { .. } => LotState::QrBackup, Qr::Backup2 { .. } => LotState::QrBackup2, Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance, + Qr::Socks5Proxy { .. } => LotState::QrSocks5Proxy, Qr::Addr { .. } => LotState::QrAddr, Qr::Url { .. } => LotState::QrUrl, Qr::Text { .. } => LotState::QrText, @@ -131,6 +136,7 @@ impl Lot { Qr::Backup { .. } => Default::default(), Qr::Backup2 { .. } => Default::default(), Qr::WebrtcInstance { .. } => Default::default(), + Qr::Socks5Proxy { .. } => Default::default(), Qr::Addr { contact_id, .. } => contact_id.to_u32(), Qr::Url { .. } => Default::default(), Qr::Text { .. } => Default::default(), @@ -185,6 +191,9 @@ pub enum LotState { /// text1=domain, text2=instance pattern QrWebrtcInstance = 260, + /// text1=host, text2=port + QrSocks5Proxy = 270, + /// id=contact QrAddr = 320, diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index 8cda4ac766..f961edbe02 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -44,6 +44,10 @@ pub enum QrObject { domain: String, instance_pattern: String, }, + Socks5Proxy { + host: String, + port: u16, + }, Addr { contact_id: u32, draft: Option, @@ -152,6 +156,7 @@ impl From for QrObject { domain, instance_pattern, }, + Qr::Socks5Proxy { host, port } => QrObject::Socks5Proxy { host, port }, Qr::Addr { contact_id, draft } => { let contact_id = contact_id.to_u32(); QrObject::Addr { contact_id, draft } diff --git a/src/qr.rs b/src/qr.rs index 304298c69b..5666e2ae1a 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -30,6 +30,7 @@ const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#"; const DCACCOUNT_SCHEME: &str = "DCACCOUNT:"; pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:"; const DCWEBRTC_SCHEME: &str = "DCWEBRTC:"; +const TG_SOCKS_SCHEME: &str = "https://t.me/socks"; const MAILTO_SCHEME: &str = "mailto:"; const MATMSG_SCHEME: &str = "MATMSG:"; const VCARD_SCHEME: &str = "BEGIN:VCARD"; @@ -142,6 +143,15 @@ pub enum Qr { instance_pattern: String, }, + /// As the user if they want to add or use the given SOCKS5 proxy + Socks5Proxy { + /// SOCKS5 server + host: String, + + /// SOCKS5 port + port: u16, + }, + /// Contact address is scanned. /// /// Optionally, a draft message could be provided. @@ -277,6 +287,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result { dclogin_scheme::decode_login(qr)? } else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) { decode_webrtc_instance(context, qr)? + } else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) { + decode_tg_socks_proxy(context, qr)? } else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) { decode_backup(qr)? } else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) { @@ -539,6 +551,28 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result { } } +/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123` +fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result { + let url = url::Url::parse(qr).context("Invalid t.me/socks url")?; + + const SOCKS5_DEFAULT_PORT: u16 = 1080; + let mut host: Option = None; + let mut port: u16 = SOCKS5_DEFAULT_PORT; + 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); + } + } + + if let Some(host) = host { + Ok(Qr::Socks5Proxy { host, port }) + } else { + bail!("Bad t.me/socks url: {:?}", url); + } +} + /// Decodes a [`DCBACKUP_SCHEME`] QR code. /// /// The format of this scheme is `DCBACKUP:`. The encoding is the @@ -649,6 +683,15 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { .set_config_internal(Config::WebrtcInstance, Some(&instance_pattern)) .await?; } + Qr::Socks5Proxy { host, port } => { + context.set_config(Config::Socks5Host, Some(&host)).await?; + context + .set_config_u32(Config::Socks5Port, port as u32) + .await?; + context.set_config(Config::Socks5User, None).await?; + context.set_config(Config::Socks5Password, None).await?; + context.set_config_bool(Config::Socks5Enabled, true).await?; + } Qr::WithdrawVerifyContact { invitenumber, authcode, @@ -870,6 +913,7 @@ mod tests { use super::*; use crate::aheader::EncryptPreference; use crate::chat::{create_group_chat, ProtectionStatus}; + use crate::config::Config; use crate::key::DcKey; use crate::securejoin::get_securejoin_qr; use crate::test_utils::{alice_keypair, TestContext}; @@ -1478,6 +1522,52 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_decode_tg_socks_proxy() -> Result<()> { + let t = TestContext::new().await; + + let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?; + assert_eq!( + qr, + Qr::Socks5Proxy { + host: "84.53.239.95".to_string(), + port: 4145 + } + ); + + let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?; + assert_eq!( + qr, + Qr::Socks5Proxy { + host: "foo.bar".to_string(), + port: 123 + } + ); + + let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?; + assert_eq!( + qr, + Qr::Socks5Proxy { + host: "foo.baz".to_string(), + port: 1080 + } + ); + + // wrong domain results in Qr:Url instead of Qr::Socks5Proxy + let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?; + assert_eq!( + qr, + Qr::Url { + url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string() + } + ); + + let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await; + assert!(qr.is_err()); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_account_bad_scheme() { let ctx = TestContext::new().await; @@ -1498,7 +1588,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_config_from_qr() -> Result<()> { + async fn test_set_webrtc_instance_config_from_qr() -> Result<()> { let ctx = TestContext::new().await; assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); @@ -1528,4 +1618,37 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + 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); + + 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(Config::Socks5Host).await?, + Some("foo".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); + + t.set_config(Config::Socks5User, Some("alice")).await?; + t.set_config(Config::Socks5Password, Some("secret")).await?; + 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(Config::Socks5Host).await?, + Some("1.2.3.4".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); + + Ok(()) + } }