LCOV - code coverage report
Current view: top level - http/server - request_factory.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 91.9 % 210 193
Test Date: 2026-11-10 10:38:07 Functions: 93.3 % 15 14

            Line data    Source code
       1              : #include <algorithm>
       2              : #include <cctype>
       3              : #include <sstream>
       4              : #include <string_view>
       5              : #include "request_factory.hpp"
       6              : #include "../common/http_request.hpp"
       7              : #include "../util/url.hpp"
       8              : #include "../../util/ascii.hpp"
       9              : #include "../../util/logger.hpp"
      10              : 
      11              : namespace thinger::http {
      12              : 
      13              :     namespace {
      14              : 
      15         1663 :         bool is_percent_encoded(std::string_view text) {
      16         2813 :             for (auto percent = text.find('%'); percent != std::string_view::npos; percent = text.find('%', percent + 3)) {
      17         2730 :                 if (percent + 2 >= text.size() || !std::isxdigit(static_cast<unsigned char>(text[percent + 1])) ||
      18         1150 :                     !std::isxdigit(static_cast<unsigned char>(text[percent + 2]))) {
      19          430 :                     return false;
      20              :                 }
      21              :             }
      22         1233 :             return true;
      23              :         }
      24              : 
      25        34630 :         bool has_dot_dot_segment(std::string_view path) {
      26        99098 :             for (size_t start = 0; start < path.size();) {
      27        65590 :                 auto end = std::min(path.find('/', start), path.size());
      28        65590 :                 if (end - start == 2 && path[start] == '.' && path[start + 1] == '.') return true;
      29        64468 :                 start = end + 1;
      30              :             }
      31        33508 :             return false;
      32              :         }
      33              : 
      34              :         // A target in origin form is accepted when it starts with "/" as sent (RFC 9112 section
      35              :         // 3.2.1: an encoded "%2F" is not one), its percent-encoding is valid, and no segment of
      36              :         // its decoded path is "..". The path ends at the first "?" as sent (an encoded "%3F" is
      37              :         // part of it) and the query is not checked for "..", as "?range=1..5" or
      38              :         // "/v1..v2.txt" are no traversal
      39        35319 :         bool is_accepted_target(std::string_view target) {
      40        35319 :             if (target.empty() || target[0] != '/') return false;
      41        35300 :             auto query = target.find('?');
      42        35300 :             if (query != std::string_view::npos && !is_percent_encoded(target.substr(query + 1))) return false;
      43        34870 :             auto path = target.substr(0, query);
      44              :             // without escapes, the path is checked as sent
      45        34870 :             if (path.find('%') == std::string_view::npos) return !has_dot_dot_segment(path);
      46         1206 :             std::string decoded;
      47         1206 :             return util::url::detail::path_decode(path, decoded) && !has_dot_dot_segment(decoded);
      48         1206 :         }
      49              : 
      50              :     }
      51              : 
      52        35288 :     request_factory::request_factory() : state_(method_start) {
      53        35288 :     }
      54              : 
      55      1024818 :     boost::tribool request_factory::consume(char input) {
      56              :         // bound the request line and headers, as they are buffered whole
      57      1024818 :         if (state_ != content && ++header_section_size_ > max_header_section_size) {
      58           38 :             return header_section_exceeded();
      59              :         }
      60      1024780 :         switch (state_) {
      61        35392 :             case method_start:
      62        35392 :                 if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
      63            0 :                     return false;
      64              :                 }
      65              :                 else {
      66        35392 :                     state_ = method;
      67        35392 :                     tempString1_.push_back(input);
      68        35392 :                     return boost::indeterminate;
      69              :                 }
      70       111424 :             case method:
      71       111424 :                 if (input == ' ') {
      72              :                     // initialize a new http request if necessary
      73        35392 :                     if(!req){
      74        35392 :                         req = std::make_shared<http_request>();
      75              :                         // room for the headers of most requests, without growing
      76        35392 :                         req->get_headers().reserve(16);
      77              :                     }
      78              :                     // store read method
      79        35392 :                     on_http_method(tempString1_);
      80        35392 :                     tempString1_.clear();
      81              :                     // CONNECT asks for a tunnel to the authority in its target (authority-form,
      82              :                     // RFC 9112 section 3.2.3), which is not served. What follows its header
      83              :                     // section is tunnel data, not content nor another request (RFC 9110 section
      84              :                     // 9.3.6): reading it as HTTP would frame the stream unlike a proxy does
      85        35392 :                     if (req->get_method() == method::CONNECT) {
      86           22 :                         return false;
      87              :                     }
      88        35370 :                     state_ = uri;
      89        35370 :                     return boost::indeterminate;
      90              :                 }
      91        76032 :                 else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
      92            0 :                     return false;
      93              :                 }
      94              :                 else {
      95        76032 :                     tempString1_.push_back(input);
      96        76032 :                     return boost::indeterminate;
      97              :                 }
      98        35352 :             case uri:
      99        35352 :                 if (input == ' ') {
     100        35319 :                     if (!is_accepted_target(tempString1_)) {
     101         1811 :                         tempString1_.clear();
     102         1811 :                         return false;
     103              :                     }
     104        33508 :                     on_http_uri(tempString1_);
     105        33508 :                     tempString1_.clear();
     106              :                     // next step reading http version
     107        33508 :                     state_ = http_version_h;
     108        33508 :                     return boost::indeterminate;
     109              :                 }
     110              :                 // The request-target is ASCII (RFC 9112 section 3.2, RFC 3986): other bytes must
     111              :                 // be percent-encoded. An invalid request line is rejected, not autocorrected, as
     112              :                 // it may be crafted to bypass filters along the request chain (RFC 9112 section 3)
     113           33 :                 else if (!is_char(input) || is_ctl(input)) {
     114           33 :                     return false;
     115              :                 }
     116              :                 else {
     117            0 :                     tempString1_.push_back(input);
     118            0 :                     return boost::indeterminate;
     119              :                 }
     120        33508 :             case http_version_h:
     121        33508 :                 if (input == 'H') {
     122        33508 :                     state_ = http_version_t_1;
     123        33508 :                     return boost::indeterminate;
     124              :                 }
     125            0 :                 return false;
     126        33508 :             case http_version_t_1:
     127        33508 :                 if (input == 'T') {
     128        33508 :                     state_ = http_version_t_2;
     129        33508 :                     return boost::indeterminate;
     130              :                 }
     131            0 :                 return false;
     132        33508 :             case http_version_t_2:
     133        33508 :                 if (input == 'T') {
     134        33508 :                     state_ = http_version_p;
     135        33508 :                     return boost::indeterminate;
     136              :                 }
     137            0 :                 return false;
     138        33508 :             case http_version_p:
     139        33508 :                 if (input == 'P') {
     140        33508 :                     state_ = http_version_slash;
     141        33508 :                     return boost::indeterminate;
     142              :                 }
     143            0 :                 return false;
     144        33508 :             case http_version_slash:
     145        33508 :                 if (input == '/') {
     146        33508 :                     state_ = http_version_major_start;
     147        33508 :                     return boost::indeterminate;
     148              :                 }
     149            0 :                 return false;
     150              :             // HTTP-version = HTTP-name "/" DIGIT "." DIGIT (RFC 9112, section 2.3): a single
     151              :             // digit each, as longer numbers would be truncated (e.g. "HTTP/1.10" as 1.1). Only
     152              :             // major version 1 uses this message syntax; a higher minor version is handled as
     153              :             // 1.1 (RFC 9110, section 2.5)
     154        33508 :             case http_version_major_start:
     155        33508 :                 if (input == '1') {
     156        33496 :                     tempInt_ = input - '0';
     157        33496 :                     state_ = http_version_major;
     158        33496 :                     return boost::indeterminate;
     159              :                 }
     160           12 :                 return false;
     161        33496 :             case http_version_major:
     162        33496 :                 if (input == '.') {
     163        33492 :                     on_http_major_version(tempInt_);
     164        33492 :                     state_ = http_version_minor_start;
     165        33492 :                     return boost::indeterminate;
     166              :                 }
     167            4 :                 return false;
     168        33492 :             case http_version_minor_start:
     169        33492 :                 if (is_digit(input)) {
     170        33490 :                     tempInt_ = input - '0';
     171        33490 :                     state_ = http_version_minor;
     172        33490 :                     return boost::indeterminate;
     173              :                 }
     174            2 :                 return false;
     175        33490 :             case http_version_minor:
     176        33490 :                 if (input == '\r') {
     177        33484 :                     on_http_minor_version(tempInt_);
     178        33484 :                     tempInt_ = -1; // reserve temp int for storing content-lenght value
     179        33484 :                     state_ = expecting_newline_1;
     180        33484 :                     return boost::indeterminate;
     181              :                 }
     182            6 :                 return false;
     183        33484 :             case expecting_newline_1:
     184        33484 :                 if (input == '\n') {
     185        33484 :                     state_ = header_line_start;
     186        33484 :                     return boost::indeterminate;
     187              :                 }
     188            0 :                 return false;
     189       105717 :             case header_line_start:
     190       105717 :                 if (input == '\r') {
     191        30249 :                     state_ = expecting_newline_3;
     192        30249 :                     return boost::indeterminate;
     193              :                 }
     194              :                 // A line starting with whitespace (obsolete line folding, RFC 9112 section 5.2)
     195              :                 // is rejected, as is any other byte that cannot start a header name
     196        75468 :                 else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
     197          495 :                     return false;
     198              :                 }
     199              :                 else {
     200        74973 :                     if (++header_lines_ > max_header_lines) {
     201           22 :                         return header_section_exceeded();
     202              :                     }
     203        74951 :                     tempString1_.clear();
     204        74951 :                     tempString1_.push_back(input);
     205        74951 :                     state_ = header_name;
     206        74951 :                     return boost::indeterminate;
     207              :                 }
     208              :             // field-line = field-name ":" OWS field-value OWS (RFC 9112 section 5): no whitespace
     209              :             // between the name and the colon (a header name is a token, so it fails here)
     210        74951 :             case header_name:
     211        74951 :                 if (input == ':') {
     212        74419 :                     tempString2_.clear();
     213        74419 :                     state_ = space_before_header_value;
     214        74419 :                     return boost::indeterminate;
     215              :                 }
     216          532 :                 else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
     217          532 :                     return false;
     218              :                 }
     219              :                 else {
     220            0 :                     tempString1_.push_back(input);
     221            0 :                     return boost::indeterminate;
     222              :                 }
     223              :             // Any SP and HTAB before the value are skipped, as Node, Go and Beast do; the value
     224              :             // may be empty
     225       149014 :             case space_before_header_value:
     226       149014 :                 if (is_ows(input)) {
     227        74595 :                     return boost::indeterminate;
     228              :                 }
     229        74419 :                 state_ = header_value;
     230              :                 [[fallthrough]];
     231              :             // Trailing SP and HTAB are not part of the value (RFC 9110 section 5.5): trimmed
     232              :             // before the field is stored, so framing fields are validated without them
     233       146876 :             case header_value:
     234       146876 :                 if (input == '\r') {
     235        73502 :                     tempString2_.erase(tempString2_.find_last_not_of(" \t") + 1);
     236              :                     // A single Host field (RFC 9110 section 7.2), whatever the values: two may
     237              :                     // route differently along a proxy chain (Go rejects them too; Node keeps the
     238              :                     // first)
     239        73502 :                     if (::thinger::util::ascii::iequals(tempString1_, header::host)) {
     240        33949 :                         if (host_field_) return false;
     241        32772 :                         host_field_ = true;
     242              :                     }
     243        72325 :                     on_http_header(tempString1_, tempString2_);
     244        72325 :                     state_ = expecting_newline_2;
     245        72325 :                     return boost::indeterminate;
     246              :                 }
     247        73374 :                 else if (is_ctl(input) && input != '\t') {
     248          896 :                     return false;
     249              :                 }
     250              :                 else {
     251        72478 :                     tempString2_.push_back(input);
     252        72478 :                     return boost::indeterminate;
     253              :                 }
     254        72325 :             case expecting_newline_2:
     255        72325 :                 if (input == '\n') {
     256        72233 :                     state_ = header_line_start;
     257        72233 :                     return boost::indeterminate;
     258              :                 }
     259           92 :                 return false;
     260        30238 :             case expecting_newline_3:
     261        30238 :                 if (input == '\n'){
     262              :                     // An HTTP/1.1 request carries a Host field, maybe empty (RFC 9110 section
     263              :                     // 7.2), as Node and Go require; an HTTP/1.0 one may omit it
     264        30238 :                     if (!host_field_ && req->get_http_version_minor() >= 1) {
     265          388 :                         return false;
     266              :                     }
     267        29850 :                     if(headers_only_ || get_content_length() == 0){
     268        29270 :                         return true;
     269              :                     }
     270          580 :                     state_ = content;
     271          580 :                     return boost::indeterminate;
     272              :                 }
     273            0 :                 return false;
     274         2900 :             case content:
     275         2900 :                 on_content(input);
     276         2900 :                 if(get_content_read()<get_content_length()){
     277         2320 :                     return boost::indeterminate;
     278              :                 }
     279          580 :                 return true;
     280            0 :             default:
     281            0 :                 return false;
     282              :         }
     283              :     }
     284              : 
     285        29836 :     std::shared_ptr<http_request> request_factory::consume_request() {
     286        29836 :         std::shared_ptr<http_request> request(req);
     287        29836 :         req.reset();
     288        29836 :         state_ =  method_start;
     289        29836 :         tempString1_.clear();
     290        29836 :         tempString2_.clear();
     291        29836 :         header_lines_ = 0;
     292        29836 :         header_section_size_ = 0;
     293        29836 :         header_section_too_large_ = false;
     294        29836 :         host_field_ = false;
     295        29836 :         return request;
     296              :     }
     297              : 
     298        35392 :     void request_factory::on_http_method(const std::string& method){
     299        35392 :         req->set_method(method);
     300        35392 :     }
     301              : 
     302            0 :     void request_factory::on_http_status_code(unsigned short status_code){
     303              : 
     304            0 :     }
     305              : 
     306        33508 :     void request_factory::on_http_uri(const std::string& uri){
     307        33508 :         req->set_uri(uri);
     308        33508 :     }
     309              : 
     310        33492 :     void request_factory::on_http_major_version(uint8_t major)
     311              :     {
     312        33492 :         req->set_http_version_major(major);
     313        33492 :     }
     314              : 
     315        33484 :     void request_factory::on_http_minor_version(uint16_t minor){
     316        33484 :         req->set_http_version_minor(minor);
     317        33484 :     }
     318              : 
     319        72325 :     void request_factory::on_http_header(const std::string& name, const std::string& value){
     320        72325 :         req->process_header(name, value);
     321        72325 :     }
     322              : 
     323         2900 :     void request_factory::on_content(char content){
     324         2900 :         req->get_body().push_back(content);
     325         2900 :     }
     326              : 
     327         9122 :     size_t request_factory::get_content_length(){
     328         9122 :         return req->get_content_length();
     329              :     }
     330              : 
     331         2900 :     size_t request_factory::get_content_read(){
     332         2900 :         return req->get_body().size();
     333              :     }
     334              : }
        

Generated by: LCOV version 2.0-1