LCOV - code coverage report
Current view: top level - http/server - trusted_proxies.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 97.7 % 177 173
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 19 19

            Line data    Source code
       1              : #include "trusted_proxies.hpp"
       2              : #include "../common/headers.hpp"
       3              : #include <boost/algorithm/string.hpp>
       4              : #include <algorithm>
       5              : #include <charconv>
       6              : #include <optional>
       7              : 
       8              : namespace thinger::http {
       9              : 
      10              : namespace {
      11              : 
      12              :     using boost::asio::ip::address;
      13              : 
      14         1977 :     std::string_view trim(std::string_view value) {
      15         2127 :         while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.remove_prefix(1);
      16         1989 :         while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.remove_suffix(1);
      17         1977 :         return value;
      18              :     }
      19              : 
      20              :     // IPv4-mapped IPv6 addresses (::ffff:10.0.0.1, as reported by dual-stack sockets) as IPv4
      21          753 :     address normalize(const address& ip) {
      22          753 :         if (ip.is_v6() && ip.to_v6().is_v4_mapped()) {
      23            6 :             return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, ip.to_v6());
      24              :         }
      25          747 :         return ip;
      26              :     }
      27              : 
      28              :     // Address of a forwarded entry: "1.2.3.4", "1.2.3.4:80", "2001:db8::1", "[2001:db8::1]:80"
      29          285 :     std::optional<address> parse_address(std::string_view entry) {
      30          285 :         entry = trim(entry);
      31          285 :         if (entry.starts_with('[')) {
      32           15 :             auto close = entry.find(']');
      33           15 :             if (close == std::string_view::npos) return std::nullopt;
      34           15 :             entry = entry.substr(1, close - 1);
      35          270 :         } else if (std::count(entry.begin(), entry.end(), ':') == 1) {
      36            6 :             entry = entry.substr(0, entry.find(':'));
      37              :         }
      38          285 :         boost::system::error_code ec;
      39          285 :         auto ip = boost::asio::ip::make_address(std::string(entry), ec);
      40          285 :         if (ec) return std::nullopt;
      41          264 :         return normalize(ip);
      42              :     }
      43              : 
      44              :     // Split by `separator`, except inside quoted strings
      45          342 :     std::vector<std::string_view> split_unquoted(std::string_view value, char separator) {
      46          342 :         std::vector<std::string_view> parts;
      47          342 :         bool quoted = false;
      48          342 :         size_t start = 0;
      49         7023 :         for (size_t i = 0; i < value.size(); ++i) {
      50         6681 :             if (value[i] == '\\' && quoted) {
      51            0 :                 ++i;
      52         6681 :             } else if (value[i] == '"') {
      53           84 :                 quoted = !quoted;
      54         6597 :             } else if (value[i] == separator && !quoted) {
      55          207 :                 parts.push_back(value.substr(start, i - start));
      56          207 :                 start = i + 1;
      57              :             }
      58              :         }
      59          342 :         parts.push_back(value.substr(start));
      60          342 :         return parts;
      61            0 :     }
      62              : 
      63          543 :     std::string unquote(std::string_view value) {
      64          543 :         value = trim(value);
      65         1581 :         if (value.size() < 2 || value.front() != '"' || value.back() != '"') return std::string(value);
      66           24 :         std::string result;
      67          363 :         for (size_t i = 1; i + 1 < value.size(); ++i) {
      68          339 :             if (value[i] == '\\' && i + 2 < value.size()) ++i;
      69          339 :             result += value[i];
      70              :         }
      71           24 :         return result;
      72           24 :     }
      73              : 
      74          288 :     std::string to_entry(const std::optional<address>& ip) {
      75          288 :         return ip ? ip->to_string() : std::string{};
      76              :     }
      77              : 
      78              :     // An element of a Forwarded header: its address (empty if it is not an IP) and its proto
      79              :     struct forwarded_element {
      80              :         std::string address;
      81              :         std::string proto;
      82              :     };
      83              : 
      84           63 :     std::vector<forwarded_element> parse_forwarded_elements(std::string_view value) {
      85           63 :         std::vector<forwarded_element> result;
      86           63 :         if (trim(value).empty()) return result;
      87          168 :         for (auto element : split_unquoted(value, ',')) {
      88              :             // element: pairs such as for=192.0.2.60;proto=http;by=203.0.113.43
      89          105 :             std::optional<address> ip;
      90          105 :             std::string proto;
      91          105 :             bool has_for = false;
      92          105 :             bool has_proto = false;
      93          276 :             for (auto pair : split_unquoted(element, ';')) {
      94          171 :                 auto equals = pair.find('=');
      95          171 :                 if (equals == std::string_view::npos) continue;
      96          171 :                 auto name = trim(pair.substr(0, equals));
      97              :                 // the first one of each parameter (each may appear once)
      98          171 :                 if (!has_for && boost::iequals(name, "for")) {
      99          102 :                     has_for = true;
     100          102 :                     ip = parse_address(unquote(pair.substr(equals + 1)));
     101           69 :                 } else if (!has_proto && boost::iequals(name, "proto")) {
     102           63 :                     has_proto = true;
     103           63 :                     proto = unquote(pair.substr(equals + 1));
     104              :                 }
     105          105 :             }
     106          105 :             result.push_back({to_entry(ip), std::move(proto)});
     107          168 :         }
     108           63 :         return result;
     109          105 :     }
     110              : 
     111              : }
     112              : 
     113          138 : bool trusted_proxies::add(std::string_view value) {
     114          138 :     value = trim(value);
     115          138 :     auto slash = value.find('/');
     116              : 
     117          138 :     boost::system::error_code ec;
     118          138 :     auto ip = boost::asio::ip::make_address(std::string(value.substr(0, slash)), ec);
     119          138 :     if (ec) return false;
     120              : 
     121          132 :     unsigned max_prefix = ip.is_v4() ? 32 : 128;
     122          132 :     unsigned prefix = max_prefix;
     123          132 :     if (slash != std::string_view::npos) {
     124          102 :         auto bits = value.substr(slash + 1);
     125          102 :         auto [end, error] = std::from_chars(bits.data(), bits.data() + bits.size(), prefix);
     126          102 :         if (error != std::errc{} || end != bits.data() + bits.size() || bits.empty() || prefix > max_prefix) {
     127           15 :             return false;
     128              :         }
     129              :     }
     130              : 
     131              :     // A range of IPv4-mapped addresses is an IPv4 range
     132          117 :     if (ip.is_v6() && ip.to_v6().is_v4_mapped() && prefix >= 96) {
     133            3 :         ip = normalize(ip);
     134            3 :         prefix -= 96;
     135              :     }
     136              : 
     137          117 :     networks_.push_back({ip, prefix});
     138          117 :     return true;
     139              : }
     140              : 
     141          597 : bool trusted_proxies::in_network(const address& ip, const network& net) {
     142          597 :     if (ip.is_v4() != net.address.is_v4()) return false;
     143              : 
     144          513 :     auto compare = [&](const auto& a, const auto& b) {
     145          513 :         unsigned full_bytes = net.prefix / 8;
     146          513 :         unsigned remaining_bits = net.prefix % 8;
     147          513 :         if (!std::equal(a.begin(), a.begin() + full_bytes, b.begin())) return false;
     148          315 :         if (remaining_bits == 0) return true;
     149           12 :         auto mask = static_cast<uint8_t>(0xFF << (8 - remaining_bits));
     150           12 :         return (a[full_bytes] & mask) == (b[full_bytes] & mask);
     151          513 :     };
     152              : 
     153          513 :     if (ip.is_v4()) return compare(ip.to_v4().to_bytes(), net.address.to_v4().to_bytes());
     154           84 :     return compare(ip.to_v6().to_bytes(), net.address.to_v6().to_bytes());
     155              : }
     156              : 
     157          507 : bool trusted_proxies::contains(std::string_view ip) const {
     158          507 :     boost::system::error_code ec;
     159          507 :     auto parsed = boost::asio::ip::make_address(std::string(trim(ip)), ec);
     160          507 :     if (ec) return false;
     161          486 :     parsed = normalize(parsed);
     162          486 :     return std::any_of(networks_.begin(), networks_.end(), [&](const network& net) {
     163          597 :         return in_network(parsed, net);
     164          486 :     });
     165              : }
     166              : 
     167          180 : const std::string& trusted_proxies::header_name() const {
     168          180 :     static const std::string x_forwarded_for = "X-Forwarded-For";
     169          180 :     static const std::string forwarded = "Forwarded";
     170          180 :     return header_ == forwarded_header::forwarded ? forwarded : x_forwarded_for;
     171              : }
     172              : 
     173           33 : std::string trusted_proxies::client_ip(const std::string& peer, const std::vector<std::string>& header_values) const {
     174           33 :     if (!contains(peer)) return peer;
     175              : 
     176              :     // Forwarded addresses, left (client) to right (nearest proxy)
     177           27 :     std::vector<std::string> chain;
     178           54 :     for (const auto& value : header_values) {
     179           27 :         auto entries = header_ == forwarded_header::forwarded ? parse_forwarded(value) : parse_x_forwarded_for(value);
     180           27 :         chain.insert(chain.end(), entries.begin(), entries.end());
     181           27 :     }
     182              : 
     183           27 :     std::string client = peer;
     184           48 :     for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
     185           42 :         if (it->empty()) break;     // not an address: the proxy after it is the best we know
     186           36 :         client = *it;
     187           36 :         if (!contains(client)) break;
     188              :     }
     189           27 :     return client;
     190           27 : }
     191              : 
     192          228 : forwarded_client trusted_proxies::resolve(const std::string& peer, std::string_view scheme,
     193              :                                           const headers& request) const {
     194          456 :     forwarded_client client{peer, std::string(scheme)};
     195          228 :     if (!contains(peer)) return client;
     196              : 
     197              :     // Entries, left (client) to right (nearest proxy), with their scheme
     198          177 :     std::vector<forwarded_element> chain;
     199              :     // The scheme of every entry with X-Forwarded-For when the counts do not match: the last one
     200          177 :     std::optional<std::string> nearest_scheme;
     201          177 :     if (header_ == forwarded_header::forwarded) {
     202          105 :         for (const auto& value : request.get_headers_with_key(header_name())) {
     203           45 :             auto elements = parse_forwarded_elements(value);
     204           45 :             chain.insert(chain.end(), std::make_move_iterator(elements.begin()), std::make_move_iterator(elements.end()));
     205          105 :         }
     206              :     } else {
     207          189 :         for (const auto& value : request.get_headers_with_key(header_name())) {
     208          186 :             for (auto& address : parse_x_forwarded_for(value)) chain.push_back({std::move(address), {}});
     209          117 :         }
     210          117 :         std::vector<std::string> schemes;
     211          186 :         for (const auto& value : request.get_headers_with_key("X-Forwarded-Proto")) {
     212           69 :             if (trim(value).empty()) continue;
     213          156 :             for (auto entry : split_unquoted(value, ',')) schemes.emplace_back(trim(entry));
     214          117 :         }
     215          117 :         if (schemes.size() == chain.size()) {
     216           81 :             for (size_t i = 0; i < chain.size(); ++i) chain[i].proto = std::move(schemes[i]);
     217           75 :         } else if (!schemes.empty()) {
     218           39 :             nearest_scheme = std::move(schemes.back());
     219              :         }
     220          117 :     }
     221              : 
     222              :     // Walked from the right, as client_ip()
     223          177 :     const forwarded_element* last = nullptr;
     224          231 :     for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
     225          153 :         last = &*it;
     226          153 :         if (it->address.empty()) break;     // not an address: the proxy after it is the best we know
     227          150 :         client.ip = it->address;
     228          150 :         if (!contains(client.ip)) break;
     229              :     }
     230              : 
     231          177 :     auto forwarded = parse_scheme(nearest_scheme ? *nearest_scheme : last ? last->proto : std::string{});
     232          177 :     if (!forwarded.empty()) client.scheme = std::move(forwarded);
     233          177 :     return client;
     234          291 : }
     235              : 
     236          195 : std::string trusted_proxies::parse_scheme(std::string_view value) {
     237          195 :     auto scheme = unquote(value);
     238          297 :     if (boost::iequals(scheme, "https")) return "https";
     239          234 :     if (boost::iequals(scheme, "http")) return "http";
     240           99 :     return {};
     241          195 : }
     242              : 
     243          111 : std::vector<std::string> trusted_proxies::parse_x_forwarded_for(std::string_view value) {
     244          111 :     std::vector<std::string> result;
     245          111 :     if (trim(value).empty()) return result;
     246          291 :     for (auto entry : split_unquoted(value, ',')) {
     247          183 :         result.push_back(to_entry(parse_address(unquote(entry))));
     248          108 :     }
     249          108 :     return result;
     250            0 : }
     251              : 
     252           18 : std::vector<std::string> trusted_proxies::parse_forwarded(std::string_view value) {
     253           18 :     std::vector<std::string> result;
     254           51 :     for (auto& element : parse_forwarded_elements(value)) result.push_back(std::move(element.address));
     255           18 :     return result;
     256            0 : }
     257              : 
     258              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1