LCOV - code coverage report
Current view: top level - http/util - url.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 98.0 % 150 147
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 21 21

            Line data    Source code
       1              : #include "url.hpp"
       2              : #include "../../util/ascii.hpp"
       3              : 
       4              : namespace thinger::http::util::url{
       5              : 
       6              :     namespace {
       7              :         constexpr char hex_chars[] = "0123456789ABCDEF";
       8              : 
       9       132780 :         inline int hex_digit(char c) {
      10       132780 :             if (c >= '0' && c <= '9') return c - '0';
      11        85402 :             if (c >= 'a' && c <= 'f') return c - 'a' + 10;
      12        73259 :             if (c >= 'A' && c <= 'F') return c - 'A' + 10;
      13        34801 :             return -1;
      14              :         }
      15              : 
      16      1567158 :         inline bool is_unreserved(char c) {
      17      1567158 :             return std::isalnum(static_cast<unsigned char>(c))
      18      1567158 :                 || c == '-' || c == '_' || c == '.' || c == '~';
      19              :         }
      20              :     }
      21              : 
      22              :     // RFC 3986 Section 2.3: unreserved characters are not percent-encoded
      23       120579 :     std::string url_encode(const std::string &value) {
      24       120579 :         std::string result;
      25       120579 :         result.reserve(value.size() * 1.2);
      26              : 
      27       604638 :         for (unsigned char c : value) {
      28       484059 :             if (is_unreserved(c)) {
      29       483585 :                 result += static_cast<char>(c);
      30              :             } else {
      31          474 :                 result += '%';
      32          474 :                 result += hex_chars[c >> 4];
      33          474 :                 result += hex_chars[c & 0x0F];
      34              :             }
      35              :         }
      36              : 
      37       120579 :         return result;
      38            0 :     }
      39              : 
      40              :     // RFC 3986 path encoding: also preserves '/' and '~'
      41       120348 :     std::string uri_path_encode(const std::string &value) {
      42       120348 :         std::string result;
      43       120348 :         result.reserve(value.size() * 1.2);
      44              : 
      45      1203447 :         for (unsigned char c : value) {
      46      1083099 :             if (is_unreserved(c) || c == '/' ) {
      47      1083084 :                 result += static_cast<char>(c);
      48              :             } else {
      49           15 :                 result += '%';
      50           15 :                 result += hex_chars[c >> 4];
      51           15 :                 result += hex_chars[c & 0x0F];
      52              :             }
      53              :         }
      54              : 
      55       120348 :         return result;
      56            0 :     }
      57              : 
      58              :     namespace {
      59       365372 :         bool percent_decode(std::string_view in, std::string &out, bool plus_is_space) {
      60       365372 :             out.clear();
      61       365372 :             out.reserve(in.size());
      62              : 
      63      2821092 :             for (size_t i = 0; i < in.size(); ++i) {
      64      2504119 :                 if (in[i] == '%') {
      65        88975 :                     if (i + 2 >= in.size()) return false;
      66        66390 :                     int hi = hex_digit(in[i + 1]);
      67        66390 :                     int lo = hex_digit(in[i + 2]);
      68        66390 :                     if (hi < 0 || lo < 0) return false;
      69        40576 :                     out += static_cast<char>((hi << 4) | lo);
      70        40576 :                     i += 2;
      71      2415144 :                 } else if (in[i] == '+' && plus_is_space) {
      72        20797 :                     out += ' ';
      73              :                 } else {
      74      2394347 :                     out += in[i];
      75              :                 }
      76              :             }
      77       316973 :             return true;
      78              :         }
      79              :     }
      80              : 
      81       187435 :     bool url_decode(const std::string &in, std::string &out) {
      82       187435 :         return percent_decode(in, out, true);
      83              :     }
      84              : 
      85       177937 :     bool detail::path_decode(std::string_view in, std::string &out) {
      86       177937 :         return percent_decode(in, out, false);
      87              :     }
      88              : 
      89       187015 :     std::string url_decode(const std::string &in) {
      90       187015 :         std::string out;
      91       187015 :         if (url_decode(in, out)) {
      92       153634 :             return out;
      93              :         }
      94        33381 :         return {};
      95       187015 :     }
      96              : 
      97           30 :     void parse_url_encoded_data(const std::string &data, std::multimap<std::string, std::string>& store) {
      98           30 :         auto start = data.cbegin();
      99           30 :         auto end = data.cend();
     100           30 :         parse_url_encoded_data(start, end, store);
     101           30 :     }
     102              : 
     103        50652 :     void parse_url_encoded_data(std::string::const_iterator& start, std::string::const_iterator& end, std::multimap<std::string, std::string>& store) {
     104        50652 :         if (start == end) return;
     105              : 
     106        45966 :         auto it = start;
     107       115943 :         while (it != end) {
     108              :             // find the end of the current key=value pair
     109        69977 :             auto pair_end = std::find(it, end, '&');
     110              : 
     111              :             // find the '=' separator within the pair
     112        69977 :             auto eq_pos = std::find(it, pair_end, '=');
     113              : 
     114        69977 :             std::string key(it, eq_pos);
     115        69977 :             std::string value;
     116        69977 :             if (eq_pos != pair_end) {
     117        23067 :                 value.assign(eq_pos + 1, pair_end);
     118              :             }
     119              : 
     120        69977 :             if (!key.empty()) {
     121        60077 :                 store.emplace(url_decode(key), url_decode(value));
     122              :             }
     123              : 
     124        69977 :             it = (pair_end != end) ? pair_end + 1 : end;
     125        69977 :         }
     126              : 
     127        45966 :         start = it;
     128              :     }
     129              : 
     130        60195 :     std::string get_url_encoded_data(const std::multimap<std::string, std::string>& store) {
     131        60195 :         std::string result;
     132        60195 :         bool first = true;
     133       120417 :         for (const auto& [key, value] : store) {
     134        60222 :             if (!first) result += '&';
     135        60222 :             result += url_encode(key);
     136        60222 :             result += '=';
     137        60222 :             result += url_encode(value);
     138        60222 :             first = false;
     139              :         }
     140        60195 :         return result;
     141            0 :     }
     142              : 
     143              :     namespace {
     144              : 
     145              :         constexpr std::string_view redacted = "[redacted]";
     146              : 
     147              :         // "scheme://user:password@host...": the password redacted
     148          886 :         std::string redact_user_info(std::string_view uri) {
     149          886 :             auto scheme_end = uri.find("://");
     150         2026 :             if (scheme_end == std::string_view::npos) return std::string(uri);
     151          316 :             auto authority_start = scheme_end + 3;
     152          316 :             auto authority_end = uri.find_first_of("/?#", authority_start);
     153          316 :             auto authority = uri.substr(authority_start, authority_end == std::string_view::npos ? std::string_view::npos
     154              :                                                                                                    : authority_end - authority_start);
     155          316 :             auto at = authority.rfind('@');
     156          918 :             if (at == std::string_view::npos) return std::string(uri);
     157           15 :             auto colon = authority.substr(0, at).find(':');
     158           21 :             if (colon == std::string_view::npos) return std::string(uri);
     159           12 :             std::string out(uri.substr(0, authority_start + colon + 1));
     160           12 :             out += redacted;
     161           12 :             out += uri.substr(authority_start + at);
     162           12 :             return out;
     163           12 :         }
     164              : 
     165              :     }
     166              : 
     167         2401 :     redacted_parameters::redacted_parameters(std::initializer_list<std::string_view> names) {
     168        42216 :         for (auto name : names) add(name);
     169         2401 :     }
     170              : 
     171        46722 :     const redacted_parameters& redacted_parameters::defaults() {
     172              :         static const redacted_parameters names{
     173              :             "access_token", "api_key", "apikey", "authorization", "client_secret", "code",
     174              :             "id_token", "key", "password", "refresh_token", "secret", "sig", "signature", "token",
     175        46722 :             "x-amz-security-token", "x-amz-signature", "x-goog-signature"};
     176        46722 :         return names;
     177              :     }
     178              : 
     179              :     // The names are shared with the copies of this set: both change a copy of them
     180        39842 :     redacted_parameters& redacted_parameters::add(std::string_view name) {
     181        39842 :         if (name.empty() || contains(name)) return *this;
     182        39830 :         auto names = names_ ? std::make_shared<std::vector<std::string>>(*names_)
     183        39830 :                             : std::make_shared<std::vector<std::string>>();
     184        39830 :         names->emplace_back(name);
     185        39830 :         names_ = std::move(names);
     186        39830 :         return *this;
     187        39830 :     }
     188              : 
     189           15 :     redacted_parameters& redacted_parameters::remove(std::string_view name) {
     190           15 :         if (!contains(name)) return *this;
     191           12 :         auto names = std::make_shared<std::vector<std::string>>(*names_);
     192          171 :         std::erase_if(*names, [&](const std::string& candidate) { return thinger::util::ascii::iequals(candidate, name); });
     193           12 :         names_ = std::move(names);
     194           12 :         return *this;
     195           12 :     }
     196              : 
     197        40859 :     bool redacted_parameters::contains(std::string_view name) const {
     198        40859 :         return names_ && std::any_of(names_->begin(), names_->end(), [&](const std::string& candidate) {
     199       329461 :             return thinger::util::ascii::iequals(candidate, name);
     200        81718 :         });
     201              :     }
     202              : 
     203          174 :     std::string loggable_uri(std::string_view uri) {
     204          174 :         return loggable_uri(uri, redacted_parameters::defaults());
     205              :     }
     206              : 
     207          886 :     std::string loggable_uri(std::string_view uri, const redacted_parameters& names) {
     208          886 :         auto query = uri.find('?');
     209          886 :         if (query == std::string_view::npos) return redact_user_info(uri);
     210          435 :         auto fragment = uri.find('#', query);
     211          435 :         auto query_end = fragment == std::string_view::npos ? uri.size() : fragment;
     212              : 
     213          435 :         std::string out = redact_user_info(uri.substr(0, query + 1));
     214          435 :         auto pos = query + 1;
     215         1041 :         while (pos <= query_end) {
     216         1041 :             auto amp = uri.find('&', pos);
     217         1041 :             auto end = (amp == std::string_view::npos || amp > query_end) ? query_end : amp;
     218         1041 :             auto pair = uri.substr(pos, end - pos);
     219         1041 :             auto eq = pair.find('=');
     220         2889 :             if (eq != std::string_view::npos && names.contains(url_decode(std::string(pair.substr(0, eq))))) {
     221          402 :                 out += pair.substr(0, eq + 1);
     222          402 :                 out += redacted;
     223              :             } else {
     224          639 :                 out += pair;
     225              :             }
     226         1041 :             if (end == query_end) break;
     227          606 :             out += '&';
     228          606 :             pos = end + 1;
     229              :         }
     230          435 :         out += uri.substr(query_end);
     231          435 :         return out;
     232          435 :     }
     233              : 
     234              : }
        

Generated by: LCOV version 2.0-1