/
githubmirror
/
nghttp2
Обзор
Документация
Войти
/
githubmirror
/
nghttp2
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/shrpx_http2_upstream.cc
2 462 строки
72 KB
Tatsuhiro Tsujikawa
nghttpx: NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE is not allowed in on_frame_recv_callback
30 июн 2026, 14:54
30 июн 2026, 14:54
8955d1d
Код
Авторство
О чём код?
/* * nghttp2 - HTTP/2 C Library * * Copyright (c) 2012 Tatsuhiro Tsujikawa * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "shrpx_http2_upstream.h" #include <netinet/tcp.h> #include <assert.h> #include <cerrno> #include "shrpx_client_handler.h" #include "shrpx_https_upstream.h" #include "shrpx_downstream.h" #include "shrpx_downstream_connection.h" #include "shrpx_config.h" #include "shrpx_http.h" #include "shrpx_worker.h" #include "shrpx_http2_session.h" #include "shrpx_log.h" #ifdef HAVE_MRUBY # include "shrpx_mruby.h" #endif // defined(HAVE_MRUBY) #include "http2.h" #include "util.h" #include "base64.h" #include "app_helper.h" #include "template.h" using namespace nghttp2; namespace shrpx { namespace { int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data) { auto upstream = static_cast<Http2Upstream *>(user_data); if (log_enabled(INFO)) { Log{INFO, upstream} << "Stream stream_id=" << stream_id << " is being closed"; } auto downstream = static_cast<Downstream *>( nghttp2_session_get_stream_user_data(session, stream_id)); if (!downstream) { return 0; } auto &req = downstream->request(); if (!upstream->consume(stream_id, req.unconsumed_body_length)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } req.unconsumed_body_length = 0; if (downstream->get_request_state() == DownstreamState::CONNECT_FAIL) { if (!upstream->remove_downstream(downstream)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } // downstream was deleted return 0; } if (downstream->can_detach_downstream_connection()) { // Keep-alive if (!downstream->detach_downstream_connection()) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } downstream->set_request_state(DownstreamState::STREAM_CLOSED); // At this point, downstream read may be paused. // If shrpx_downstream::push_request_headers() failed, the // error is handled here. if (!upstream->remove_downstream(downstream)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } // downstream was deleted // How to test this case? Request sufficient large download // and make client send RST_STREAM after it gets first DATA // frame chunk. return 0; } } // namespace std::expected<void, Error> Http2Upstream::upgrade_upstream(HttpsUpstream *http) { int rv; auto &balloc = http->get_downstream()->get_block_allocator(); auto http2_settings = http->get_downstream()->get_http2_settings(); http2_settings = util::to_base64(balloc, http2_settings); auto settings_payload = base64::decode(balloc, http2_settings); rv = nghttp2_session_upgrade2( session_, settings_payload.data(), settings_payload.size(), http->get_downstream()->request().method == HTTP_HEAD, nullptr); if (rv != 0) { if (log_enabled(INFO)) { Log{INFO, this} << "nghttp2_session_upgrade() returned error: " << nghttp2_strerror(rv); } return std::unexpected{Error::HTTP2}; } pre_upstream_.reset(http); auto downstream = http->pop_downstream(); downstream->reset_upstream(this); downstream->set_stream_id(1); downstream->reset_upstream_rtimer(); downstream->set_stream_id(1); auto ptr = downstream.get(); nghttp2_session_set_stream_user_data(session_, 1, ptr); downstream_queue_.add_pending(std::move(downstream)); downstream_queue_.mark_active(ptr); // TODO This might not be necessary handler_->stop_read_timer(); if (log_enabled(INFO)) { Log{INFO, this} << "Connection upgraded to HTTP/2"; } return {}; } void Http2Upstream::start_settings_timer() { ev_timer_start(handler_->get_loop(), &settings_timer_); } void Http2Upstream::stop_settings_timer() { ev_timer_stop(handler_->get_loop(), &settings_timer_); } namespace { int on_header_callback2(nghttp2_session *session, const nghttp2_frame *frame, nghttp2_rcbuf *name, nghttp2_rcbuf *value, uint8_t flags, void *user_data) { auto namebuf = nghttp2_rcbuf_get_buf(name); auto valuebuf = nghttp2_rcbuf_get_buf(value); auto config = get_config(); if (config->http2.upstream.debug.frame_debug) { verbose_on_header_callback(session, frame, namebuf.base, namebuf.len, valuebuf.base, valuebuf.len, flags, user_data); } if (frame->hd.type != NGHTTP2_HEADERS) { return 0; } auto upstream = static_cast<Http2Upstream *>(user_data); auto downstream = static_cast<Downstream *>( nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); if (!downstream) { return 0; } auto &req = downstream->request(); auto &httpconf = config->http; if (req.fs.buffer_size() + namebuf.len + valuebuf.len > httpconf.request_header_field_buffer || req.fs.num_fields() >= httpconf.max_request_header_fields) { if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { return 0; } if (log_enabled(INFO)) { Log{INFO, upstream} << "Too large or many header field size=" << req.fs.buffer_size() + namebuf.len + valuebuf.len << ", num=" << req.fs.num_fields() + 1; } // just ignore header fields if this is trailer part. if (frame->headers.cat == NGHTTP2_HCAT_HEADERS) { return 0; } if (!upstream->error_reply(downstream, 431)) { return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } return 0; } auto nameref = as_string_view(namebuf.base, namebuf.len); auto valueref = as_string_view(valuebuf.base, valuebuf.len); auto token = http2::lookup_token(nameref); auto no_index = flags & NGHTTP2_NV_FLAG_NO_INDEX; downstream->add_rcbuf(name); downstream->add_rcbuf(value); if (frame->headers.cat == NGHTTP2_HCAT_HEADERS) { // just store header fields for trailer part req.fs.add_trailer_token(nameref, valueref, no_index, token); return 0; } req.fs.add_header_token(nameref, valueref, no_index, token); return 0; } } // namespace namespace { int on_begin_headers_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) { auto upstream = static_cast<Http2Upstream *>(user_data); if (frame->headers.cat != NGHTTP2_HCAT_REQUEST) { return 0; } if (log_enabled(INFO)) { Log{INFO, upstream} << "Received upstream request HEADERS stream_id=" << frame->hd.stream_id; } upstream->on_start_request(frame); return 0; } } // namespace void Http2Upstream::on_start_request(const nghttp2_frame *frame) { auto downstream = std::make_unique<Downstream>(this, handler_->get_mcpool(), frame->hd.stream_id); nghttp2_session_set_stream_user_data(session_, frame->hd.stream_id, downstream.get()); downstream->reset_upstream_rtimer(); auto config = get_config(); auto &httpconf = config->http; handler_->reset_upstream_read_timeout(httpconf.timeout.header); auto &req = downstream->request(); // Although, we deprecated minor version from HTTP/2, we supply // minor version 0 to use via header field in a conventional way. req.http_major = 2; req.http_minor = 0; add_pending_downstream(std::move(downstream)); ++num_requests_; if (httpconf.max_requests <= num_requests_) { start_graceful_shutdown(); } } std::expected<void, Error> Http2Upstream::on_request_headers(Downstream *downstream, const nghttp2_frame *frame) { auto lgconf = log_config(); lgconf->update_tstamp(std::chrono::system_clock::now()); auto &req = downstream->request(); req.tstamp = lgconf->tstamp; if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { return {}; } auto &nva = req.fs.headers(); if (log_enabled(INFO)) { std::string ss; for (auto &nv : nva) { if (nv.name == "authorization"sv) { ss += tty_http_hd(); ss += nv.name; ss += tty_rst(); ss += ": <redacted>\n"; continue; } ss += tty_http_hd(); ss += nv.name; ss += tty_rst(); ss += ": "; ss += nv.value; ss += '\n'; } Log{INFO, this} << "HTTP request headers. stream_id=" << downstream->get_stream_id() << "\n" << ss; } auto config = get_config(); auto &dump = config->http2.upstream.debug.dump; if (dump.request_header) { http2::dump_nv(dump.request_header, nva); } auto content_length = req.fs.header(http2::HD_CONTENT_LENGTH); if (content_length) { // libnghttp2 guarantees this can be parsed req.fs.content_length = static_cast<int64_t>(*util::parse_uint(content_length->value)); } // presence of mandatory header fields are guaranteed by libnghttp2. auto authority = req.fs.header(http2::HD__AUTHORITY); auto path = req.fs.header(http2::HD__PATH); auto method = req.fs.header(http2::HD__METHOD); auto scheme = req.fs.header(http2::HD__SCHEME); auto method_token = http2::lookup_method_token(method->value); if (method_token == -1) { return error_reply(downstream, 501); } if (method_token == HTTP_CONNECT && content_length) { if (log_enabled(INFO)) { Log{INFO, this} << "content-length are not allowed in CONNECT request"; } return error_reply(downstream, 400); } auto faddr = handler_->get_upstream_addr(); // For HTTP/2 proxy, we require :authority. if (method_token != HTTP_CONNECT && config->http2_proxy && faddr->alt_mode == UpstreamAltMode::NONE && !authority) { return rst_stream(downstream, NGHTTP2_PROTOCOL_ERROR); } req.method = method_token; if (scheme) { req.scheme = scheme->value; } // nghttp2 library guarantees either :authority or host exist if (!authority) { req.no_authority = true; authority = req.fs.header(http2::HD_HOST); } if (authority) { req.authority = authority->value; } if (path) { if (method_token == HTTP_OPTIONS && path->value == "*"sv) { // Server-wide OPTIONS request. Path is empty. } else if (config->http2_proxy && faddr->alt_mode == UpstreamAltMode::NONE) { req.path = path->value; } else { req.path = http2::rewrite_clean_path(downstream->get_block_allocator(), path->value); } } auto connect_proto = req.fs.header(http2::HD__PROTOCOL); if (connect_proto) { if (connect_proto->value != "websocket"sv) { return error_reply(downstream, 400); } req.connect_proto = ConnectProto::WEBSOCKET; } if (!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) { req.http2_expect_body = true; } else if (req.fs.content_length == -1) { // If END_STREAM flag is set to HEADERS frame, we are sure that // content-length is 0. req.fs.content_length = 0; } downstream->inspect_http2_request(); downstream->set_request_state(DownstreamState::HEADER_COMPLETE); if (config->http.require_http_scheme && !http::check_http_scheme(req.scheme, handler_->get_ssl() != nullptr)) { return error_reply(downstream, 400); } #ifdef HAVE_MRUBY auto worker = handler_->get_worker(); auto mruby_ctx = worker->get_mruby_context(); if (!mruby_ctx->run_on_request_proc(downstream)) { return error_reply(downstream, 500); } #endif // defined(HAVE_MRUBY) if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { downstream->disable_upstream_rtimer(); downstream->set_request_state(DownstreamState::MSG_COMPLETE); } if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { return {}; } return start_downstream(downstream); } std::expected<void, Error> Http2Upstream::start_downstream(Downstream *downstream) { if (downstream_queue_.can_activate(downstream->request().authority)) { return initiate_downstream(downstream); } downstream_queue_.mark_blocked(downstream); return {}; } std::expected<void, Error> Http2Upstream::initiate_downstream(Downstream *downstream) { #ifdef HAVE_MRUBY DownstreamConnection *dconn_ptr; #endif // defined(HAVE_MRUBY) for (;;) { auto maybe_dconn = handler_->get_downstream_connection(downstream); if (!maybe_dconn) { if (!(maybe_dconn.error() == Error::TLS_REQUIRED ? redirect_to_https(downstream) : error_reply(downstream, 502))) { if (auto rv = rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); !rv) { return rv; } } downstream->set_request_state(DownstreamState::CONNECT_FAIL); downstream_queue_.mark_failure(downstream); return {}; } auto dconn = std::move(*maybe_dconn); #ifdef HAVE_MRUBY dconn_ptr = dconn.get(); #endif // defined(HAVE_MRUBY) if (downstream->attach_downstream_connection(std::move(dconn))) { break; } } #ifdef HAVE_MRUBY const auto &group = dconn_ptr->get_downstream_addr_group(); if (group) { const auto &mruby_ctx = group->shared_addr->mruby_ctx; if (!mruby_ctx->run_on_request_proc(downstream)) { if (!error_reply(downstream, 500)) { if (auto rv = rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); !rv) { return rv; } } downstream_queue_.mark_failure(downstream); return {}; } if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { return {}; } } #endif // defined(HAVE_MRUBY) if (!downstream->push_request_headers()) { if (!error_reply(downstream, 502)) { if (auto rv = rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); !rv) { return rv; } } downstream_queue_.mark_failure(downstream); return {}; } downstream_queue_.mark_active(downstream); auto &req = downstream->request(); if (!req.http2_expect_body && !downstream->end_upload_data()) { if (auto rv = rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); !rv) { return rv; } } return {}; } namespace { int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) { if (get_config()->http2.upstream.debug.frame_debug) { verbose_on_frame_recv_callback(session, frame, user_data); } auto upstream = static_cast<Http2Upstream *>(user_data); auto handler = upstream->get_client_handler(); switch (frame->hd.type) { case NGHTTP2_DATA: { auto downstream = static_cast<Downstream *>( nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); if (!downstream) { return 0; } if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { downstream->disable_upstream_rtimer(); if (!downstream->end_upload_data() && downstream->get_response_state() != DownstreamState::MSG_COMPLETE && !upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } downstream->set_request_state(DownstreamState::MSG_COMPLETE); } return 0; } case NGHTTP2_HEADERS: { auto downstream = static_cast<Downstream *>( nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); if (!downstream) { return 0; } if (frame->headers.cat == NGHTTP2_HCAT_REQUEST) { downstream->reset_upstream_rtimer(); handler->stop_read_timer(); if (!upstream->on_request_headers(downstream, frame)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { downstream->disable_upstream_rtimer(); if (!downstream->end_upload_data() && downstream->get_response_state() != DownstreamState::MSG_COMPLETE && !upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } downstream->set_request_state(DownstreamState::MSG_COMPLETE); } return 0; } case NGHTTP2_SETTINGS: if ((frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { return 0; } upstream->stop_settings_timer(); return 0; case NGHTTP2_GOAWAY: if (log_enabled(INFO)) { auto debug_data = util::ascii_dump(frame->goaway.opaque_data, frame->goaway.opaque_data_len); Log{INFO, upstream} << "GOAWAY received: last-stream-id=" << frame->goaway.last_stream_id << ", error_code=" << frame->goaway.error_code << ", debug_data=" << debug_data; } return 0; default: return 0; } } } // namespace namespace { int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, int32_t stream_id, const uint8_t *data, size_t len, void *user_data) { auto upstream = static_cast<Http2Upstream *>(user_data); auto downstream = static_cast<Downstream *>( nghttp2_session_get_stream_user_data(session, stream_id)); if (!downstream) { if (!upstream->consume(stream_id, len)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } downstream->reset_upstream_rtimer(); if (!downstream->push_upload_data_chunk({data, len})) { if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE && !upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } if (!upstream->consume(stream_id, len)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } return 0; } } // namespace namespace { int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) { if (get_config()->http2.upstream.debug.frame_debug) { verbose_on_frame_send_callback(session, frame, user_data); } auto upstream = static_cast<Http2Upstream *>(user_data); auto handler = upstream->get_client_handler(); switch (frame->hd.type) { case NGHTTP2_DATA: case NGHTTP2_HEADERS: { if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) { return 0; } // RST_STREAM if request is still incomplete. auto stream_id = frame->hd.stream_id; auto downstream = static_cast<Downstream *>( nghttp2_session_get_stream_user_data(session, stream_id)); if (!downstream) { return 0; } // For tunneling, issue RST_STREAM to finish the stream. if (downstream->get_upgraded() || nghttp2_session_get_stream_remote_close(session, stream_id) == 0) { if (log_enabled(INFO)) { Log{INFO, upstream} << "Send RST_STREAM to " << (downstream->get_upgraded() ? "tunneled " : "") << "stream stream_id=" << downstream->get_stream_id() << " to finish off incomplete request"; } if (!upstream->rst_stream(downstream, NGHTTP2_NO_ERROR)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } return 0; } case NGHTTP2_SETTINGS: if ((frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { upstream->start_settings_timer(); } return 0; case NGHTTP2_PUSH_PROMISE: { auto promised_stream_id = frame->push_promise.promised_stream_id; if (nghttp2_session_get_stream_user_data(session, promised_stream_id)) { // In case of push from backend, downstream object was already // created. return 0; } auto promised_downstream = std::make_unique<Downstream>( upstream, handler->get_mcpool(), promised_stream_id); auto &req = promised_downstream->request(); // As long as we use nghttp2_session_mem_send2(), setting stream // user data here should not fail. This is because this callback // is called just after frame was serialized. So no worries about // hanging Downstream. nghttp2_session_set_stream_user_data(session, promised_stream_id, promised_downstream.get()); promised_downstream->set_assoc_stream_id(frame->hd.stream_id); promised_downstream->disable_upstream_rtimer(); req.http_major = 2; req.http_minor = 0; req.fs.content_length = 0; req.http2_expect_body = false; auto &promised_balloc = promised_downstream->get_block_allocator(); for (size_t i = 0; i < frame->push_promise.nvlen; ++i) { auto &nv = frame->push_promise.nva[i]; auto name = make_string_ref(promised_balloc, as_string_view(nv.name, nv.namelen)); auto value = make_string_ref(promised_balloc, as_string_view(nv.value, nv.valuelen)); auto token = http2::lookup_token(name); switch (token) { case http2::HD__METHOD: req.method = http2::lookup_method_token(value); break; case http2::HD__SCHEME: req.scheme = value; break; case http2::HD__AUTHORITY: req.authority = value; break; case http2::HD__PATH: req.path = http2::rewrite_clean_path(promised_balloc, value); break; } req.fs.add_header_token(name, value, nv.flags & NGHTTP2_NV_FLAG_NO_INDEX, token); } promised_downstream->inspect_http2_request(); promised_downstream->set_request_state(DownstreamState::MSG_COMPLETE); // a bit weird but start_downstream() expects that given // downstream is in pending queue. auto ptr = promised_downstream.get(); upstream->add_pending_downstream(std::move(promised_downstream)); #ifdef HAVE_MRUBY auto worker = handler->get_worker(); auto mruby_ctx = worker->get_mruby_context(); if (!mruby_ctx->run_on_request_proc(ptr)) { if (!upstream->error_reply(ptr, 500) && !upstream->rst_stream(ptr, NGHTTP2_INTERNAL_ERROR)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } #endif // defined(HAVE_MRUBY) if (!upstream->start_downstream(ptr)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } case NGHTTP2_GOAWAY: if (log_enabled(INFO)) { auto debug_data = util::ascii_dump(frame->goaway.opaque_data, frame->goaway.opaque_data_len); Log{INFO, upstream} << "Sending GOAWAY: last-stream-id=" << frame->goaway.last_stream_id << ", error_code=" << frame->goaway.error_code << ", debug_data=" << debug_data; } return 0; default: return 0; } } } // namespace namespace { int on_frame_not_send_callback(nghttp2_session *session, const nghttp2_frame *frame, int lib_error_code, void *user_data) { auto upstream = static_cast<Http2Upstream *>(user_data); if (log_enabled(INFO)) { Log{INFO, upstream} << "Failed to send control frame type=" << static_cast<uint32_t>(frame->hd.type) << ", lib_error_code=" << lib_error_code << ":" << nghttp2_strerror(lib_error_code); } if (frame->hd.type == NGHTTP2_HEADERS && lib_error_code != NGHTTP2_ERR_STREAM_CLOSED && lib_error_code != NGHTTP2_ERR_STREAM_CLOSING) { // To avoid stream hanging around, issue RST_STREAM. auto downstream = static_cast<Downstream *>( nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); if (downstream && !upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } return 0; } } // namespace constexpr auto PADDING = std::array<uint8_t, 256>{}; namespace { int send_data_callback(nghttp2_session *session, nghttp2_frame *frame, const uint8_t *framehd, size_t length, nghttp2_data_source *source, void *user_data) { auto downstream = static_cast<Downstream *>(source->ptr); auto upstream = static_cast<Http2Upstream *>(downstream->get_upstream()); auto handler = upstream->get_client_handler(); auto body = downstream->get_response_buf(); auto wb = upstream->get_response_buf(); size_t padlen = 0; wb->append(framehd, 9); if (frame->data.padlen > 0) { padlen = frame->data.padlen - 1; wb->append(static_cast<char>(padlen)); } body->remove(*wb, length); wb->append(PADDING.data(), padlen); if (body->rleft() == 0) { downstream->disable_upstream_wtimer(); downstream->unregister_upstream_write_rate_timer(); } else { downstream->reset_upstream_wtimer(); handler->extend_write_rate_timer(length); } if (length > 0 && !downstream->resume_read(SHRPX_NO_BUFFER, length)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } // We have to add length here, so that we can log this amount of // data transferred. downstream->response_sent_body_length += length; auto max_buffer_size = upstream->get_max_buffer_size(); return wb->rleft() >= max_buffer_size ? NGHTTP2_ERR_PAUSE : 0; } } // namespace namespace { uint32_t infer_upstream_rst_stream_error_code(uint32_t downstream_error_code) { // NGHTTP2_REFUSED_STREAM is important because it tells upstream // client to retry. switch (downstream_error_code) { case NGHTTP2_NO_ERROR: case NGHTTP2_REFUSED_STREAM: return downstream_error_code; default: return NGHTTP2_INTERNAL_ERROR; } } } // namespace namespace { void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { auto upstream = static_cast<Http2Upstream *>(w->data); auto handler = upstream->get_client_handler(); Log{INFO, upstream} << "SETTINGS timeout"; if (!upstream->terminate_session(NGHTTP2_SETTINGS_TIMEOUT)) { delete handler; return; } handler->signal_write(); } } // namespace namespace { void shutdown_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { auto upstream = static_cast<Http2Upstream *>(w->data); auto handler = upstream->get_client_handler(); upstream->submit_goaway(); handler->signal_write(); } } // namespace namespace { void prepare_cb(struct ev_loop *loop, ev_prepare *w, int revents) { auto upstream = static_cast<Http2Upstream *>(w->data); upstream->check_shutdown(); } } // namespace void Http2Upstream::submit_goaway() { auto last_stream_id = nghttp2_session_get_last_proc_stream_id(session_); nghttp2_submit_goaway(session_, NGHTTP2_FLAG_NONE, last_stream_id, NGHTTP2_NO_ERROR, nullptr, 0); } void Http2Upstream::check_shutdown() { auto worker = handler_->get_worker(); if (!worker->get_graceful_shutdown()) { return; } ev_prepare_stop(handler_->get_loop(), &prep_); start_graceful_shutdown(); } void Http2Upstream::start_graceful_shutdown() { int rv; if (ev_is_active(&shutdown_timer_)) { return; } rv = nghttp2_submit_shutdown_notice(session_); if (rv != 0) { Log{FATAL, this} << "nghttp2_submit_shutdown_notice() failed: " << nghttp2_strerror(rv); return; } handler_->signal_write(); ev_timer_start(handler_->get_loop(), &shutdown_timer_); } nghttp2_session_callbacks *create_http2_upstream_callbacks() { int rv; nghttp2_session_callbacks *callbacks; rv = nghttp2_session_callbacks_new(&callbacks); if (rv != 0) { return nullptr; } nghttp2_session_callbacks_set_on_stream_close_callback( callbacks, on_stream_close_callback); nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, on_frame_recv_callback); nghttp2_session_callbacks_set_on_data_chunk_recv_callback( callbacks, on_data_chunk_recv_callback); nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, on_frame_send_callback); nghttp2_session_callbacks_set_on_frame_not_send_callback( callbacks, on_frame_not_send_callback); nghttp2_session_callbacks_set_on_header_callback2(callbacks, on_header_callback2); nghttp2_session_callbacks_set_on_begin_headers_callback( callbacks, on_begin_headers_callback); nghttp2_session_callbacks_set_send_data_callback(callbacks, send_data_callback); auto config = get_config(); if (config->padding) { nghttp2_session_callbacks_set_select_padding_callback2( callbacks, http::select_padding_callback); } if (config->http2.upstream.debug.frame_debug) { nghttp2_session_callbacks_set_error_callback2(callbacks, verbose_error_callback); } nghttp2_session_callbacks_set_rand_callback(callbacks, util::secure_random); return callbacks; } namespace { size_t downstream_queue_size(Worker *worker) { auto &downstreamconf = *worker->get_downstream_config(); if (get_config()->http2_proxy) { return downstreamconf.connections_per_host; } return downstreamconf.connections_per_frontend; } } // namespace Http2Upstream::Http2Upstream(ClientHandler *handler) : wb_(handler->get_worker()->get_mcpool()), downstream_queue_(downstream_queue_size(handler->get_worker()), !get_config()->http2_proxy), handler_(handler) { int rv; auto config = get_config(); auto &http2conf = config->http2; auto faddr = handler_->get_upstream_addr(); rv = nghttp2_session_server_new2(&session_, http2conf.upstream.callbacks, this, faddr->alt_mode != UpstreamAltMode::NONE ? http2conf.upstream.alt_mode_option : http2conf.upstream.option); assert(rv == 0); // TODO Maybe call from outside? std::array<nghttp2_settings_entry, 5> entry; size_t nentry = 3; entry[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; entry[0].value = static_cast<uint32_t>(http2conf.upstream.max_concurrent_streams); entry[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; if (faddr->alt_mode != UpstreamAltMode::NONE) { entry[1].value = (1u << 31) - 1; } else { entry[1].value = as_unsigned(http2conf.upstream.window_size); } entry[2].settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; entry[2].value = 1; if (!config->http2_proxy) { entry[nentry].settings_id = NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL; entry[nentry].value = 1; ++nentry; } if (http2conf.upstream.decoder_dynamic_table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { entry[nentry].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; entry[nentry].value = static_cast<uint32_t>(http2conf.upstream.decoder_dynamic_table_size); ++nentry; } rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry.data(), nentry); if (rv != 0) { Log{ERROR, this} << "nghttp2_submit_settings() returned error: " << nghttp2_strerror(rv); } auto window_size = faddr->alt_mode != UpstreamAltMode::NONE ? std::numeric_limits<int32_t>::max() : http2conf.upstream.optimize_window_size ? std::min(http2conf.upstream.connection_window_size, NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE) : http2conf.upstream.connection_window_size; rv = nghttp2_session_set_local_window_size(session_, NGHTTP2_FLAG_NONE, 0, window_size); if (rv != 0) { Log{ERROR, this} << "nghttp2_session_set_local_window_size() returned error: " << nghttp2_strerror(rv); } // We wait for SETTINGS ACK at least 10 seconds. ev_timer_init(&settings_timer_, settings_timeout_cb, http2conf.upstream.timeout.settings, 0.); settings_timer_.data = this; // timer for 2nd GOAWAY. HTTP/2 spec recommend 1 RTT. We wait for // 2 seconds. ev_timer_init(&shutdown_timer_, shutdown_timeout_cb, 2., 0); shutdown_timer_.data = this; ev_prepare_init(&prep_, prepare_cb); prep_.data = this; ev_prepare_start(handler_->get_loop(), &prep_); #if defined(TCP_INFO) && defined(TCP_NOTSENT_LOWAT) if (http2conf.upstream.optimize_write_buffer_size) { auto conn = handler_->get_connection(); conn->tls_dyn_rec_warmup_threshold = 0; uint32_t pollout_thres = 1; rv = setsockopt(conn->fd, IPPROTO_TCP, TCP_NOTSENT_LOWAT, &pollout_thres, static_cast<socklen_t>(sizeof(pollout_thres))); if (rv != 0) { if (log_enabled(INFO)) { auto error = errno; Log{INFO} << "setsockopt(TCP_NOTSENT_LOWAT, " << pollout_thres << ") failed: errno=" << error; } } } #endif // defined(TCP_INFO) && defined(TCP_NOTSENT_LOWAT) handler_->reset_upstream_read_timeout( config->conn.upstream.timeout.http2_idle); handler_->signal_write(); } Http2Upstream::~Http2Upstream() { nghttp2_session_del(session_); ev_prepare_stop(handler_->get_loop(), &prep_); ev_timer_stop(handler_->get_loop(), &shutdown_timer_); ev_timer_stop(handler_->get_loop(), &settings_timer_); } std::expected<void, Error> Http2Upstream::on_read() { auto rb = handler_->get_rb(); auto rlimit = handler_->get_rlimit(); if (rb->rleft()) { auto rv = nghttp2_session_mem_recv2(session_, rb->pos(), rb->rleft()); if (rv < 0) { if (rv != NGHTTP2_ERR_BAD_CLIENT_MAGIC) { Log{ERROR, this} << "nghttp2_session_mem_recv2() returned error: " << nghttp2_strerror(static_cast<int>(rv)); } return std::unexpected{Error::HTTP2}; } // nghttp2_session_mem_recv2 should consume all input bytes on // success. assert(static_cast<size_t>(rv) == rb->rleft()); rb->reset(); rlimit->startw(); } if (nghttp2_session_want_read(session_) == 0 && nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { if (log_enabled(INFO)) { Log{INFO, this} << "No more read/write for this HTTP2 session"; } return std::unexpected{Error::DONE}; } handler_->signal_write(); return {}; } // After this function call, downstream may be deleted. std::expected<void, Error> Http2Upstream::on_write() { int rv; auto config = get_config(); auto &http2conf = config->http2; if ((http2conf.upstream.optimize_write_buffer_size || http2conf.upstream.optimize_window_size) && handler_->get_ssl()) { auto conn = handler_->get_connection(); auto maybe_hint = conn->get_tcp_hint(); if (maybe_hint) { const auto &hint = *maybe_hint; if (http2conf.upstream.optimize_write_buffer_size) { max_buffer_size_ = std::min(SHRPX_HTTP2_MAX_BUFFER_SIZE, hint.write_buffer_size); } if (http2conf.upstream.optimize_window_size) { auto faddr = handler_->get_upstream_addr(); if (faddr->alt_mode == UpstreamAltMode::NONE) { auto window_size = std::min(http2conf.upstream.connection_window_size, static_cast<int32_t>(hint.rwin * 2)); rv = nghttp2_session_set_local_window_size( session_, NGHTTP2_FLAG_NONE, 0, window_size); if (rv != 0) { if (log_enabled(INFO)) { Log{INFO, this} << "nghttp2_session_set_local_window_size() with window_size=" << window_size << " failed: " << nghttp2_strerror(rv); } } } } } } for (;;) { if (wb_.rleft() >= max_buffer_size_) { return {}; } const uint8_t *data; auto datalen = nghttp2_session_mem_send2(session_, &data); if (datalen < 0) { Log{ERROR, this} << "nghttp2_session_mem_send2() returned error: " << nghttp2_strerror(static_cast<int>(datalen)); return std::unexpected{Error::HTTP2}; } if (datalen == 0) { break; } wb_.append(data, as_unsigned(datalen)); } if (nghttp2_session_want_read(session_) == 0 && nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { if (log_enabled(INFO)) { Log{INFO, this} << "No more read/write for this HTTP2 session"; } return std::unexpected{Error::DONE}; } return {}; } ClientHandler *Http2Upstream::get_client_handler() const { return handler_; } std::expected<void, Error> Http2Upstream::downstream_read(DownstreamConnection *dconn) { auto downstream = dconn->get_downstream(); if (downstream->get_response_state() == DownstreamState::MSG_RESET) { // The downstream stream was reset (canceled). In this case, // RST_STREAM to the upstream and delete downstream connection // here. Deleting downstream will be taken place at // on_stream_close_callback. if (auto rv = rst_stream( downstream, infer_upstream_rst_stream_error_code( downstream->get_response_rst_stream_error_code())); !rv) { return rv; } downstream->pop_downstream_connection(); // dconn was deleted dconn = nullptr; } else if (downstream->get_response_state() == DownstreamState::MSG_BAD_HEADER) { if (auto rv = error_reply(downstream, 502); !rv) { return rv; } downstream->pop_downstream_connection(); // dconn was deleted dconn = nullptr; } else { auto rv = downstream->on_read(); if (!rv) { if (rv.error() == Error::RECV_EOF) { if (downstream->get_request_header_sent()) { return downstream_eof(dconn); } return std::unexpected{Error::DCONN_RETRY}; } if (rv.error() == Error::DCONN_CANCELED) { downstream->pop_downstream_connection(); handler_->signal_write(); return {}; } if (rv.error() != Error::NETWORK) { if (log_enabled(INFO)) { Log{INFO, dconn} << "HTTP parser failure"; } } return downstream_error(dconn, Downstream::EVENT_ERROR); } if (downstream->can_detach_downstream_connection()) { // Keep-alive if (auto rv = downstream->detach_downstream_connection(); !rv) { return rv; } } } handler_->signal_write(); // At this point, downstream may be deleted. return {}; } std::expected<void, Error> Http2Upstream::downstream_write(DownstreamConnection *dconn) { auto rv = dconn->on_write(); if (!rv) { if (rv.error() == Error::NETWORK) { return downstream_error(dconn, Downstream::EVENT_ERROR); } return rv; } return {}; } std::expected<void, Error> Http2Upstream::downstream_eof(DownstreamConnection *dconn) { auto downstream = dconn->get_downstream(); if (log_enabled(INFO)) { Log{INFO, dconn} << "EOF. stream_id=" << downstream->get_stream_id(); } // Delete downstream connection. If we don't delete it here, it will // be pooled in on_stream_close_callback. downstream->pop_downstream_connection(); // dconn was deleted dconn = nullptr; // downstream will be deleted in on_stream_close_callback. if (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE) { // Server may indicate the end of the request by EOF if (log_enabled(INFO)) { Log{INFO, this} << "Downstream body was ended by EOF"; } downstream->set_response_state(DownstreamState::MSG_COMPLETE); // For tunneled connection, MSG_COMPLETE signals // downstream_data_read_callback to send RST_STREAM after pending // response body is sent. This is needed to ensure that RST_STREAM // is sent after all pending data are sent. if (auto rv = on_downstream_body_complete(downstream); !rv) { return rv; } } else if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE) { // If stream was not closed, then we set MSG_COMPLETE and let // on_stream_close_callback delete downstream. if (auto rv = error_reply(downstream, 502); !rv) { return rv; } } handler_->signal_write(); // At this point, downstream may be deleted. return {}; } std::expected<void, Error> Http2Upstream::downstream_error(DownstreamConnection *dconn, int events) { auto downstream = dconn->get_downstream(); if (log_enabled(INFO)) { if (events & Downstream::EVENT_ERROR) { Log{INFO, dconn} << "Downstream network/general error"; } else { Log{INFO, dconn} << "Timeout"; } if (downstream->get_upgraded()) { Log{INFO, dconn} << "Note: this is tunnel connection"; } } // Delete downstream connection. If we don't delete it here, it will // be pooled in on_stream_close_callback. downstream->pop_downstream_connection(); // dconn was deleted dconn = nullptr; if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { // For SSL tunneling, we issue RST_STREAM. For other types of // stream, we don't have to do anything since response was // complete. if (downstream->get_upgraded()) { if (auto rv = rst_stream(downstream, NGHTTP2_NO_ERROR); !rv) { return rv; } } } else { if (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE) { if (downstream->get_upgraded()) { if (auto rv = on_downstream_body_complete(downstream); !rv) { return rv; } } else if (auto rv = rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); !rv) { return rv; } } else { unsigned int status; if (events & Downstream::EVENT_TIMEOUT) { if (downstream->get_request_header_sent()) { status = 504; } else { status = 408; } } else { status = 502; } if (auto rv = error_reply(downstream, status); !rv) { return rv; } } downstream->set_response_state(DownstreamState::MSG_COMPLETE); } handler_->signal_write(); // At this point, downstream may be deleted. return {}; } std::expected<void, Error> Http2Upstream::rst_stream(Downstream *downstream, uint32_t error_code) { if (log_enabled(INFO)) { Log{INFO, this} << "RST_STREAM stream_id=" << downstream->get_stream_id() << " with error_code=" << error_code; } int rv; rv = nghttp2_submit_rst_stream( session_, NGHTTP2_FLAG_NONE, static_cast<int32_t>(downstream->get_stream_id()), error_code); if (rv < NGHTTP2_ERR_FATAL) { Log{FATAL, this} << "nghttp2_submit_rst_stream() failed: " << nghttp2_strerror(rv); return std::unexpected{Error::HTTP2}; } return {}; } std::expected<void, Error> Http2Upstream::terminate_session(uint32_t error_code) { int rv; rv = nghttp2_session_terminate_session(session_, error_code); if (rv != 0) { return std::unexpected{Error::HTTP2}; } return {}; } namespace { nghttp2_ssize downstream_data_read_callback(nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length, uint32_t *data_flags, nghttp2_data_source *source, void *user_data) { int rv; auto downstream = static_cast<Downstream *>(source->ptr); auto body = downstream->get_response_buf(); assert(body); auto upstream = static_cast<Http2Upstream *>(user_data); const auto &resp = downstream->response(); auto nread = std::min(body->rleft(), length); auto max_buffer_size = upstream->get_max_buffer_size(); auto buffer = upstream->get_response_buf(); if (max_buffer_size < std::min(nread, 256UZ) + 9 + buffer->rleft()) { if (log_enabled(INFO)) { Log{INFO, upstream} << "Buffer is almost full. Skip write DATA"; } return NGHTTP2_ERR_PAUSE; } nread = std::min(nread, max_buffer_size - 9 - buffer->rleft()); auto body_empty = body->rleft() == nread; *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; if (body_empty && downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { *data_flags |= NGHTTP2_DATA_FLAG_EOF; if (!downstream->get_upgraded()) { const auto &trailers = resp.fs.trailers(); if (!trailers.empty()) { std::vector<nghttp2_nv> nva; nva.reserve(trailers.size()); http2::copy_headers_to_nva_nocopy(nva, trailers, http2::HDOP_STRIP_ALL); if (!nva.empty()) { rv = nghttp2_submit_trailer(session, stream_id, nva.data(), nva.size()); if (rv != 0) { if (nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } else { *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; } } } } } if (nread == 0 && ((*data_flags) & NGHTTP2_DATA_FLAG_EOF) == 0) { if (body->rleft() == 0) { downstream->disable_upstream_wtimer(); } return NGHTTP2_ERR_DEFERRED; } return as_signed(nread); } } // namespace std::expected<void, Error> Http2Upstream::send_reply(Downstream *downstream, std::span<const uint8_t> body) { int rv; nghttp2_data_provider2 data_prd, *data_prd_ptr = nullptr; const auto &req = downstream->request(); if (req.method != HTTP_HEAD && !body.empty()) { data_prd.source.ptr = downstream; data_prd.read_callback = downstream_data_read_callback; data_prd_ptr = &data_prd; auto buf = downstream->get_response_buf(); buf->append(body); } const auto &resp = downstream->response(); auto config = get_config(); auto &httpconf = config->http; auto &balloc = downstream->get_block_allocator(); const auto &headers = resp.fs.headers(); auto nva = std::vector<nghttp2_nv>(); // 2 for :status and server nva.reserve(2 + headers.size() + httpconf.add_response_headers.size()); auto response_status = http2::stringify_status(balloc, resp.http_status); nva.push_back(http2::make_field(":status"sv, response_status)); for (auto &kv : headers) { if (kv.name.empty() || kv.name[0] == ':') { continue; } switch (kv.token) { case http2::HD_CONNECTION: case http2::HD_KEEP_ALIVE: case http2::HD_PROXY_CONNECTION: case http2::HD_TE: case http2::HD_TRANSFER_ENCODING: case http2::HD_UPGRADE: continue; } nva.push_back( http2::make_field(kv.name, kv.value, http2::no_index(kv.no_index))); } if (!resp.fs.header(http2::HD_SERVER)) { nva.push_back(http2::make_field("server"sv, config->http.server_name)); } for (auto &p : httpconf.add_response_headers) { nva.push_back(http2::make_field(p.name, p.value)); } rv = nghttp2_submit_response2( session_, static_cast<int32_t>(downstream->get_stream_id()), nva.data(), nva.size(), data_prd_ptr); if (nghttp2_is_fatal(rv)) { Log{FATAL, this} << "nghttp2_submit_response2() failed: " << nghttp2_strerror(rv); return std::unexpected{Error::HTTP2}; } downstream->set_response_state(DownstreamState::MSG_COMPLETE); if (data_prd_ptr) { downstream->reset_upstream_wtimer(); downstream->register_upstream_write_rate_timer(); } return {}; } std::expected<void, Error> Http2Upstream::error_reply(Downstream *downstream, unsigned int status_code) { int rv; auto &resp = downstream->response(); auto &balloc = downstream->get_block_allocator(); auto html = http::create_error_html(balloc, status_code); resp.http_status = status_code; nghttp2_data_provider2 data_prd, *data_prd_ptr = nullptr; const auto &req = downstream->request(); if (req.method != HTTP_HEAD) { data_prd.source.ptr = downstream; data_prd.read_callback = downstream_data_read_callback; data_prd_ptr = &data_prd; auto body = downstream->get_response_buf(); body->append(html); } downstream->set_response_state(DownstreamState::MSG_COMPLETE); auto lgconf = log_config(); lgconf->update_tstamp(std::chrono::system_clock::now()); auto response_status = http2::stringify_status(balloc, status_code); auto content_length = util::make_string_ref_uint(balloc, html.size()); auto date = make_string_ref(balloc, lgconf->tstamp->time_http); auto nva = std::to_array( {http2::make_field(":status"sv, response_status), http2::make_field("content-type"sv, "text/html; charset=UTF-8"sv), http2::make_field("server"sv, get_config()->http.server_name), http2::make_field("content-length"sv, content_length), http2::make_field("date"sv, date)}); rv = nghttp2_submit_response2( session_, static_cast<int32_t>(downstream->get_stream_id()), nva.data(), nva.size(), data_prd_ptr); if (rv < NGHTTP2_ERR_FATAL) { Log{FATAL, this} << "nghttp2_submit_response2() failed: " << nghttp2_strerror(rv); return std::unexpected{Error::HTTP2}; } downstream->reset_upstream_wtimer(); downstream->register_upstream_write_rate_timer(); return {}; } void Http2Upstream::add_pending_downstream( std::unique_ptr<Downstream> downstream) { downstream_queue_.add_pending(std::move(downstream)); } std::expected<void, Error> Http2Upstream::remove_downstream(Downstream *downstream) { if (downstream->accesslog_ready()) { handler_->write_accesslog(downstream); } nghttp2_session_set_stream_user_data( session_, static_cast<int32_t>(downstream->get_stream_id()), nullptr); auto next_downstream = downstream_queue_.remove_and_get_blocked(downstream); if (next_downstream) { if (auto rv = initiate_downstream(next_downstream); !rv) { return rv; } } if (downstream_queue_.get_downstreams() == nullptr) { // There is no downstream at the moment. Start idle timer now. auto config = get_config(); auto &upstreamconf = config->conn.upstream; handler_->reset_upstream_read_timeout(upstreamconf.timeout.http2_idle); } return {}; } // WARNING: Never call directly or indirectly nghttp2_session_send or // nghttp2_session_recv. These calls may delete downstream. std::expected<void, Error> Http2Upstream::on_downstream_header_complete(Downstream *downstream) { int rv; const auto &req = downstream->request(); auto &resp = downstream->response(); auto &balloc = downstream->get_block_allocator(); if (log_enabled(INFO)) { if (downstream->get_non_final_response()) { Log{INFO, downstream} << "HTTP non-final response header"; } else { Log{INFO, downstream} << "HTTP response header completed"; } } auto config = get_config(); auto &httpconf = config->http; if (!config->http2_proxy && !httpconf.no_location_rewrite) { downstream->rewrite_location_response_header(req.scheme); } #ifdef HAVE_MRUBY if (!downstream->get_non_final_response()) { auto dconn = downstream->get_downstream_connection(); const auto &group = dconn->get_downstream_addr_group(); if (group) { const auto &dmruby_ctx = group->shared_addr->mruby_ctx; if (auto rv = dmruby_ctx->run_on_response_proc(downstream); !rv) { if (auto rv = error_reply(downstream, 500); !rv) { return rv; } // Returning an error will signal deletion of dconn. return rv; } if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { return std::unexpected{Error::INTERNAL}; } } auto worker = handler_->get_worker(); auto mruby_ctx = worker->get_mruby_context(); if (auto rv = mruby_ctx->run_on_response_proc(downstream); !rv) { if (auto rv = error_reply(downstream, 500); !rv) { return rv; } // Returning an error will signal deletion of dconn. return rv; } if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { return std::unexpected{Error::INTERNAL}; } } #endif // defined(HAVE_MRUBY) auto &http2conf = config->http2; // We need some conditions that must be fulfilled to initiate server // push. // // * Server push is disabled for http2 proxy or client proxy, since // incoming headers are mixed origins. We don't know how to // reliably determine the authority yet. // // * We need non-final response or 200 response code for associated // resource. This is too restrictive, we will review this later. // // * We requires GET or POST for associated resource. Probably we // don't want to push for HEAD request. Not sure other methods // are also eligible for push. if (!http2conf.no_server_push && nghttp2_session_get_remote_settings(session_, NGHTTP2_SETTINGS_ENABLE_PUSH) == 1 && !config->http2_proxy && (downstream->get_stream_id() % 2) && resp.fs.header(http2::HD_LINK) && (downstream->get_non_final_response() || resp.http_status == 200) && (req.method == HTTP_GET || req.method == HTTP_POST)) { if (!prepare_push_promise(downstream)) { // Continue to send response even if push was failed. } } auto nva = std::vector<nghttp2_nv>(); // 6 means :status and possible server, via, x-http2-push, alt-svc, // and set-cookie (for affinity cookie) header field. nva.reserve(resp.fs.headers().size() + 6 + httpconf.add_response_headers.size()); if (downstream->get_non_final_response()) { auto response_status = http2::stringify_status(balloc, resp.http_status); nva.push_back(http2::make_field(":status"sv, response_status)); http2::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), http2::HDOP_STRIP_ALL); if (log_enabled(INFO)) { log_response_headers(downstream, nva); } rv = nghttp2_submit_headers(session_, NGHTTP2_FLAG_NONE, static_cast<int32_t>(downstream->get_stream_id()), nullptr, nva.data(), nva.size(), nullptr); resp.fs.clear_headers(); if (rv != 0) { Log{FATAL, this} << "nghttp2_submit_headers() failed"; return std::unexpected{Error::HTTP2}; } return {}; } auto striphd_flags = static_cast<uint32_t>(http2::HDOP_STRIP_ALL & ~http2::HDOP_STRIP_VIA); std::string_view response_status; if (req.connect_proto == ConnectProto::WEBSOCKET && resp.http_status == 101) { response_status = http2::stringify_status(balloc, 200); striphd_flags |= http2::HDOP_STRIP_SEC_WEBSOCKET_ACCEPT; } else { response_status = http2::stringify_status(balloc, resp.http_status); } nva.push_back(http2::make_field(":status"sv, response_status)); http2::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), striphd_flags); if (!config->http2_proxy && !httpconf.no_server_rewrite) { nva.push_back(http2::make_field("server"sv, httpconf.server_name)); } else { auto server = resp.fs.header(http2::HD_SERVER); if (server) { nva.push_back(http2::make_field("server"sv, (*server).value)); } } if (!req.regular_connect_method() || !downstream->get_upgraded()) { auto affinity_cookie = downstream->get_affinity_cookie_to_send(); if (affinity_cookie) { auto dconn = downstream->get_downstream_connection(); assert(dconn); auto &group = dconn->get_downstream_addr_group(); auto &shared_addr = group->shared_addr; auto &cookieconf = shared_addr->affinity.cookie; auto secure = http::require_cookie_secure_attribute(cookieconf.secure, req.scheme); auto cookie_str = http::create_affinity_cookie( balloc, cookieconf.name, affinity_cookie, cookieconf.path, secure); nva.push_back(http2::make_field("set-cookie"sv, cookie_str)); } } if (!resp.fs.header(http2::HD_ALT_SVC)) { // We won't change or alter alt-svc from backend for now if (!httpconf.http2_altsvc_header_value.empty()) { nva.push_back( http2::make_field("alt-svc"sv, httpconf.http2_altsvc_header_value)); } } auto via = resp.fs.header(http2::HD_VIA); if (httpconf.no_via) { if (via) { nva.push_back(http2::make_field("via"sv, (*via).value)); } } else { // we don't create more than 16 bytes in // http::create_via_header_value. size_t len = 16; if (via) { len += via->value.size() + 2; } auto iov = make_byte_ref(balloc, len + 1); auto p = std::ranges::begin(iov); if (via) { p = std::ranges::copy(via->value, p).out; p = std::ranges::copy(", "sv, p).out; } p = http::create_via_header_value(p, resp.http_major, resp.http_minor); *p = '\0'; nva.push_back( http2::make_field("via"sv, as_string_view(std::ranges::begin(iov), p))); } for (auto &p : httpconf.add_response_headers) { nva.push_back(http2::make_field(p.name, p.value)); } if (downstream->get_stream_id() % 2 == 0) { // This header field is basically for human on client side to // figure out that the resource is pushed. nva.push_back(http2::make_field("x-http2-push"sv, "1"sv)); } if (log_enabled(INFO)) { log_response_headers(downstream, nva); } if (http2conf.upstream.debug.dump.response_header) { http2::dump_nv(http2conf.upstream.debug.dump.response_header, nva.data(), nva.size()); } auto priority = resp.fs.header(http2::HD_PRIORITY); if (priority) { nghttp2_extpri extpri; if (nghttp2_session_get_extpri_stream_priority( session_, &extpri, static_cast<int32_t>(downstream->get_stream_id())) == 0 && nghttp2_extpri_parse_priority( &extpri, reinterpret_cast<const uint8_t *>(priority->value.data()), priority->value.size()) == 0) { rv = nghttp2_session_change_extpri_stream_priority( session_, static_cast<int32_t>(downstream->get_stream_id()), &extpri, /* ignore_client_signal = */ 1); if (rv != 0) { Log{ERROR, this} << "nghttp2_session_change_extpri_stream_priority: " << nghttp2_strerror(rv); } } } nghttp2_data_provider2 data_prd; data_prd.source.ptr = downstream; data_prd.read_callback = downstream_data_read_callback; nghttp2_data_provider2 *data_prdptr; if (downstream->expect_response_body() || downstream->expect_response_trailer()) { data_prdptr = &data_prd; } else { data_prdptr = nullptr; } rv = nghttp2_submit_response2( session_, static_cast<int32_t>(downstream->get_stream_id()), nva.data(), nva.size(), data_prdptr); if (rv != 0) { Log{FATAL, this} << "nghttp2_submit_response2() failed"; return std::unexpected{Error::HTTP2}; } return {}; } // WARNING: Never call directly or indirectly nghttp2_session_send or // nghttp2_session_recv. These calls may delete downstream. std::expected<void, Error> Http2Upstream::on_downstream_body(Downstream *downstream, std::span<const uint8_t> data, bool flush) { auto body = downstream->get_response_buf(); body->append(data); if (flush) { nghttp2_session_resume_data( session_, static_cast<int32_t>(downstream->get_stream_id())); downstream->ensure_upstream_wtimer(); downstream->register_upstream_write_rate_timer(); } return {}; } // WARNING: Never call directly or indirectly nghttp2_session_send or // nghttp2_session_recv. These calls may delete downstream. std::expected<void, Error> Http2Upstream::on_downstream_body_complete(Downstream *downstream) { if (log_enabled(INFO)) { Log{INFO, downstream} << "HTTP response completed"; } auto &resp = downstream->response(); if (!downstream->validate_response_recv_body_length()) { if (auto rv = rst_stream(downstream, NGHTTP2_PROTOCOL_ERROR); !rv) { return rv; } resp.connection_close = true; return {}; } nghttp2_session_resume_data( session_, static_cast<int32_t>(downstream->get_stream_id())); downstream->ensure_upstream_wtimer(); downstream->register_upstream_write_rate_timer(); return {}; } bool Http2Upstream::get_flow_control() const { return flow_control_; } void Http2Upstream::pause_read(IOCtrlReason reason) {} std::expected<void, Error> Http2Upstream::resume_read(IOCtrlReason reason, Downstream *downstream, size_t consumed) { if (get_flow_control()) { if (auto rv = consume(static_cast<int32_t>(downstream->get_stream_id()), consumed); !rv) { return rv; } auto &req = downstream->request(); req.consume(consumed); } handler_->signal_write(); return {}; } std::expected<void, Error> Http2Upstream::on_downstream_abort_request(Downstream *downstream, unsigned int status_code) { if (auto rv = error_reply(downstream, status_code); !rv) { return rv; } handler_->signal_write(); return {}; } std::expected<void, Error> Http2Upstream::on_downstream_abort_request_with_https_redirect( Downstream *downstream) { if (auto rv = redirect_to_https(downstream); !rv) { return rv; } handler_->signal_write(); return {}; } std::expected<void, Error> Http2Upstream::redirect_to_https(Downstream *downstream) { auto &req = downstream->request(); if (req.regular_connect_method() || req.scheme != "http"sv) { return error_reply(downstream, 400); } auto maybe_authority = util::extract_host(req.authority); if (!maybe_authority) { return error_reply(downstream, 400); } auto &balloc = downstream->get_block_allocator(); auto config = get_config(); auto &httpconf = config->http; std::string_view loc; if (httpconf.redirect_https_port == "443"sv) { loc = concat_string_ref(balloc, "https://"sv, *maybe_authority, req.path); } else { loc = concat_string_ref(balloc, "https://"sv, *maybe_authority, ":"sv, httpconf.redirect_https_port, req.path); } auto &resp = downstream->response(); resp.http_status = 308; resp.fs.add_header_token("location"sv, loc, false, http2::HD_LOCATION); return send_reply(downstream, {}); } std::expected<void, Error> Http2Upstream::consume(int32_t stream_id, size_t len) { int rv; auto faddr = handler_->get_upstream_addr(); if (faddr->alt_mode != UpstreamAltMode::NONE) { return {}; } rv = nghttp2_session_consume(session_, stream_id, len); if (rv != 0) { Log{WARN, this} << "nghttp2_session_consume() returned error: " << nghttp2_strerror(rv); return std::unexpected{Error::HTTP2}; } return {}; } namespace { std::string format_nva(std::span<const nghttp2_nv> nva) { std::string s; for (auto &nv : nva) { s += tty_http_hd(); s += as_string_view(nv.name, nv.namelen); s += tty_rst(); s += ": "; s += as_string_view(nv.value, nv.valuelen); s += '\n'; } return s; } } // namespace void Http2Upstream::log_response_headers( Downstream *downstream, const std::vector<nghttp2_nv> &nva) const { Log{INFO, this} << "HTTP response headers. stream_id=" << downstream->get_stream_id() << "\n" << format_nva(nva); } std::expected<void, Error> Http2Upstream::on_timeout(Downstream *downstream) { if (log_enabled(INFO)) { Log{INFO, this} << "Stream timeout stream_id=" << downstream->get_stream_id(); } if (auto rv = rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); !rv) { return rv; } handler_->signal_write(); return {}; } void Http2Upstream::on_handler_delete() { for (auto d = downstream_queue_.get_downstreams(); d; d = d->dlnext) { if (d->get_dispatch_state() == DispatchState::ACTIVE && d->accesslog_ready()) { handler_->write_accesslog(d); } } } std::expected<void, Error> Http2Upstream::on_downstream_reset(Downstream *downstream, bool no_retry) { if (downstream->get_dispatch_state() != DispatchState::ACTIVE) { // This is error condition when we failed push_request_headers() // in initiate_downstream(). Otherwise, we have // DispatchState::ACTIVE state, or we did not set // DownstreamConnection. downstream->pop_downstream_connection(); handler_->signal_write(); return {}; } if (!downstream->request_submission_ready()) { if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { // We have got all response body already. Send it off. downstream->pop_downstream_connection(); return {}; } // pushed stream is handled here // Ignore the error otherwise we might delete ClientHandler twice. // See Http2Session. (void)rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); downstream->pop_downstream_connection(); handler_->signal_write(); return {}; } downstream->pop_downstream_connection(); downstream->add_retry(); std::unique_ptr<DownstreamConnection> dconn; auto err = Error::INTERNAL; if (no_retry || downstream->no_more_retry()) { goto fail; } // downstream connection is clean; we can retry with new // downstream connection. for (;;) { auto maybe_dconn = handler_->get_downstream_connection(downstream); if (!maybe_dconn) { err = maybe_dconn.error(); goto fail; } if (downstream->attach_downstream_connection(std::move(*maybe_dconn))) { break; } } if (auto rv = downstream->push_request_headers(); !rv) { err = rv.error(); goto fail; } return {}; fail: if (!(err == Error::TLS_REQUIRED ? on_downstream_abort_request_with_https_redirect(downstream) : on_downstream_abort_request(downstream, 502))) { // Ignore the error otherwise we might delete ClientHandler // twice. See Http2Session. (void)rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); } downstream->pop_downstream_connection(); handler_->signal_write(); return {}; } std::expected<void, Error> Http2Upstream::prepare_push_promise(Downstream *downstream) { const auto &req = downstream->request(); auto &resp = downstream->response(); auto base = http2::get_pure_path_component(req.path); if (base.empty()) { return {}; } auto &balloc = downstream->get_block_allocator(); for (auto &kv : resp.fs.headers()) { if (kv.token != http2::HD_LINK) { continue; } for (auto &link : http2::parse_link_header(kv.value)) { auto maybe_push_comp = http2::construct_push_component(balloc, base, link.uri); if (!maybe_push_comp) { continue; } auto push_comp = *maybe_push_comp; if (push_comp.scheme.empty()) { push_comp.scheme = req.scheme; } if (push_comp.authority.empty()) { push_comp.authority = req.authority; } if (resp.is_resource_pushed(push_comp.scheme, push_comp.authority, push_comp.path)) { continue; } if (auto rv = submit_push_promise(push_comp.scheme, push_comp.authority, push_comp.path, downstream); !rv) { return rv; } resp.resource_pushed(push_comp.scheme, push_comp.authority, push_comp.path); } } return {}; } std::expected<void, Error> Http2Upstream::submit_push_promise( std::string_view scheme, std::string_view authority, std::string_view path, Downstream *downstream) { const auto &req = downstream->request(); std::vector<nghttp2_nv> nva; // 4 for :method, :scheme, :path and :authority nva.reserve(4 + req.fs.headers().size()); // just use "GET" for now nva.push_back(http2::make_field(":method"sv, "GET"sv)); nva.push_back(http2::make_field(":scheme"sv, scheme)); nva.push_back(http2::make_field(":path"sv, path)); nva.push_back(http2::make_field(":authority"sv, authority)); for (auto &kv : req.fs.headers()) { switch (kv.token) { // TODO generate referer case http2::HD__AUTHORITY: case http2::HD__SCHEME: case http2::HD__METHOD: case http2::HD__PATH: continue; case http2::HD_ACCEPT_ENCODING: case http2::HD_ACCEPT_LANGUAGE: case http2::HD_CACHE_CONTROL: case http2::HD_HOST: case http2::HD_USER_AGENT: nva.push_back( http2::make_field(kv.name, kv.value, http2::no_index(kv.no_index))); break; } } auto promised_stream_id = nghttp2_submit_push_promise( session_, NGHTTP2_FLAG_NONE, static_cast<int32_t>(downstream->get_stream_id()), nva.data(), nva.size(), nullptr); if (promised_stream_id < 0) { if (log_enabled(INFO)) { Log{INFO, this} << "nghttp2_submit_push_promise() failed: " << nghttp2_strerror(promised_stream_id); } if (nghttp2_is_fatal(promised_stream_id)) { return std::unexpected{Error::HTTP2}; } return {}; } if (log_enabled(INFO)) { Log{INFO, this} << "HTTP push request headers. promised_stream_id=" << promised_stream_id << "\n" << format_nva(nva); } return {}; } bool Http2Upstream::push_enabled() const { auto config = get_config(); return !(config->http2.no_server_push || nghttp2_session_get_remote_settings( session_, NGHTTP2_SETTINGS_ENABLE_PUSH) == 0 || config->http2_proxy); } std::expected<void, Error> Http2Upstream::initiate_push(Downstream *downstream, std::string_view uri) { if (uri.empty() || !push_enabled() || (downstream->get_stream_id() % 2) == 0) { return {}; } const auto &req = downstream->request(); auto base = http2::get_pure_path_component(req.path); if (base.empty()) { return std::unexpected{Error::INTERNAL}; } auto &balloc = downstream->get_block_allocator(); auto maybe_push_comp = http2::construct_push_component(balloc, base, uri); if (!maybe_push_comp) { return std::unexpected{maybe_push_comp.error()}; } auto push_comp = *maybe_push_comp; if (push_comp.scheme.empty()) { push_comp.scheme = req.scheme; } if (push_comp.authority.empty()) { push_comp.authority = req.authority; } auto &resp = downstream->response(); if (resp.is_resource_pushed(push_comp.scheme, push_comp.authority, push_comp.path)) { return {}; } if (auto rv = submit_push_promise(push_comp.scheme, push_comp.authority, push_comp.path, downstream); !rv) { return rv; } resp.resource_pushed(push_comp.scheme, push_comp.authority, push_comp.path); return {}; } std::span<struct iovec> Http2Upstream::response_riovec(std::span<struct iovec> iov) const { return wb_.riovec(iov); } std::span<const uint8_t> Http2Upstream::response_peek() const { return wb_.peek(); } void Http2Upstream::response_drain(size_t n) { wb_.drain(n); } bool Http2Upstream::response_empty() const { return wb_.rleft() == 0; } DefaultMemchunks *Http2Upstream::get_response_buf() { return &wb_; } Downstream * Http2Upstream::on_downstream_push_promise(Downstream *downstream, int32_t promised_stream_id) { // promised_stream_id is for backend HTTP/2 session, not for // frontend. auto promised_downstream = std::make_unique<Downstream>(this, handler_->get_mcpool(), 0); auto &promised_req = promised_downstream->request(); promised_downstream->set_downstream_stream_id(promised_stream_id); // Set associated stream in frontend promised_downstream->set_assoc_stream_id(downstream->get_stream_id()); promised_downstream->disable_upstream_rtimer(); promised_req.http_major = 2; promised_req.http_minor = 0; promised_req.fs.content_length = 0; promised_req.http2_expect_body = false; auto ptr = promised_downstream.get(); add_pending_downstream(std::move(promised_downstream)); downstream_queue_.mark_active(ptr); return ptr; } std::expected<void, Error> Http2Upstream::on_downstream_push_promise_complete( Downstream *downstream, Downstream *promised_downstream) { std::vector<nghttp2_nv> nva; const auto &promised_req = promised_downstream->request(); const auto &headers = promised_req.fs.headers(); nva.reserve(headers.size()); for (auto &kv : headers) { nva.push_back( http2::make_field_nv(kv.name, kv.value, http2::no_index(kv.no_index))); } auto promised_stream_id = nghttp2_submit_push_promise( session_, NGHTTP2_FLAG_NONE, static_cast<int32_t>(downstream->get_stream_id()), nva.data(), nva.size(), promised_downstream); if (promised_stream_id < 0) { return std::unexpected{Error::HTTP2}; } promised_downstream->set_stream_id(promised_stream_id); return {}; } void Http2Upstream::cancel_premature_downstream( Downstream *promised_downstream) { if (log_enabled(INFO)) { Log{INFO, this} << "Remove premature promised stream " << promised_downstream; } downstream_queue_.remove_and_get_blocked(promised_downstream, false); } size_t Http2Upstream::get_max_buffer_size() const { return max_buffer_size_; } } // namespace shrpx