api!: make QR code type for proxy not specific to SOCKS5 (#5980)

This commit is contained in:
link2xt
2024-09-21 18:26:07 +00:00
committed by GitHub
parent b47b96d5d6
commit 624ae86913
7 changed files with 233 additions and 108 deletions

View File

@@ -2501,7 +2501,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_BACKUP 251 #define DC_QR_BACKUP 251
#define DC_QR_BACKUP2 252 #define DC_QR_BACKUP2 252
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern #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_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text #define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL #define DC_QR_URL 332 // text1=URL

View File

@@ -34,44 +34,41 @@ pub enum Meaning {
} }
impl Lot { impl Lot {
pub fn get_text1(&self) -> Option<&str> { pub fn get_text1(&self) -> Option<Cow<str>> {
match self { match self {
Self::Summary(summary) => match &summary.prefix { Self::Summary(summary) => match &summary.prefix {
None => None, None => None,
Some(SummaryPrefix::Draft(text)) => Some(text), Some(SummaryPrefix::Draft(text)) => Some(Cow::Borrowed(text)),
Some(SummaryPrefix::Username(username)) => Some(username), Some(SummaryPrefix::Username(username)) => Some(Cow::Borrowed(username)),
Some(SummaryPrefix::Me(text)) => Some(text), Some(SummaryPrefix::Me(text)) => Some(Cow::Borrowed(text)),
}, },
Self::Qr(qr) => match qr { Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => None, Qr::AskVerifyContact { .. } => None,
Qr::AskVerifyGroup { grpname, .. } => Some(grpname), Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::FprOk { .. } => None, Qr::FprOk { .. } => None,
Qr::FprMismatch { .. } => None, Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint), Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
Qr::Account { domain } => Some(domain), Qr::Account { domain } => Some(Cow::Borrowed(domain)),
Qr::Backup2 { .. } => None, Qr::Backup2 { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(domain), Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
Qr::Socks5Proxy { host, .. } => Some(host), Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
Qr::Addr { draft, .. } => draft.as_deref(), Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
Qr::Url { url } => Some(url), Qr::Url { url } => Some(Cow::Borrowed(url)),
Qr::Text { text } => Some(text), Qr::Text { text } => Some(Cow::Borrowed(text)),
Qr::WithdrawVerifyContact { .. } => None, Qr::WithdrawVerifyContact { .. } => None,
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname), Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::ReviveVerifyContact { .. } => None, Qr::ReviveVerifyContact { .. } => None,
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname), Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::Login { address, .. } => Some(address), 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<Cow<str>> { pub fn get_text2(&self) -> Option<Cow<str>> {
match self { match self {
Self::Summary(summary) => Some(summary.truncated_text(160)), Self::Summary(summary) => Some(summary.truncated_text(160)),
Self::Qr(qr) => match qr { Self::Qr(_) => None,
Qr::Socks5Proxy { port, .. } => Some(Cow::Owned(format!("{port}"))),
_ => None,
},
Self::Error(_) => None, Self::Error(_) => None,
} }
} }
@@ -107,7 +104,7 @@ impl Lot {
Qr::Account { .. } => LotState::QrAccount, Qr::Account { .. } => LotState::QrAccount,
Qr::Backup2 { .. } => LotState::QrBackup2, Qr::Backup2 { .. } => LotState::QrBackup2,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance, Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Socks5Proxy { .. } => LotState::QrSocks5Proxy, Qr::Proxy { .. } => LotState::QrProxy,
Qr::Addr { .. } => LotState::QrAddr, Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl, Qr::Url { .. } => LotState::QrUrl,
Qr::Text { .. } => LotState::QrText, Qr::Text { .. } => LotState::QrText,
@@ -133,7 +130,7 @@ impl Lot {
Qr::Account { .. } => Default::default(), Qr::Account { .. } => Default::default(),
Qr::Backup2 { .. } => Default::default(), Qr::Backup2 { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(), Qr::WebrtcInstance { .. } => Default::default(),
Qr::Socks5Proxy { .. } => Default::default(), Qr::Proxy { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(), Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(), Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(), Qr::Text { .. } => Default::default(),
@@ -188,8 +185,8 @@ pub enum LotState {
/// text1=domain, text2=instance pattern /// text1=domain, text2=instance pattern
QrWebrtcInstance = 260, QrWebrtcInstance = 260,
/// text1=host, text2=port /// text1=address, text2=protocol
QrSocks5Proxy = 270, QrProxy = 271,
/// id=contact /// id=contact
QrAddr = 320, QrAddr = 320,

View File

@@ -41,11 +41,10 @@ pub enum QrObject {
domain: String, domain: String,
instance_pattern: String, instance_pattern: String,
}, },
Socks5Proxy { Proxy {
url: String,
host: String, host: String,
port: u16, port: u16,
user: Option<String>,
pass: Option<String>,
}, },
Addr { Addr {
contact_id: u32, contact_id: u32,
@@ -152,17 +151,7 @@ impl From<Qr> for QrObject {
domain, domain,
instance_pattern, instance_pattern,
}, },
Qr::Socks5Proxy { Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
host,
port,
user,
pass,
} => QrObject::Socks5Proxy {
host,
port,
user,
pass,
},
Qr::Addr { contact_id, draft } => { Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32(); let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft } QrObject::Addr { contact_id, draft }

View File

@@ -134,9 +134,9 @@ module.exports = {
DC_QR_FPR_OK: 210, DC_QR_FPR_OK: 210,
DC_QR_FPR_WITHOUT_ADDR: 230, DC_QR_FPR_WITHOUT_ADDR: 230,
DC_QR_LOGIN: 520, DC_QR_LOGIN: 520,
DC_QR_PROXY: 271,
DC_QR_REVIVE_VERIFYCONTACT: 510, DC_QR_REVIVE_VERIFYCONTACT: 510,
DC_QR_REVIVE_VERIFYGROUP: 512, DC_QR_REVIVE_VERIFYGROUP: 512,
DC_QR_SOCKS5_PROXY: 270,
DC_QR_TEXT: 330, DC_QR_TEXT: 330,
DC_QR_URL: 332, DC_QR_URL: 332,
DC_QR_WEBRTC_INSTANCE: 260, DC_QR_WEBRTC_INSTANCE: 260,

View File

@@ -134,9 +134,9 @@ export enum C {
DC_QR_FPR_OK = 210, DC_QR_FPR_OK = 210,
DC_QR_FPR_WITHOUT_ADDR = 230, DC_QR_FPR_WITHOUT_ADDR = 230,
DC_QR_LOGIN = 520, DC_QR_LOGIN = 520,
DC_QR_PROXY = 271,
DC_QR_REVIVE_VERIFYCONTACT = 510, DC_QR_REVIVE_VERIFYCONTACT = 510,
DC_QR_REVIVE_VERIFYGROUP = 512, DC_QR_REVIVE_VERIFYGROUP = 512,
DC_QR_SOCKS5_PROXY = 270,
DC_QR_TEXT = 330, DC_QR_TEXT = 330,
DC_QR_URL = 332, DC_QR_URL = 332,
DC_QR_WEBRTC_INSTANCE = 260, DC_QR_WEBRTC_INSTANCE = 260,

View File

@@ -5,7 +5,7 @@
use std::fmt; use std::fmt;
use std::pin::Pin; 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 base64::Engine;
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
use fast_socks5::client::Socks5Stream; use fast_socks5::client::Socks5Stream;
@@ -113,10 +113,6 @@ pub struct HttpConfig {
impl HttpConfig { impl HttpConfig {
fn from_url(url: Url) -> Result<Self> { fn from_url(url: Url) -> Result<Self> {
ensure!(
matches!(url.scheme(), "http" | "https"),
"Cannot create HTTP proxy config from non-HTTP URL"
);
let host = url let host = url
.host_str() .host_str()
.context("HTTP proxy URL has no host")? .context("HTTP proxy URL has no host")?

267
src/qr.rs
View File

@@ -36,8 +36,8 @@ const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:"; const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD"; const VCARD_SCHEME: &str = "BEGIN:VCARD";
const SMTP_SCHEME: &str = "SMTP:"; const SMTP_SCHEME: &str = "SMTP:";
const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://"; const HTTPS_SCHEME: &str = "https://";
const SHADOWSOCKS_SCHEME: &str = "ss://";
/// Backup transfer based on iroh-net. /// Backup transfer based on iroh-net.
pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:"; pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:";
@@ -127,19 +127,26 @@ pub enum Qr {
instance_pattern: String, instance_pattern: String,
}, },
/// Ask the user if they want to add or use the given SOCKS5 proxy /// Ask the user if they want to use the given proxy.
Socks5Proxy { ///
/// SOCKS5 server /// 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, host: String,
/// SOCKS5 port /// Port extracted from the URL to display in the UI.
port: u16, port: u16,
/// SOCKS5 user
user: Option<String>,
/// SOCKS5 password
pass: Option<String>,
}, },
/// Contact address is scanned. /// Contact address is scanned.
@@ -279,6 +286,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
decode_webrtc_instance(context, qr)? decode_webrtc_instance(context, qr)?
} else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) { } else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
decode_tg_socks_proxy(context, qr)? 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) { } else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
decode_backup2(qr)? decode_backup2(qr)?
} else if qr.starts_with(MAILTO_SCHEME) { } else if qr.starts_with(MAILTO_SCHEME) {
@@ -289,9 +298,44 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
decode_matmsg(context, qr).await? decode_matmsg(context, qr).await?
} else if qr.starts_with(VCARD_SCHEME) { } else if qr.starts_with(VCARD_SCHEME) {
decode_vcard(context, qr).await? decode_vcard(context, qr).await?
} else if qr.starts_with(HTTP_SCHEME) || qr.starts_with(HTTPS_SCHEME) { } else if let Ok(url) = url::Url::parse(qr) {
Qr::Url { match url.scheme() {
url: qr.to_string(), "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
// <https://github.com/servo/rust-url/issues/957>.
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 { } else {
Qr::Text { Qr::Text {
@@ -558,16 +602,35 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
} }
} }
if let Some(host) = host { let Some(host) = host else {
Ok(Qr::Socks5Proxy {
host,
port,
user,
pass,
})
} else {
bail!("Bad t.me/socks url: {:?}", url); 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<Qr> {
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. /// 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)) .set_config_internal(Config::WebrtcInstance, Some(&instance_pattern))
.await?; .await?;
} }
Qr::Socks5Proxy { Qr::Proxy { url, .. } => {
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();
let old_proxy_url_value = context let old_proxy_url_value = context
.get_config(Config::ProxyUrl) .get_config(Config::ProxyUrl)
.await? .await?
.unwrap_or_default(); .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( .chain(
old_proxy_url_value old_proxy_url_value
.split('\n') .split('\n')
.filter(|s| !s.is_empty() && *s != proxy_url), .filter(|s| !s.is_empty() && *s != url),
) )
.collect(); .collect();
context context
@@ -916,11 +962,38 @@ mod tests {
async fn test_decode_http() -> Result<()> { async fn test_decode_http() -> Result<()> {
let ctx = TestContext::new().await; 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?; let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?;
assert_eq!( assert_eq!(
qr, qr,
Qr::Url { 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<()> { async fn test_decode_https() -> Result<()> {
let ctx = TestContext::new().await; 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?; let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?;
assert_eq!( assert_eq!(
qr, qr,
Qr::Url { 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?; let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
assert_eq!( assert_eq!(
qr, qr,
Qr::Socks5Proxy { Qr::Proxy {
url: "socks5://84.53.239.95:4145".to_string(),
host: "84.53.239.95".to_string(), host: "84.53.239.95".to_string(),
port: 4145, port: 4145,
user: None,
pass: None,
} }
); );
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?; let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
assert_eq!( assert_eq!(
qr, qr,
Qr::Socks5Proxy { Qr::Proxy {
url: "socks5://foo.bar:123".to_string(),
host: "foo.bar".to_string(), host: "foo.bar".to_string(),
port: 123, port: 123,
user: None,
pass: None,
} }
); );
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?; let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
assert_eq!( assert_eq!(
qr, qr,
Qr::Socks5Proxy { Qr::Proxy {
url: "socks5://foo.baz:1080".to_string(),
host: "foo.baz".to_string(), host: "foo.baz".to_string(),
port: 1080, port: 1080,
user: None,
pass: None,
} }
); );
@@ -1560,11 +1657,10 @@ mod tests {
.await?; .await?;
assert_eq!( assert_eq!(
qr, qr,
Qr::Socks5Proxy { Qr::Proxy {
url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(),
host: "foo.baz".to_string(), host: "foo.baz".to_string(),
port: 12345, port: 12345,
user: Some("ada".to_string()),
pass: Some("ms!/$".to_string()),
} }
); );
@@ -1612,10 +1708,6 @@ mod tests {
assert!(res.is_err()); assert!(res.is_err());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); 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; let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!( assert_eq!(
@@ -1635,7 +1727,7 @@ mod tests {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[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; let t = TestContext::new().await;
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false); 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(()) Ok(())
} }
} }