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