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