Line data Source code
1 : #include "proxy.hpp"
2 : #include "../../util/ascii.hpp"
3 : #include "../../util/base64.hpp"
4 : #include <algorithm>
5 : #include <cstdlib>
6 : #include <initializer_list>
7 :
8 : namespace thinger::http {
9 :
10 : namespace {
11 :
12 : using boost::asio::ip::address;
13 :
14 612 : std::string_view trim(std::string_view value) {
15 690 : while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.remove_prefix(1);
16 627 : while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.remove_suffix(1);
17 612 : return value;
18 : }
19 :
20 1578 : std::string lowercase(std::string_view value) {
21 1578 : std::string result(value);
22 1578 : util::ascii::to_lower(result);
23 1578 : return result;
24 : }
25 :
26 390 : bool all_digits(std::string_view value) {
27 1959 : return !value.empty() && std::all_of(value.begin(), value.end(), [](char c) { return c >= '0' && c <= '9'; });
28 : }
29 :
30 363 : bool is_port(std::string_view port) {
31 363 : if (port.size() > 5 || !all_digits(port)) return false;
32 357 : auto value = std::stoul(std::string(port));
33 357 : return value > 0 && value <= 65535;
34 : }
35 :
36 96 : int hex_digit(char c) {
37 96 : if (c >= '0' && c <= '9') return c - '0';
38 30 : if (c >= 'a' && c <= 'f') return c - 'a' + 10;
39 30 : if (c >= 'A' && c <= 'F') return c - 'A' + 10;
40 6 : return -1;
41 : }
42 :
43 : // Percent-decoding of URL user info ('+' is not a space there, unlike in a query)
44 222 : std::optional<std::string> percent_decode(std::string_view in) {
45 222 : std::string out;
46 222 : out.reserve(in.size());
47 1314 : for (size_t i = 0; i < in.size(); ++i) {
48 1095 : if (in[i] != '%') {
49 1047 : out += in[i];
50 1047 : continue;
51 : }
52 48 : if (i + 2 >= in.size()) return std::nullopt;
53 48 : int hi = hex_digit(in[i + 1]);
54 48 : int lo = hex_digit(in[i + 2]);
55 48 : if (hi < 0 || lo < 0) return std::nullopt;
56 45 : out += static_cast<char>((hi << 4) | lo);
57 45 : i += 2;
58 : }
59 219 : return out;
60 222 : }
61 :
62 : // An IP address (IPv4-mapped IPv6 addresses as IPv4), or nothing
63 807 : std::optional<address> parse_ip(std::string_view value) {
64 807 : boost::system::error_code ec;
65 807 : auto ip = boost::asio::ip::make_address(std::string(value), ec);
66 807 : if (ec) return std::nullopt;
67 249 : if (ip.is_v6() && ip.to_v6().is_v4_mapped()) {
68 6 : return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, ip.to_v6());
69 : }
70 243 : return ip;
71 : }
72 :
73 54 : bool in_network(const address& ip, const address& network, unsigned prefix) {
74 54 : if (ip.is_v4() != network.is_v4()) return false;
75 60 : auto bytes = [](const address& a) {
76 60 : std::vector<unsigned char> result;
77 60 : if (a.is_v4()) {
78 30 : auto b = a.to_v4().to_bytes();
79 30 : result.assign(b.begin(), b.end());
80 : } else {
81 30 : auto b = a.to_v6().to_bytes();
82 30 : result.assign(b.begin(), b.end());
83 : }
84 60 : return result;
85 0 : };
86 30 : auto a = bytes(ip);
87 30 : auto b = bytes(network);
88 69 : for (size_t i = 0; i < a.size() && prefix > 0; ++i) {
89 54 : unsigned bits = std::min(prefix, 8u);
90 54 : auto mask = static_cast<unsigned char>(0xFF << (8 - bits));
91 54 : if ((a[i] & mask) != (b[i] & mask)) return false;
92 39 : prefix -= bits;
93 : }
94 15 : return true;
95 30 : }
96 :
97 : // Host name for comparisons: lowercase, without IPv6 brackets nor the trailing dot of a
98 : // fully qualified name
99 696 : std::string normalize_host(std::string_view host) {
100 696 : if (host.size() >= 2 && host.front() == '[' && host.back() == ']') {
101 6 : host = host.substr(1, host.size() - 2);
102 : }
103 696 : if (host.size() > 1 && host.back() == '.') host.remove_suffix(1);
104 696 : return lowercase(host);
105 : }
106 :
107 : // Take the scheme out of a proxy URL: why it is not supported, or empty if it is http or
108 : // there is none
109 405 : std::string take_scheme(std::string_view& url) {
110 405 : auto separator = url.find("://");
111 405 : if (separator == std::string_view::npos) return {};
112 396 : auto scheme = lowercase(url.substr(0, separator));
113 432 : if (scheme == "https") return "HTTPS proxies (TLS to the proxy) are not supported: use an http:// proxy URL";
114 414 : if (scheme.starts_with("socks")) return "SOCKS proxies are not supported";
115 360 : if (scheme != "http") return "unsupported proxy scheme: " + scheme;
116 357 : url.remove_prefix(separator + 3);
117 357 : return {};
118 396 : }
119 :
120 : // Basic credentials from the user info of a URL; nothing if its percent-encoding is invalid
121 111 : std::optional<std::string> basic_credentials(std::string_view user_info) {
122 111 : auto colon = user_info.find(':');
123 111 : auto user = percent_decode(user_info.substr(0, colon));
124 111 : auto password = percent_decode(colon == std::string_view::npos ? std::string_view{} : user_info.substr(colon + 1));
125 111 : if (!user || !password) return std::nullopt;
126 108 : return "Basic " + util::base64::encode(*user + ":" + *password);
127 111 : }
128 :
129 357 : bool is_host_name(std::string_view host) {
130 357 : return !host.empty() && std::all_of(host.begin(), host.end(), [](char c) {
131 2745 : return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
132 5490 : c == '-' || c == '.' || c == '_';
133 357 : });
134 : }
135 :
136 : // The host and port of a proxy URL authority (without user info); false if the host is
137 : // invalid
138 363 : bool split_authority(std::string_view authority, std::string_view& host, std::string_view& port) {
139 363 : if (!authority.starts_with('[')) {
140 357 : auto colon = authority.find(':');
141 357 : host = authority.substr(0, colon);
142 357 : if (colon != std::string_view::npos) port = authority.substr(colon + 1);
143 357 : return is_host_name(host);
144 : }
145 6 : auto close = authority.find(']');
146 6 : if (close == std::string_view::npos) return false;
147 3 : host = authority.substr(1, close - 1);
148 3 : auto rest = authority.substr(close + 1);
149 3 : if (!rest.empty()) {
150 3 : if (rest.front() != ':') return false;
151 3 : port = rest.substr(1);
152 : }
153 3 : auto ip = parse_ip(host);
154 3 : return ip && ip->is_v6();
155 : }
156 :
157 : // ".example.com" and "*.example.com" are "example.com": the domain and its subdomains
158 69 : std::string_view no_proxy_domain(std::string_view host) {
159 69 : if (host.starts_with("*.")) host.remove_prefix(1);
160 84 : while (host.starts_with('.')) host.remove_prefix(1);
161 69 : if (host.ends_with('.')) host.remove_suffix(1);
162 69 : return host;
163 : }
164 :
165 : // Split a NO_PROXY entry into its host, port and the bits of a CIDR range: "[::1]",
166 : // "[::1]:8080", "[fd00::]/8", "host:port", "10.0.0.0/8". False if it cannot be parsed
167 138 : bool split_no_proxy_entry(std::string_view& host, std::string& port, std::optional<std::string_view>& bits) {
168 138 : if (host.starts_with('[')) {
169 9 : auto close = host.find(']');
170 9 : if (close == std::string_view::npos) return false;
171 6 : auto rest = host.substr(close + 1);
172 6 : host = host.substr(1, close - 1);
173 6 : if (rest.starts_with(':')) {
174 3 : port = std::string(rest.substr(1));
175 3 : } else if (rest.starts_with('/')) {
176 3 : bits = rest.substr(1);
177 0 : } else if (!rest.empty()) {
178 0 : return false;
179 : }
180 129 : } else if (std::count(host.begin(), host.end(), ':') == 1) {
181 : // A bare IPv6 address has more than one colon
182 15 : auto colon = host.find(':');
183 15 : port = std::string(host.substr(colon + 1));
184 15 : host = host.substr(0, colon);
185 : }
186 135 : if (!bits) {
187 132 : if (auto slash = host.find('/'); slash != std::string_view::npos) {
188 24 : bits = host.substr(slash + 1);
189 24 : host = host.substr(0, slash);
190 : }
191 : }
192 135 : return true;
193 : }
194 :
195 : }
196 :
197 : // --- proxy_config ---
198 :
199 2731 : proxy_config proxy_config::from_environment() {
200 0 : return from_environment([](const char* name) -> std::optional<std::string> {
201 19117 : const char* value = std::getenv(name);
202 19117 : if (!value) return std::nullopt;
203 42 : return std::string(value);
204 2731 : });
205 : }
206 :
207 2755 : proxy_config proxy_config::from_environment(const env_reader& getenv) {
208 11020 : auto first = [&](std::initializer_list<const char*> names) -> std::string {
209 30245 : for (const char* name : names) {
210 19273 : auto value = getenv(name);
211 19273 : if (!value) continue;
212 51 : auto trimmed = trim(*value);
213 147 : if (!trimmed.empty()) return std::string(trimmed);
214 19273 : }
215 10972 : return {};
216 2755 : };
217 :
218 2755 : proxy_config config;
219 : // Never HTTP_PROXY: a CGI program gets the Proxy header of its request there (httpoxy)
220 2755 : config.http = first({"http_proxy"});
221 2755 : config.https = first({"https_proxy", "HTTPS_PROXY"});
222 2755 : auto all = first({"all_proxy", "ALL_PROXY"});
223 2755 : if (config.http.empty()) config.http = all;
224 2755 : if (config.https.empty()) config.https = all;
225 2755 : config.no_proxy = first({"no_proxy", "NO_PROXY"});
226 5510 : return config;
227 2755 : }
228 :
229 : // --- proxy_endpoint ---
230 :
231 405 : std::optional<proxy_endpoint> proxy_endpoint::parse(std::string_view url, std::string* error) {
232 75 : auto fail = [&](std::string why) -> std::optional<proxy_endpoint> {
233 75 : if (error) *error = std::move(why);
234 75 : return std::nullopt;
235 405 : };
236 :
237 405 : url = trim(url);
238 : // Without a scheme, an HTTP proxy, as curl and Go take it
239 405 : if (auto why = take_scheme(url); !why.empty()) return fail(std::move(why));
240 :
241 : // The authority ends the URL, or a path (ignored)
242 366 : auto authority = url.substr(0, url.find_first_of("/?#"));
243 :
244 366 : proxy_endpoint endpoint;
245 366 : if (auto at = authority.rfind('@'); at != std::string_view::npos) {
246 111 : auto credentials = basic_credentials(authority.substr(0, at));
247 117 : if (!credentials) return fail("invalid percent-encoding in the proxy credentials");
248 108 : endpoint.authorization = std::move(*credentials);
249 108 : authority.remove_prefix(at + 1);
250 111 : }
251 :
252 363 : std::string_view host;
253 363 : std::string_view port;
254 399 : if (!split_authority(authority, host, port)) return fail("invalid proxy host");
255 : // The default port of the scheme, as Go and Node (curl uses 1080)
256 345 : if (port.empty()) port = "80";
257 375 : if (!is_port(port)) return fail("invalid proxy port");
258 :
259 330 : endpoint.host = lowercase(host);
260 330 : endpoint.port = std::string(port);
261 330 : return endpoint;
262 366 : }
263 :
264 387 : std::string proxy_endpoint::url() const {
265 387 : auto name = host.find(':') != std::string::npos ? "[" + host + "]" : host;
266 1161 : return "http://" + name + ":" + port;
267 387 : }
268 :
269 135 : std::string proxy_endpoint::key() const {
270 270 : return host + "|" + port + "|" + authorization;
271 : }
272 :
273 : // --- no_proxy_list ---
274 :
275 2950 : no_proxy_list::no_proxy_list(std::string_view list) {
276 3106 : while (!list.empty()) {
277 156 : auto comma = list.find(',');
278 156 : auto token = lowercase(trim(list.substr(0, comma)));
279 156 : list.remove_prefix(comma == std::string_view::npos ? list.size() : comma + 1);
280 156 : if (token == "*") {
281 9 : all_ = true;
282 147 : } else if (!token.empty()) {
283 138 : if (auto e = parse_entry(token)) entries_.push_back(std::move(*e));
284 : }
285 156 : }
286 2950 : }
287 :
288 138 : std::optional<no_proxy_list::entry> no_proxy_list::parse_entry(std::string_view host) {
289 : // Unparsable entries are ignored, as curl and Go do
290 138 : entry e;
291 138 : std::optional<std::string_view> bits; // of a CIDR range
292 138 : if (!split_no_proxy_entry(host, e.port, bits)) return std::nullopt;
293 135 : if (!e.port.empty() && !is_port(e.port)) return std::nullopt;
294 132 : if (bits) {
295 27 : if (!all_digits(*bits) || bits->size() > 3) return std::nullopt;
296 27 : e.prefix = static_cast<unsigned>(std::stoul(std::string(*bits)));
297 27 : e.range = true;
298 : }
299 :
300 132 : if (auto ip = parse_ip(host)) {
301 123 : if (e.range && e.prefix > (ip->is_v4() ? 32u : 128u)) return std::nullopt;
302 60 : e.ip = *ip;
303 60 : return e;
304 : }
305 69 : auto domain = no_proxy_domain(host);
306 69 : if (e.range || domain.empty()) return std::nullopt;
307 63 : e.domain = std::string(domain);
308 63 : return e;
309 138 : }
310 :
311 399 : bool no_proxy_list::matches(std::string_view host, std::string_view port) const {
312 399 : if (all_) return true;
313 390 : auto name = normalize_host(host);
314 390 : auto ip = parse_ip(name);
315 621 : for (const auto& e : entries_) {
316 366 : if (!e.port.empty() && e.port != port) continue;
317 327 : if (e.ip) {
318 225 : if (ip && (e.range ? in_network(*ip, *e.ip, e.prefix) : *ip == *e.ip)) return true;
319 411 : } else if (name == e.domain ||
320 219 : (name.size() > e.domain.size() && name.ends_with(e.domain) &&
321 54 : name[name.size() - e.domain.size() - 1] == '.')) {
322 90 : return true;
323 : }
324 : }
325 255 : return false;
326 390 : }
327 :
328 306 : bool is_loopback_host(std::string_view host) {
329 306 : auto name = normalize_host(host);
330 306 : if (name == "localhost" || name.ends_with(".localhost")) return true;
331 282 : auto ip = parse_ip(name);
332 282 : return ip && ip->is_loopback();
333 306 : }
334 :
335 : // --- proxy_resolver ---
336 :
337 5822 : proxy_resolver::proxy proxy_resolver::parse(const std::string& url) {
338 5822 : proxy result;
339 5822 : if (url.empty()) return result;
340 336 : result.configured = true;
341 336 : std::string error;
342 336 : if (auto endpoint = proxy_endpoint::parse(url, &error)) {
343 300 : result.endpoint = std::make_shared<const proxy_endpoint>(std::move(*endpoint));
344 : } else {
345 36 : result.error = std::move(error);
346 336 : }
347 336 : return result;
348 336 : }
349 :
350 2911 : proxy_resolver::proxy_resolver(const proxy_config& config)
351 2911 : : http_(parse(config.http))
352 2911 : , https_(parse(config.https))
353 2911 : , no_proxy_(config.no_proxy) {
354 2911 : }
355 :
356 7955 : proxy_resolver::choice proxy_resolver::select(bool secure, std::string_view host, std::string_view port) const {
357 7955 : const auto& proxy = secure ? https_ : http_;
358 7955 : if (!proxy.configured) return {};
359 : // Whatever a proxy would do with them, these are this host, or listed as reached directly
360 261 : if (is_loopback_host(host) || no_proxy_.matches(host, port)) return {};
361 567 : return {proxy.endpoint, proxy.error};
362 7955 : }
363 :
364 : }
|