/* * 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_https_upstream.h" #include #include #include #include "shrpx_client_handler.h" #include "shrpx_downstream.h" #include "shrpx_downstream_connection.h" #include "shrpx_http.h" #include "shrpx_config.h" #include "shrpx_error.h" #include "shrpx_log_config.h" #include "shrpx_worker.h" #include "shrpx_http2_session.h" #include "shrpx_log.h" #ifdef HAVE_MRUBY # include "shrpx_mruby.h" #endif // HAVE_MRUBY #include "http2.h" #include "util.h" #include "template.h" using namespace nghttp2; namespace shrpx { HttpsUpstream::HttpsUpstream(ClientHandler *handler) : handler_(handler), current_header_length_(0), ioctrl_(handler->get_rlimit()), num_requests_(0) { http_parser_init(&htp_, HTTP_REQUEST); htp_.data = this; } HttpsUpstream::~HttpsUpstream() {} void HttpsUpstream::reset_current_header_length() { current_header_length_ = 0; } void HttpsUpstream::on_start_request() { if (LOG_ENABLED(INFO)) { ULOG(INFO, this) << "HTTP request started"; } reset_current_header_length(); auto downstream = make_unique(this, handler_->get_mcpool(), 0); attach_downstream(std::move(downstream)); auto conn = handler_->get_connection(); auto &upstreamconf = get_config()->conn.upstream; conn->rt.repeat = upstreamconf.timeout.read; handler_->repeat_read_timer(); ++num_requests_; } namespace { int htp_msg_begin(http_parser *htp) { auto upstream = static_cast(htp->data); upstream->on_start_request(); return 0; } } // namespace namespace { int htp_uricb(http_parser *htp, const char *data, size_t len) { auto upstream = static_cast(htp->data); auto downstream = upstream->get_downstream(); auto &req = downstream->request(); auto &balloc = downstream->get_block_allocator(); // We happen to have the same value for method token. req.method = htp->method; if (req.fs.buffer_size() + len > get_config()->http.request_header_field_buffer) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "Too large URI size=" << req.fs.buffer_size() + len; } assert(downstream->get_request_state() == Downstream::INITIAL); downstream->set_request_state(Downstream::HTTP1_REQUEST_HEADER_TOO_LARGE); return -1; } req.fs.add_extra_buffer_size(len); if (req.method == HTTP_CONNECT) { req.authority = concat_string_ref(balloc, req.authority, StringRef{data, len}); } else { req.path = concat_string_ref(balloc, req.path, StringRef{data, len}); } return 0; } } // namespace namespace { int htp_hdr_keycb(http_parser *htp, const char *data, size_t len) { auto upstream = static_cast(htp->data); auto downstream = upstream->get_downstream(); auto &req = downstream->request(); auto &httpconf = get_config()->http; if (req.fs.buffer_size() + len > httpconf.request_header_field_buffer) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "Too large header block size=" << req.fs.buffer_size() + len; } if (downstream->get_request_state() == Downstream::INITIAL) { downstream->set_request_state(Downstream::HTTP1_REQUEST_HEADER_TOO_LARGE); } return -1; } if (downstream->get_request_state() == Downstream::INITIAL) { if (req.fs.header_key_prev()) { req.fs.append_last_header_key(data, len); } else { if (req.fs.num_fields() >= httpconf.max_request_header_fields) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "Too many header field num=" << req.fs.num_fields() + 1; } downstream->set_request_state( Downstream::HTTP1_REQUEST_HEADER_TOO_LARGE); return -1; } req.fs.alloc_add_header_name(StringRef{data, len}); } } else { // trailer part if (req.fs.trailer_key_prev()) { req.fs.append_last_trailer_key(data, len); } else { if (req.fs.num_fields() >= httpconf.max_request_header_fields) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "Too many header field num=" << req.fs.num_fields() + 1; } return -1; } req.fs.alloc_add_trailer_name(StringRef{data, len}); } } return 0; } } // namespace namespace { int htp_hdr_valcb(http_parser *htp, const char *data, size_t len) { auto upstream = static_cast(htp->data); auto downstream = upstream->get_downstream(); auto &req = downstream->request(); if (req.fs.buffer_size() + len > get_config()->http.request_header_field_buffer) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "Too large header block size=" << req.fs.buffer_size() + len; } if (downstream->get_request_state() == Downstream::INITIAL) { downstream->set_request_state(Downstream::HTTP1_REQUEST_HEADER_TOO_LARGE); } return -1; } if (downstream->get_request_state() == Downstream::INITIAL) { req.fs.append_last_header_value(data, len); } else { req.fs.append_last_trailer_value(data, len); } return 0; } } // namespace namespace { void rewrite_request_host_path_from_uri(BlockAllocator &balloc, Request &req, const StringRef &uri, http_parser_url &u) { assert(u.field_set & (1 << UF_HOST)); // As per https://tools.ietf.org/html/rfc7230#section-5.4, we // rewrite host header field with authority component. auto authority = util::get_uri_field(uri.c_str(), u, UF_HOST); // TODO properly check IPv6 numeric address auto ipv6 = std::find(std::begin(authority), std::end(authority), ':') != std::end(authority); auto authoritylen = authority.size(); if (ipv6) { authoritylen += 2; } if (u.field_set & (1 << UF_PORT)) { authoritylen += 1 + str_size("65535"); } if (authoritylen > authority.size()) { auto iovec = make_byte_ref(balloc, authoritylen + 1); auto p = iovec.base; if (ipv6) { *p++ = '['; } p = std::copy(std::begin(authority), std::end(authority), p); if (ipv6) { *p++ = ']'; } if (u.field_set & (1 << UF_PORT)) { *p++ = ':'; p = util::utos(p, u.port); } *p = '\0'; req.authority = StringRef{iovec.base, p}; } else { req.authority = authority; } req.scheme = util::get_uri_field(uri.c_str(), u, UF_SCHEMA); StringRef path; if (u.field_set & (1 << UF_PATH)) { path = util::get_uri_field(uri.c_str(), u, UF_PATH); } else if (req.method == HTTP_OPTIONS) { // Server-wide OPTIONS takes following form in proxy request: // // OPTIONS http://example.org HTTP/1.1 // // Notice that no slash after authority. See // http://tools.ietf.org/html/rfc7230#section-5.3.4 req.path = StringRef::from_lit(""); // we ignore query component here return; } else { path = StringRef::from_lit("/"); } if (u.field_set & (1 << UF_QUERY)) { auto &fdata = u.field_data[UF_QUERY]; if (u.field_set & (1 << UF_PATH)) { auto q = util::get_uri_field(uri.c_str(), u, UF_QUERY); path = StringRef{std::begin(path), std::end(q)}; } else { path = concat_string_ref(balloc, path, StringRef::from_lit("?"), StringRef{&uri[fdata.off], fdata.len}); } } req.path = http2::rewrite_clean_path(balloc, path); } } // namespace namespace { int htp_hdrs_completecb(http_parser *htp) { int rv; auto upstream = static_cast(htp->data); if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "HTTP request headers completed"; } auto handler = upstream->get_client_handler(); auto downstream = upstream->get_downstream(); auto &req = downstream->request(); auto lgconf = log_config(); lgconf->update_tstamp(std::chrono::system_clock::now()); req.tstamp = lgconf->tstamp; req.http_major = htp->http_major; req.http_minor = htp->http_minor; req.connection_close = !http_should_keep_alive(htp); handler->stop_read_timer(); auto method = req.method; if (LOG_ENABLED(INFO)) { std::stringstream ss; ss << http2::to_method_string(method) << " " << (method == HTTP_CONNECT ? req.authority : req.path) << " " << "HTTP/" << req.http_major << "." << req.http_minor << "\n"; for (const auto &kv : req.fs.headers()) { ss << TTY_HTTP_HD << kv.name << TTY_RST << ": " << kv.value << "\n"; } ULOG(INFO, upstream) << "HTTP request headers\n" << ss.str(); } // set content-length if method is not CONNECT, and no // transfer-encoding is given. If transfer-encoding is given, leave // req.fs.content_length to -1. if (method != HTTP_CONNECT && !req.fs.header(http2::HD_TRANSFER_ENCODING)) { // http-parser returns (uint64_t)-1 if there is no content-length // header field. If we don't have both transfer-encoding, and // content-length header field, we assume that there is no request // body. if (htp->content_length == std::numeric_limits::max()) { req.fs.content_length = 0; } else { req.fs.content_length = htp->content_length; } } auto host = req.fs.header(http2::HD_HOST); if (req.http_major > 1 || req.http_minor > 1) { req.http_major = 1; req.http_minor = 1; return -1; } if (req.http_major == 1 && req.http_minor == 1 && !host) { return -1; } if (host) { const auto &value = host->value; // Not allow at least '"' or '\' in host. They are illegal in // authority component, also they cause headaches when we put them // in quoted-string. if (std::find_if(std::begin(value), std::end(value), [](char c) { return c == '"' || c == '\\'; }) != std::end(value)) { return -1; } } downstream->inspect_http1_request(); auto faddr = handler->get_upstream_addr(); auto &balloc = downstream->get_block_allocator(); auto config = get_config(); if (method != HTTP_CONNECT) { http_parser_url u{}; rv = http_parser_parse_url(req.path.c_str(), req.path.size(), 0, &u); if (rv != 0) { // Expect to respond with 400 bad request return -1; } // checking UF_HOST could be redundant, but just in case ... if (!(u.field_set & (1 << UF_SCHEMA)) || !(u.field_set & (1 << UF_HOST))) { req.no_authority = true; if (method == HTTP_OPTIONS && req.path == StringRef::from_lit("*")) { req.path = StringRef{}; } else { req.path = http2::rewrite_clean_path(balloc, req.path); } if (host) { req.authority = host->value; } if (handler->get_ssl()) { req.scheme = StringRef::from_lit("https"); } else { req.scheme = StringRef::from_lit("http"); } } else { rewrite_request_host_path_from_uri(balloc, req, req.path, u); } } downstream->set_request_state(Downstream::HEADER_COMPLETE); #ifdef HAVE_MRUBY auto worker = handler->get_worker(); auto mruby_ctx = worker->get_mruby_context(); auto &resp = downstream->response(); if (mruby_ctx->run_on_request_proc(downstream) != 0) { resp.http_status = 500; return -1; } #endif // HAVE_MRUBY // mruby hook may change method value if (req.no_authority && config->http2_proxy && !faddr->alt_mode) { // Request URI should be absolute-form for client proxy mode return -1; } if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { return 0; } auto dconn = handler->get_downstream_connection(rv, downstream); if (!dconn) { if (rv == SHRPX_ERR_TLS_REQUIRED) { upstream->redirect_to_https(downstream); } downstream->set_request_state(Downstream::CONNECT_FAIL); return -1; } #ifdef HAVE_MRUBY auto dconn_ptr = dconn.get(); #endif // HAVE_MRUBY if (downstream->attach_downstream_connection(std::move(dconn)) != 0) { downstream->set_request_state(Downstream::CONNECT_FAIL); return -1; } #ifdef HAVE_MRUBY const auto &group = dconn_ptr->get_downstream_addr_group(); if (group) { const auto &dmruby_ctx = group->mruby_ctx; if (dmruby_ctx->run_on_request_proc(downstream) != 0) { resp.http_status = 500; return -1; } if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { return 0; } } #endif // HAVE_MRUBY rv = downstream->push_request_headers(); if (rv != 0) { return -1; } if (faddr->alt_mode) { // Normally, we forward expect: 100-continue to backend server, // and let them decide whether responds with 100 Continue or not. // For alternative mode, we have no backend, so just send 100 // Continue here to make the client happy. auto expect = req.fs.header(http2::HD_EXPECT); if (expect && util::strieq(expect->value, StringRef::from_lit("100-continue"))) { auto output = downstream->get_response_buf(); constexpr auto res = StringRef::from_lit("HTTP/1.1 100 Continue\r\n\r\n"); output->append(res); handler->signal_write(); } } return 0; } } // namespace namespace { int htp_bodycb(http_parser *htp, const char *data, size_t len) { int rv; auto upstream = static_cast(htp->data); auto downstream = upstream->get_downstream(); rv = downstream->push_upload_data_chunk( reinterpret_cast(data), len); if (rv != 0) { // Ignore error if response has been completed. We will end up in // htp_msg_completecb, and request will end gracefully. if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { return 0; } return -1; } return 0; } } // namespace namespace { int htp_msg_completecb(http_parser *htp) { int rv; auto upstream = static_cast(htp->data); if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "HTTP request completed"; } auto handler = upstream->get_client_handler(); auto downstream = upstream->get_downstream(); downstream->set_request_state(Downstream::MSG_COMPLETE); rv = downstream->end_upload_data(); if (rv != 0) { if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { // Here both response and request were completed. One of the // reason why end_upload_data() failed is when we sent response // in request phase hook. We only delete and proceed to the // next request handling (if we don't close the connection). We // first pause parser here just as we normally do, and call // signal_write() to run on_write(). http_parser_pause(htp, 1); return 0; } return -1; } if (handler->get_http2_upgrade_allowed() && downstream->get_http2_upgrade_request() && handler->perform_http2_upgrade(upstream) != 0) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "HTTP Upgrade to HTTP/2 failed"; } } // Stop further processing to complete this request http_parser_pause(htp, 1); return 0; } } // namespace namespace { constexpr http_parser_settings htp_hooks = { htp_msg_begin, // http_cb on_message_begin; htp_uricb, // http_data_cb on_url; nullptr, // http_data_cb on_status; htp_hdr_keycb, // http_data_cb on_header_field; htp_hdr_valcb, // http_data_cb on_header_value; htp_hdrs_completecb, // http_cb on_headers_complete; htp_bodycb, // http_data_cb on_body; htp_msg_completecb // http_cb on_message_complete; }; } // namespace // on_read() does not consume all available data in input buffer if // one http request is fully received. int HttpsUpstream::on_read() { auto rb = handler_->get_rb(); auto rlimit = handler_->get_rlimit(); auto downstream = get_downstream(); if (rb->rleft() == 0 || handler_->get_should_close_after_write()) { return 0; } // downstream can be nullptr here, because it is initialized in the // callback chain called by http_parser_execute() if (downstream && downstream->get_upgraded()) { auto rv = downstream->push_upload_data_chunk(rb->pos(), rb->rleft()); if (rv != 0) { return -1; } rb->reset(); rlimit->startw(); if (downstream->request_buf_full()) { if (LOG_ENABLED(INFO)) { ULOG(INFO, this) << "Downstream request buf is full"; } pause_read(SHRPX_NO_BUFFER); return 0; } return 0; } if (downstream) { // To avoid reading next pipelined request switch (downstream->get_request_state()) { case Downstream::INITIAL: case Downstream::HEADER_COMPLETE: break; default: return 0; } } // http_parser_execute() does nothing once it entered error state. auto nread = http_parser_execute(&htp_, &htp_hooks, reinterpret_cast(rb->pos()), rb->rleft()); rb->drain(nread); rlimit->startw(); // Well, actually header length + some body bytes current_header_length_ += nread; // Get downstream again because it may be initialized in http parser // execution downstream = get_downstream(); auto htperr = HTTP_PARSER_ERRNO(&htp_); if (htperr == HPE_PAUSED) { // We may pause parser in htp_msg_completecb when both side are // completed. Signal write, so that we can run on_write(). if (downstream && downstream->get_request_state() == Downstream::MSG_COMPLETE && downstream->get_response_state() == Downstream::MSG_COMPLETE) { handler_->signal_write(); } return 0; } if (htperr != HPE_OK) { if (LOG_ENABLED(INFO)) { ULOG(INFO, this) << "HTTP parse failure: " << "(" << http_errno_name(htperr) << ") " << http_errno_description(htperr); } if (downstream && downstream->get_response_state() != Downstream::INITIAL) { handler_->set_should_close_after_write(true); handler_->signal_write(); return 0; } unsigned int status_code; if (htperr == HPE_INVALID_METHOD) { status_code = 501; } else if (downstream) { status_code = downstream->response().http_status; if (status_code == 0) { if (downstream->get_request_state() == Downstream::CONNECT_FAIL) { status_code = 502; } else if (downstream->get_request_state() == Downstream::HTTP1_REQUEST_HEADER_TOO_LARGE) { status_code = 431; } else { status_code = 400; } } } else { status_code = 400; } error_reply(status_code); handler_->signal_write(); return 0; } // downstream can be NULL here. if (downstream && downstream->request_buf_full()) { if (LOG_ENABLED(INFO)) { ULOG(INFO, this) << "Downstream request buffer is full"; } pause_read(SHRPX_NO_BUFFER); return 0; } return 0; } int HttpsUpstream::on_write() { auto downstream = get_downstream(); if (!downstream) { return 0; } auto output = downstream->get_response_buf(); const auto &resp = downstream->response(); if (output->rleft() > 0) { return 0; } // We need to postpone detachment until all data are sent so that // we can notify nghttp2 library all data consumed. if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { if (downstream->can_detach_downstream_connection()) { // Keep-alive downstream->detach_downstream_connection(); } else { // Connection close downstream->pop_downstream_connection(); // dconn was deleted } // We need this if response ends before request. if (downstream->get_request_state() == Downstream::MSG_COMPLETE) { delete_downstream(); if (handler_->get_should_close_after_write()) { return 0; } auto conn = handler_->get_connection(); auto &upstreamconf = get_config()->conn.upstream; conn->rt.repeat = upstreamconf.timeout.idle_read; handler_->repeat_read_timer(); return resume_read(SHRPX_NO_BUFFER, nullptr, 0); } } return downstream->resume_read(SHRPX_NO_BUFFER, resp.unconsumed_body_length); } int HttpsUpstream::on_event() { return 0; } ClientHandler *HttpsUpstream::get_client_handler() const { return handler_; } void HttpsUpstream::pause_read(IOCtrlReason reason) { ioctrl_.pause_read(reason); } int HttpsUpstream::resume_read(IOCtrlReason reason, Downstream *downstream, size_t consumed) { // downstream could be nullptr if (downstream && downstream->request_buf_full()) { return 0; } if (ioctrl_.resume_read(reason)) { // Process remaining data in input buffer here because these bytes // are not notified by readcb until new data arrive. http_parser_pause(&htp_, 0); auto conn = handler_->get_connection(); ev_feed_event(conn->loop, &conn->rev, EV_READ); return 0; } return 0; } int HttpsUpstream::downstream_read(DownstreamConnection *dconn) { auto downstream = dconn->get_downstream(); int rv; rv = downstream->on_read(); if (rv == SHRPX_ERR_EOF) { return downstream_eof(dconn); } if (rv == SHRPX_ERR_DCONN_CANCELED) { downstream->pop_downstream_connection(); goto end; } if (rv < 0) { return downstream_error(dconn, Downstream::EVENT_ERROR); } if (downstream->get_response_state() == Downstream::MSG_RESET) { return -1; } if (downstream->get_response_state() == Downstream::MSG_BAD_HEADER) { error_reply(502); downstream->pop_downstream_connection(); goto end; } if (downstream->can_detach_downstream_connection()) { // Keep-alive downstream->detach_downstream_connection(); } end: handler_->signal_write(); return 0; } int HttpsUpstream::downstream_write(DownstreamConnection *dconn) { int rv; rv = dconn->on_write(); if (rv == SHRPX_ERR_NETWORK) { return downstream_error(dconn, Downstream::EVENT_ERROR); } if (rv != 0) { return rv; } return 0; } int HttpsUpstream::downstream_eof(DownstreamConnection *dconn) { auto downstream = dconn->get_downstream(); if (LOG_ENABLED(INFO)) { DCLOG(INFO, dconn) << "EOF"; } if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { goto end; } if (downstream->get_response_state() == Downstream::HEADER_COMPLETE) { // Server may indicate the end of the request by EOF if (LOG_ENABLED(INFO)) { DCLOG(INFO, dconn) << "The end of the response body was indicated by " << "EOF"; } on_downstream_body_complete(downstream); downstream->set_response_state(Downstream::MSG_COMPLETE); downstream->pop_downstream_connection(); goto end; } if (downstream->get_response_state() == Downstream::INITIAL) { // we did not send any response headers, so we can reply error // message. if (LOG_ENABLED(INFO)) { DCLOG(INFO, dconn) << "Return error reply"; } error_reply(502); downstream->pop_downstream_connection(); goto end; } // Otherwise, we don't know how to recover from this situation. Just // drop connection. return -1; end: handler_->signal_write(); return 0; } int HttpsUpstream::downstream_error(DownstreamConnection *dconn, int events) { auto downstream = dconn->get_downstream(); if (LOG_ENABLED(INFO)) { if (events & Downstream::EVENT_ERROR) { DCLOG(INFO, dconn) << "Network error/general error"; } else { DCLOG(INFO, dconn) << "Timeout"; } } if (downstream->get_response_state() != Downstream::INITIAL) { return -1; } unsigned int status; if (events & Downstream::EVENT_TIMEOUT) { status = 504; } else { status = 502; } error_reply(status); downstream->pop_downstream_connection(); handler_->signal_write(); return 0; } int HttpsUpstream::send_reply(Downstream *downstream, const uint8_t *body, size_t bodylen) { const auto &req = downstream->request(); auto &resp = downstream->response(); auto &balloc = downstream->get_block_allocator(); auto config = get_config(); auto &httpconf = config->http; auto connection_close = false; auto worker = handler_->get_worker(); if (httpconf.max_requests <= num_requests_ || worker->get_graceful_shutdown()) { resp.fs.add_header_token(StringRef::from_lit("connection"), StringRef::from_lit("close"), false, http2::HD_CONNECTION); connection_close = true; } else if (req.http_major <= 0 || (req.http_major == 1 && req.http_minor == 0)) { connection_close = true; } else { auto c = resp.fs.header(http2::HD_CONNECTION); if (c && util::strieq_l("close", c->value)) { connection_close = true; } } if (connection_close) { resp.connection_close = true; handler_->set_should_close_after_write(true); } auto output = downstream->get_response_buf(); output->append("HTTP/1.1 "); output->append(http2::stringify_status(balloc, resp.http_status)); output->append(' '); output->append(http2::get_reason_phrase(resp.http_status)); output->append("\r\n"); for (auto &kv : resp.fs.headers()) { if (kv.name.empty() || kv.name[0] == ':') { continue; } http2::capitalize(output, kv.name); output->append(": "); output->append(kv.value); output->append("\r\n"); } if (!resp.fs.header(http2::HD_SERVER)) { output->append("Server: "); output->append(config->http.server_name); output->append("\r\n"); } for (auto &p : httpconf.add_response_headers) { output->append(p.name); output->append(": "); output->append(p.value); output->append("\r\n"); } output->append("\r\n"); output->append(body, bodylen); downstream->response_sent_body_length += bodylen; downstream->set_response_state(Downstream::MSG_COMPLETE); return 0; } void HttpsUpstream::error_reply(unsigned int status_code) { auto downstream = get_downstream(); if (!downstream) { attach_downstream(make_unique(this, handler_->get_mcpool(), 1)); downstream = get_downstream(); } auto &resp = downstream->response(); auto &balloc = downstream->get_block_allocator(); auto html = http::create_error_html(balloc, status_code); resp.http_status = status_code; // we are going to close connection for both frontend and backend in // error condition. This is safest option. resp.connection_close = true; handler_->set_should_close_after_write(true); auto output = downstream->get_response_buf(); output->append("HTTP/1.1 "); output->append(http2::stringify_status(balloc, status_code)); output->append(' '); output->append(http2::get_reason_phrase(status_code)); output->append("\r\nServer: "); output->append(get_config()->http.server_name); output->append("\r\nContent-Length: "); std::array intbuf; output->append(StringRef{std::begin(intbuf), util::utos(std::begin(intbuf), html.size())}); output->append("\r\nDate: "); auto lgconf = log_config(); lgconf->update_tstamp(std::chrono::system_clock::now()); output->append(lgconf->tstamp->time_http); output->append("\r\nContent-Type: text/html; " "charset=UTF-8\r\nConnection: close\r\n\r\n"); output->append(html); downstream->response_sent_body_length += html.size(); downstream->set_response_state(Downstream::MSG_COMPLETE); } void HttpsUpstream::attach_downstream(std::unique_ptr downstream) { assert(!downstream_); downstream_ = std::move(downstream); } void HttpsUpstream::delete_downstream() { if (downstream_ && downstream_->accesslog_ready()) { handler_->write_accesslog(downstream_.get()); } downstream_.reset(); } Downstream *HttpsUpstream::get_downstream() const { return downstream_.get(); } std::unique_ptr HttpsUpstream::pop_downstream() { return std::unique_ptr(downstream_.release()); } namespace { void write_altsvc(DefaultMemchunks *buf, BlockAllocator &balloc, const AltSvc &altsvc) { buf->append(util::percent_encode_token(balloc, altsvc.protocol_id)); buf->append("=\""); buf->append(util::quote_string(balloc, altsvc.host)); buf->append(':'); buf->append(altsvc.service); buf->append('"'); } } // namespace int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) { if (LOG_ENABLED(INFO)) { if (downstream->get_non_final_response()) { DLOG(INFO, downstream) << "HTTP non-final response header"; } else { DLOG(INFO, downstream) << "HTTP response header completed"; } } const auto &req = downstream->request(); auto &resp = downstream->response(); auto &balloc = downstream->get_block_allocator(); auto dconn = downstream->get_downstream_connection(); assert(dconn); if (downstream->get_non_final_response() && !downstream->supports_non_final_response()) { resp.fs.clear_headers(); return 0; } #ifdef HAVE_MRUBY if (!downstream->get_non_final_response()) { const auto &group = dconn->get_downstream_addr_group(); if (group) { const auto &dmruby_ctx = group->mruby_ctx; if (dmruby_ctx->run_on_response_proc(downstream) != 0) { error_reply(500); return -1; } if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { return -1; } } auto worker = handler_->get_worker(); auto mruby_ctx = worker->get_mruby_context(); if (mruby_ctx->run_on_response_proc(downstream) != 0) { error_reply(500); return -1; } if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { return -1; } } #endif // HAVE_MRUBY auto connect_method = req.method == HTTP_CONNECT; auto buf = downstream->get_response_buf(); buf->append("HTTP/"); buf->append('0' + req.http_major); buf->append('.'); buf->append('0' + req.http_minor); buf->append(' '); buf->append(http2::stringify_status(balloc, resp.http_status)); buf->append(' '); buf->append(http2::get_reason_phrase(resp.http_status)); buf->append("\r\n"); auto config = get_config(); auto &httpconf = config->http; if (!config->http2_proxy && !httpconf.no_location_rewrite) { downstream->rewrite_location_response_header( get_client_handler()->get_upstream_scheme()); } if (downstream->get_non_final_response()) { http2::build_http1_headers_from_headers(buf, resp.fs.headers(), http2::HDOP_STRIP_ALL); buf->append("\r\n"); if (LOG_ENABLED(INFO)) { log_response_headers(buf); } resp.fs.clear_headers(); return 0; } http2::build_http1_headers_from_headers( buf, resp.fs.headers(), http2::HDOP_STRIP_ALL & ~http2::HDOP_STRIP_VIA); auto worker = handler_->get_worker(); // after graceful shutdown commenced, add connection: close header // field. if (httpconf.max_requests <= num_requests_ || worker->get_graceful_shutdown()) { resp.connection_close = true; } // We check downstream->get_response_connection_close() in case when // the Content-Length is not available. if (!req.connection_close && !resp.connection_close) { if (req.http_major <= 0 || req.http_minor <= 0) { // We add this header for HTTP/1.0 or HTTP/0.9 clients buf->append("Connection: Keep-Alive\r\n"); } } else if (!downstream->get_upgraded()) { buf->append("Connection: close\r\n"); } if (!connect_method && downstream->get_upgraded()) { auto connection = resp.fs.header(http2::HD_CONNECTION); if (connection) { buf->append("Connection: "); buf->append((*connection).value); buf->append("\r\n"); } auto upgrade = resp.fs.header(http2::HD_UPGRADE); if (upgrade) { buf->append("Upgrade: "); buf->append((*upgrade).value); buf->append("\r\n"); } } if (!resp.fs.header(http2::HD_ALT_SVC)) { // We won't change or alter alt-svc from backend for now if (!httpconf.altsvcs.empty()) { buf->append("Alt-Svc: "); auto &altsvcs = httpconf.altsvcs; write_altsvc(buf, downstream->get_block_allocator(), altsvcs[0]); for (size_t i = 1; i < altsvcs.size(); ++i) { buf->append(", "); write_altsvc(buf, downstream->get_block_allocator(), altsvcs[i]); } buf->append("\r\n"); } } if (!config->http2_proxy && !httpconf.no_server_rewrite) { buf->append("Server: "); buf->append(httpconf.server_name); buf->append("\r\n"); } else { auto server = resp.fs.header(http2::HD_SERVER); if (server) { buf->append("Server: "); buf->append((*server).value); buf->append("\r\n"); } } if (req.method != HTTP_CONNECT || !downstream->get_upgraded()) { auto affinity_cookie = downstream->get_affinity_cookie_to_send(); if (affinity_cookie) { 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); buf->append("Set-Cookie: "); buf->append(cookie_str); buf->append("\r\n"); } } auto via = resp.fs.header(http2::HD_VIA); if (httpconf.no_via) { if (via) { buf->append("Via: "); buf->append((*via).value); buf->append("\r\n"); } } else { buf->append("Via: "); if (via) { buf->append((*via).value); buf->append(", "); } std::array viabuf; auto end = http::create_via_header_value(viabuf.data(), resp.http_major, resp.http_minor); buf->append(viabuf.data(), end - std::begin(viabuf)); buf->append("\r\n"); } for (auto &p : httpconf.add_response_headers) { buf->append(p.name); buf->append(": "); buf->append(p.value); buf->append("\r\n"); } buf->append("\r\n"); if (LOG_ENABLED(INFO)) { log_response_headers(buf); } return 0; } int HttpsUpstream::on_downstream_body(Downstream *downstream, const uint8_t *data, size_t len, bool flush) { if (len == 0) { return 0; } auto output = downstream->get_response_buf(); if (downstream->get_chunked_response()) { output->append(util::utox(len)); output->append("\r\n"); } output->append(data, len); downstream->response_sent_body_length += len; if (downstream->get_chunked_response()) { output->append("\r\n"); } return 0; } int HttpsUpstream::on_downstream_body_complete(Downstream *downstream) { const auto &req = downstream->request(); auto &resp = downstream->response(); if (downstream->get_chunked_response()) { auto output = downstream->get_response_buf(); const auto &trailers = resp.fs.trailers(); if (trailers.empty()) { output->append("0\r\n\r\n"); } else { output->append("0\r\n"); http2::build_http1_headers_from_headers(output, trailers, http2::HDOP_STRIP_ALL); output->append("\r\n"); } } if (LOG_ENABLED(INFO)) { DLOG(INFO, downstream) << "HTTP response completed"; } if (!downstream->validate_response_recv_body_length()) { resp.connection_close = true; } if (req.connection_close || resp.connection_close || // To avoid to stall upload body downstream->get_request_state() != Downstream::MSG_COMPLETE) { auto handler = get_client_handler(); handler->set_should_close_after_write(true); } return 0; } int HttpsUpstream::on_downstream_abort_request(Downstream *downstream, unsigned int status_code) { error_reply(status_code); handler_->signal_write(); return 0; } int HttpsUpstream::on_downstream_abort_request_with_https_redirect( Downstream *downstream) { redirect_to_https(downstream); handler_->signal_write(); return 0; } int HttpsUpstream::redirect_to_https(Downstream *downstream) { auto &req = downstream->request(); if (req.method == HTTP_CONNECT || req.scheme != "http" || req.authority.empty()) { error_reply(400); return 0; } auto authority = util::extract_host(req.authority); if (authority.empty()) { error_reply(400); return 0; } auto &balloc = downstream->get_block_allocator(); auto config = get_config(); auto &httpconf = config->http; StringRef loc; if (httpconf.redirect_https_port == StringRef::from_lit("443")) { loc = concat_string_ref(balloc, StringRef::from_lit("https://"), authority, req.path); } else { loc = concat_string_ref(balloc, StringRef::from_lit("https://"), authority, StringRef::from_lit(":"), httpconf.redirect_https_port, req.path); } auto &resp = downstream->response(); resp.http_status = 308; resp.fs.add_header_token(StringRef::from_lit("location"), loc, false, http2::HD_LOCATION); resp.fs.add_header_token(StringRef::from_lit("connection"), StringRef::from_lit("close"), false, http2::HD_CONNECTION); return send_reply(downstream, nullptr, 0); } void HttpsUpstream::log_response_headers(DefaultMemchunks *buf) const { std::string nhdrs; for (auto chunk = buf->head; chunk; chunk = chunk->next) { nhdrs.append(chunk->pos, chunk->last); } if (log_config()->errorlog_tty) { nhdrs = http::colorizeHeaders(nhdrs.c_str()); } ULOG(INFO, this) << "HTTP response headers\n" << nhdrs; } void HttpsUpstream::on_handler_delete() { if (downstream_ && downstream_->accesslog_ready()) { handler_->write_accesslog(downstream_.get()); } } int HttpsUpstream::on_downstream_reset(Downstream *downstream, bool no_retry) { int rv; std::unique_ptr dconn; assert(downstream == downstream_.get()); downstream_->pop_downstream_connection(); if (!downstream_->request_submission_ready()) { switch (downstream_->get_response_state()) { case Downstream::MSG_COMPLETE: // We have got all response body already. Send it off. return 0; case Downstream::INITIAL: if (on_downstream_abort_request(downstream_.get(), 502) != 0) { return -1; } return 0; } // Return error so that caller can delete handler return -1; } downstream_->add_retry(); rv = 0; if (no_retry || downstream_->no_more_retry()) { goto fail; } dconn = handler_->get_downstream_connection(rv, downstream_.get()); if (!dconn) { goto fail; } rv = downstream_->attach_downstream_connection(std::move(dconn)); if (rv != 0) { goto fail; } rv = downstream_->push_request_headers(); if (rv != 0) { goto fail; } return 0; fail: if (rv == SHRPX_ERR_TLS_REQUIRED) { rv = on_downstream_abort_request_with_https_redirect(downstream); } else { rv = on_downstream_abort_request(downstream_.get(), 502); } if (rv != 0) { return -1; } downstream_->pop_downstream_connection(); return 0; } int HttpsUpstream::initiate_push(Downstream *downstream, const StringRef &uri) { return 0; } int HttpsUpstream::response_riovec(struct iovec *iov, int iovcnt) const { if (!downstream_) { return 0; } auto buf = downstream_->get_response_buf(); return buf->riovec(iov, iovcnt); } void HttpsUpstream::response_drain(size_t n) { if (!downstream_) { return; } auto buf = downstream_->get_response_buf(); buf->drain(n); } bool HttpsUpstream::response_empty() const { if (!downstream_) { return true; } auto buf = downstream_->get_response_buf(); return buf->rleft() == 0; } Downstream * HttpsUpstream::on_downstream_push_promise(Downstream *downstream, int32_t promised_stream_id) { return nullptr; } int HttpsUpstream::on_downstream_push_promise_complete( Downstream *downstream, Downstream *promised_downstream) { return -1; } bool HttpsUpstream::push_enabled() const { return false; } void HttpsUpstream::cancel_premature_downstream( Downstream *promised_downstream) {} } // namespace shrpx