1use hyper::body::Incoming;
2use hyper::server::conn::http1;
3use hyper::service::service_fn;
4use hyper::{Method, Request, Response, StatusCode};
5use hyper_util::rt::TokioIo;
6use std::io;
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::{TcpListener, TcpStream};
11use base64::Engine;
12use tracing::{debug, error, info};
13
14#[derive(Debug, Clone)]
19struct ProxyConfig {
20 bind_addr: SocketAddr,
21 upstream_proxy: Option<String>,
22 upstream_username: Option<String>,
23 upstream_password: Option<String>,
24}
25
26impl Default for ProxyConfig {
27 fn default() -> Self {
28 Self {
29 bind_addr: "127.0.0.1:0".parse().unwrap(),
30 upstream_proxy: None,
31 upstream_username: None,
32 upstream_password: None,
33 }
34 }
35}
36
37impl ProxyConfig {
38 fn from_proxy_url(url: &str) -> Result<Self, String> {
39 let rest = url
40 .strip_prefix("https://")
41 .or_else(|| url.strip_prefix("http://"))
42 .ok_or_else(|| format!("Unsupported or missing scheme in proxy URL: {url}"))?;
43
44 let (creds, hostport) = if let Some(at) = rest.rfind('@') {
45 (Some(&rest[..at]), &rest[at + 1..])
46 } else {
47 (None, rest)
48 };
49
50 let (username, password) = match creds {
51 Some(c) => {
52 if let Some((u, p)) = c.split_once(':') {
53 (Some(u.to_string()), Some(p.to_string()))
54 } else {
55 (Some(c.to_string()), None)
56 }
57 }
58 None => (None, None),
59 };
60
61 if hostport.is_empty() {
62 return Err("Missing host in proxy URL".into());
63 }
64
65 Ok(Self {
66 bind_addr: "127.0.0.1:0".parse().unwrap(),
67 upstream_proxy: Some(hostport.to_string()),
68 upstream_username: username,
69 upstream_password: password,
70 })
71 }
72
73}
74
75struct ProxyServer {
80 config: Arc<ProxyConfig>,
81}
82
83impl ProxyServer {
84 fn new(config: ProxyConfig) -> Self {
85 Self { config: Arc::new(config) }
86 }
87
88 async fn start_background(self) -> io::Result<SocketAddr> {
89 let listener = TcpListener::bind(self.config.bind_addr).await?;
90 let addr = listener.local_addr()?;
91 info!("Local proxy overlay listening on {}", addr);
92 tokio::spawn(async move {
93 if let Err(e) = self.run(listener).await {
94 error!("Proxy server exited: {}", e);
95 }
96 });
97 Ok(addr)
98 }
99
100 async fn run(&self, listener: TcpListener) -> Result<(), Box<dyn std::error::Error>> {
101 loop {
102 let (stream, client_addr) = listener.accept().await?;
103 let config = self.config.clone();
104 tokio::spawn(async move {
105 let io = TokioIo::new(stream);
106 let service = service_fn(move |req| {
107 let config = config.clone();
108 async move { handle_request(req, config, client_addr).await }
109 });
110 if let Err(e) = http1::Builder::new()
111 .preserve_header_case(true)
112 .title_case_headers(true)
113 .serve_connection(io, service)
114 .with_upgrades()
115 .await
116 {
117 error!("Connection error from {}: {}", client_addr, e);
118 }
119 });
120 }
121 }
122}
123
124async fn handle_request(
129 req: Request<Incoming>,
130 config: Arc<ProxyConfig>,
131 client_addr: SocketAddr,
132) -> Result<Response<String>, hyper::Error> {
133 let method = req.method().clone();
134 let uri = req.uri().clone();
135 debug!("[{}] {} {}", client_addr, method, uri);
136
137 if method != Method::CONNECT {
138 return Ok(Response::builder()
139 .status(StatusCode::METHOD_NOT_ALLOWED)
140 .body("Only CONNECT is supported".into())
141 .unwrap());
142 }
143
144 let destination = uri
145 .authority()
146 .map(|a| a.as_str().to_string())
147 .unwrap_or_default();
148
149 if destination.is_empty() {
150 return Ok(Response::builder()
151 .status(StatusCode::BAD_REQUEST)
152 .body("Missing CONNECT destination".into())
153 .unwrap());
154 }
155
156 debug!("[{}] CONNECT → {}", client_addr, destination);
157
158 tokio::spawn(async move {
159 match hyper::upgrade::on(req).await {
160 Ok(upgraded) => {
161 let io = TokioIo::new(upgraded);
162 if let Err(e) = handle_tunnel(io, destination, config).await {
163 error!("[{}] Tunnel error: {}", client_addr, e);
164 }
165 }
166 Err(e) => error!("[{}] Upgrade error: {}", client_addr, e),
167 }
168 });
169
170 Ok(Response::builder()
171 .status(StatusCode::OK)
172 .body(String::new())
173 .unwrap())
174}
175
176async fn handle_tunnel(
181 client: TokioIo<hyper::upgrade::Upgraded>,
182 destination: String,
183 config: Arc<ProxyConfig>,
184) -> io::Result<()> {
185 let mut upstream = match &config.upstream_proxy {
186 Some(proxy_addr) => {
187 connect_via_upstream(
188 proxy_addr,
189 &destination,
190 config.upstream_username.as_deref(),
191 config.upstream_password.as_deref(),
192 )
193 .await?
194 }
195 None => TcpStream::connect(&destination).await?,
196 };
197
198 let (mut cr, mut cw) = tokio::io::split(client);
199 let (mut ur, mut uw) = upstream.split();
200
201 match tokio::try_join!(
202 tokio::io::copy(&mut cr, &mut uw),
203 tokio::io::copy(&mut ur, &mut cw),
204 ) {
205 Ok((up, down)) => {
206 debug!("Tunnel closed {}: ↑{}B ↓{}B", destination, up, down);
207 Ok(())
208 }
209 Err(e) => Err(e),
210 }
211}
212
213async fn connect_via_upstream(
214 proxy_addr: &str,
215 destination: &str,
216 username: Option<&str>,
217 password: Option<&str>,
218) -> io::Result<TcpStream> {
219 let mut stream = TcpStream::connect(proxy_addr).await?;
220
221 let auth_header = match (username, password) {
222 (Some(u), Some(p)) => {
223 let encoded = base64::engine::general_purpose::STANDARD
224 .encode(format!("{u}:{p}"));
225 format!("Proxy-Authorization: Basic {encoded}\r\n")
226 }
227 _ => String::new(),
228 };
229
230 let req = format!(
231 "CONNECT {destination} HTTP/1.1\r\nHost: {destination}\r\n{auth_header}Connection: keep-alive\r\n\r\n"
232 );
233 stream.write_all(req.as_bytes()).await?;
234
235 let mut buf = vec![0u8; 4096];
236 let n = stream.read(&mut buf).await?;
237 let resp = String::from_utf8_lossy(&buf[..n]);
238 if !resp.contains("200") {
239 return Err(io::Error::new(
240 io::ErrorKind::ConnectionRefused,
241 format!(
242 "Upstream proxy rejected CONNECT: {}",
243 resp.lines().next().unwrap_or("unknown")
244 ),
245 ));
246 }
247
248 Ok(stream)
249}
250
251pub async fn start_overlay(proxy_url: &str) -> Result<SocketAddr, Box<dyn std::error::Error>> {
259 let config = ProxyConfig::from_proxy_url(proxy_url)
260 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
261 let addr = ProxyServer::new(config).start_background().await?;
262 Ok(addr)
263}
264
265#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn parse_http_url_with_creds() {
275 let c = ProxyConfig::from_proxy_url("http://alice:secret@proxy.example.com:8080").unwrap();
276 assert_eq!(c.upstream_proxy.unwrap(), "proxy.example.com:8080");
277 assert_eq!(c.upstream_username.unwrap(), "alice");
278 assert_eq!(c.upstream_password.unwrap(), "secret");
279 }
280
281 #[test]
282 fn parse_https_url_no_creds() {
283 let c = ProxyConfig::from_proxy_url("https://10.0.0.1:3128").unwrap();
284 assert_eq!(c.upstream_proxy.unwrap(), "10.0.0.1:3128");
285 assert!(c.upstream_username.is_none());
286 }
287
288 #[test]
289 fn parse_invalid_scheme() {
290 assert!(ProxyConfig::from_proxy_url("socks5://host:1080").is_err());
291 }
292
293 #[test]
294 fn default_config() {
295 let c = ProxyConfig::default();
296 assert_eq!(c.bind_addr.to_string(), "127.0.0.1:0");
297 assert!(c.upstream_proxy.is_none());
298 }
299}