LCOV - code coverage report
Current view: top level - http/server/reverse_proxy - proxy_request.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 91.9 % 211 194
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 30 30

            Line data    Source code
       1              : // The request forwarded by the reverse proxy: its path, headers and framing
       2              : #include "proxy_exchange.hpp"
       3              : #include "../trusted_proxies.hpp"
       4              : #include "../../common/syntax.hpp"
       5              : #include "../../../util/ascii.hpp"
       6              : #include <boost/asio/ip/address.hpp>
       7              : #include <algorithm>
       8              : 
       9              : namespace thinger::http::proxy_detail {
      10              : 
      11              : namespace {
      12              : 
      13              :     namespace ascii = ::thinger::util::ascii;
      14              : 
      15              :     // Hop-by-hop headers (RFC 9110, section 7.6.1), meaningful only for a single connection,
      16              :     // and the Proxy-* ones, addressed to proxies
      17         3099 :     bool is_hop_by_hop(std::string_view name) {
      18              :         static constexpr std::string_view names[] = {
      19              :             "Connection", "Keep-Alive", "TE", "Trailer", "Transfer-Encoding", "Upgrade"
      20              :         };
      21        17769 :         for (auto hop : names) {
      22        15498 :             if (ascii::iequals(name, hop)) return true;
      23              :         }
      24         2271 :         return ascii::istarts_with(name, "Proxy-");
      25              :     }
      26              : 
      27         2460 :     bool is_forwarding_header(std::string_view name) {
      28         7320 :         return ascii::iequals(name, "Forwarded") || ascii::iequals(name, "X-Forwarded-For") ||
      29         7320 :                ascii::iequals(name, "X-Forwarded-Proto") || ascii::iequals(name, "X-Forwarded-Host");
      30              :     }
      31              : 
      32              :     // Values of all the `name` headers, as a single comma-separated list
      33            3 :     std::string joined(const headers& message, std::string_view name) {
      34            3 :         std::string list;
      35            6 :         for (const auto& value : message.get_headers_with_key(name)) {
      36            3 :             auto item = syntax::trim(value);
      37            3 :             if (item.empty()) continue;
      38            3 :             if (!list.empty()) list += ", ";
      39            3 :             list += item;
      40            3 :         }
      41            3 :         return list;
      42            0 :     }
      43              : 
      44              :     // Node of the Forwarded header (RFC 7239, section 6): IPv6 addresses in brackets and quoted
      45            3 :     std::string forwarded_node(const std::string& ip) {
      46            3 :         if (!is_ip_address(ip)) return "unknown";
      47            3 :         if (ip.find(':') != std::string::npos) return "\"[" + ip + "]\"";
      48            3 :         return ip;
      49              :     }
      50              : 
      51              :     // Parameter value of the Forwarded header: a token, or a quoted string
      52            3 :     std::string forwarded_value(std::string_view value) {
      53            3 :         if (syntax::is_token(value)) return std::string(value);
      54            3 :         std::string quoted = "\"";
      55           57 :         for (char c : value) {
      56           54 :             if (c == '"' || c == '\\') quoted += '\\';
      57           54 :             quoted += c;
      58              :         }
      59            3 :         return quoted + "\"";
      60            3 :     }
      61              : 
      62          663 :     std::string serialize_head(const http_request& outgoing) {
      63          663 :         std::string head = get_method(outgoing.get_method()) + " " + outgoing.get_uri() + " HTTP/1.1\r\n";
      64         4440 :         for (const auto& [name, value] : outgoing.get_headers()) {
      65         3777 :             head += name;
      66         3777 :             head += ": ";
      67         3777 :             head += value;
      68         3777 :             head += "\r\n";
      69              :         }
      70          663 :         head += "\r\n";
      71          663 :         return head;
      72            0 :     }
      73              : 
      74              : }
      75              : 
      76          690 : bool is_request_target(std::string_view target) {
      77         1377 :     return !target.empty() && target.front() == '/' &&
      78          687 :            std::all_of(target.begin(), target.end(), [](unsigned char c) {
      79         4278 :                return c > 0x20 && c != 0x7f && c != '#';
      80          690 :            });
      81              : }
      82              : 
      83         1224 : bool has_valid_headers(const headers& message) {
      84              :     // a header refused by a setter (e.g. in a hook) is not stored: still an invalid message
      85         1224 :     return !message.has_refused_header() && std::all_of(message.get_headers().begin(), message.get_headers().end(), [](const auto& header) {
      86         3657 :         return syntax::is_token(header.first) && syntax::is_field_value(header.second);
      87         1224 :     });
      88              : }
      89              : 
      90         1227 : std::vector<std::string> connection_options(const headers& message) {
      91         1227 :     std::vector<std::string> options;
      92         1815 :     for (const auto& value : message.get_headers_with_key(header::connection)) {
      93         1182 :         syntax::for_each_element(value, [&](std::string_view option) { options.emplace_back(option); });
      94         1227 :     }
      95         1227 :     return options;
      96            0 : }
      97              : 
      98         2934 : bool is_listed(const std::vector<std::string>& options, std::string_view name) {
      99         2934 :     return std::any_of(options.begin(), options.end(), [&](const std::string& option) {
     100         1356 :         return ascii::iequals(option, name);
     101         2934 :     });
     102              : }
     103              : 
     104         3099 : bool is_end_to_end(std::string_view name, const std::vector<std::string>& connection) {
     105         3099 :     return !is_hop_by_hop(name) && !is_listed(connection, name);
     106              : }
     107              : 
     108          729 : void remove_framing_headers(headers& message) {
     109         4374 :     for (auto name : {header::content_length, header::transfer_encoding, header::connection,
     110         9477 :                       std::string("Keep-Alive"), header::upgrade}) {
     111         3645 :         message.remove_headers(name);
     112         8019 :     }
     113          729 : }
     114              : 
     115          552 : void copy_end_to_end(const http_response& head, http_response& out) {
     116          552 :     auto connection = connection_options(head);
     117         2310 :     for (const auto& [name, value] : head.get_headers()) {
     118         1758 :         if (!is_end_to_end(name, connection) || headers::is_header(name, header::content_length)) continue;
     119          726 :         out.add_header(name, value);
     120              :     }
     121          552 : }
     122              : 
     123          783 : bool is_ip_address(const std::string& value) {
     124          783 :     boost::system::error_code ec;
     125          783 :     boost::asio::ip::make_address(value, ec);
     126          783 :     return !ec;
     127              : }
     128              : 
     129          675 : std::string proxy_exchange::target_path() const {
     130          675 :     const auto& uri = incoming_->get_uri();
     131          675 :     std::string path = options_.path;
     132          675 :     if (path.empty()) {
     133          615 :         path = uri;
     134           60 :     } else if (path.find('?') == std::string::npos) {
     135              :         // keep the query of the request
     136           60 :         if (auto query = uri.find('?'); query != std::string::npos) path += uri.substr(query);
     137              :     }
     138          675 :     std::string base = target_.base_path;
     139          675 :     while (!base.empty() && base.back() == '/') base.pop_back();
     140          675 :     if (!base.empty() && !path.empty() && path.front() != '/') base += '/';
     141         1350 :     return base + path;
     142          675 : }
     143              : 
     144          675 : std::string proxy_exchange::target_host() const {
     145          675 :     if (options_.preserve_host) return incoming_->get_header(header::host);
     146          669 :     if (!options_.host.empty()) return options_.host;
     147          666 :     return target_.authority();
     148              : }
     149              : 
     150          675 : void proxy_exchange::add_forwarding_headers(http_request& outgoing) const {
     151              :     // The forwarding headers of the request are kept only if they come from a trusted
     152              :     // proxy, and only the one it sets: the others are as the client sent them
     153          675 :     const auto& proxies = req_.get_trusted_proxies();
     154          675 :     auto peer = req_.get_peer_ip();
     155          675 :     bool trusted_peer = proxies && !peer.empty() && proxies->contains(peer);
     156          675 :     bool keep_x_forwarded = trusted_peer && proxies->header() == forwarded_header::x_forwarded_for;
     157          675 :     bool keep_forwarded = trusted_peer && proxies->header() == forwarded_header::forwarded;
     158              : 
     159         2016 :     for (const auto& [name, value] : incoming_->get_headers()) {
     160         1341 :         if (!is_forwarding_header(name)) continue;
     161           33 :         bool forwarded = ascii::iequals(name, "Forwarded");
     162              :         // the ones this proxy sets are added below, once
     163           33 :         bool set_here = options_.forwarding == (forwarded ? forwarding_headers::forwarded : forwarding_headers::x_forwarded);
     164           66 :         if ((forwarded ? keep_forwarded : keep_x_forwarded) && !set_here) outgoing.add_header(name, value);
     165              :     }
     166              : 
     167          675 :     if (options_.forwarding == forwarding_headers::x_forwarded) {
     168          669 :         add_x_forwarded(outgoing, peer, keep_x_forwarded);
     169            6 :     } else if (options_.forwarding == forwarding_headers::forwarded) {
     170            3 :         add_forwarded(outgoing, peer, keep_forwarded);
     171              :     }
     172          708 : }
     173              : 
     174          669 : void proxy_exchange::add_x_forwarded(http_request& outgoing, const std::string& peer, bool keep_received) const {
     175          795 :     std::string node = is_ip_address(peer) ? peer : "unknown";
     176          669 :     auto chain = keep_received ? joined(*incoming_, "X-Forwarded-For") : std::string{};
     177         2007 :     outgoing.add_header("X-Forwarded-For", chain.empty() ? node : chain + ", " + node);
     178              : 
     179          669 :     auto proto = keep_received ? incoming_->get_header("X-Forwarded-Proto") : std::string{};
     180          669 :     if (proto.empty()) proto = incoming_->is_ssl() ? "https" : "http";
     181         2007 :     outgoing.add_header("X-Forwarded-Proto", proto);
     182              : 
     183          669 :     auto host = keep_received ? incoming_->get_header("X-Forwarded-Host") : std::string{};
     184          669 :     if (host.empty()) host = incoming_->get_header(header::host);
     185         2001 :     if (!host.empty()) outgoing.add_header("X-Forwarded-Host", host);
     186          669 : }
     187              : 
     188            3 : void proxy_exchange::add_forwarded(http_request& outgoing, const std::string& peer, bool keep_received) const {
     189            3 :     auto chain = keep_received ? joined(*incoming_, "Forwarded") : std::string{};
     190            3 :     std::string element = "for=" + forwarded_node(peer) + ";proto=" + (incoming_->is_ssl() ? "https" : "http");
     191            3 :     const auto& host = incoming_->get_header(header::host);
     192            3 :     if (!host.empty()) element += ";host=" + forwarded_value(host);
     193            9 :     outgoing.add_header("Forwarded", chain.empty() ? element : chain + ", " + element);
     194            3 : }
     195              : 
     196          675 : bool proxy_exchange::check_method() {
     197          675 :     auto method = incoming_->get_method();
     198          675 :     if (method == method::UNKNOWN || method == method::CONNECT) {
     199            0 :         fail(http_response::status::not_implemented, "Method not supported", "method not supported");
     200            0 :         return false;
     201              :     }
     202          675 :     if (method == method::TRACE) {
     203            0 :         fail(http_response::status::not_allowed, "", "TRACE is not forwarded");
     204            0 :         return false;
     205              :     }
     206          675 :     return true;
     207              : }
     208              : 
     209          675 : bool proxy_exchange::check_upgrade(const std::vector<std::string>& connection) {
     210              :     // A protocol upgrade (Connection: upgrade, and Upgrade), except to HTTP/2 over cleartext,
     211              :     // which this proxy does not speak: the request is forwarded without it
     212          675 :     const auto& protocol = incoming_->get_header(header::upgrade);
     213          729 :     upgrade_requested_ = options_.allow_upgrade && is_listed(connection, "upgrade") && !protocol.empty() &&
     214           54 :                          !ascii::istarts_with(protocol, "h2c");
     215          675 :     if (upgrade_requested_ && !res_.get_connection()) {
     216            0 :         fail(http_response::status::not_implemented, "Protocol upgrades are not available for in-memory requests",
     217              :              "protocol upgrade without a connection");
     218            0 :         return false;
     219              :     }
     220          675 :     return true;
     221              : }
     222              : 
     223          675 : void proxy_exchange::copy_request_headers(http_request& outgoing, const std::vector<std::string>& connection) const {
     224              :     // Not Expect: a client waiting for 100 Continue gets it from the server once the body is
     225              :     // streamed to the target, the first time it is read, as nginx does
     226         2016 :     for (const auto& [name, value] : incoming_->get_headers()) {
     227         3546 :         if (!is_end_to_end(name, connection) || is_forwarding_header(name) ||
     228         3831 :             headers::is_header(name, header::host) || headers::is_header(name, header::content_length) ||
     229         1626 :             headers::is_header(name, "Expect")) {
     230         1083 :             continue;
     231              :         }
     232          258 :         outgoing.add_header(name, value);
     233              :     }
     234          675 : }
     235              : 
     236          675 : bool proxy_exchange::check_outgoing(const http_request& outgoing) {
     237          675 :     auto method = outgoing.get_method();
     238          675 :     if (method == method::UNKNOWN || method == method::CONNECT) {
     239            0 :         fail(http_response::status::internal_server_error, "", "invalid method for the target");
     240            0 :         return false;
     241              :     }
     242          675 :     if (!is_request_target(outgoing.get_uri())) {
     243            9 :         fail(http_response::status::internal_server_error, "", "invalid path for the target: " + outgoing.get_path());
     244            3 :         return false;
     245              :     }
     246          672 :     if (!has_valid_headers(outgoing)) {
     247            9 :         fail(http_response::status::internal_server_error, "", "invalid header for the target");
     248            3 :         return false;
     249              :     }
     250          669 :     return true;
     251              : }
     252              : 
     253          669 : bool proxy_exchange::frame_body(http_request& outgoing) {
     254              :     // The body as the server parser framed it: streamed with its Content-Length, or chunked
     255              :     // again by the proxy. Never the framing headers of the client, so the target cannot
     256              :     // read the body (and what follows) any other way.
     257          669 :     if (req_.has_pending_body() && req_.is_chunked()) {
     258           48 :         body_mode_ = body_mode::chunked;
     259          144 :         outgoing.add_header(header::transfer_encoding, "chunked");
     260          621 :     } else if (req_.has_pending_body()) {
     261          123 :         body_mode_ = body_mode::length;
     262          123 :         auto length = incoming_->get_content_length();
     263          123 :         if (options_.max_body_size && length > options_.max_body_size) {
     264            6 :             reject_large_body();
     265            6 :             return false;
     266              :         }
     267          117 :         outgoing.add_header(header::content_length, std::to_string(length));
     268          498 :     } else if (!incoming_->get_body().empty()) {
     269              :         // already read by the server (e.g. a route that is not deferred)
     270            0 :         body_mode_ = body_mode::length;
     271            0 :         body_buffered_ = true;
     272            0 :         outgoing.add_header(header::content_length, std::to_string(incoming_->get_body().size()));
     273          990 :     } else if (incoming_->has_content_length_header() || outgoing_method_ == method::POST ||
     274          990 :                outgoing_method_ == method::PUT || outgoing_method_ == method::PATCH) {
     275           18 :         outgoing.add_header(header::content_length, "0");
     276              :     }
     277          663 :     return true;
     278              : }
     279              : 
     280          663 : void proxy_exchange::add_connection_headers(http_request& outgoing) const {
     281          663 :     if (upgrade_requested_) {
     282          108 :         outgoing.add_header(header::connection, "Upgrade");
     283           54 :         outgoing.add_header(header::upgrade, incoming_->get_header(header::upgrade));
     284              :     } else {
     285              :         // a connection for this request only (see reverse_proxy)
     286         1827 :         outgoing.add_header(header::connection, "close");
     287              :     }
     288          663 : }
     289              : 
     290          675 : bool proxy_exchange::prepare() {
     291          675 :     if (res_.has_responded()) {
     292            0 :         result_.error = "response already sent";
     293            0 :         LOG_ERROR("reverse proxy {}: the response was already sent", describe());
     294            0 :         return false;
     295              :     }
     296          675 :     if (!check_method()) return false;
     297          675 :     auto connection = connection_options(*incoming_);
     298          675 :     if (!check_upgrade(connection)) return false;
     299              : 
     300              :     // Host first, then the end-to-end headers of the request
     301          675 :     auto outgoing = std::make_shared<http_request>();
     302          675 :     outgoing->set_method(incoming_->get_method());
     303          675 :     outgoing->set_uri(target_path());
     304              :     // Always sent, empty when the preserved one is (an HTTP/1.0 request may have none): an
     305              :     // HTTP/1.1 request without Host is rejected (RFC 9112 section 3.2)
     306          675 :     outgoing->add_header(header::host, target_host());
     307          675 :     copy_request_headers(*outgoing, connection);
     308          675 :     add_forwarding_headers(*outgoing);
     309              : 
     310              :     // Framing and connection headers are set here, whatever the hook did
     311          675 :     if (options_.on_request) options_.on_request(req_, *outgoing);
     312          675 :     remove_framing_headers(*outgoing);
     313          675 :     outgoing_method_ = outgoing->get_method();
     314          675 :     if (!check_outgoing(*outgoing) || !frame_body(*outgoing)) return false;
     315          663 :     add_connection_headers(*outgoing);
     316              : 
     317          663 :     request_head_ = serialize_head(*outgoing);
     318          663 :     return true;
     319          675 : }
     320              : 
     321              : }
        

Generated by: LCOV version 2.0-1