1use crate::{InvocationError, TransportKind};
10use tokio::net::TcpStream;
11
12#[derive(Clone, Debug)]
14pub struct MtProxyConfig {
15 pub host: String,
17 pub port: u16,
19 pub secret: Vec<u8>,
21 pub transport: TransportKind,
23}
24
25impl MtProxyConfig {
26 pub async fn connect(&self) -> Result<TcpStream, InvocationError> {
29 let addr = format!("{}:{}", self.host, self.port);
30 tracing::debug!("[layer] MTProxy TCP connect → {addr}");
31 TcpStream::connect(&addr).await.map_err(InvocationError::Io)
32 }
33
34 pub fn addr(&self) -> String {
36 format!("{}:{}", self.host, self.port)
37 }
38}
39
40pub fn parse_proxy_link(url: &str) -> Option<MtProxyConfig> {
42 let query = url
43 .strip_prefix("tg://proxy?")
44 .or_else(|| url.strip_prefix("https://t.me/proxy?"))?;
45
46 let mut server = None;
47 let mut port: Option<u16> = None;
48 let mut secret_hex = None;
49
50 for pair in query.split('&') {
51 if let Some((k, v)) = pair.split_once('=') {
52 match k {
53 "server" => server = Some(v.to_string()),
54 "port" => port = v.parse().ok(),
55 "secret" => secret_hex = Some(v.to_string()),
56 _ => {}
57 }
58 }
59 }
60
61 let host = server?;
62 let port = port?;
63 let secret = decode_secret_hex(&secret_hex?)?;
64 let transport = secret_to_transport(&secret);
65 Some(MtProxyConfig {
66 host,
67 port,
68 secret,
69 transport,
70 })
71}
72
73fn decode_secret_hex(s: &str) -> Option<Vec<u8>> {
74 if s.len() >= 32 && s.chars().all(|c| c.is_ascii_hexdigit()) {
75 let bytes: Option<Vec<u8>> = (0..s.len())
76 .step_by(2)
77 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
78 .collect();
79 if let Some(b) = bytes {
80 return Some(b);
81 }
82 }
83 use base64::Engine as _;
84 base64::engine::general_purpose::URL_SAFE_NO_PAD
85 .decode(s.trim_end_matches('='))
86 .ok()
87}
88
89pub fn secret_to_transport(secret: &[u8]) -> TransportKind {
91 match secret.first() {
92 Some(&0xDD) => {
93 let key = extract_key_bytes(secret, 1);
94 TransportKind::PaddedIntermediate { secret: key }
95 }
96 Some(&0xEE) => {
97 let key = extract_key_bytes(secret, 1);
98 let domain = if secret.len() > 17 {
99 String::from_utf8_lossy(&secret[17..]).into_owned()
100 } else {
101 String::new()
102 };
103 match key {
104 Some(k) => TransportKind::FakeTls { secret: k, domain },
105 None => TransportKind::Obfuscated { secret: None },
106 }
107 }
108 _ => {
109 let key = extract_key_bytes(secret, 0);
110 TransportKind::Obfuscated { secret: key }
111 }
112 }
113}
114
115fn extract_key_bytes(secret: &[u8], offset: usize) -> Option<[u8; 16]> {
116 let slice = secret.get(offset..offset + 16)?;
117 let mut arr = [0u8; 16];
118 arr.copy_from_slice(slice);
119 Some(arr)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn parse_plain_secret() {
128 let url = "tg://proxy?server=1.2.3.4&port=443&secret=deadbeefdeadbeefdeadbeefdeadbeef";
129 let cfg = parse_proxy_link(url).unwrap();
130 assert_eq!(cfg.host, "1.2.3.4");
131 assert_eq!(cfg.port, 443);
132 assert!(matches!(cfg.transport, TransportKind::Obfuscated { .. }));
133 assert_eq!(cfg.addr(), "1.2.3.4:443");
134 }
135
136 #[test]
137 fn parse_dd_secret() {
138 let url =
139 "tg://proxy?server=p.example.com&port=8888&secret=dddeadbeefdeadbeefdeadbeefdeadbeef";
140 let cfg = parse_proxy_link(url).unwrap();
141 assert!(matches!(
142 cfg.transport,
143 TransportKind::PaddedIntermediate { .. }
144 ));
145 }
146
147 #[test]
148 fn parse_ee_secret() {
149 let mut raw = vec![0xeeu8];
150 raw.extend_from_slice(&[0xabu8; 16]);
151 raw.extend_from_slice(b"example.com");
152 let hex: String = raw.iter().map(|b| format!("{b:02x}")).collect();
153 let url = format!("tg://proxy?server=p.example.com&port=443&secret={hex}");
154 let cfg = parse_proxy_link(&url).unwrap();
155 if let TransportKind::FakeTls { domain, .. } = &cfg.transport {
156 assert_eq!(domain, "example.com");
157 } else {
158 panic!("expected FakeTls");
159 }
160 }
161
162 #[test]
163 fn invalid_url_returns_none() {
164 assert!(parse_proxy_link("https://example.com").is_none());
165 assert!(parse_proxy_link("tg://proxy?server=x&port=443").is_none());
166 }
167}