From 624ae869130d332505b2923414b553f8746a0f5b Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 21 Sep 2024 18:26:07 +0000 Subject: [PATCH] api!: make QR code type for proxy not specific to SOCKS5 (#5980) --- deltachat-ffi/deltachat.h | 2 +- deltachat-ffi/src/lot.rs | 45 ++--- deltachat-jsonrpc/src/api/types/qr.rs | 17 +- node/constants.js | 2 +- node/lib/constants.ts | 2 +- src/net/proxy.rs | 6 +- src/qr.rs | 267 ++++++++++++++++++++------ 7 files changed, 233 insertions(+), 108 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index e4f57662ed..f5450dddd5 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2501,7 +2501,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_PROXY 271 // text1=address (e.g. "127.0.0.1:9050") #define DC_QR_ADDR 320 // id=contact #define DC_QR_TEXT 330 // text1=text #define DC_QR_URL 332 // text1=URL diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index 9f891c7c0b..6c283404b6 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -34,44 +34,41 @@ pub enum Meaning { } impl Lot { - pub fn get_text1(&self) -> Option<&str> { + pub fn get_text1(&self) -> Option> { match self { Self::Summary(summary) => match &summary.prefix { None => None, - Some(SummaryPrefix::Draft(text)) => Some(text), - Some(SummaryPrefix::Username(username)) => Some(username), - Some(SummaryPrefix::Me(text)) => Some(text), + Some(SummaryPrefix::Draft(text)) => Some(Cow::Borrowed(text)), + Some(SummaryPrefix::Username(username)) => Some(Cow::Borrowed(username)), + Some(SummaryPrefix::Me(text)) => Some(Cow::Borrowed(text)), }, Self::Qr(qr) => match qr { Qr::AskVerifyContact { .. } => None, - Qr::AskVerifyGroup { grpname, .. } => Some(grpname), + Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)), Qr::FprOk { .. } => None, Qr::FprMismatch { .. } => None, - Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint), - Qr::Account { domain } => Some(domain), + Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)), + Qr::Account { domain } => Some(Cow::Borrowed(domain)), 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), + Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)), + Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))), + Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed), + Qr::Url { url } => Some(Cow::Borrowed(url)), + Qr::Text { text } => Some(Cow::Borrowed(text)), Qr::WithdrawVerifyContact { .. } => None, - Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname), + Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)), Qr::ReviveVerifyContact { .. } => None, - Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname), - Qr::Login { address, .. } => Some(address), + Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)), + Qr::Login { address, .. } => Some(Cow::Borrowed(address)), }, - Self::Error(err) => Some(err), + Self::Error(err) => Some(Cow::Borrowed(err)), } } pub fn get_text2(&self) -> Option> { match self { Self::Summary(summary) => Some(summary.truncated_text(160)), - Self::Qr(qr) => match qr { - Qr::Socks5Proxy { port, .. } => Some(Cow::Owned(format!("{port}"))), - _ => None, - }, + Self::Qr(_) => None, Self::Error(_) => None, } } @@ -107,7 +104,7 @@ impl Lot { Qr::Account { .. } => LotState::QrAccount, Qr::Backup2 { .. } => LotState::QrBackup2, Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance, - Qr::Socks5Proxy { .. } => LotState::QrSocks5Proxy, + Qr::Proxy { .. } => LotState::QrProxy, Qr::Addr { .. } => LotState::QrAddr, Qr::Url { .. } => LotState::QrUrl, Qr::Text { .. } => LotState::QrText, @@ -133,7 +130,7 @@ impl Lot { Qr::Account { .. } => Default::default(), Qr::Backup2 { .. } => Default::default(), Qr::WebrtcInstance { .. } => Default::default(), - Qr::Socks5Proxy { .. } => Default::default(), + Qr::Proxy { .. } => Default::default(), Qr::Addr { contact_id, .. } => contact_id.to_u32(), Qr::Url { .. } => Default::default(), Qr::Text { .. } => Default::default(), @@ -188,8 +185,8 @@ pub enum LotState { /// text1=domain, text2=instance pattern QrWebrtcInstance = 260, - /// text1=host, text2=port - QrSocks5Proxy = 270, + /// text1=address, text2=protocol + QrProxy = 271, /// id=contact QrAddr = 320, diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index 863debcf38..72ac25ee00 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -41,11 +41,10 @@ pub enum QrObject { domain: String, instance_pattern: String, }, - Socks5Proxy { + Proxy { + url: String, host: String, port: u16, - user: Option, - pass: Option, }, Addr { contact_id: u32, @@ -152,17 +151,7 @@ impl From for QrObject { domain, instance_pattern, }, - Qr::Socks5Proxy { - host, - port, - user, - pass, - } => QrObject::Socks5Proxy { - host, - port, - user, - pass, - }, + Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port }, Qr::Addr { contact_id, draft } => { let contact_id = contact_id.to_u32(); QrObject::Addr { contact_id, draft } diff --git a/node/constants.js b/node/constants.js index de5f64b715..1eaed5ccb5 100644 --- a/node/constants.js +++ b/node/constants.js @@ -134,9 +134,9 @@ module.exports = { DC_QR_FPR_OK: 210, DC_QR_FPR_WITHOUT_ADDR: 230, DC_QR_LOGIN: 520, + DC_QR_PROXY: 271, DC_QR_REVIVE_VERIFYCONTACT: 510, DC_QR_REVIVE_VERIFYGROUP: 512, - DC_QR_SOCKS5_PROXY: 270, DC_QR_TEXT: 330, DC_QR_URL: 332, DC_QR_WEBRTC_INSTANCE: 260, diff --git a/node/lib/constants.ts b/node/lib/constants.ts index 01d2c349cc..c901a0c7b3 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -134,9 +134,9 @@ export enum C { DC_QR_FPR_OK = 210, DC_QR_FPR_WITHOUT_ADDR = 230, DC_QR_LOGIN = 520, + DC_QR_PROXY = 271, DC_QR_REVIVE_VERIFYCONTACT = 510, DC_QR_REVIVE_VERIFYGROUP = 512, - DC_QR_SOCKS5_PROXY = 270, DC_QR_TEXT = 330, DC_QR_URL = 332, DC_QR_WEBRTC_INSTANCE = 260, diff --git a/src/net/proxy.rs b/src/net/proxy.rs index 1689d75596..1f5a56c244 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -5,7 +5,7 @@ use std::fmt; use std::pin::Pin; -use anyhow::{bail, ensure, format_err, Context as _, Result}; +use anyhow::{bail, format_err, Context as _, Result}; use base64::Engine; use bytes::{BufMut, BytesMut}; use fast_socks5::client::Socks5Stream; @@ -113,10 +113,6 @@ pub struct HttpConfig { impl HttpConfig { fn from_url(url: Url) -> Result { - ensure!( - matches!(url.scheme(), "http" | "https"), - "Cannot create HTTP proxy config from non-HTTP URL" - ); let host = url .host_str() .context("HTTP proxy URL has no host")? diff --git a/src/qr.rs b/src/qr.rs index b2aa8e4d56..ed9d5dcb39 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -36,8 +36,8 @@ const MAILTO_SCHEME: &str = "mailto:"; const MATMSG_SCHEME: &str = "MATMSG:"; const VCARD_SCHEME: &str = "BEGIN:VCARD"; const SMTP_SCHEME: &str = "SMTP:"; -const HTTP_SCHEME: &str = "http://"; const HTTPS_SCHEME: &str = "https://"; +const SHADOWSOCKS_SCHEME: &str = "ss://"; /// Backup transfer based on iroh-net. pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:"; @@ -127,19 +127,26 @@ pub enum Qr { instance_pattern: String, }, - /// Ask the user if they want to add or use the given SOCKS5 proxy - Socks5Proxy { - /// SOCKS5 server + /// Ask the user if they want to use the given proxy. + /// + /// Note that HTTP(S) URLs without a path + /// and query parameters are treated as HTTP(S) proxy URL. + /// UI may want to still offer to open the URL + /// in the browser if QR code contents + /// starts with `http://` or `https://` + /// and the QR code was not scanned from + /// the proxy configuration screen. + Proxy { + /// Proxy URL. + /// + /// This is the URL that is going to be added. + url: String, + + /// Host extracted from the URL to display in the UI. host: String, - /// SOCKS5 port + /// Port extracted from the URL to display in the UI. port: u16, - - /// SOCKS5 user - user: Option, - - /// SOCKS5 password - pass: Option, }, /// Contact address is scanned. @@ -279,6 +286,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result { decode_webrtc_instance(context, qr)? } else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) { decode_tg_socks_proxy(context, qr)? + } else if qr.starts_with(SHADOWSOCKS_SCHEME) { + decode_shadowsocks_proxy(qr)? } else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) { decode_backup2(qr)? } else if qr.starts_with(MAILTO_SCHEME) { @@ -289,9 +298,44 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result { decode_matmsg(context, qr).await? } else if qr.starts_with(VCARD_SCHEME) { decode_vcard(context, qr).await? - } else if qr.starts_with(HTTP_SCHEME) || qr.starts_with(HTTPS_SCHEME) { - Qr::Url { - url: qr.to_string(), + } else if let Ok(url) = url::Url::parse(qr) { + match url.scheme() { + "socks5" => Qr::Proxy { + url: qr.to_string(), + host: url.host_str().context("URL has no host")?.to_string(), + port: url.port().unwrap_or(DEFAULT_SOCKS_PORT), + }, + "http" | "https" => { + // Parsing with a non-standard scheme + // is a hack to work around the `url` crate bug + // . + let url = if let Some(rest) = qr.strip_prefix("http://") { + url::Url::parse(&format!("foobarbaz://{rest}"))? + } else if let Some(rest) = qr.strip_prefix("https://") { + url::Url::parse(&format!("foobarbaz://{rest}"))? + } else { + // Should not happen. + url + }; + + if url.port().is_none() | (url.path() != "") | url.query().is_some() { + // URL without a port, with a path or query cannot be a proxy URL. + Qr::Url { + url: qr.to_string(), + } + } else { + Qr::Proxy { + url: qr.to_string(), + host: url.host_str().context("URL has no host")?.to_string(), + port: url + .port_or_known_default() + .context("HTTP(S) URLs are guaranteed to return Some port")?, + } + } + } + _ => Qr::Url { + url: qr.to_string(), + }, } } else { Qr::Text { @@ -558,16 +602,35 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result { } } - if let Some(host) = host { - Ok(Qr::Socks5Proxy { - host, - port, - user, - pass, - }) - } else { + let Some(host) = host else { bail!("Bad t.me/socks url: {:?}", url); - } + }; + + let mut url = "socks5://".to_string(); + if let Some(pass) = pass { + url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC).to_string(); + url += ":"; + url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string(); + url += "@"; + }; + url += &host; + url += ":"; + url += &port.to_string(); + + Ok(Qr::Proxy { url, host, port }) +} + +/// Decodes `ss://` URLs for Shadowsocks proxies. +fn decode_shadowsocks_proxy(qr: &str) -> Result { + let server_config = shadowsocks::config::ServerConfig::from_url(qr)?; + let addr = server_config.addr(); + let host = addr.host().to_string(); + let port = addr.port(); + Ok(Qr::Proxy { + url: qr.to_string(), + host, + port, + }) } /// Decodes a [`DCBACKUP2_SCHEME`] QR code. @@ -655,33 +718,16 @@ 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, - user, - pass, - } => { - 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(); - + Qr::Proxy { url, .. } => { 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()) + let proxy_urls: Vec<&str> = std::iter::once(url.as_str()) .chain( old_proxy_url_value .split('\n') - .filter(|s| !s.is_empty() && *s != proxy_url), + .filter(|s| !s.is_empty() && *s != url), ) .collect(); context @@ -916,11 +962,38 @@ mod tests { async fn test_decode_http() -> Result<()> { let ctx = TestContext::new().await; + let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "http://www.hello.com:80".to_string(), + host: "www.hello.com".to_string(), + port: 80 + } + ); + + // If it has no explicit port, then it is not a proxy. let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?; assert_eq!( qr, Qr::Url { - url: "http://www.hello.com".to_string() + url: "http://www.hello.com".to_string(), + } + ); + + // If it has a path, then it is not a proxy. + let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?; + assert_eq!( + qr, + Qr::Url { + url: "http://www.hello.com/".to_string(), + } + ); + let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?; + assert_eq!( + qr, + Qr::Url { + url: "http://www.hello.com/hello".to_string(), } ); @@ -931,11 +1004,38 @@ mod tests { async fn test_decode_https() -> Result<()> { let ctx = TestContext::new().await; + let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "https://www.hello.com:443".to_string(), + host: "www.hello.com".to_string(), + port: 443 + } + ); + + // If it has no explicit port, then it is not a proxy. let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?; assert_eq!( qr, Qr::Url { - url: "https://www.hello.com".to_string() + url: "https://www.hello.com".to_string(), + } + ); + + // If it has a path, then it is not a proxy. + let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?; + assert_eq!( + qr, + Qr::Url { + url: "https://www.hello.com/".to_string(), + } + ); + let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?; + assert_eq!( + qr, + Qr::Url { + url: "https://www.hello.com/hello".to_string(), } ); @@ -1523,33 +1623,30 @@ mod tests { let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?; assert_eq!( qr, - Qr::Socks5Proxy { + Qr::Proxy { + url: "socks5://84.53.239.95:4145".to_string(), host: "84.53.239.95".to_string(), port: 4145, - user: None, - pass: None, } ); let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?; assert_eq!( qr, - Qr::Socks5Proxy { + Qr::Proxy { + url: "socks5://foo.bar:123".to_string(), host: "foo.bar".to_string(), port: 123, - user: None, - pass: None, } ); let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?; assert_eq!( qr, - Qr::Socks5Proxy { + Qr::Proxy { + url: "socks5://foo.baz:1080".to_string(), host: "foo.baz".to_string(), port: 1080, - user: None, - pass: None, } ); @@ -1560,11 +1657,10 @@ mod tests { .await?; assert_eq!( qr, - Qr::Socks5Proxy { + Qr::Proxy { + url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(), host: "foo.baz".to_string(), port: 12345, - user: Some("ada".to_string()), - pass: Some("ms!/$".to_string()), } ); @@ -1612,10 +1708,6 @@ mod tests { assert!(res.is_err()); assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); - let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await; - assert!(res.is_err()); - assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); - let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await; assert!(res.is_ok()); assert_eq!( @@ -1635,7 +1727,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_socks5_proxy_config_from_qr() -> Result<()> { + async fn test_set_proxy_config_from_qr() -> Result<()> { let t = TestContext::new().await; assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false); @@ -1682,6 +1774,57 @@ mod tests { ) ); + set_config_from_qr( + &t, + "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1", + ) + .await?; + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some( + "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080" + .to_string() + ) + ); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_decode_shadowsocks() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1", + ) + .await?; + assert_eq!( + qr, + Qr::Proxy { + url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(), + host: "192.168.100.1".to_string(), + port: 8888, + } + ); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_decode_socks5() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "socks5://127.0.0.1:9050".to_string(), + host: "127.0.0.1".to_string(), + port: 9050, + } + ); + Ok(()) } }