recognize t.me proxy qr codes (#5895)

this PR adds the type DC_QR_SOCKS5_PROXY to `dc_check_qr()` for
**supporting telegram proxy QR codes**. if returned, the UI should ask
the user if they want to us the proxy and call
`dc_set_config_from_qr();` afterwards (plus maybe `dc_configure()`).

idea is to improve our proxy story, follow ups may be:

- in UI, - move proxy out of "Account & Password", as a **separate
"Proxy Activity"** (it should stay in "Advanced" for now, however, below
"Server", which might be moved up)

- allow **opening the "Proxy Activity" from the welcome screens**
three-dot-menu (that would also solve a long standing issue that
entering the email address bypasses the proxy

- show proxy usage in the "Connectivity View" and/or add an **icon** to
the main chatlist screen (beside three-dot menu) in case some proxy is
in use; tapping this icon will open the "Proxy Activity"

- the the new "Proxy Activity", add a **share / show proxy QR code**
button. that would generate invite links in the form
`https://i.delta.chat/socks#...` - so that tapping then opens the app.
support for these links need to be added to core then.

- handle a list of proxies in core, offer selection in UI. the list
could be one for all profiles and could be filled eg. by normal invite
links or other channels

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
This commit is contained in:
bjoern
2024-08-23 09:49:49 +02:00
committed by GitHub
parent 3599e4be16
commit 6b3de9d7da
6 changed files with 230 additions and 2 deletions

View File

@@ -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;

View File

@@ -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<Cow<str>> {
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,

View File

@@ -44,6 +44,12 @@ pub enum QrObject {
domain: String,
instance_pattern: String,
},
Socks5Proxy {
host: String,
port: u16,
user: Option<String>,
pass: Option<String>,
},
Addr {
contact_id: u32,
draft: Option<String>,
@@ -152,6 +158,17 @@ impl From<Qr> for QrObject {
domain,
instance_pattern,
},
Qr::Socks5Proxy {
host,
port,
user,
pass,
} => QrObject::Socks5Proxy {
host,
port,
user,
pass,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }

View File

@@ -136,6 +136,7 @@ module.exports = {
DC_QR_LOGIN: 520,
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,

View File

@@ -136,6 +136,7 @@ export enum C {
DC_QR_LOGIN = 520,
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,

197
src/qr.rs
View File

@@ -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,21 @@ pub enum Qr {
instance_pattern: String,
},
/// Ask the user if they want to add or use the given SOCKS5 proxy
Socks5Proxy {
/// SOCKS5 server
host: String,
/// SOCKS5 port
port: u16,
/// SOCKS5 user
user: Option<String>,
/// SOCKS5 password
pass: Option<String>,
},
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
@@ -277,6 +293,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
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 +557,39 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
}
}
/// 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<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 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);
} else if key == "user" {
user = Some(value.to_string());
} else if key == "pass" {
pass = Some(value.to_string());
}
}
if let Some(host) = host {
Ok(Qr::Socks5Proxy {
host,
port,
user,
pass,
})
} else {
bail!("Bad t.me/socks url: {:?}", url);
}
}
/// Decodes a [`DCBACKUP_SCHEME`] QR code.
///
/// The format of this scheme is `DCBACKUP:<encoded ticket>`. The encoding is the
@@ -649,6 +700,29 @@ 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,
} => {
// disable proxy before changing settings to not use a combination of old and new
context
.set_config_bool(Config::Socks5Enabled, false)
.await?;
context.set_config(Config::Socks5Host, Some(&host)).await?;
context
.set_config_u32(Config::Socks5Port, u32::from(port))
.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?;
}
Qr::WithdrawVerifyContact {
invitenumber,
authcode,
@@ -870,6 +944,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 +1553,73 @@ 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,
user: None,
pass: None,
}
);
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,
user: None,
pass: None,
}
);
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,
user: None,
pass: None,
}
);
let qr = check_qr(
&t,
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
)
.await?;
assert_eq!(
qr,
Qr::Socks5Proxy {
host: "foo.baz".to_string(),
port: 12345,
user: Some("ada".to_string()),
pass: Some("ms!/$".to_string()),
}
);
// 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 +1640,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 +1670,57 @@ 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);
// 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?;
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);
// make sure, user&password are set when specified in the URL
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())
);
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
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())
);
Ok(())
}
}