1*cf78ab8cSAndroid Build Coastguard Worker // Copyright 2024 Google LLC
2*cf78ab8cSAndroid Build Coastguard Worker //
3*cf78ab8cSAndroid Build Coastguard Worker // Licensed under the Apache License, Version 2.0 (the "License");
4*cf78ab8cSAndroid Build Coastguard Worker // you may not use this file except in compliance with the License.
5*cf78ab8cSAndroid Build Coastguard Worker // You may obtain a copy of the License at
6*cf78ab8cSAndroid Build Coastguard Worker //
7*cf78ab8cSAndroid Build Coastguard Worker // https://www.apache.org/licenses/LICENSE-2.0
8*cf78ab8cSAndroid Build Coastguard Worker //
9*cf78ab8cSAndroid Build Coastguard Worker // Unless required by applicable law or agreed to in writing, software
10*cf78ab8cSAndroid Build Coastguard Worker // distributed under the License is distributed on an "AS IS" BASIS,
11*cf78ab8cSAndroid Build Coastguard Worker // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*cf78ab8cSAndroid Build Coastguard Worker // See the License for the specific language governing permissions and
13*cf78ab8cSAndroid Build Coastguard Worker // limitations under the License.
14*cf78ab8cSAndroid Build Coastguard Worker
15*cf78ab8cSAndroid Build Coastguard Worker use crate::error::Error;
16*cf78ab8cSAndroid Build Coastguard Worker use base64::{engine::general_purpose, Engine as _};
17*cf78ab8cSAndroid Build Coastguard Worker use std::net::SocketAddr;
18*cf78ab8cSAndroid Build Coastguard Worker use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
19*cf78ab8cSAndroid Build Coastguard Worker use tokio::net::TcpStream;
20*cf78ab8cSAndroid Build Coastguard Worker
21*cf78ab8cSAndroid Build Coastguard Worker const HTTP_VERSION: &str = "1.1";
22*cf78ab8cSAndroid Build Coastguard Worker
23*cf78ab8cSAndroid Build Coastguard Worker pub type Result<T> = core::result::Result<T, Error>;
24*cf78ab8cSAndroid Build Coastguard Worker
25*cf78ab8cSAndroid Build Coastguard Worker /// Establishes a TCP connection to a target address through an HTTP proxy.
26*cf78ab8cSAndroid Build Coastguard Worker ///
27*cf78ab8cSAndroid Build Coastguard Worker /// The `Connector` handles the CONNECT request handshake with the proxy, including
28*cf78ab8cSAndroid Build Coastguard Worker /// optional Basic authentication.
29*cf78ab8cSAndroid Build Coastguard Worker #[derive(Clone)]
30*cf78ab8cSAndroid Build Coastguard Worker pub struct Connector {
31*cf78ab8cSAndroid Build Coastguard Worker proxy_addr: SocketAddr,
32*cf78ab8cSAndroid Build Coastguard Worker username: Option<String>,
33*cf78ab8cSAndroid Build Coastguard Worker password: Option<String>,
34*cf78ab8cSAndroid Build Coastguard Worker }
35*cf78ab8cSAndroid Build Coastguard Worker
36*cf78ab8cSAndroid Build Coastguard Worker impl Connector {
new(proxy_addr: SocketAddr, username: Option<String>, password: Option<String>) -> Self37*cf78ab8cSAndroid Build Coastguard Worker pub fn new(proxy_addr: SocketAddr, username: Option<String>, password: Option<String>) -> Self {
38*cf78ab8cSAndroid Build Coastguard Worker Connector { proxy_addr, username, password }
39*cf78ab8cSAndroid Build Coastguard Worker }
40*cf78ab8cSAndroid Build Coastguard Worker
connect(&self, addr: SocketAddr) -> Result<TcpStream>41*cf78ab8cSAndroid Build Coastguard Worker pub async fn connect(&self, addr: SocketAddr) -> Result<TcpStream> {
42*cf78ab8cSAndroid Build Coastguard Worker let mut stream = TcpStream::connect(self.proxy_addr).await?;
43*cf78ab8cSAndroid Build Coastguard Worker
44*cf78ab8cSAndroid Build Coastguard Worker // Construct the CONNECT request
45*cf78ab8cSAndroid Build Coastguard Worker let mut request = format!("CONNECT {} HTTP/{}\r\n", addr.to_string(), HTTP_VERSION);
46*cf78ab8cSAndroid Build Coastguard Worker
47*cf78ab8cSAndroid Build Coastguard Worker // Authentication
48*cf78ab8cSAndroid Build Coastguard Worker if let (Some(username), Some(password)) = (&self.username, &self.password) {
49*cf78ab8cSAndroid Build Coastguard Worker let encoded_auth = base64_encode(format!("{}:{}", username, password).as_bytes());
50*cf78ab8cSAndroid Build Coastguard Worker let auth_header = format!(
51*cf78ab8cSAndroid Build Coastguard Worker "Proxy-Authorization: Basic {}\r\n",
52*cf78ab8cSAndroid Build Coastguard Worker String::from_utf8_lossy(&encoded_auth)
53*cf78ab8cSAndroid Build Coastguard Worker );
54*cf78ab8cSAndroid Build Coastguard Worker // Add the header to the request
55*cf78ab8cSAndroid Build Coastguard Worker request.push_str(&auth_header);
56*cf78ab8cSAndroid Build Coastguard Worker }
57*cf78ab8cSAndroid Build Coastguard Worker
58*cf78ab8cSAndroid Build Coastguard Worker // Add the final CRLF
59*cf78ab8cSAndroid Build Coastguard Worker request.push_str("\r\n");
60*cf78ab8cSAndroid Build Coastguard Worker stream.write_all(request.as_bytes()).await?;
61*cf78ab8cSAndroid Build Coastguard Worker
62*cf78ab8cSAndroid Build Coastguard Worker // Read the proxy's response
63*cf78ab8cSAndroid Build Coastguard Worker let mut reader = BufReader::new(stream);
64*cf78ab8cSAndroid Build Coastguard Worker let mut response = String::new();
65*cf78ab8cSAndroid Build Coastguard Worker reader.read_line(&mut response).await?;
66*cf78ab8cSAndroid Build Coastguard Worker if response.starts_with(&format!("HTTP/{} 200", HTTP_VERSION)) {
67*cf78ab8cSAndroid Build Coastguard Worker Ok(reader.into_inner())
68*cf78ab8cSAndroid Build Coastguard Worker } else {
69*cf78ab8cSAndroid Build Coastguard Worker Err(Error::ConnectionError(addr, response.trim_end_matches("\r\n").to_string()))
70*cf78ab8cSAndroid Build Coastguard Worker }
71*cf78ab8cSAndroid Build Coastguard Worker }
72*cf78ab8cSAndroid Build Coastguard Worker }
73*cf78ab8cSAndroid Build Coastguard Worker
base64_encode(src: &[u8]) -> Vec<u8>74*cf78ab8cSAndroid Build Coastguard Worker fn base64_encode(src: &[u8]) -> Vec<u8> {
75*cf78ab8cSAndroid Build Coastguard Worker general_purpose::STANDARD.encode(src).into_bytes()
76*cf78ab8cSAndroid Build Coastguard Worker }
77*cf78ab8cSAndroid Build Coastguard Worker
78*cf78ab8cSAndroid Build Coastguard Worker #[cfg(test)]
79*cf78ab8cSAndroid Build Coastguard Worker mod tests {
80*cf78ab8cSAndroid Build Coastguard Worker use super::*;
81*cf78ab8cSAndroid Build Coastguard Worker use tokio::io::AsyncReadExt;
82*cf78ab8cSAndroid Build Coastguard Worker use tokio::net::{lookup_host, TcpListener};
83*cf78ab8cSAndroid Build Coastguard Worker
84*cf78ab8cSAndroid Build Coastguard Worker #[tokio::test]
test_connect() -> Result<()>85*cf78ab8cSAndroid Build Coastguard Worker async fn test_connect() -> Result<()> {
86*cf78ab8cSAndroid Build Coastguard Worker let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
87*cf78ab8cSAndroid Build Coastguard Worker let proxy_addr = listener.local_addr().unwrap();
88*cf78ab8cSAndroid Build Coastguard Worker
89*cf78ab8cSAndroid Build Coastguard Worker let addr: SocketAddr = lookup_host("localhost:8000").await.unwrap().next().unwrap();
90*cf78ab8cSAndroid Build Coastguard Worker
91*cf78ab8cSAndroid Build Coastguard Worker let handle = tokio::spawn(async move {
92*cf78ab8cSAndroid Build Coastguard Worker let (stream, _) = listener.accept().await.unwrap();
93*cf78ab8cSAndroid Build Coastguard Worker // Server expects a client greeting with no auth methods
94*cf78ab8cSAndroid Build Coastguard Worker let expected_greeting = format!("CONNECT {} HTTP/1.1\r\n", &addr);
95*cf78ab8cSAndroid Build Coastguard Worker
96*cf78ab8cSAndroid Build Coastguard Worker let mut reader = BufReader::new(stream);
97*cf78ab8cSAndroid Build Coastguard Worker let mut line = String::new();
98*cf78ab8cSAndroid Build Coastguard Worker
99*cf78ab8cSAndroid Build Coastguard Worker reader.read_line(&mut line).await.unwrap();
100*cf78ab8cSAndroid Build Coastguard Worker
101*cf78ab8cSAndroid Build Coastguard Worker assert_eq!(line, expected_greeting);
102*cf78ab8cSAndroid Build Coastguard Worker
103*cf78ab8cSAndroid Build Coastguard Worker // Server sends a response with no auth method selected
104*cf78ab8cSAndroid Build Coastguard Worker let response = "HTTP/1.1 200 Connection established\r\n\r\n";
105*cf78ab8cSAndroid Build Coastguard Worker let mut stream = reader.into_inner();
106*cf78ab8cSAndroid Build Coastguard Worker stream.write_all(response.as_bytes()).await.unwrap();
107*cf78ab8cSAndroid Build Coastguard Worker });
108*cf78ab8cSAndroid Build Coastguard Worker
109*cf78ab8cSAndroid Build Coastguard Worker let client = Connector::new(proxy_addr, None, None);
110*cf78ab8cSAndroid Build Coastguard Worker
111*cf78ab8cSAndroid Build Coastguard Worker client.connect(addr).await.unwrap();
112*cf78ab8cSAndroid Build Coastguard Worker
113*cf78ab8cSAndroid Build Coastguard Worker handle.await.unwrap(); // Wait for the task to complete
114*cf78ab8cSAndroid Build Coastguard Worker
115*cf78ab8cSAndroid Build Coastguard Worker Ok(())
116*cf78ab8cSAndroid Build Coastguard Worker }
117*cf78ab8cSAndroid Build Coastguard Worker
118*cf78ab8cSAndroid Build Coastguard Worker #[tokio::test]
test_connect_with_auth() -> Result<()>119*cf78ab8cSAndroid Build Coastguard Worker async fn test_connect_with_auth() -> Result<()> {
120*cf78ab8cSAndroid Build Coastguard Worker let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
121*cf78ab8cSAndroid Build Coastguard Worker let proxy_addr = listener.local_addr().unwrap();
122*cf78ab8cSAndroid Build Coastguard Worker
123*cf78ab8cSAndroid Build Coastguard Worker let addr: SocketAddr = lookup_host("localhost:8000").await.unwrap().next().unwrap();
124*cf78ab8cSAndroid Build Coastguard Worker
125*cf78ab8cSAndroid Build Coastguard Worker let handle = tokio::spawn(async move {
126*cf78ab8cSAndroid Build Coastguard Worker let (mut stream, _) = listener.accept().await.unwrap();
127*cf78ab8cSAndroid Build Coastguard Worker
128*cf78ab8cSAndroid Build Coastguard Worker // Server expects a client greeting with auth header
129*cf78ab8cSAndroid Build Coastguard Worker let expected_greeting = format!(
130*cf78ab8cSAndroid Build Coastguard Worker "CONNECT {} HTTP/1.1\r\nProxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n\r\n",
131*cf78ab8cSAndroid Build Coastguard Worker &addr
132*cf78ab8cSAndroid Build Coastguard Worker );
133*cf78ab8cSAndroid Build Coastguard Worker
134*cf78ab8cSAndroid Build Coastguard Worker let mut buf = [0; 1024];
135*cf78ab8cSAndroid Build Coastguard Worker let n = stream.read(&mut buf).await.unwrap();
136*cf78ab8cSAndroid Build Coastguard Worker let actual_greeting = String::from_utf8_lossy(&buf[..n]);
137*cf78ab8cSAndroid Build Coastguard Worker
138*cf78ab8cSAndroid Build Coastguard Worker assert_eq!(actual_greeting, expected_greeting);
139*cf78ab8cSAndroid Build Coastguard Worker
140*cf78ab8cSAndroid Build Coastguard Worker // Server sends a response
141*cf78ab8cSAndroid Build Coastguard Worker let response = "HTTP/1.1 200 Connection established\r\n\r\n";
142*cf78ab8cSAndroid Build Coastguard Worker
143*cf78ab8cSAndroid Build Coastguard Worker stream.write_all(response.as_bytes()).await.unwrap();
144*cf78ab8cSAndroid Build Coastguard Worker });
145*cf78ab8cSAndroid Build Coastguard Worker
146*cf78ab8cSAndroid Build Coastguard Worker let client = Connector::new(proxy_addr, Some("user".into()), Some("password".into()));
147*cf78ab8cSAndroid Build Coastguard Worker
148*cf78ab8cSAndroid Build Coastguard Worker client.connect(addr).await.unwrap();
149*cf78ab8cSAndroid Build Coastguard Worker
150*cf78ab8cSAndroid Build Coastguard Worker handle.await.unwrap(); // Wait for the task to complete
151*cf78ab8cSAndroid Build Coastguard Worker
152*cf78ab8cSAndroid Build Coastguard Worker Ok(())
153*cf78ab8cSAndroid Build Coastguard Worker }
154*cf78ab8cSAndroid Build Coastguard Worker
155*cf78ab8cSAndroid Build Coastguard Worker #[test]
test_proxy_base64_encode_success()156*cf78ab8cSAndroid Build Coastguard Worker fn test_proxy_base64_encode_success() {
157*cf78ab8cSAndroid Build Coastguard Worker let input = b"hello world";
158*cf78ab8cSAndroid Build Coastguard Worker let encoded = base64_encode(input);
159*cf78ab8cSAndroid Build Coastguard Worker assert_eq!(encoded, b"aGVsbG8gd29ybGQ=");
160*cf78ab8cSAndroid Build Coastguard Worker }
161*cf78ab8cSAndroid Build Coastguard Worker
162*cf78ab8cSAndroid Build Coastguard Worker #[test]
test_proxy_base64_encode_empty_input()163*cf78ab8cSAndroid Build Coastguard Worker fn test_proxy_base64_encode_empty_input() {
164*cf78ab8cSAndroid Build Coastguard Worker let input = b"";
165*cf78ab8cSAndroid Build Coastguard Worker let encoded = base64_encode(input);
166*cf78ab8cSAndroid Build Coastguard Worker assert_eq!(encoded, b"");
167*cf78ab8cSAndroid Build Coastguard Worker }
168*cf78ab8cSAndroid Build Coastguard Worker }
169