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