LCOV - code coverage report
Current view: top level - http/common - headers.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 97.2 % 246 239
Test Date: 2026-11-10 10:38:07 Functions: 97.9 % 48 47

            Line data    Source code
       1              : #include "headers.hpp"
       2              : #include <array>
       3              : #include <charconv>
       4              : #include <cstdint>
       5              : #include <cstring>
       6              : #include <regex>
       7              : #include "syntax.hpp"
       8              : #include "../../util/ascii.hpp"
       9              : #include "../../util/logger.hpp"
      10              : 
      11              : namespace thinger::http{
      12              : 
      13       302489 :     void headers::process_header(std::string key, std::string value){
      14       302489 :         if(boost::indeterminate(keep_alive_) && is_header(key, header::connection)){
      15              :             /*
      16              :              * Firefox send both keep-alive and upgrade values in Connection header, i.e., when opening a WebSocket,
      17              :              * so it is necessary to test values separately.
      18              :              */
      19              :             static constexpr std::string_view whitespace = " \t\n\v\f\r";
      20        10638 :             std::string_view options = value;
      21              :             while(true){
      22        10743 :                 auto comma = options.find(',');
      23        10743 :                 auto option = options.substr(0, comma);
      24        10743 :                 auto first = option.find_first_not_of(whitespace);
      25        10743 :                 option = first == std::string_view::npos ? std::string_view{} :
      26        10734 :                          option.substr(first, option.find_last_not_of(whitespace) - first + 1);
      27        10743 :                 if(is_header(option, connection::keep_alive)){
      28         2427 :                     keep_alive_ = true;
      29              :                 }
      30         8316 :                 else if (is_header(option, connection::close)){
      31         4135 :                     keep_alive_ = false;
      32              :                 }
      33         4181 :                 else if(is_header(option, connection::upgrade)){
      34         4154 :                     upgrade_ = true;
      35              :                 }
      36        10743 :                 if(comma == std::string_view::npos) break;
      37          105 :                 options.remove_prefix(comma + 1);
      38          105 :             }
      39              :         }
      40              : 
      41       302489 :         if(is_header(key, header::accept)){
      42          396 :             stream_ = ::thinger::util::ascii::iequals(value, accept::event_stream);
      43              :         }else{
      44       302093 :             process_framing_header(key, value);
      45              :         }
      46              : 
      47       302489 :         store_header(std::move(key), std::move(value));
      48       302489 :     }
      49              : 
      50       302093 :     void headers::process_framing_header(std::string_view key, std::string_view value){
      51       302093 :         if(is_header(key, header::content_length)){
      52              :             // Content-Length = 1*DIGIT: anything else (sign, spaces, lists, overflow) or a
      53              :             // repeated header with a different value makes the message framing ambiguous
      54        76702 :             size_t length = 0;
      55        76702 :             auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), length);
      56        76702 :             if (ec != std::errc{} || ptr != value.data() + value.size()) {
      57          949 :                 invalid_content_length_ = true;
      58          949 :                 length = 0;
      59        75753 :             } else if (content_length_header_ && length != content_length_) {
      60           69 :                 invalid_content_length_ = true;
      61              :             }
      62        76702 :             content_length_ = length;
      63       225391 :         }else if(is_header(key, header::transfer_encoding)){
      64              :             // chunked must be the only coding, in a single header
      65        61872 :             if(::thinger::util::ascii::iequals(value, "chunked") && !transfer_encoding_header_){
      66         1202 :                 chunked_transfer_ = true;
      67              :             }else{
      68        60670 :                 invalid_transfer_encoding_ = true;
      69              :             }
      70              :         }
      71       302093 :     }
      72              : 
      73       638304 :     void headers::add_header(std::string key, std::string value){
      74       638304 :         if(key.empty() || !accept_header(key, value)) return;
      75       395049 :         store_header(std::move(key), std::move(value));
      76              :     }
      77              : 
      78       876922 :     void headers::store_header(std::string&& key, std::string&& value){
      79       876922 :         on_header_added(key);
      80       876922 :         headers_.emplace_back(std::move(key), std::move(value));
      81       876922 :     }
      82              : 
      83       876922 :     void headers::on_header_added(std::string_view key){
      84       876922 :         if(is_header(key, header::content_length)){
      85       101241 :             content_length_header_ = true;
      86       775681 :         }else if(is_header(key, header::transfer_encoding)){
      87        62308 :             transfer_encoding_header_ = true;
      88              :         }
      89       876922 :     }
      90              : 
      91         1017 :     void headers::on_header_removed(std::string_view key){
      92         1017 :         if(is_header(key, header::content_length)){
      93          731 :             content_length_header_ = has_header(header::content_length);
      94          286 :         }else if(is_header(key, header::transfer_encoding)){
      95           18 :             transfer_encoding_header_ = has_header(header::transfer_encoding);
      96              :         }
      97         1017 :     }
      98              : 
      99           21 :     void headers::add_proxy(std::string key, std::string value){
     100           21 :         if(key.empty() || !accept_header(key, value)) return;
     101           15 :         proxy_headers_.emplace_back(std::move(key), std::move(value));
     102              :     }
     103              : 
     104       111833 :     void headers::set_header(std::string key, std::string value){
     105       111833 :         if(key.empty() || !accept_header(key, value)) return;
     106       111809 :         replace_header(std::move(key), std::move(value));
     107              :     }
     108              : 
     109           66 :     void headers::replace_headers(std::string key, std::string value){
     110          114 :         if(key.empty() || !accept_header(key, value)) return;
     111           30 :         auto same_name = [&key](const http_header& header){ return is_header(header.first, key); };
     112           57 :         auto first = std::find_if(headers_.begin(), headers_.end(), same_name);
     113           57 :         if(first == headers_.end()){
     114           48 :             store_header(std::move(key), std::move(value));
     115           48 :             return;
     116              :         }
     117            9 :         first->second = std::move(value);
     118            9 :         headers_.erase(std::remove_if(first + 1, headers_.end(), same_name), headers_.end());
     119              :     }
     120              : 
     121       189728 :     void headers::replace_header(std::string&& key, std::string&& value){
     122       277633 :         for(auto & header : headers_)
     123              :         {
     124        98297 :             if(is_header(header.first, key)){
     125        10392 :                 header.second = std::move(value);
     126        10392 :                 return;
     127              :             }
     128              :         }
     129       179336 :         store_header(std::move(key), std::move(value));
     130              :     }
     131              : 
     132           18 :     void headers::set_proxy(std::string key, std::string value){
     133           18 :         if(key.empty() || !accept_header(key, value)) return;
     134            9 :         for(auto & header : proxy_headers_)
     135              :         {
     136            3 :             if(is_header(header.first, key)){
     137            3 :                 header.second = std::move(value);
     138            3 :                 return;
     139              :             }
     140              :         }
     141            6 :         proxy_headers_.emplace_back(std::move(key), std::move(value));
     142              :     }
     143              : 
     144           18 :     bool headers::upgrade() const{
     145           18 :         return upgrade_;
     146              :     }
     147              : 
     148            9 :     bool headers::stream() const{
     149            9 :         return stream_;
     150              :     }
     151              : 
     152       628927 :     bool headers::has_header(std::string_view key) const{
     153       955481 :         for(const auto & header : headers_)
     154              :         {
     155       675224 :             if(is_header(header.first, key)){
     156       348670 :                 return true;
     157              :             }
     158              :         }
     159       280257 :         return false;
     160              :     }
     161              : 
     162        34130 :     const std::string& headers::get_header(std::string_view key) const
     163              :     {
     164       103323 :         for(const auto & header : headers_)
     165              :         {
     166        90706 :             if(is_header(header.first, key)){
     167        21513 :                 return header.second;
     168              :             }
     169              :         }
     170        12617 :         static std::string emtpy;
     171        12617 :         return emtpy;
     172              :     }
     173              : 
     174        17404 :     std::vector<std::string> headers::get_headers_with_key(std::string_view key) const{
     175        17404 :         std::vector<std::string> headers;
     176        79286 :         for(const auto & header : headers_)
     177              :         {
     178        61882 :             if(is_header(header.first, key)){
     179         6471 :                 headers.push_back(header.second);
     180              :             }
     181              :         }
     182        17404 :         return headers;
     183            0 :     }
     184              : 
     185         5830 :     const std::vector<headers::http_header>& headers::get_headers() const{
     186         5830 :         return headers_;
     187              :     }
     188              : 
     189        50497 :     std::vector<headers::http_header>& headers::get_headers(){
     190        50497 :         return headers_;
     191              :     }
     192              : 
     193            9 :     bool headers::remove_header(std::string_view key)
     194              :     {
     195            9 :         for(auto it=headers_.begin(); it!=headers_.end(); ++it){
     196            6 :             if(is_header(it->first, key)){
     197            6 :                 headers_.erase(it);
     198            6 :                 on_header_removed(key);
     199            6 :                 return true;
     200              :             }
     201              :         }
     202            3 :         return false;
     203              :     }
     204              : 
     205        17607 :     size_t headers::remove_headers(std::string_view key)
     206              :     {
     207        17607 :         auto removed = std::erase_if(headers_, [&](const http_header& header){
     208        50780 :             return is_header(header.first, key);
     209              :         });
     210        17607 :         if(removed) on_header_removed(key);
     211        17607 :         return removed;
     212              :     }
     213              : 
     214            6 :     const std::string& headers::get_authorization() const
     215              :     {
     216            6 :         return get_header(header::authorization);
     217              :     }
     218              : 
     219            6 :     const std::string& headers::get_cookie() const
     220              :     {
     221            6 :         return get_header(header::cookie);
     222              :     }
     223              : 
     224            6 :     const std::string& headers::get_user_agent() const{
     225            6 :         return get_header(header::user_agent);
     226              :     }
     227              : 
     228         2674 :     const std::string& headers::get_content_type() const
     229              :     {
     230         2674 :         return get_header(header::content_type);
     231              :     }
     232              : 
     233            9 :     bool headers::is_content_type(const std::string& value) const
     234              :     {
     235            9 :         return ::thinger::util::ascii::istarts_with(get_header(header::content_type), value);
     236              :     }
     237              : 
     238            6 :     bool headers::empty_headers() const{
     239            6 :         return headers_.empty();
     240              :     }
     241              : 
     242            3 :     void headers::debug_headers(std::ostream& os) const{
     243            9 :         for(const std::pair<std::string, std::string>& t: headers_){
     244            6 :             os << "\t> " << t.first << ": " << t.second << std::endl;
     245              :         }
     246            3 :     }
     247              : 
     248              :     namespace {
     249              : 
     250              :         [[maybe_unused]] constexpr std::string_view redacted = "[redacted]";
     251              : 
     252              :         // The first `max_length` bytes of `data` for a log line: control characters escaped,
     253              :         // so a value cannot forge log lines, and how much is left out
     254       245733 :         [[maybe_unused]] std::string printable(std::string_view data, size_t max_length) {
     255              :             static constexpr char hex[] = "0123456789abcdef";
     256       245733 :             auto shown = data.substr(0, max_length);
     257       245733 :             std::string out;
     258       245733 :             out.reserve(shown.size() + 32);
     259      4415814 :             for (unsigned char c : shown) {
     260      4170081 :                 switch (c) {
     261         1188 :                     case '\n': out += "\\n"; break;
     262         1185 :                     case '\r': out += "\\r"; break;
     263         1155 :                     case '\t': out += "\\t"; break;
     264         1152 :                     case '\\': out += "\\\\"; break;
     265      4165401 :                     default:
     266      4165401 :                         if (c < 0x20 || c == 0x7f) {
     267        34572 :                             out += "\\x";
     268        34572 :                             out += hex[c >> 4];
     269        34572 :                             out += hex[c & 0xf];
     270              :                         } else {
     271      4130829 :                             out += static_cast<char>(c);
     272              :                         }
     273              :                 }
     274              :             }
     275       245733 :             if (shown.size() < data.size()) {
     276           18 :                 out += "... (";
     277           18 :                 out += std::to_string(data.size() - shown.size());
     278           18 :                 out += " more bytes)";
     279              :             }
     280       491466 :             return out;
     281            0 :         }
     282              : 
     283              :         // The headers whose value is a URI, which may have secrets in its query
     284          228 :         [[maybe_unused]] bool holds_uri(std::string_view name) {
     285          420 :             return headers::is_header(name, "Location") || headers::is_header(name, "Content-Location") ||
     286          420 :                    headers::is_header(name, "Referer");
     287              :         }
     288              : 
     289         2172 :         [[maybe_unused]] void log_header(std::string_view label, const std::string& name, const std::string& value,
     290              :                         const body_logging& options) {
     291         2172 :             if (!options.enabled) {
     292         1923 :                 LOG_DEBUG("  {}{}", label, printable(name, name.size()));
     293          249 :             } else if (headers::is_sensitive_header(name)) {
     294           21 :                 LOG_DEBUG("  {}{}: {}", label, printable(name, name.size()), redacted);
     295          228 :             } else if (holds_uri(name)) {
     296              :                 // Redacted first: truncating it may leave a part of the secret unrecognized
     297           54 :                 LOG_DEBUG("  {}{}: {}", label, printable(name, name.size()),
     298              :                           printable(util::url::loggable_uri(value, options.redact), options.max_length));
     299              :             } else {
     300          174 :                 LOG_DEBUG("  {}{}: {}", label, printable(name, name.size()), printable(value, options.max_length));
     301              :             }
     302         2172 :         }
     303              : 
     304              :         // syntax::is_token_char of each byte
     305              :         constexpr auto token_chars = [] {
     306              :             std::array<bool, 256> table{};
     307              :             for (unsigned c = 0; c < table.size(); c++) table[c] = syntax::is_token_char(static_cast<unsigned char>(c));
     308              :             return table;
     309              :         }();
     310              : 
     311              :         // syntax::is_token, with a table lookup per byte
     312       750233 :         bool valid_name(std::string_view name) {
     313      6522397 :             for (unsigned char c : name) {
     314      5978453 :                 if (!token_chars[c]) return false;
     315              :             }
     316       543944 :             return !name.empty();
     317              :         }
     318              : 
     319              :         // syntax::is_field_value, eight bytes at a time: a word without a byte under 0x20 or
     320              :         // equal to 0x7F is valid (the exact any-byte tests of "Bit Twiddling Hacks"). A value
     321              :         // under eight bytes, or with such a byte (HTAB is one), is checked a byte at a time
     322       543944 :         bool valid_value(std::string_view value) {
     323       543944 :             constexpr uint64_t ones = 0x0101010101010101u, highs = 0x8080808080808080u;
     324      1053432 :             auto valid_word = [&value](size_t at) {
     325              :                 uint64_t word;
     326      1053432 :                 std::memcpy(&word, value.data() + at, sizeof word);
     327      1053432 :                 uint64_t del = word ^ (ones * 0x7f);
     328      1053432 :                 return ((((word - ones * 0x20) & ~word) | ((del - ones) & ~del)) & highs) == 0;
     329       543944 :             };
     330       543944 :             size_t size = value.size();
     331       543944 :             if (size >= 8) {
     332       420219 :                 size_t at = 0;
     333      1155315 :                 while (at + 8 <= size && valid_word(at)) at += 8;
     334       420219 :                 if (at + 8 > size && (at == size || valid_word(size - 8))) return true;
     335              :             }
     336       159470 :             return syntax::is_field_value(value);
     337              :         }
     338              : 
     339              :     }
     340              : 
     341       750233 :     bool headers::accept_header(std::string_view key, std::string_view value) {
     342       750233 :         if (valid_name(key) && valid_value(value)) [[likely]] return true;
     343              :         // The value is never logged: it may be a credential
     344       243294 :         refused_header_ = true;
     345       243294 :         LOG_DEBUG("Refused header {}: invalid {}", printable(key, 64), syntax::is_token(key) ? "value" : "name");
     346       243294 :         return false;
     347              :     }
     348              : 
     349          249 :     bool headers::is_sensitive_header(std::string_view name) {
     350          732 :         return is_header(name, "Authorization") || is_header(name, "Proxy-Authorization") ||
     351          732 :                is_header(name, "Cookie") || is_header(name, "Set-Cookie");
     352              :     }
     353              : 
     354          672 :     void headers::log_headers(const body_logging& options) const {
     355          672 :         if (!LOG_DEBUG_ENABLED()) return;
     356         2844 :         for (const auto& [name, value] : headers_) {
     357         2172 :             log_header("", name, value, options);
     358              :         }
     359          672 :         for (const auto& [name, value] : proxy_headers_) {
     360            0 :             log_header("(PROXY) ", name, value, options);
     361              :         }
     362              :     }
     363              : 
     364          696 :     void headers::log_body(std::string_view body, size_t size, const body_logging& options) {
     365          696 :         if (size == 0 || !LOG_TRACE_ENABLED()) return;
     366          297 :         LOG_TRACE("Body: {} bytes", size);
     367          297 :         if (options.enabled && !body.empty()) {
     368           39 :             LOG_TRACE("  {}", printable(body, options.max_length));
     369              :         }
     370              :     }
     371              : 
     372            0 :     void headers::log(const char* scope, const body_logging& options) const {
     373            0 :         if (!LOG_DEBUG_ENABLED()) return;
     374            0 :         LOG_DEBUG("[{}] Headers:", scope);
     375            0 :         log_headers(options);
     376              :     }
     377              : 
     378       157979 :     size_t headers::get_content_length() const{
     379       157979 :         return content_length_;
     380              :     }
     381              : 
     382        35290 :     bool headers::keep_alive() const
     383              :     {
     384        35290 :         if(boost::indeterminate(keep_alive_)){
     385        28740 :             return http_version_major_>=1 && http_version_minor_>=1;
     386              :         }else{
     387         6550 :             return (bool) keep_alive_;
     388              :         }
     389              :     }
     390              : 
     391        19595 :     void headers::set_keep_alive(bool keep_alive){
     392        19595 :             keep_alive_ = keep_alive;
     393        19595 :             replace_header(std::string(http::header::connection), keep_alive ? "Keep-Alive" : "Close");
     394        19595 :     }
     395              : 
     396        41882 :     void headers::set_http_version_major(uint8_t http_version_major) {
     397        41882 :         http_version_major_ = http_version_major;
     398        41882 :     }
     399              : 
     400        41907 :     void headers::set_http_version_minor(uint8_t http_version_minor) {
     401        41907 :         http_version_minor_ = http_version_minor;
     402        41907 :     }
     403              : 
     404         7832 :     int headers::get_http_version_major() const {
     405         7832 :         return http_version_major_;
     406              :     }
     407              : 
     408         4472 :     int headers::get_http_version_minor() const {
     409         4472 :         return http_version_minor_;
     410              :     }
     411              : 
     412           24 :     std::string headers::get_parameter(const std::string& header_value, std::string_view name) {
     413              : 
     414           24 :         auto start = header_value.begin();
     415           24 :                 auto end   = header_value.end();
     416              : 
     417           30 :                 if(start==end) return "";
     418              : 
     419           21 :                 static std::regex cookie_regex("([^;^\\s]+)=\"?([^;^\"]*)\"?");
     420           21 :                 std::smatch what;
     421              : 
     422           36 :                 while (std::regex_search(start, end, what, cookie_regex))
     423              :                 {
     424           66 :                         std::string key(what[1].first, what[1].second);
     425           33 :                         std::string value(what[2].first, what[2].second);
     426              : 
     427           33 :                         if(key==name) return value;
     428              : 
     429           15 :                         start = what[0].second;
     430           51 :                 }
     431              : 
     432            6 :                 return "";
     433           21 :         }
     434              : 
     435              : }
        

Generated by: LCOV version 2.0-1