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

            Line data    Source code
       1              : // Protocol upgrades (WebSocket...) through the reverse proxy: a tunnel between the client and
       2              : // the target
       3              : #include "proxy_exchange.hpp"
       4              : #include "../connection_tracker.hpp"
       5              : #include "../../../asio/socket_pipe.hpp"
       6              : #include "../../../asio/sockets/websocket.hpp"
       7              : #include "../../../asio/with_timeout.hpp"
       8              : 
       9              : namespace thinger::http::proxy_detail {
      10              : 
      11              : // A quiet WebSocket behind the proxy, with the server's default keepalive, pings before the
      12              : // default tunnel deadline cuts it (see websocket_connection, upgrade.hpp's ping_interval)
      13              : static_assert(proxy_options::DEFAULT_TUNNEL_IDLE_TIMEOUT > asio::websocket::CONNECTION_TIMEOUT_SECONDS,
      14              :               "the default tunnel idle timeout must exceed the default WebSocket ping interval");
      15              : 
      16              : namespace {
      17              : 
      18              : // Write what a side sent right after the handshake, within the idle deadline, if any: a
      19              : // peer that does not read it ends the tunnel as the pipe would
      20          108 : awaitable<bool> write_early(asio::socket& to, const std::string& data, std::chrono::milliseconds idle_timeout) {
      21              :     if (data.empty()) co_return true;
      22              :     if (idle_timeout.count() <= 0) {
      23              :         auto [ec, bytes] = co_await to.write(data);
      24              :         co_return !ec;
      25              :     }
      26              :     auto result = co_await asio::with_timeout(to.write(data), idle_timeout);
      27              :     co_return result && !std::get<0>(*result);
      28          216 : }
      29              : 
      30              : }
      31              : 
      32           54 : awaitable<void> run_tunnel(std::shared_ptr<asio::socket> client, std::shared_ptr<asio::socket> target,
      33              :                            std::string to_client, std::string to_target,
      34              :                            std::chrono::milliseconds idle_timeout) {
      35              :     // Bytes each side sent right after the handshake go first
      36              :     if (!co_await write_early(*client, to_client, idle_timeout) ||
      37              :         !co_await write_early(*target, to_target, idle_timeout)) {
      38              :         client->close();
      39              :         target->close();
      40              :         co_return;
      41              :     }
      42              :     auto pipe = std::make_shared<asio::socket_pipe>(client, target);
      43              :     pipe->set_idle_timeout(idle_timeout);
      44              :     co_await pipe->run();
      45          108 : }
      46              : 
      47           60 : void proxy_exchange::upgrade() {
      48           60 :     const auto& protocol = head_->get_header(header::upgrade);
      49           60 :     if (!upgrade_requested_ || protocol.empty()) {
      50           18 :         fail(http_response::status::bad_gateway, "Invalid response from the target", "unexpected protocol switch");
      51            6 :         return;
      52              :     }
      53              : 
      54           54 :     http_response out;
      55           54 :     out.set_status(http_response::status::switching_protocols);
      56           54 :     copy_end_to_end(*head_, out);
      57           54 :     if (options_.on_response) options_.on_response(req_, out);
      58           54 :     remove_framing_headers(out);
      59           54 :     if (!has_valid_headers(out)) {
      60            0 :         fail(http_response::status::bad_gateway, "Invalid response from the target", "invalid response header");
      61            0 :         return;
      62              :     }
      63              : 
      64           54 :     res_.status(http_response::status::switching_protocols);
      65           60 :     for (const auto& [name, value] : out.get_headers()) res_.header(name, value);
      66           54 :     res_.header(header::upgrade, protocol);
      67          108 :     res_.header(header::connection, "Upgrade");
      68              : 
      69              :     // Once the 101 is written, the client connection and the target become a tunnel,
      70              :     // starting with what each one sent past its head
      71           54 :     auto target = std::exchange(target_socket_, nullptr);
      72          108 :     res_.take_over([target, early = std::exchange(pending_, {}),
      73           54 :                     idle_timeout = options_.tunnel_idle_timeout](upgraded_connection client) {
      74           54 :         client.scope.set_type(connection_type::tunnel);
      75           54 :         auto& io_context = client.socket->get_io_context();
      76           54 :         co_spawn(io_context, run_tunnel(std::move(client.socket), target, early, std::move(client.buffered),
      77              :                                         idle_timeout), detached);
      78           54 :     });
      79           54 :     result_.status = 101;
      80           54 :     result_.from_target = true;
      81           54 :     result_.upgraded = true;
      82           54 :     result_.complete = true;
      83           54 : }
      84              : 
      85              : }
        

Generated by: LCOV version 2.0-1