LCOV - code coverage report
Current view: top level - http/client - proxy.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 98.4 % 255 251
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 33 33

            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              : }
        

Generated by: LCOV version 2.0-1