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 : }
|