Skip to content

Commit

Permalink
recognize t.me proxy qr codes
Browse files Browse the repository at this point in the history
  • Loading branch information
r10s committed Aug 19, 2024
1 parent 8538a3c commit ef92fef
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 2 deletions.
5 changes: 5 additions & 0 deletions deltachat-ffi/deltachat.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 10 additions & 1 deletion deltachat-ffi/src/lot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -185,6 +191,9 @@ pub enum LotState {
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,

/// text1=domain:port
QrSocks5Proxy = 270,

/// id=contact
QrAddr = 320,

Expand Down
5 changes: 5 additions & 0 deletions deltachat-jsonrpc/src/api/types/qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub enum QrObject {
domain: String,
instance_pattern: String,
},
Socks5Proxy {
host: String,
port: u16,
},
Addr {
contact_id: u32,
draft: Option<String>,
Expand Down Expand Up @@ -152,6 +156,7 @@ impl From<Qr> 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 }
Expand Down
125 changes: 124 additions & 1 deletion src/qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -277,6 +287,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) {
Expand Down Expand Up @@ -539,6 +551,28 @@ 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;
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:<encoded ticket>`. The encoding is the
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand All @@ -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());
Expand Down Expand Up @@ -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(())
}
}

0 comments on commit ef92fef

Please sign in to comment.