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

            Line data    Source code
       1              : // The response of the target, relayed by the reverse proxy to the client
       2              : #include "proxy_exchange.hpp"
       3              : #include "../../common/chunked_decoder.hpp"
       4              : #include <algorithm>
       5              : 
       6              : namespace thinger::http::proxy_detail {
       7              : 
       8              : namespace {
       9              : 
      10              :     // Interim responses (1xx) skipped before giving up on a target
      11              :     constexpr unsigned max_interim_responses = 16;
      12              : 
      13              :     // Whether a read from the target ended at the end of its stream (not an error). A TLS
      14              :     // target closing without close_notify (stream_truncated) does not end a body delimited
      15              :     // by the connection: it may have been cut, so it is relayed as incomplete
      16           60 :     bool is_end_of_stream(const boost::system::error_code& ec) {
      17           60 :         return ec == boost::asio::error::eof;
      18              :     }
      19              : 
      20              : }
      21              : 
      22         7527 : awaitable<bool> proxy_exchange::send_to_client(std::string data) {
      23              :     auto size = data.size();
      24              :     if (!co_await res_.write_stream(std::move(data))) {
      25              :         result_.error = "client connection lost";
      26              :         co_return false;
      27              :     }
      28              :     result_.response_bytes += size;
      29              :     touch();
      30              :     co_return true;
      31        15054 : }
      32              : 
      33          633 : awaitable<bool> proxy_exchange::read_head() {
      34              :     std::vector<uint8_t> buffer(buffer_size);
      35              :     for (unsigned interim = 0; ; ++interim) {
      36              :         // The head alone: the body is relayed as it is read. Its framing depends on the
      37              :         // request the target got (the head for the client, on the request of the client:
      38              :         // see response::start_stream())
      39              :         response_factory parser;
      40              :         parser.set_headers_only(true);
      41              :         parser.set_request_method(outgoing_method_);
      42              : 
      43              :         while (true) {
      44              :             if (pending_.empty()) {
      45              :                 auto [ec, bytes] = co_await read_target(buffer.data(), buffer.size());
      46              :                 if (ec || bytes == 0) {
      47              :                     fail(http_response::status::bad_gateway, "Invalid response from the target",
      48              :                          "the target closed the connection without a response" +
      49              :                          (ec ? ": " + ec.message() : std::string{}));
      50              :                     co_return false;
      51              :                 }
      52              :                 pending_.assign(reinterpret_cast<const char*>(buffer.data()), bytes);
      53              :             }
      54              : 
      55              :             const char* begin = pending_.data();
      56              :             const char* end = begin + pending_.size();
      57              :             boost::tribool parsed = parser.parse_some(begin, end);
      58              :             pending_.erase(0, static_cast<size_t>(begin - pending_.data()));
      59              :             if (parsed) break;
      60              :             if (!parsed) {
      61              :                 fail(http_response::status::bad_gateway, "Invalid response from the target",
      62              :                      make_error_code(parser.error()).message());
      63              :                 co_return false;
      64              :             }
      65              :         }
      66              : 
      67              :         auto head = parser.consume_response();
      68              :         int code = head->get_status_code();
      69              : 
      70              :         // Interim responses (100 Continue, 103 Early Hints...) are not relayed
      71              :         if (code < 200 && code != 101) {
      72              :             if (interim + 1 >= max_interim_responses) {
      73              :                 fail(http_response::status::bad_gateway, "Invalid response from the target",
      74              :                      "too many interim responses");
      75              :                 co_return false;
      76              :             }
      77              :             continue;
      78              :         }
      79              : 
      80              :         head_ = std::move(head);
      81              :         status_code_ = code;
      82              :         body_framing_ = parser.get_body_framing();
      83              :         co_return true;
      84              :     }
      85         1266 : }
      86              : 
      87          897 : std::optional<size_t> proxy_exchange::content_length() const {
      88              :     // The framing of the target is unambiguous (the parser rejected anything else): a valid
      89              :     // Content-Length, or "Transfer-Encoding: chunked" alone, or neither (the body ends with
      90              :     // the connection)
      91          897 :     if (head_->is_chunked_transfer() || !head_->has_content_length_header()) return std::nullopt;
      92          810 :     return head_->get_content_length();
      93              : }
      94              : 
      95          498 : std::optional<size_t> proxy_exchange::advertised_length() const {
      96              :     // Without a body, the length of the resource is only advertised (HEAD, 304)
      97          498 :     bool no_body = body_framing_ == response_factory::body_framing::none;
      98          993 :     bool advertises = outgoing_method_ == method::HEAD || incoming_->get_method() == method::HEAD ||
      99          495 :                       status_code_ == 304;
     100          498 :     if (no_body && !advertises) return std::nullopt;
     101          495 :     return content_length();
     102              : }
     103              : 
     104          402 : awaitable<bool> proxy_exchange::relay_length(size_t length) {
     105              :     while (length > 0) {
     106              :         std::string data;
     107              :         if (!pending_.empty()) {
     108              :             auto size = std::min(length, pending_.size());
     109              :             data = pending_.substr(0, size);
     110              :             pending_.erase(0, size);
     111              :         } else {
     112              :             data.resize(std::min(length, buffer_size));
     113              :             auto [ec, bytes] = co_await read_target(reinterpret_cast<uint8_t*>(data.data()), data.size());
     114              :             if (ec || bytes == 0) {
     115              :                 fail(http_response::status::bad_gateway, "", "response body truncated by the target");
     116              :                 co_return false;
     117              :             }
     118              :             data.resize(bytes);
     119              :         }
     120              :         length -= data.size();
     121              :         if (!co_await send_to_client(std::move(data))) co_return false;
     122              :     }
     123              :     co_return true;
     124          804 : }
     125              : 
     126           63 : awaitable<bool> proxy_exchange::relay_chunked() {
     127              :     // Decoded strictly (the decoder of the request bodies), and chunked again (or not) for
     128              :     // the client: chunk extensions and trailers are dropped
     129              :     chunked_decoder decoder;
     130              :     std::vector<uint8_t> buffer(buffer_size);
     131              :     while (!decoder.done()) {
     132              :         if (pending_.empty()) {
     133              :             auto [ec, bytes] = co_await read_target(buffer.data(), buffer.size());
     134              :             if (ec || bytes == 0) {
     135              :                 fail(http_response::status::bad_gateway, "", "chunked response body truncated by the target");
     136              :                 co_return false;
     137              :             }
     138              :             pending_.assign(reinterpret_cast<const char*>(buffer.data()), bytes);
     139              :         }
     140              :         std::string data(buffer_size, '\0');
     141              :         size_t produced = 0;
     142              :         auto consumed = decoder.decode(reinterpret_cast<const uint8_t*>(pending_.data()), pending_.size(),
     143              :                                        reinterpret_cast<uint8_t*>(data.data()), data.size(), produced);
     144              :         pending_.erase(0, consumed);
     145              :         if (decoder.failed()) {
     146              :             fail(http_response::status::bad_gateway, "", "malformed chunked response body");
     147              :             co_return false;
     148              :         }
     149              :         if (produced == 0) continue;
     150              :         data.resize(produced);
     151              :         if (!co_await send_to_client(std::move(data))) co_return false;
     152              :     }
     153              :     co_return true;
     154          126 : }
     155              : 
     156           24 : awaitable<bool> proxy_exchange::relay_until_close() {
     157              :     if (!pending_.empty() && !co_await send_to_client(std::exchange(pending_, {}))) co_return false;
     158              :     while (true) {
     159              :         std::string data(buffer_size, '\0');
     160              :         auto [ec, bytes] = co_await read_target(reinterpret_cast<uint8_t*>(data.data()), data.size());
     161              :         if (is_end_of_stream(ec)) co_return true;
     162              :         if (ec) {
     163              :             fail(http_response::status::bad_gateway, "", "response body interrupted: " + ec.message());
     164              :             co_return false;
     165              :         }
     166              :         data.resize(bytes);
     167              :         if (!co_await send_to_client(std::move(data))) co_return false;
     168              :     }
     169           48 : }
     170              : 
     171          498 : awaitable<bool> proxy_exchange::relay_body() {
     172              :     switch (body_framing_) {
     173              :         case response_factory::body_framing::chunked:
     174              :             co_return co_await relay_chunked();
     175              :         case response_factory::body_framing::content_length:
     176              :             co_return co_await relay_length(*content_length());
     177              :         case response_factory::body_framing::until_close:
     178              :             co_return co_await relay_until_close();
     179              :         case response_factory::body_framing::none:
     180              :             break;
     181              :     }
     182              :     co_return true;
     183          996 : }
     184              : 
     185          498 : awaitable<void> proxy_exchange::relay_response() {
     186              :     // Head for the client: the status and end-to-end headers of the target
     187              :     http_response out;
     188              :     out.set_status(static_cast<uint16_t>(status_code_));
     189              :     out.set_reason_phrase(head_->get_reason());
     190              :     copy_end_to_end(*head_, out);
     191              :     if (options_.on_response) options_.on_response(req_, out);
     192              :     if (!has_valid_headers(out)) {
     193              :         fail(http_response::status::bad_gateway, "Invalid response from the target", "invalid response header");
     194              :         co_return;
     195              :     }
     196              : 
     197              :     if (!res_.start_stream(out, advertised_length())) {
     198              :         result_.error = "the response could not be sent";
     199              :         co_return;
     200              :     }
     201              :     response_started_ = true;
     202              :     result_.status = out.get_status_code();
     203              :     result_.from_target = true;
     204              : 
     205              :     if (!co_await relay_body()) {
     206              :         // the client sees the response incomplete (no last chunk, or connection closed)
     207              :         res_.abort_stream();
     208              :         co_return;
     209              :     }
     210              :     response_ended_ = true;
     211              :     bool written = co_await res_.end_stream();
     212              :     // Posted to watch_client(), if it waits: what follows does not suspend
     213              :     response_written_->set();
     214              :     if (!written) {
     215              :         result_.error = "client connection lost";
     216              :         co_return;
     217              :     }
     218              :     result_.complete = request_sent_;
     219          996 : }
     220              : 
     221              : }
        

Generated by: LCOV version 2.0-1