Line data Source code
1 : #include "response.hpp"
2 : #include "http_application.hpp"
3 : #include "request.hpp"
4 : #include "mime_types.hpp"
5 : #include "body_decoders.hpp"
6 : #include "response_compression.hpp"
7 : #include "file_server/file_info.hpp"
8 : #include "file_server/file_response.hpp"
9 : #include "../common/syntax.hpp"
10 : #include "../../util/ascii.hpp"
11 : #include "../../util/logger.hpp"
12 : #include "../../util/base64.hpp"
13 : #include "../../util/sha1.hpp"
14 : #include "response_sink.hpp"
15 : #include "server_connection.hpp"
16 : #include "http_stream.hpp"
17 : #include "connection_tracker.hpp"
18 : #include "detail/spawn_handler.hpp"
19 : #include "websocket_connection.hpp"
20 : #include "sse_connection.hpp"
21 : #include "../../asio/sockets/websocket.hpp"
22 : #include <algorithm>
23 : #include <atomic>
24 : #include <mutex>
25 : #include <optional>
26 :
27 : namespace thinger::http {
28 :
29 : // State shared by the copies of a response. Each answer builds its own message and sends it
30 : // only if it is the first one (responded); the others drop theirs
31 : struct response::state {
32 : std::shared_ptr<response_sink> sink;
33 : std::shared_ptr<http_request> request;
34 : std::optional<http_response> draft; // status and headers set so far
35 : std::shared_ptr<const error_formatter> formatter; // default format if null
36 : std::atomic<bool> responded{false};
37 : std::shared_ptr<const detail::cors_policy> cors; // off if null
38 : detail::cors_decision cors_decision; // what it means for this request
39 :
40 : // Request timeout: called once the response starts, and whether it was answered by it
41 : std::function<void()> on_start;
42 : std::atomic<bool> timed_out{false};
43 :
44 : // Chunked response: chunks are only written between its headers and its end
45 : enum class chunked_phase { none, open, ended };
46 : std::mutex chunked_mutex;
47 : chunked_phase chunked = chunked_phase::none;
48 :
49 : // Streamed response (under chunked_mutex too): its phase, framing, and the bytes left
50 : // of a body with Content-Length
51 : chunked_phase stream = chunked_phase::none;
52 : body_framing stream_framing = body_framing::none;
53 : size_t stream_remaining = 0;
54 : };
55 :
56 24 : response::response(const std::shared_ptr<server_connection>& connection,
57 : const std::shared_ptr<http_stream>& stream,
58 24 : const std::shared_ptr<http::http_request>& http_request)
59 24 : : response(std::make_shared<connection_sink>(connection, stream), http_request) {}
60 :
61 28505 : response::response(std::shared_ptr<response_sink> sink,
62 28505 : const std::shared_ptr<http::http_request>& http_request)
63 28505 : : state_(std::make_shared<state>()) {
64 28505 : state_->sink = std::move(sink);
65 28505 : state_->request = http_request;
66 28505 : }
67 :
68 767 : void response::set_cors(std::shared_ptr<const detail::cors_policy> policy, detail::cors_decision decision) {
69 767 : state_->cors = std::move(policy);
70 767 : state_->cors_decision = std::move(decision);
71 767 : }
72 :
73 3864 : void response::log_late_answer(const char* message) const {
74 3864 : if (state_->timed_out) {
75 222 : LOG_DEBUG("{} (the request timed out)", message);
76 : } else {
77 3642 : LOG_ERROR("{}", message);
78 : }
79 3864 : }
80 :
81 34668 : bool response::ensure_not_responded() const {
82 34668 : if (state_->responded) {
83 3832 : log_late_answer("Response already sent");
84 3832 : return false;
85 : }
86 30836 : return true;
87 : }
88 :
89 27549 : bool response::mark_responded() {
90 27549 : if (state_->responded.exchange(true)) {
91 14 : log_late_answer("Response already sent");
92 14 : return false;
93 : }
94 27535 : if (state_->on_start) state_->on_start();
95 27535 : return true;
96 : }
97 :
98 261 : void response::on_start(std::function<void()> hook) {
99 261 : state_->on_start = std::move(hook);
100 261 : }
101 :
102 333 : bool response::answer_timeout(http::http_response::status status, bool with_headers) {
103 333 : if (state_->responded) return false;
104 : // Set before answering: whoever sees the response sent sees why
105 333 : state_->timed_out = true;
106 333 : if (state_->responded.exchange(true)) {
107 : // Another answer started meanwhile: it was not late
108 0 : state_->timed_out = false;
109 0 : return false;
110 : }
111 333 : if (state_->on_start) state_->on_start();
112 333 : auto message = take_draft(with_headers);
113 333 : message->set_status(status);
114 666 : format_error(state_->formatter.get(), {status, "No response within the timeout", nullptr}, *message);
115 333 : prepare_body(*message, false);
116 333 : state_->sink->send(std::move(message));
117 333 : return true;
118 1332 : }
119 :
120 52848 : bool response::timed_out() const {
121 : // A handler may have moved the response away
122 52848 : return state_ && state_->timed_out;
123 : }
124 :
125 27265 : std::shared_ptr<http_response> response::new_response() {
126 27265 : if (!mark_responded()) return nullptr;
127 27251 : return take_draft();
128 : }
129 :
130 27584 : std::shared_ptr<http_response> response::take_draft(bool with_draft) {
131 : // Moved, not copied: only the answer sent reads it, once it is the first one
132 27584 : bool drafted = with_draft && state_->draft;
133 27584 : auto message = drafted ? std::make_shared<http_response>(std::move(*state_->draft))
134 27584 : : std::make_shared<http_response>();
135 : // Once the headers set so far are there, which it does not replace
136 27584 : if (state_->cors) detail::apply_cors(*state_->cors, state_->cors_decision, *message);
137 27584 : return message;
138 0 : }
139 :
140 3288 : http_response& response::draft() {
141 3288 : if (!state_->draft) state_->draft.emplace();
142 3288 : return *state_->draft;
143 : }
144 :
145 26038 : bool response::head_request() const {
146 26038 : return state_->request->get_method() == method::HEAD;
147 : }
148 :
149 : namespace {
150 :
151 : // Whether a message sent whole is framed as response::frame_message() leaves it: with a
152 : // Content-Length (set with its content) and no Transfer-Encoding, and a status with body
153 21764 : bool framed(const http_response& message) {
154 21764 : int code = message.get_status_code();
155 43177 : return message.has_content_length_header() && !message.has_transfer_encoding_header() &&
156 43177 : code >= 200 && code != 204 && code != 304;
157 : }
158 :
159 : }
160 :
161 387 : void response::frame_message(http_response& message) const {
162 : // As Node does, a message sent whole tells where its body ends, else the client would
163 : // read it until the connection closes (RFC 9112, 6.3): its Content-Length, never
164 : // Transfer-Encoding (its body is not chunked). 1xx, 204 and 304 never have a body, nor
165 : // its length for 1xx and 204 (RFC 9110, 8.6); a 304 may tell the length of the
166 : // representation. A response to HEAD gets the length of the content it was given, if
167 : // any: it has no body anyway.
168 387 : int code = message.get_status_code();
169 387 : if (code < 200 || code == 204 || code == 304) {
170 241 : message.get_content().clear();
171 241 : if (message.has_transfer_encoding_header()) message.remove_headers(header::transfer_encoding);
172 241 : if (code != 304 && message.has_content_length_header()) message.remove_headers(header::content_length);
173 241 : return;
174 : }
175 146 : if (message.has_transfer_encoding_header()) message.remove_headers(header::transfer_encoding);
176 146 : if (!message.has_content_length_header() && (!head_request() || !message.get_content().empty())) {
177 137 : message.set_content_length(message.get_content().size());
178 : }
179 : }
180 :
181 4317 : body_framing response::frame_takeover(http_response& message) const {
182 : // What follows the response belongs to the handler. Switching protocols (101, or 2xx to
183 : // CONNECT: a tunnel), nothing tells a body (RFC 9110, 9.3.6). Otherwise, unless the
184 : // handler framed it, what the handler writes is the body, ending when the connection
185 : // closes: the response says so, as Node does for a body of unknown length not chunked
186 4317 : int code = message.get_status_code();
187 4317 : bool tunnel = code == 101 || (state_->request->get_method() == method::CONNECT && code >= 200 && code < 300);
188 4317 : if (tunnel) {
189 3670 : message.remove_headers(header::content_length);
190 3670 : message.remove_headers(header::transfer_encoding);
191 647 : } else if (!message.has_content_length_header() && !message.has_transfer_encoding_header()) {
192 638 : return body_framing::close;
193 : }
194 3679 : return body_framing::none;
195 : }
196 :
197 21764 : void response::prepare_body(http_response& message, bool compress) const {
198 : // The usual answer is framed already: its Content-Length was set with its content
199 21764 : if (!framed(message)) [[unlikely]] {
200 387 : frame_message(message);
201 : }
202 21764 : if (head_request()) {
203 : // The headers of the answer to GET, Content-Length included, without its body: the
204 : // client reads none (RFC 9110, 9.3.2), so it would be taken as the next response.
205 : // Not compressed (like Express): its length is the one of the identity body.
206 304 : if (compress && compression::compressible(message)) compression::add_vary(message, "Accept-Encoding");
207 304 : message.get_content().clear();
208 21460 : } else if (compress && message.get_content().size() >= compression::threshold) {
209 1309 : compression::compress(message, *state_->request);
210 : }
211 21764 : }
212 :
213 21431 : void response::send_message(std::shared_ptr<http_response> message, bool compress) {
214 21431 : prepare_body(*message, compress);
215 21431 : state_->sink->send(std::move(message));
216 21431 : }
217 :
218 4509 : bool response::reject_takeover(const char* feature) {
219 4509 : if (state_->sink->can_take_over()) return false;
220 24 : error(http::http_response::status::not_implemented,
221 36 : std::string(feature) + " is not available for in-memory requests");
222 12 : return true;
223 : }
224 :
225 2785 : void response::json(const nlohmann::json& data, http::http_response::status status) {
226 2785 : if (!ensure_not_responded()) return;
227 : // Before taking the draft: dump() throws for strings that are not UTF-8, and the error
228 : // answered then keeps the headers set
229 1876 : auto body = data.dump();
230 1876 : auto message = new_response();
231 1876 : if (!message) return;
232 1876 : message->set_status(status);
233 3752 : message->set_content(std::move(body), "application/json");
234 1876 : send_message(std::move(message));
235 1876 : }
236 :
237 27 : void response::encode(const nlohmann::json& data, http::http_response::status status) {
238 27 : if (!ensure_not_responded()) return;
239 : // Repeated Accept headers make one list
240 27 : std::string accept;
241 27 : if (state_->request) {
242 42 : for (const auto& value : state_->request->get_headers_with_key(header::accept)) {
243 15 : if (!accept.empty()) accept += ", ";
244 15 : accept += value;
245 27 : }
246 : }
247 27 : auto encoding = negotiate_response_encoding(accept);
248 : // Before taking the draft, as json()
249 27 : auto body = encode_body(data, encoding.format);
250 27 : auto message = new_response();
251 27 : if (!message) return;
252 27 : message->set_status(status);
253 27 : message->set_content(std::move(body), encoding.media_type);
254 27 : compression::add_vary(*message, "Accept");
255 27 : send_message(std::move(message));
256 27 : }
257 :
258 17102 : void response::send(const std::string& text, const std::string& content_type) {
259 17103 : if (!ensure_not_responded()) return;
260 15979 : auto message = new_response();
261 15979 : if (!message) return;
262 15978 : message->set_content(text, content_type);
263 15978 : send_message(std::move(message));
264 15979 : }
265 :
266 3379 : void response::error(http::http_response::status status, const std::string& message,
267 : const nlohmann::json& details) {
268 3380 : if (!ensure_not_responded()) return;
269 2483 : auto response = new_response();
270 2483 : if (!response) return;
271 2482 : response->set_status(status);
272 4964 : format_error(state_->formatter.get(), {status, message, details}, *response);
273 2482 : send_message(std::move(response));
274 4965 : }
275 :
276 27559 : void response::set_error_formatter(std::shared_ptr<const error_formatter> formatter) {
277 27559 : state_->formatter = std::move(formatter);
278 27559 : }
279 :
280 315 : void response::status(http::http_response::status s) {
281 315 : if (!ensure_not_responded()) return;
282 315 : draft().set_status(s);
283 : }
284 :
285 2815 : void response::header(std::string key, std::string value) {
286 2815 : if (!ensure_not_responded()) return;
287 2815 : draft().add_header(std::move(key), std::move(value));
288 : }
289 :
290 57 : void response::set_header(std::string key, std::string value) {
291 57 : if (!ensure_not_responded()) return;
292 57 : draft().replace_headers(std::move(key), std::move(value));
293 : }
294 :
295 : namespace {
296 :
297 : // The headers set before a message built by hand: those of the message win over the ones
298 : // of the same name, as the headers given to writeHead() in Node. Compared only with the
299 : // fields it came with, so repeated ones (Set-Cookie) are all added. Its framing is its own
300 15 : void add_draft_headers(http_response& draft, http_response& message) {
301 15 : const auto own = static_cast<std::ptrdiff_t>(message.get_headers().size());
302 36 : for (auto& field : draft.get_headers()) {
303 39 : if (headers::is_header(field.first, header::content_length) ||
304 21 : headers::is_header(field.first, header::transfer_encoding)) continue;
305 18 : const auto& fields = message.get_headers();
306 18 : bool replaced = std::any_of(fields.begin(), fields.begin() + own, [&field](const auto& other) {
307 21 : return headers::is_header(other.first, field.first);
308 : });
309 18 : if (!replaced) message.add_header(std::move(field.first), std::move(field.second));
310 : }
311 15 : }
312 :
313 : }
314 :
315 284 : void response::send_response(const std::shared_ptr<http_response>& response) {
316 284 : if (!ensure_not_responded()) return;
317 : // A message built by hand may carry the framing of another one (e.g. copied from a
318 : // response whose body was decoded): its content is its body, sent whole (a response
319 : // to HEAD keeps the length it tells)
320 284 : int code = response->get_status_code();
321 284 : if (!head_request() && code != 304 && response->has_content_length_header()) {
322 83 : response->remove_headers(header::content_length);
323 : }
324 284 : if (!mark_responded()) return;
325 : // Before the CORS headers, which do not replace the ones set so far (see take_draft)
326 284 : if (state_->draft) add_draft_headers(*state_->draft, *response);
327 284 : if (state_->cors) detail::apply_cors(*state_->cors, state_->cors_decision, *response);
328 284 : send_message(response);
329 : }
330 :
331 647 : const http_request& response::request_message() const {
332 647 : return *state_->request;
333 : }
334 :
335 7 : const error_formatter* response::formatter() const {
336 7 : return state_->formatter.get();
337 : }
338 :
339 6382 : bool response::has_responded() const {
340 6382 : return state_->responded;
341 : }
342 :
343 282 : std::shared_ptr<server_connection> response::get_connection() const {
344 282 : return state_->sink->connection();
345 : }
346 :
347 1066 : void response::redirect(const std::string& url, http::http_response::status redirect_type) {
348 1077 : if (!ensure_not_responded()) return;
349 186 : auto response = new_response();
350 186 : if (!response) return;
351 175 : response->set_status(redirect_type);
352 : // The bytes a field value cannot hold are percent-encoded, so the redirect is still sent
353 : // (as Express's res.location); spaces, '%' and non-ASCII bytes are kept as they are
354 175 : if (syntax::is_field_value(url)) {
355 169 : response->add_header(header::location, url);
356 : } else {
357 : static constexpr char hex[] = "0123456789ABCDEF";
358 6 : std::string location;
359 6 : location.reserve(url.size() + 8);
360 108 : for (unsigned char c : url) {
361 102 : if (syntax::is_field_value_char(c)) {
362 78 : location += static_cast<char>(c);
363 : } else {
364 24 : location += '%';
365 24 : location += hex[c >> 4];
366 24 : location += hex[c & 0x0F];
367 : }
368 : }
369 6 : response->add_header(header::location, std::move(location));
370 6 : }
371 : // Without it, a keep-alive client would read the body until the connection closes
372 175 : response->set_content_length(0);
373 175 : send_message(std::move(response));
374 186 : }
375 :
376 520 : void response::send_file(const std::filesystem::path& path, bool force_download) {
377 524 : if (!ensure_not_responded()) return;
378 :
379 : // One stat for the checks, the length and the validators
380 517 : auto file = file_info::of(path);
381 517 : if (!file) {
382 6 : error(http_response::status::not_found, "File not found");
383 2 : return;
384 : }
385 515 : if (!file->regular) {
386 6 : error(http_response::status::forbidden, "Not a regular file");
387 2 : return;
388 : }
389 513 : file_server::send(*this, {path, *file, mime_types::extension_to_type(path.extension().string()), {}, force_download});
390 517 : }
391 :
392 : namespace {
393 :
394 : using ::thinger::util::ascii::iequals;
395 :
396 : // Why a WebSocket handshake is rejected: the error answered, and whether the client is
397 : // told the version supported
398 : struct handshake_rejection {
399 : http_response::status status;
400 : const char* message;
401 : bool unsupported_version = false;
402 : };
403 :
404 : // Checks of a WebSocket handshake request (RFC 6455, 4.2.1) before its subprotocol
405 3714 : std::optional<handshake_rejection> check_handshake(const http_request& request) {
406 : using status = http_response::status;
407 3714 : if (!iequals(request.get_header(header::upgrade), "websocket")) {
408 18 : return handshake_rejection{status::upgrade_required, "This service requires use of WebSockets"};
409 : }
410 3696 : if (request.get_method() != method::GET) {
411 18 : return handshake_rejection{status::bad_request, "A WebSocket handshake must be a GET request"};
412 : }
413 3678 : bool upgrade = false;
414 7356 : for (const auto& value : request.get_headers_with_key(header::connection)) {
415 7410 : upgrade = upgrade || syntax::any_element(value, [](std::string_view option) { return iequals(option, "upgrade"); });
416 3678 : }
417 3678 : if (!upgrade) return handshake_rejection{status::bad_request, "A WebSocket handshake requires Connection: Upgrade"};
418 3669 : if (request.get_header("Sec-WebSocket-Version") != "13") {
419 18 : return handshake_rejection{status::upgrade_required, "Unsupported WebSocket version", true};
420 : }
421 3651 : return std::nullopt;
422 : }
423 :
424 : // Subprotocols offered by the client, in its order of preference: the comma-separated
425 : // values of its Sec-WebSocket-Protocol headers
426 3651 : std::vector<std::string> offered_protocols(const http_request& request) {
427 3651 : std::vector<std::string> offered;
428 3944 : for (const auto& value : request.get_headers_with_key("Sec-WebSocket-Protocol")) {
429 730 : syntax::for_each_element(value, [&](std::string_view protocol) { offered.emplace_back(protocol); });
430 3651 : }
431 3651 : return offered;
432 0 : }
433 :
434 : // The first subprotocol offered that is supported, empty if none
435 3651 : std::string select_protocol(const std::vector<std::string>& offered, const std::vector<std::string>& supported) {
436 3813 : for (const auto& candidate : offered) {
437 374 : if (std::find(supported.begin(), supported.end(), candidate) != supported.end()) return candidate;
438 : }
439 3439 : return {};
440 : }
441 :
442 : // A Sec-WebSocket-Key: 16 bytes in base64 (RFC 6455, 4.1)
443 3570 : bool valid_websocket_key(const std::string& key) {
444 3570 : if (key.size() != 24 || !key.ends_with("==")) return false;
445 81489 : for (size_t i = 0; i < 22; ++i) {
446 77946 : char c = key[i];
447 77946 : if (!std::isalnum(static_cast<unsigned char>(c)) && c != '+' && c != '/') return false;
448 : }
449 3543 : return ::thinger::util::base64::decode(key).size() == 16;
450 : }
451 :
452 3543 : std::string websocket_accept(const std::string& key) {
453 3543 : return ::thinger::util::base64::encode(::thinger::util::sha1::hash(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
454 : }
455 :
456 : }
457 :
458 3714 : std::shared_ptr<http_response> response::websocket_handshake(const std::vector<std::string>& protocols,
459 : bool protocol_required, bool reject_unknown_protocol,
460 : std::string& protocol) {
461 3714 : const auto& request = *state_->request;
462 3714 : if (auto rejection = check_handshake(request)) {
463 135 : if (rejection->unsupported_version) header("Sec-WebSocket-Version", "13");
464 189 : error(rejection->status, rejection->message);
465 63 : return nullptr;
466 : }
467 :
468 3651 : auto offered = offered_protocols(request);
469 3651 : protocol = select_protocol(offered, protocols);
470 3651 : bool required = protocol_required && !protocols.empty();
471 3651 : if (protocol.empty() && !offered.empty() && (reject_unknown_protocol || required)) {
472 54 : LOG_DEBUG("Unsupported WebSocket protocol: {}", request.get_header("Sec-WebSocket-Protocol"));
473 162 : error(http_response::status::bad_request, "Unsupported WebSocket protocol");
474 54 : return nullptr;
475 : }
476 3597 : if (offered.empty() && required) {
477 81 : error(http_response::status::bad_request, "This method requires specifying a WebSocket protocol");
478 27 : return nullptr;
479 : }
480 :
481 3570 : const auto& key = request.get_header("Sec-WebSocket-Key");
482 3570 : if (!valid_websocket_key(key)) {
483 81 : error(http_response::status::bad_request, "Missing or invalid Sec-WebSocket-Key header");
484 27 : return nullptr;
485 : }
486 :
487 3543 : auto response = new_response();
488 3543 : if (!response) return nullptr;
489 3543 : response->set_status(http_response::status::switching_protocols);
490 7086 : response->add_header(header::upgrade, "websocket");
491 7086 : response->add_header(header::connection, "Upgrade");
492 10629 : response->add_header("Sec-WebSocket-Accept", websocket_accept(key));
493 3967 : if (!protocol.empty()) response->add_header("Sec-WebSocket-Protocol", protocol);
494 3543 : return response;
495 3651 : }
496 :
497 0 : void detail::log_handler_exception(const char* what, const std::exception_ptr& error) {
498 : try {
499 0 : std::rethrow_exception(error);
500 0 : } catch (const std::exception& e) {
501 0 : LOG_ERROR("Exception in the {} handler: {}", what, e.what());
502 0 : } catch (...) {
503 0 : LOG_ERROR("Unknown exception in the {} handler", what);
504 0 : }
505 0 : }
506 :
507 : using detail::spawn_handler;
508 :
509 3150 : void response::upgrade_websocket(const websocket_accept_options& options, websocket_handler handler) {
510 3150 : accept_websocket(options, [handler = std::move(handler)](std::shared_ptr<websocket_connection> connection,
511 : asio::websocket&) {
512 3063 : handler(connection);
513 3063 : connection->start();
514 3063 : });
515 3150 : }
516 :
517 15 : void response::upgrade_websocket(const websocket_accept_options& options, websocket_coroutine handler) {
518 15 : accept_websocket(options, [handler = std::move(handler)](std::shared_ptr<websocket_connection> connection,
519 : asio::websocket& websocket) mutable {
520 15 : spawn_handler(websocket.get_executor(), "WebSocket",
521 45 : [handler = std::move(handler), connection]() mutable -> awaitable<void> {
522 : co_await handler(connection);
523 : connection->start();
524 30 : });
525 15 : });
526 15 : }
527 :
528 3165 : void response::accept_websocket(const websocket_accept_options& options,
529 : std::function<void(std::shared_ptr<websocket_connection>, asio::websocket&)> deliver) {
530 3252 : if (!ensure_not_responded()) return;
531 3165 : if (reject_takeover("WebSocket upgrade")) return;
532 :
533 3159 : std::string protocol;
534 3159 : auto response = websocket_handshake(options.protocols, options.protocol_required, true, protocol);
535 3159 : if (!response) return;
536 :
537 3078 : hand_over(std::move(response), [deliver = std::move(deliver), options, protocol](upgraded_connection connection) {
538 : // binary, server; on the io_context of the connection, where it is taken over (the
539 : // default executor). The connection holds the scope: told of a shutdown, counted as a
540 : // websocket once started
541 3078 : auto websocket = std::make_shared<asio::websocket>(std::move(connection.socket), true, true);
542 3078 : websocket->set_protocol(protocol);
543 : // Frames the client sent right after its request, without waiting for the response:
544 : // read first, once the connection starts reading
545 3078 : if (!connection.buffered.empty()) websocket->set_read_ahead(std::move(connection.buffered));
546 3078 : connection.scope.set_type(connection_type::websocket);
547 3078 : auto ws_connection = std::make_shared<websocket_connection>(websocket, options, std::move(connection.scope));
548 :
549 : // The handler sets its callbacks, then the WebSocket starts
550 3078 : deliver(ws_connection, *websocket);
551 3078 : });
552 3240 : }
553 :
554 21 : void response::upgrade_websocket_stream(const websocket_accept_options& options, websocket_stream_coroutine handler) {
555 21 : upgrade_websocket_stream(options, websocket_stream_handler(
556 42 : [handler = std::move(handler)](upgraded_websocket websocket) mutable {
557 21 : auto executor = websocket.stream->get_executor();
558 21 : spawn_handler(std::move(executor), "WebSocket",
559 42 : [handler = std::move(handler), websocket = std::move(websocket)]() mutable {
560 21 : return handler(std::move(websocket));
561 : });
562 21 : }));
563 21 : }
564 :
565 555 : void response::upgrade_websocket_stream(const websocket_accept_options& options, websocket_stream_handler handler) {
566 645 : if (!ensure_not_responded()) return;
567 555 : if (reject_takeover("WebSocket upgrade")) return;
568 :
569 555 : std::string protocol;
570 555 : auto response = websocket_handshake(options.protocols, options.protocol_required, false, protocol);
571 555 : if (!response) return;
572 :
573 465 : hand_over(std::move(response), [handler = std::move(handler), options, protocol](upgraded_connection connection) {
574 : // On the io_context of the connection, where it is taken over (the default executor)
575 465 : auto websocket = std::make_shared<asio::websocket>(std::move(connection.socket), options.binary, true);
576 465 : websocket->set_protocol(protocol);
577 : // Frames the client sent right after its request, without waiting for the response
578 465 : if (!connection.buffered.empty()) websocket->set_read_ahead(std::move(connection.buffered));
579 : // No keepalive unless asked for
580 930 : websocket->set_keepalive(options.ping_interval.value_or(std::chrono::milliseconds{0}),
581 465 : options.idle_timeout.value_or(std::chrono::milliseconds{0}));
582 465 : connection.scope.set_type(connection_type::websocket);
583 465 : handler(upgraded_websocket{std::move(websocket), protocol, std::move(connection.scope)});
584 465 : });
585 645 : }
586 :
587 261 : void response::take_over(takeover_handler handler) {
588 261 : if (!ensure_not_responded()) return;
589 261 : if (reject_takeover("Connection takeover")) return;
590 258 : if (auto message = new_response()) hand_over(std::move(message), std::move(handler));
591 : }
592 :
593 3 : void response::take_over(takeover_coroutine handler) {
594 3 : take_over(takeover_handler([handler = std::move(handler)](upgraded_connection connection) mutable {
595 3 : auto& io_context = connection.socket->get_io_context();
596 3 : spawn_handler(io_context, "takeover",
597 3 : [handler = std::move(handler), connection = std::move(connection)]() mutable {
598 3 : return handler(std::move(connection));
599 : });
600 3 : }));
601 3 : }
602 :
603 4317 : void response::hand_over(std::shared_ptr<http_response> message, takeover_handler handler) {
604 4317 : auto framing = frame_takeover(*message);
605 4317 : state_->sink->take_over(std::move(message), std::move(handler), framing);
606 4317 : }
607 :
608 510 : void response::start_sse(sse_handler handler, const sse_options& options) {
609 510 : accept_sse(options, [handler = std::move(handler)](std::shared_ptr<sse_connection> connection, asio::socket&) {
610 498 : handler(std::move(connection));
611 498 : });
612 510 : }
613 :
614 18 : void response::start_sse(sse_coroutine handler, const sse_options& options) {
615 18 : accept_sse(options, [handler = std::move(handler)](std::shared_ptr<sse_connection> connection, asio::socket& socket) mutable {
616 18 : spawn_handler(socket.get_io_context(), "SSE", [handler = std::move(handler), connection = std::move(connection)]() mutable {
617 18 : return handler(std::move(connection));
618 : });
619 18 : });
620 18 : }
621 :
622 528 : void response::accept_sse(const sse_options& options,
623 : std::function<void(std::shared_ptr<sse_connection>, asio::socket&)> deliver) {
624 540 : if (!ensure_not_responded()) return;
625 528 : if (reject_takeover("Server-Sent Events")) return;
626 :
627 525 : auto response = new_response();
628 525 : if (!response) return;
629 525 : response->set_status(http_response::status::ok);
630 1050 : response->set_content_type("text/event-stream");
631 2100 : response->add_header("Cache-Control", "no-cache");
632 2100 : response->add_header("X-Accel-Buffering", "no"); // Disable nginx buffering
633 :
634 : // HEAD: the headers of the event stream alone, as a whole response (no events, nor
635 : // handler), so the connection goes on with the next request
636 525 : if (head_request()) {
637 9 : send_message(std::move(response), false);
638 9 : return;
639 : }
640 :
641 : // Send the SSE headers and take over the connection (anything the client sends
642 : // afterwards is not part of the event stream)
643 516 : hand_over(std::move(response), [deliver = std::move(deliver), keepalive = options.keepalive](upgraded_connection connection) {
644 : // The connection holds the scope: told of a shutdown, counted as an event stream
645 516 : auto& socket = *connection.socket;
646 1032 : auto sse_conn = std::make_shared<sse_connection>(std::move(connection.socket), boost::asio::any_io_executor{},
647 1032 : std::move(connection.scope));
648 516 : sse_conn->set_keepalive(keepalive);
649 516 : sse_conn->start();
650 516 : deliver(std::move(sse_conn), socket);
651 516 : });
652 525 : }
653 :
654 : // Chunked response support: the sink writes the headers, then each chunk. Chunks are
655 : // written in order (the lock), only once the headers are queued and until the end. A
656 : // response to HEAD is the headers alone, as a whole response: its chunks are dropped
657 1090 : bool response::start_chunked(const std::string& content_type, http::http_response::status status) {
658 1090 : if (!ensure_not_responded()) return false;
659 1069 : auto response = new_response();
660 1069 : if (!response) return false;
661 1068 : response->set_status(status);
662 1068 : response->set_content_type(content_type);
663 :
664 1068 : std::lock_guard lock(state_->chunked_mutex);
665 1068 : if (head_request()) {
666 : // Without Transfer-Encoding, as start_stream() does
667 9 : state_->sink->send(std::move(response));
668 1059 : } else if (!state_->sink->begin(std::move(response))) {
669 0 : return false;
670 : }
671 1068 : state_->chunked = state::chunked_phase::open;
672 1068 : return true;
673 1069 : }
674 :
675 1198 : bool response::write_chunk(const std::string& data) {
676 1198 : std::lock_guard lock(state_->chunked_mutex);
677 1198 : if (state_->chunked != state::chunked_phase::open) {
678 9 : log_late_answer("Chunks can only be written between start_chunked() and end_chunked()");
679 9 : return false;
680 : }
681 1189 : if (head_request()) return true;
682 1171 : return state_->sink->append(data);
683 1198 : }
684 :
685 1071 : bool response::end_chunked() {
686 1071 : std::lock_guard lock(state_->chunked_mutex);
687 1071 : if (state_->chunked != state::chunked_phase::open) {
688 9 : log_late_answer("Only a response started with start_chunked() can be ended, once");
689 9 : return false;
690 : }
691 1062 : state_->chunked = state::chunked_phase::ended;
692 1062 : if (head_request()) return true;
693 1053 : return state_->sink->finish();
694 1071 : }
695 :
696 :
697 : namespace {
698 :
699 1722 : bool is_framing_header(std::string_view name) {
700 3432 : return headers::is_header(name, header::content_length) || headers::is_header(name, header::transfer_encoding) ||
701 3432 : headers::is_header(name, header::connection);
702 : }
703 :
704 : // Status and headers of the head of a streamed response: those of `head` replace the ones
705 : // with the same name set before (e.g. CORS), except Vary, which adds up: the response
706 : // depends on the fields of both, and a cache must know. The framing is the response's own
707 719 : void apply_head(http_response& message, const http_response& head) {
708 719 : message.set_status(head.get_status());
709 719 : message.set_reason_phrase(head.get_reason());
710 1580 : for (const auto& [key, value] : head.get_headers()) {
711 861 : if (!is_framing_header(key) && !headers::is_header(key, "Vary")) message.remove_headers(key);
712 : }
713 : // Credentials granted by CORS mean nothing next to an origin "*", and browsers reject them
714 719 : if (head.get_header("Access-Control-Allow-Origin") == "*") message.remove_headers("Access-Control-Allow-Credentials");
715 719 : const bool merge_vary = message.has_header("Vary");
716 1580 : for (const auto& [key, value] : head.get_headers()) {
717 861 : if (is_framing_header(key)) continue;
718 855 : if (merge_vary && headers::is_header(key, "Vary")) {
719 108 : syntax::for_each_element(value, [&message](std::string_view field) { compression::add_vary(message, field); });
720 : } else {
721 804 : message.add_header(key, value);
722 : }
723 : }
724 : // headers set before (header()) may carry framing too
725 719 : message.remove_headers(header::content_length);
726 719 : message.remove_headers(header::transfer_encoding);
727 719 : }
728 :
729 240 : bool is_http_1_1(const http_request& request) {
730 480 : return request.get_http_version_major() > 1 ||
731 480 : (request.get_http_version_major() == 1 && request.get_http_version_minor() >= 1);
732 : }
733 :
734 : // Framing of a streamed response, with its Content-Length if it has one
735 719 : body_framing frame_stream(http_response& message, const http_request& request,
736 : std::optional<size_t> content_length) {
737 719 : int code = message.get_status_code();
738 719 : bool no_body = request.get_method() == method::HEAD || (code >= 100 && code < 200) || code == 204 || code == 304;
739 719 : if (no_body) {
740 : // the length of the resource (HEAD, 304), but never for 1xx and 204
741 18 : if (content_length && code >= 200 && code != 204) message.set_content_length(*content_length);
742 18 : return body_framing::none;
743 : }
744 701 : if (content_length) {
745 461 : message.set_content_length(*content_length);
746 461 : return body_framing::length;
747 : }
748 240 : return is_http_1_1(request) ? body_framing::chunked : body_framing::close;
749 : }
750 :
751 : }
752 :
753 : // Streamed response: the sink writes the head, then each part of the body once the previous
754 : // one is written. The response checks the body against its framing, so a writer can never
755 : // put more on the connection than the head announced (it would be read as what follows)
756 719 : bool response::start_stream(const http_response& head, std::optional<size_t> content_length) {
757 719 : if (!ensure_not_responded()) return false;
758 719 : auto message = new_response();
759 719 : if (!message) return false;
760 719 : apply_head(*message, head);
761 719 : auto framing = frame_stream(*message, *state_->request, content_length);
762 :
763 719 : std::lock_guard lock(state_->chunked_mutex);
764 719 : if (!state_->sink->begin_stream(std::move(message), framing)) return false;
765 719 : state_->stream = state::chunked_phase::open;
766 719 : state_->stream_framing = framing;
767 719 : state_->stream_remaining = framing == body_framing::length ? *content_length : 0;
768 719 : return true;
769 719 : }
770 :
771 14594 : awaitable<bool> response::write_stream(std::string data) {
772 : {
773 : std::lock_guard lock(state_->chunked_mutex);
774 : if (state_->stream != state::chunked_phase::open) {
775 : log_late_answer("Stream data can only be written between start_stream() and end_stream()");
776 : co_return false;
777 : }
778 : if (state_->stream_framing == body_framing::none) {
779 : if (!data.empty()) LOG_ERROR("Response without body: stream data dropped");
780 : co_return data.empty();
781 : }
782 : if (state_->stream_framing == body_framing::length) {
783 : if (data.size() > state_->stream_remaining) {
784 : LOG_ERROR("Stream data exceeds the Content-Length of the response: dropped");
785 : co_return false;
786 : }
787 : state_->stream_remaining -= data.size();
788 : }
789 : }
790 : // An empty chunk would end a chunked body
791 : if (data.empty()) co_return true;
792 : co_return co_await state_->sink->write_stream(std::move(data));
793 29188 : }
794 :
795 675 : awaitable<bool> response::end_stream() {
796 : {
797 : std::lock_guard lock(state_->chunked_mutex);
798 : if (state_->stream != state::chunked_phase::open) {
799 : log_late_answer("Only a response started with start_stream() can be ended, once");
800 : co_return false;
801 : }
802 : state_->stream = state::chunked_phase::ended;
803 : if (state_->stream_framing == body_framing::length && state_->stream_remaining > 0) {
804 : LOG_ERROR("Stream ended {} bytes before its Content-Length: closing the connection",
805 : state_->stream_remaining);
806 : state_->sink->abort_stream();
807 : co_return false;
808 : }
809 : }
810 : co_return co_await state_->sink->end_stream();
811 1350 : }
812 :
813 61 : void response::abort_stream() {
814 61 : std::lock_guard lock(state_->chunked_mutex);
815 61 : if (state_->stream != state::chunked_phase::open) return;
816 43 : state_->stream = state::chunked_phase::ended;
817 43 : state_->sink->abort_stream();
818 61 : }
819 :
820 : } // namespace thinger::http
821 :
|