xref: /aosp_15_r20/tools/netsim/rust/http-proxy/src/util.rs (revision cf78ab8cffb8fc9207af348f23af247fb04370a6)
1 // Copyright 2024 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 // # Http Proxy Utils
16 //
17 // This module provides functionality for parsing proxy configuration
18 // strings and converting `TcpStream` objects to raw file
19 // descriptors.
20 //
21 // The `ProxyConfig` struct holds the parsed proxy configuration,
22 // including protocol, address, username, and password. The
23 // `from_string` function parses a proxy configuration string in the
24 // format `[protocol://][username:password@]host:port` or
25 // `[protocol://][username:password@]/[host/]:port` and returns a
26 // `ProxyConfig` struct.
27 //
28 // The `into_raw_descriptor` function converts a `TcpStream` object
29 // to a raw file descriptor (`RawDescriptor`), which is an `i32`
30 // representing the underlying socket. This is used for compatibility
31 // with libraries that require raw file descriptors, such as
32 // `libslirp_rs`.
33 
34 use crate::Error;
35 use regex::Regex;
36 use std::net::{SocketAddr, ToSocketAddrs};
37 #[cfg(unix)]
38 use std::os::fd::IntoRawFd;
39 #[cfg(windows)]
40 use std::os::windows::io::IntoRawSocket;
41 use tokio::net::TcpStream;
42 
43 pub type RawDescriptor = i32;
44 
45 /// Proxy configuration
46 pub struct ProxyConfig {
47     pub protocol: String,
48     pub addr: SocketAddr,
49     pub username: Option<String>,
50     pub password: Option<String>,
51 }
52 
53 impl ProxyConfig {
54     /// Parses a proxy configuration string and returns a `ProxyConfig` struct.
55     ///
56     /// The function expects the proxy configuration string to be in the following format:
57     ///
58     /// [protocol://][username:password@]host:port
59     /// [protocol://][username:password@]/[host/]:port
60     ///
61     /// where:
62     ///
63     /// * `protocol`: The network protocol (e.g., `http`, `https`,
64     /// `socks5`). If not provided, defaults to `http`.
65     /// * `username`: and `password` are optional credentials for authentication.
66     /// * `host`: The hostname or IP address of the proxy server. If
67     /// it's an IPv6 address, it should be enclosed in square brackets
68     /// (e.g., "[::1]").
69     /// * `port`: The port number on which the proxy server is listening.
70     ///
71     /// # Errors
72     /// Returns a `Error` if the input string is not in a
73     /// valid format or if the hostname/port resolution fails.
74     ///
75     /// # Limitations
76     /// * Usernames and passwords cannot contain `@` or `:`.
from_string(config_string: &str) -> Result<ProxyConfig, Error>77     pub fn from_string(config_string: &str) -> Result<ProxyConfig, Error> {
78         let re = Regex::new(r"^(?:(?P<protocol>\w+)://)?(?:(?P<user>\w+):(?P<pass>\w+)@)?(?P<host>(?:[\w\.-]+|\[[^\]]+\])):(?P<port>\d+)$").unwrap();
79         let caps = re.captures(config_string).ok_or(Error::MalformedConfigString)?;
80 
81         let protocol =
82             caps.name("protocol").map_or_else(|| "http".to_string(), |m| m.as_str().to_string());
83         let username = caps.name("user").map(|m| m.as_str().to_string());
84         let password = caps.name("pass").map(|m| m.as_str().to_string());
85 
86         // Extract host, removing surrounding brackets if present
87         let hostname = caps
88             .name("host")
89             .ok_or(Error::MalformedConfigString)?
90             .as_str()
91             .trim_matches(|c| c == '[' || c == ']')
92             .to_string();
93 
94         let port = caps
95             .name("port")
96             .ok_or(Error::MalformedConfigString)?
97             .as_str()
98             .parse::<u16>()
99             .map_err(|_| Error::InvalidPortNumber)?;
100 
101         let host = (hostname, port)
102             .to_socket_addrs()
103             .map_err(|_| Error::InvalidHost)?
104             .next() // Take the first resolved address
105             .ok_or(Error::InvalidHost)?
106             .ip();
107 
108         Ok(ProxyConfig { protocol, username, password, addr: SocketAddr::from((host, port)) })
109     }
110 }
111 
112 /// Convert TcpStream to RawDescriptor (i32)
into_raw_descriptor(stream: TcpStream) -> RawDescriptor113 pub fn into_raw_descriptor(stream: TcpStream) -> RawDescriptor {
114     let std_stream = stream.into_std().expect("into_raw_descriptor's into_std() failed");
115 
116     std_stream.set_nonblocking(false).expect("non-blocking");
117 
118     // Use into_raw_fd for Unix to pass raw file descriptor to C
119     #[cfg(unix)]
120     return std_stream.into_raw_fd();
121 
122     // Use into_raw_socket for Windows to pass raw socket to C
123     #[cfg(windows)]
124     std_stream.into_raw_socket().try_into().expect("Failed to convert Raw Socket value into i32")
125 }
126 
127 #[cfg(test)]
128 mod tests {
129     use super::*;
130     use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
131 
132     #[test]
parse_configuration_string_success()133     fn parse_configuration_string_success() {
134         // Test data
135         let data = [
136             (
137                 "127.0.0.1:8080",
138                 ProxyConfig {
139                     protocol: "http".to_owned(),
140                     addr: SocketAddr::from((IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)),
141                     username: None,
142                     password: None,
143                 },
144             ),
145             (
146                 "http://127.0.0.1:8080",
147                 ProxyConfig {
148                     protocol: "http".to_owned(),
149                     addr: SocketAddr::from((IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)),
150                     username: None,
151                     password: None,
152                 },
153             ),
154             (
155                 "https://127.0.0.1:8080",
156                 ProxyConfig {
157                     protocol: "https".to_owned(),
158                     addr: SocketAddr::from((IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)),
159                     username: None,
160                     password: None,
161                 },
162             ),
163             (
164                 "sock5://127.0.0.1:8080",
165                 ProxyConfig {
166                     protocol: "sock5".to_owned(),
167                     addr: SocketAddr::from((IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)),
168                     username: None,
169                     password: None,
170                 },
171             ),
172             (
173                 "user:[email protected]:3128",
174                 ProxyConfig {
175                     protocol: "http".to_owned(),
176                     addr: SocketAddr::from((IpAddr::V4(Ipv4Addr::new(192, 168, 0, 18)), 3128)),
177                     username: Some("user".to_string()),
178                     password: Some("pass".to_string()),
179                 },
180             ),
181             (
182                 "https://[::1]:7000",
183                 ProxyConfig {
184                     protocol: "https".to_owned(),
185                     addr: SocketAddr::from((
186                         IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
187                         7000,
188                     )),
189                     username: None,
190                     password: None,
191                 },
192             ),
193             (
194                 "[::1]:7000",
195                 ProxyConfig {
196                     protocol: "http".to_owned(),
197                     addr: SocketAddr::from((
198                         IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
199                         7000,
200                     )),
201                     username: None,
202                     password: None,
203                 },
204             ),
205         ];
206 
207         // TODO: Mock DNS server to to test hostname. e.g. "proxy.example.com:3000".
208         for (input, expected) in data {
209             let result = ProxyConfig::from_string(input);
210             assert!(
211                 result.is_ok(),
212                 "Unexpected error {} for input: {}",
213                 result.err().unwrap(),
214                 input
215             );
216             let result = result.ok().unwrap();
217             assert_eq!(result.addr, expected.addr, "For input: {}", input);
218             assert_eq!(result.username, expected.username, "For input: {}", input);
219             assert_eq!(result.password, expected.password, "For input: {}", input);
220         }
221     }
222 
223     #[test]
parse_configuration_string_with_errors()224     fn parse_configuration_string_with_errors() {
225         let data = [
226             ("http://", Error::MalformedConfigString),
227             ("", Error::MalformedConfigString),
228             ("256.0.0.1:8080", Error::InvalidHost),
229             ("127.0.0.1:foo", Error::MalformedConfigString),
230             ("127.0.0.1:-2", Error::MalformedConfigString),
231             ("127.0.0.1:100000", Error::InvalidPortNumber),
232             ("127.0.0.1", Error::MalformedConfigString),
233             ("http:127.0.0.1:8080", Error::MalformedConfigString),
234             ("::1:8080", Error::MalformedConfigString),
235             ("user@pass:127.0.0.1:8080", Error::MalformedConfigString),
236             ("[email protected]:8080", Error::MalformedConfigString),
237             ("proxy.example.com:7000", Error::InvalidHost),
238             ("[::1}:7000", Error::MalformedConfigString),
239         ];
240 
241         for (input, expected_error) in data {
242             let result = ProxyConfig::from_string(input);
243             assert_eq!(
244                 result.err().unwrap().to_string(),
245                 expected_error.to_string(),
246                 "Expected an error for input: {}",
247                 input
248             );
249         }
250     }
251 }
252