1use std::{net::SocketAddr, sync::Arc};
2
3use tokio::{
4 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
5 net::{TcpListener, TcpStream},
6 sync::watch,
7};
8use tracing::{debug, info, warn};
9
10use crate::{allowlist::DomainAllowlist, error::ProxyError};
11
12pub struct ProxyServer {
18 listener: TcpListener,
19 allowlist: Arc<DomainAllowlist>,
20 shutdown_rx: watch::Receiver<bool>,
21}
22
23impl ProxyServer {
24 pub async fn bind(
26 allowlist: DomainAllowlist,
27 shutdown_rx: watch::Receiver<bool>,
28 ) -> Result<(Self, u16), ProxyError> {
29 let listener = TcpListener::bind("127.0.0.1:0")
30 .await
31 .map_err(ProxyError::Bind)?;
32 let port = listener.local_addr().map_err(ProxyError::Bind)?.port();
33
34 info!(port, "sbe proxy listening");
35
36 Ok((
37 Self {
38 listener,
39 allowlist: Arc::new(allowlist),
40 shutdown_rx,
41 },
42 port,
43 ))
44 }
45
46 pub async fn run(self) -> Result<(), ProxyError> {
48 let mut shutdown = self.shutdown_rx;
49
50 loop {
51 tokio::select! {
52 result = self.listener.accept() => {
53 let (stream, addr): (TcpStream, SocketAddr) = result.map_err(ProxyError::Accept)?;
54 let allowlist = Arc::clone(&self.allowlist);
55 tokio::spawn(async move {
56 if let Err(e) = handle_connection(stream, addr, &allowlist).await {
57 debug!(error = %e, "proxy connection error");
58 }
59 });
60 }
61 _ = shutdown.changed() => {
62 info!("sbe proxy shutting down");
63 break;
64 }
65 }
66 }
67 Ok(())
68 }
69}
70
71async fn handle_connection(
76 client: TcpStream,
77 addr: SocketAddr,
78 allowlist: &DomainAllowlist,
79) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
80 let mut client = BufReader::new(client);
82
83 let mut request_line = String::new();
85 client.read_line(&mut request_line).await?;
86 let request_line = request_line.trim().to_owned();
87
88 if request_line.is_empty() {
89 return Ok(());
90 }
91
92 let parts: Vec<&str> = request_line.split_whitespace().collect();
93 if parts.len() < 2 {
94 client
95 .write_all(b"HTTP/1.1 400 Bad Request\r\n\r\n")
96 .await?;
97 return Ok(());
98 }
99
100 let method = parts[0].to_uppercase();
101 let target = parts[1].to_owned();
102
103 if method == "CONNECT" {
104 handle_connect(client, addr, &target, allowlist).await
105 } else {
106 client
107 .write_all(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n")
108 .await?;
109 warn!(method = %method, addr = %addr, "rejected non-CONNECT request");
110 Ok(())
111 }
112}
113
114async fn handle_connect(
116 mut client: BufReader<TcpStream>,
117 addr: SocketAddr,
118 target: &str,
119 allowlist: &DomainAllowlist,
120) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
121 let (host, port) = parse_host_port(target)?;
123
124 let mut header_line = String::new();
126 loop {
127 header_line.clear();
128 client.read_line(&mut header_line).await?;
129 if header_line.trim().is_empty() {
130 break;
131 }
132 }
133
134 if !allowlist.is_allowed(&host) {
136 warn!(
137 host = %host,
138 port = port,
139 client = %addr,
140 "blocked connection to non-allowed domain"
141 );
142 let response = format!(
143 "HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nsbe: \
144 domain '{host}' is not in the allowlist\n"
145 );
146 client.write_all(response.as_bytes()).await?;
147 return Ok(());
148 }
149
150 let upstream_addr = format!("{host}:{port}");
152 let mut upstream = TcpStream::connect(&upstream_addr).await.map_err(|e| {
153 Box::new(ProxyError::UpstreamConnect {
154 host: host.clone(),
155 port,
156 source: e,
157 })
158 })?;
159
160 client
162 .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
163 .await?;
164 client.flush().await?;
165
166 debug!(host = %host, port = port, client = %addr, "tunnel established");
167
168 let buffered = client.buffer().to_vec();
170 if !buffered.is_empty() {
171 upstream.write_all(&buffered).await?;
172 }
173
174 let mut client_stream = client.into_inner();
176
177 let _ = tokio::io::copy_bidirectional(&mut client_stream, &mut upstream).await;
179
180 Ok(())
181}
182
183fn parse_host_port(
185 target: &str,
186) -> Result<(String, u16), Box<dyn std::error::Error + Send + Sync>> {
187 if let Some(bracket_end) = target.find("]:") {
189 let host = target[1..bracket_end].to_owned();
190 let port: u16 = target[bracket_end + 2..].parse()?;
191 return Ok((host, port));
192 }
193
194 let mut parts = target.rsplitn(2, ':');
195 let port_str = parts.next().ok_or("missing port")?;
196 let host = parts.next().ok_or("missing host")?;
197 let port: u16 = port_str.parse()?;
198 Ok((host.to_owned(), port))
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_should_parse_host_port() {
207 let (host, port) = parse_host_port("registry.npmjs.org:443").unwrap();
208 assert_eq!(host, "registry.npmjs.org");
209 assert_eq!(port, 443);
210 }
211
212 #[test]
213 fn test_should_parse_host_port_8000() {
214 let (host, port) = parse_host_port("evil.com:8000").unwrap();
215 assert_eq!(host, "evil.com");
216 assert_eq!(port, 8000);
217 }
218}