Line data Source code
1 : #ifndef THINGER_HTTP_SERVER_REVERSE_PROXY_HPP
2 : #define THINGER_HTTP_SERVER_REVERSE_PROXY_HPP
3 :
4 : #include "../common/http_request.hpp"
5 : #include "../common/http_response.hpp"
6 : #include "../../util/types.hpp"
7 : #include <boost/asio/ssl/context.hpp>
8 : #include <chrono>
9 : #include <cstdint>
10 : #include <functional>
11 : #include <map>
12 : #include <memory>
13 : #include <mutex>
14 : #include <optional>
15 : #include <string>
16 : #include <string_view>
17 : #include <tuple>
18 :
19 : namespace thinger::http {
20 :
21 : class request;
22 : class response;
23 :
24 : // TLS settings to reach a target over HTTPS (or WSS)
25 : struct proxy_tls_options {
26 : // Verify the certificate of the target: its chain, against the trusted CAs, and that it
27 : // was issued for `server_name`. Only disable it for testing.
28 : bool verify = true;
29 :
30 : // Name sent with SNI and verified in the certificate (a host name or an IP address);
31 : // the target host if empty
32 : std::string server_name;
33 :
34 : // CAs trusted to verify the target instead of the system ones: a PEM file, or PEM text.
35 : // A target with a certificate from a private CA is only accepted when its CA is set here.
36 : std::string ca_file;
37 : std::string ca_pem;
38 : };
39 :
40 : // Where a reverse proxy forwards a request: a TCP address, over TLS or not, or a Unix
41 : // socket, with a base path prepended to the forwarded path
42 : struct proxy_target {
43 : enum class transport { tcp, tls, unix_socket };
44 :
45 : transport type = transport::tcp;
46 :
47 : // TCP and TLS targets: host name or IP address (IPv6 without brackets), and port
48 : std::string host;
49 : uint16_t port = 80;
50 :
51 : // Unix socket targets
52 : std::string socket_path;
53 :
54 : // Prepended to the path forwarded (e.g. "/api"); empty for none
55 : std::string base_path;
56 :
57 : // TLS targets
58 : proxy_tls_options tls;
59 :
60 : // Target from a URL: "http://host[:port][/base]", "https://..." (TLS), or "ws://" and
61 : // "wss://" alike. Empty if the URL is not valid (or has a query or fragment).
62 : static std::optional<proxy_target> from_url(std::string_view url);
63 :
64 : // Unix socket target
65 : static proxy_target unix_socket(std::string path, std::string base_path = {});
66 :
67 : // Host header for the target: its host, with the port unless it is the default one
68 : // ("localhost" for Unix sockets)
69 : std::string authority() const;
70 : };
71 :
72 : // Forwarding headers added to the requests sent to the target
73 : enum class forwarding_headers {
74 : x_forwarded, // X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host
75 : forwarded, // Forwarded (RFC 7239): for=...;proto=...;host=...
76 : none // none added
77 : };
78 :
79 : // How a request is forwarded
80 : struct proxy_options {
81 : // Path sent to the target (after the target base path), e.g. "/" + req["path"], as it is
82 : // to be sent (percent-encoded). The path of the request, as received, if empty. The query
83 : // of the request is appended to a path without one.
84 : std::string path;
85 :
86 : // Host header sent: the target authority by default, `host` if set, or the Host of the
87 : // request if `preserve_host` (the original host is also in the forwarding headers)
88 : bool preserve_host = false;
89 : std::string host;
90 :
91 : // Headers reporting the client to the target. Those received from a trusted proxy (see
92 : // http_application::set_trusted_proxies, the header it sets) are kept and extended; from
93 : // any other peer, the forwarding headers are dropped, as the client could forge them.
94 : forwarding_headers forwarding = forwarding_headers::x_forwarded;
95 :
96 : // Time to connect to the target (TLS handshake included); 504 if exceeded
97 21 : std::chrono::milliseconds connect_timeout{10000};
98 :
99 : // Maximum time without any data exchanged once connected (waiting for the response
100 : // included): 504 if the response has not started, or the connection to the client is
101 : // closed if it has. Zero disables it (e.g. for event streams). It covers the exchange
102 : // only: a tunnel has its own, tunnel_idle_timeout.
103 12 : std::chrono::milliseconds idle_timeout{60000};
104 :
105 : // Maximum time for the whole exchange, connection included; zero for no limit
106 21 : std::chrono::milliseconds total_timeout{0};
107 :
108 : // Maximum request body forwarded (413 if larger); zero for no limit. The maximum body
109 : // size of the route (route::max_body_size, or the application one) applies too, as to
110 : // any body read: the smaller one wins. The body is never held whole: it is streamed to
111 : // the target as it arrives.
112 : size_t max_body_size = 0;
113 :
114 : // Forward protocol upgrades (WebSocket...) as a tunnel between the client and the target
115 : bool allow_upgrade = true;
116 :
117 : // Default of tunnel_idle_timeout: above the 60 s after which a thinger-http WebSocket
118 : // pings a quiet peer, so a quiet WebSocket is not cut before its keepalive sends anything,
119 : // and equal to its WebSocket and connection idle timeouts (120 s). nginx (60 s), AWS ALB
120 : // (60 s) and Cloudflare (100 s) close idle tunnels earlier
121 : static constexpr std::chrono::milliseconds DEFAULT_TUNNEL_IDLE_TIMEOUT{120000};
122 :
123 : // Maximum time a tunnel (see allow_upgrade) stays open with nothing forwarded in either
124 : // direction: then the connections to the client and to the target are closed. Any byte
125 : // forwarded counts, so WebSocket data, pings and pongs keep it open, while a peer that
126 : // stops reading lets it end. Independent of idle_timeout: zero disables it.
127 : std::chrono::milliseconds tunnel_idle_timeout = DEFAULT_TUNNEL_IDLE_TIMEOUT;
128 :
129 : // Change the request sent to the target before it is sent: its method, path (get_uri())
130 : // and headers, e.g. to add credentials or remove the Authorization of the client. The
131 : // body framing headers are set by the proxy afterwards.
132 : sync_callback<void(request& incoming, http_request& outgoing)> on_request;
133 :
134 : // Change the response of the target before it is sent to the client: its status and
135 : // headers (the framing headers are set by the proxy afterwards). Also called for the
136 : // response of a protocol upgrade (101).
137 : sync_callback<void(request& incoming, http_response& response)> on_response;
138 : };
139 :
140 : // What happened to a forwarded request
141 : struct proxy_result {
142 : // Status sent to the client: the target's, or the error of the proxy (502, 504...);
143 : // 0 if nothing could be sent (e.g. the response was already sent)
144 : int status = 0;
145 :
146 : // Whether the response came from the target (then `status` is its status)
147 : bool from_target = false;
148 :
149 : // Whether the connection became a tunnel to the target (protocol upgrade)
150 : bool upgraded = false;
151 :
152 : // Whether the exchange was complete: the request body sent, and the response received
153 : // and sent whole to the client
154 : bool complete = false;
155 :
156 : // Body bytes forwarded: of the request to the target, and of the response to the client
157 : size_t request_bytes = 0;
158 : size_t response_bytes = 0;
159 :
160 : // Why the exchange failed, empty if it did not
161 : std::string error;
162 : };
163 :
164 : // Reverse proxy: forwards the request of a route handler to a target (TCP, TLS or Unix
165 : // socket), and its response back, streaming both bodies with flow control, or turns the
166 : // connection into a tunnel to the target on a protocol upgrade (WebSocket). Use it from a
167 : // coroutine handler that has not read the body:
168 : //
169 : // http::reverse_proxy proxy;
170 : // auto target = *http::proxy_target::from_url("http://127.0.0.1:3000");
171 : // server.host("app.example.com").set_not_found_handler(
172 : // [&](http::request& req, http::response& res) -> thinger::awaitable<void> {
173 : // co_await proxy.forward(req, res, target);
174 : // });
175 : //
176 : // It is thread-safe: one proxy can serve every route and thread. The request is forwarded
177 : // without hop-by-hop headers and with forwarding headers; the body is framed again by the
178 : // proxy, from what the server parser accepted. The response of the target is parsed
179 : // strictly: a response that cannot be framed safely is answered with 502. The connection
180 : // to the target is used for a single request.
181 : class reverse_proxy {
182 : public:
183 447 : reverse_proxy() = default;
184 :
185 : // Proxy with the options used by forward() calls that do not give their own
186 69 : explicit reverse_proxy(proxy_options defaults) : defaults_(std::move(defaults)) {}
187 :
188 : // Forward the request to `target` and send its response (errors through the error
189 : // formatter: 502 if the target cannot be reached or its response is invalid, 504 on a
190 : // timeout). Call it before reading the request body, with the response not sent yet.
191 : awaitable<proxy_result> forward(request& req, response& res, const proxy_target& target);
192 : awaitable<proxy_result> forward(request& req, response& res, const proxy_target& target,
193 : const proxy_options& options);
194 :
195 : // Options used by default (set them before forwarding)
196 : proxy_options& options() { return defaults_; }
197 : const proxy_options& options() const { return defaults_; }
198 :
199 : // TLS context for targets with the given settings (shared by the targets with the same
200 : // CAs and verification); throws if the CAs cannot be loaded
201 : std::shared_ptr<boost::asio::ssl::context> tls_context(const proxy_tls_options& tls);
202 :
203 : private:
204 : proxy_options defaults_;
205 :
206 : std::mutex tls_mutex_;
207 : std::map<std::tuple<bool, std::string, std::string>, std::shared_ptr<boost::asio::ssl::context>> tls_contexts_;
208 : };
209 :
210 : } // namespace thinger::http
211 :
212 : #endif // THINGER_HTTP_SERVER_REVERSE_PROXY_HPP
|