From 4825293fad2886d9e75e40149de92c45c1220710 Mon Sep 17 00:00:00 2001 From: dengjie <1171276417@qq.com> Date: Fri, 27 Oct 2023 01:05:59 +0800 Subject: [PATCH] add libverto support --- ...ent-implementation-based-on-libverto.patch | 3466 ++++++++++ ...ver-implementation-based-on-libverto.patch | 3086 +++++++++ ...ing-implementation-based-on-libverto.patch | 6093 +++++++++++++++++ 0004-updata-build-file.patch | 213 + nghttp2-1.55.1 | 1 + nghttp2.spec | 34 +- 6 files changed, 12889 insertions(+), 4 deletions(-) create mode 100644 0001-nghttp-client-implementation-based-on-libverto.patch create mode 100644 0002-nghttpd-server-implementation-based-on-libverto.patch create mode 100644 0003-h2load-Benchmarking-implementation-based-on-libverto.patch create mode 100644 0004-updata-build-file.patch create mode 160000 nghttp2-1.55.1 diff --git a/0001-nghttp-client-implementation-based-on-libverto.patch b/0001-nghttp-client-implementation-based-on-libverto.patch new file mode 100644 index 0000000..3d7443c --- /dev/null +++ b/0001-nghttp-client-implementation-based-on-libverto.patch @@ -0,0 +1,3466 @@ +From 57bbd9545831326c46c1d8f3cfa4da585cded9c9 Mon Sep 17 00:00:00 2001 +From: dengjie <1171276417@qq.com> +Date: Thu, 26 Oct 2023 23:39:45 +0800 +Subject: [PATCH 1/4] nghttp client implementation based on libverto + +--- + src/nghttp_verto.cc | 3124 +++++++++++++++++++++++++++++++++++++++++++ + src/nghttp_verto.h | 315 +++++ + 2 files changed, 3439 insertions(+) + create mode 100644 src/nghttp_verto.cc + create mode 100644 src/nghttp_verto.h + +diff --git a/src/nghttp_verto.cc b/src/nghttp_verto.cc +new file mode 100644 +index 0000000..be20f4b +--- /dev/null ++++ b/src/nghttp_verto.cc +@@ -0,0 +1,3124 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2013 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 "nghttp_verto.h" ++ ++#include ++#include ++#ifdef HAVE_UNISTD_H ++# include ++#endif // HAVE_UNISTD_H ++#ifdef HAVE_FCNTL_H ++# include ++#endif // HAVE_FCNTL_H ++#ifdef HAVE_NETINET_IN_H ++# include ++#endif // HAVE_NETINET_IN_H ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#ifdef HAVE_JANSSON ++# include ++#endif // HAVE_JANSSON ++ ++#include "app_helper.h" ++#include "HtmlParser.h" ++#include "util.h" ++#include "base64.h" ++#include "tls.h" ++#include "template.h" ++#include "ssl_compat.h" ++ ++#ifndef O_BINARY ++# define O_BINARY (0) ++#endif // O_BINARY ++ ++namespace nghttp2 { ++ ++// The anchor stream nodes when --no-dep is not used. The stream ID = ++// 1 is excluded since it is used as first stream in upgrade case. We ++// follows the same dependency anchor nodes as Firefox does. ++struct Anchor { ++ int32_t stream_id; ++ // stream ID this anchor depends on ++ int32_t dep_stream_id; ++ // .. with this weight. ++ int32_t weight; ++}; ++ ++// This is index into anchors. Firefox uses ANCHOR_FOLLOWERS for html ++// file. ++enum { ++ ANCHOR_LEADERS, ++ ANCHOR_UNBLOCKED, ++ ANCHOR_BACKGROUND, ++ ANCHOR_SPECULATIVE, ++ ANCHOR_FOLLOWERS, ++}; ++ ++namespace { ++constexpr auto anchors = std::array{{ ++ {3, 0, 201}, ++ {5, 0, 101}, ++ {7, 0, 1}, ++ {9, 7, 1}, ++ {11, 3, 1}, ++}}; ++} // namespace ++ ++Config::Config() ++ : header_table_size(-1), ++ min_header_table_size(std::numeric_limits::max()), ++ encoder_header_table_size(-1), ++ padding(0), ++ max_concurrent_streams(100), ++ peer_max_concurrent_streams(100), ++ multiply(1), ++ window_bits(-1), ++ connection_window_bits(-1), ++ verbose(0), ++ timeout(0.), ++ port_override(0), ++ null_out(false), ++ remote_name(false), ++ get_assets(false), ++ stat(false), ++ upgrade(false), ++ continuation(false), ++ no_content_length(false), ++ no_dep(false), ++ hexdump(false), ++ no_push(false), ++ expect_continue(false), ++ verify_peer(true), ++ ktls(false), ++ no_rfc7540_pri(false) { ++ nghttp2_option_new(&http2_option); ++ nghttp2_option_set_peer_max_concurrent_streams(http2_option, ++ peer_max_concurrent_streams); ++ nghttp2_option_set_builtin_recv_extension_type(http2_option, NGHTTP2_ALTSVC); ++ nghttp2_option_set_builtin_recv_extension_type(http2_option, NGHTTP2_ORIGIN); ++} ++ ++Config::~Config() { nghttp2_option_del(http2_option); } ++ ++namespace { ++Config config; ++} // namespace ++ ++namespace { ++void print_protocol_nego_error() { ++ std::cerr << "[ERROR] HTTP/2 protocol was not selected." ++ << " (nghttp2 expects " << NGHTTP2_PROTO_VERSION_ID << ")" ++ << std::endl; ++} ++} // namespace ++ ++namespace { ++std::string strip_fragment(const char *raw_uri) { ++ const char *end; ++ for (end = raw_uri; *end && *end != '#'; ++end) ++ ; ++ size_t len = end - raw_uri; ++ return std::string(raw_uri, len); ++} ++} // namespace ++ ++Request::Request(const std::string &uri, const http_parser_url &u, ++ const nghttp2_data_provider *data_prd, int64_t data_length, ++ const nghttp2_priority_spec &pri_spec, int level) ++ : uri(uri), ++ u(u), ++ pri_spec(pri_spec), ++ data_length(data_length), ++ data_offset(0), ++ response_len(0), ++ inflater(nullptr), ++ data_prd(data_prd), ++ header_buffer_size(0), ++ stream_id(-1), ++ status(0), ++ level(level), ++ expect_final_response(false) { ++ http2::init_hdidx(res_hdidx); ++ http2::init_hdidx(req_hdidx); ++} ++ ++Request::~Request() { nghttp2_gzip_inflate_del(inflater); } ++ ++void Request::init_inflater() { ++ int rv; ++ // This is required with --disable-assert. ++ (void)rv; ++ rv = nghttp2_gzip_inflate_new(&inflater); ++ assert(rv == 0); ++} ++ ++StringRef Request::get_real_scheme() const { ++ return config.scheme_override.empty() ++ ? util::get_uri_field(uri.c_str(), u, UF_SCHEMA) ++ : StringRef{config.scheme_override}; ++} ++ ++StringRef Request::get_real_host() const { ++ return config.host_override.empty() ++ ? util::get_uri_field(uri.c_str(), u, UF_HOST) ++ : StringRef{config.host_override}; ++} ++ ++uint16_t Request::get_real_port() const { ++ auto scheme = get_real_scheme(); ++ return config.host_override.empty() ? util::has_uri_field(u, UF_PORT) ? u.port ++ : scheme == "https" ? 443 ++ : 80 ++ : config.port_override == 0 ? scheme == "https" ? 443 : 80 ++ : config.port_override; ++} ++ ++void Request::init_html_parser() { ++ // We crawl HTML using overridden scheme, host, and port. ++ auto scheme = get_real_scheme(); ++ auto host = get_real_host(); ++ auto port = get_real_port(); ++ auto ipv6_lit = ++ std::find(std::begin(host), std::end(host), ':') != std::end(host); ++ ++ auto base_uri = scheme.str(); ++ base_uri += "://"; ++ if (ipv6_lit) { ++ base_uri += '['; ++ } ++ base_uri += host; ++ if (ipv6_lit) { ++ base_uri += ']'; ++ } ++ if (!((scheme == "https" && port == 443) || ++ (scheme == "http" && port == 80))) { ++ base_uri += ':'; ++ base_uri += util::utos(port); ++ } ++ base_uri += util::get_uri_field(uri.c_str(), u, UF_PATH); ++ if (util::has_uri_field(u, UF_QUERY)) { ++ base_uri += '?'; ++ base_uri += util::get_uri_field(uri.c_str(), u, UF_QUERY); ++ } ++ ++ html_parser = std::make_unique(base_uri); ++} ++ ++int Request::update_html_parser(const uint8_t *data, size_t len, int fin) { ++ if (!html_parser) { ++ return 0; ++ } ++ return html_parser->parse_chunk(reinterpret_cast(data), len, ++ fin); ++} ++ ++std::string Request::make_reqpath() const { ++ std::string path = util::has_uri_field(u, UF_PATH) ++ ? util::get_uri_field(uri.c_str(), u, UF_PATH).str() ++ : "/"; ++ if (util::has_uri_field(u, UF_QUERY)) { ++ path += '?'; ++ path.append(uri.c_str() + u.field_data[UF_QUERY].off, ++ u.field_data[UF_QUERY].len); ++ } ++ return path; ++} ++ ++namespace { ++// Perform special handling |host| if it is IPv6 literal and includes ++// zone ID per RFC 6874. ++std::string decode_host(const StringRef &host) { ++ auto zone_start = std::find(std::begin(host), std::end(host), '%'); ++ if (zone_start == std::end(host) || ++ !util::ipv6_numeric_addr( ++ std::string(std::begin(host), zone_start).c_str())) { ++ return host.str(); ++ } ++ // case: ::1% ++ if (zone_start + 1 == std::end(host)) { ++ return StringRef{host.c_str(), host.size() - 1}.str(); ++ } ++ // case: ::1%12 or ::1%1 ++ if (zone_start + 3 >= std::end(host)) { ++ return host.str(); ++ } ++ // If we see "%25", followed by more characters, then decode %25 as ++ // '%'. ++ auto zone_id_src = (*(zone_start + 1) == '2' && *(zone_start + 2) == '5') ++ ? zone_start + 3 ++ : zone_start + 1; ++ auto zone_id = util::percent_decode(zone_id_src, std::end(host)); ++ auto res = std::string(std::begin(host), zone_start + 1); ++ res += zone_id; ++ return res; ++} ++} // namespace ++ ++namespace { ++nghttp2_priority_spec resolve_dep(int res_type) { ++ nghttp2_priority_spec pri_spec; ++ ++ if (config.no_dep) { ++ nghttp2_priority_spec_default_init(&pri_spec); ++ ++ return pri_spec; ++ } ++ ++ int32_t anchor_id; ++ int32_t weight; ++ switch (res_type) { ++ case REQ_CSS: ++ case REQ_JS: ++ anchor_id = anchors[ANCHOR_LEADERS].stream_id; ++ weight = 32; ++ break; ++ case REQ_UNBLOCK_JS: ++ anchor_id = anchors[ANCHOR_UNBLOCKED].stream_id; ++ weight = 32; ++ break; ++ case REQ_IMG: ++ anchor_id = anchors[ANCHOR_FOLLOWERS].stream_id; ++ weight = 12; ++ break; ++ default: ++ anchor_id = anchors[ANCHOR_FOLLOWERS].stream_id; ++ weight = 32; ++ } ++ ++ nghttp2_priority_spec_init(&pri_spec, anchor_id, weight, 0); ++ return pri_spec; ++} ++} // namespace ++ ++bool Request::is_ipv6_literal_addr() const { ++ if (util::has_uri_field(u, UF_HOST)) { ++ return memchr(uri.c_str() + u.field_data[UF_HOST].off, ':', ++ u.field_data[UF_HOST].len); ++ } else { ++ return false; ++ } ++} ++ ++Headers::value_type *Request::get_res_header(int32_t token) { ++ auto idx = res_hdidx[token]; ++ if (idx == -1) { ++ return nullptr; ++ } ++ return &res_nva[idx]; ++} ++ ++Headers::value_type *Request::get_req_header(int32_t token) { ++ auto idx = req_hdidx[token]; ++ if (idx == -1) { ++ return nullptr; ++ } ++ return &req_nva[idx]; ++} ++ ++void Request::record_request_start_time() { ++ timing.state = RequestState::ON_REQUEST; ++ timing.request_start_time = get_time(); ++} ++ ++void Request::record_response_start_time() { ++ timing.state = RequestState::ON_RESPONSE; ++ timing.response_start_time = get_time(); ++} ++ ++void Request::record_response_end_time() { ++ timing.state = RequestState::ON_COMPLETE; ++ timing.response_end_time = get_time(); ++} ++ ++namespace { ++void verto_continue_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto req = static_cast(verto_get_private(ev)); ++ auto client = static_cast(req->client); ++ ++ int error; ++ ++ error = nghttp2_submit_data(client->session, NGHTTP2_FLAG_END_STREAM, ++ req->stream_id, req->data_prd); ++ ++ if (error) { ++ std::cerr << "[ERROR] nghttp2_submit_data() returned error: " ++ << nghttp2_strerror(error) << std::endl; ++ nghttp2_submit_rst_stream(client->session, NGHTTP2_FLAG_NONE, ++ req->stream_id, NGHTTP2_INTERNAL_ERROR); ++ } ++} ++} // namespace ++ ++ContinueTimer::ContinueTimer(verto_ctx *verto_loop, Request *req, HttpClient *client): ++ verto_loop(verto_loop), ++ req(req) { ++ req->client = client; ++} ++ ++ContinueTimer::~ContinueTimer() { stop(); } ++ ++void ContinueTimer::start() { ++ // Add a timeout event observer to the verto_loop ++ verto_timer = verto_add_timeout(verto_loop, VERTO_EV_FLAG_NONE, verto_continue_timeout_cb, 1000); ++ verto_set_private(verto_timer, req, NULL); ++} ++ ++void ContinueTimer::stop() { ++ // Remove the timeout event observer from the verto_loop ++ verto_del(verto_timer); ++} ++ ++void ContinueTimer::dispatch_continue() { ++ // A callback that triggers the timeout event manually ++ verto_continue_timeout_cb(verto_loop, verto_timer); ++} ++ ++namespace { ++int htp_msg_begincb(llhttp_t *htp) { ++ if (config.verbose) { ++ print_timer(); ++ std::cout << " HTTP Upgrade response" << std::endl; ++ } ++ return 0; ++} ++} // namespace ++ ++namespace { ++int htp_msg_completecb(llhttp_t *htp) { ++ auto client = static_cast(htp->data); ++ client->upgrade_response_status_code = htp->status_code; ++ client->upgrade_response_complete = true; ++ return 0; ++} ++} // namespace ++ ++namespace { ++constexpr llhttp_settings_t htp_hooks = { ++ htp_msg_begincb, // llhttp_cb on_message_begin; ++ nullptr, // llhttp_data_cb on_url; ++ nullptr, // llhttp_data_cb on_status; ++ nullptr, // llhttp_data_cb on_method; ++ nullptr, // llhttp_data_cb on_version; ++ nullptr, // llhttp_data_cb on_header_field; ++ nullptr, // llhttp_data_cb on_header_value; ++ nullptr, // llhttp_data_cb on_chunk_extension_name; ++ nullptr, // llhttp_data_cb on_chunk_extension_value; ++ nullptr, // llhttp_cb on_headers_complete; ++ nullptr, // llhttp_data_cb on_body; ++ htp_msg_completecb, // llhttp_cb on_message_complete; ++ nullptr, // llhttp_cb on_url_complete; ++ nullptr, // llhttp_cb on_status_complete; ++ nullptr, // llhttp_cb on_method_complete; ++ nullptr, // llhttp_cb on_version_complete; ++ nullptr, // llhttp_cb on_header_field_complete; ++ nullptr, // llhttp_cb on_header_value_complete; ++ nullptr, // llhttp_cb on_chunk_extension_name_complete; ++ nullptr, // llhttp_cb on_chunk_extension_value_complete; ++ nullptr, // llhttp_cb on_chunk_header; ++ nullptr, // llhttp_cb on_chunk_complete; ++ nullptr, // llhttp_cb on_reset; ++}; ++} // namespace ++ ++namespace { ++int submit_request(HttpClient *client, const Headers &headers, Request *req) { ++ auto scheme = util::get_uri_field(req->uri.c_str(), req->u, UF_SCHEMA); ++ auto build_headers = Headers{{":method", req->data_prd ? "POST" : "GET"}, ++ {":path", req->make_reqpath()}, ++ {":scheme", scheme.str()}, ++ {":authority", client->hostport}, ++ {"accept", "*/*"}, ++ {"accept-encoding", "gzip, deflate"}, ++ {"user-agent", "nghttp2/" NGHTTP2_VERSION}}; ++ bool expect_continue = false; ++ ++ if (config.continuation) { ++ for (size_t i = 0; i < 6; ++i) { ++ build_headers.emplace_back("continuation-test-" + util::utos(i + 1), ++ std::string(4_k, '-')); ++ } ++ } ++ ++ auto num_initial_headers = build_headers.size(); ++ ++ if (req->data_prd) { ++ if (!config.no_content_length) { ++ build_headers.emplace_back("content-length", ++ util::utos(req->data_length)); ++ } ++ if (config.expect_continue) { ++ expect_continue = true; ++ build_headers.emplace_back("expect", "100-continue"); ++ } ++ } ++ ++ for (auto &kv : headers) { ++ size_t i; ++ for (i = 0; i < num_initial_headers; ++i) { ++ if (kv.name == build_headers[i].name) { ++ build_headers[i].value = kv.value; ++ break; ++ } ++ } ++ if (i < num_initial_headers) { ++ continue; ++ } ++ ++ build_headers.emplace_back(kv.name, kv.value, kv.no_index); ++ } ++ ++ auto nva = std::vector(); ++ nva.reserve(build_headers.size()); ++ ++ for (auto &kv : build_headers) { ++ nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); ++ } ++ ++ auto method = http2::get_header(build_headers, ":method"); ++ assert(method); ++ ++ req->method = method->value; ++ ++ std::string trailer_names; ++ if (!config.trailer.empty()) { ++ trailer_names = config.trailer[0].name; ++ for (size_t i = 1; i < config.trailer.size(); ++i) { ++ trailer_names += ", "; ++ trailer_names += config.trailer[i].name; ++ } ++ nva.push_back(http2::make_nv_ls("trailer", trailer_names)); ++ } ++ ++ int32_t stream_id; ++ ++ if (expect_continue) { ++ stream_id = nghttp2_submit_headers(client->session, 0, -1, &req->pri_spec, ++ nva.data(), nva.size(), req); ++ } else { ++ stream_id = ++ nghttp2_submit_request(client->session, &req->pri_spec, nva.data(), ++ nva.size(), req->data_prd, req); ++ } ++ ++ if (stream_id < 0) { ++ std::cerr << "[ERROR] nghttp2_submit_" ++ << (expect_continue ? "headers" : "request") ++ << "() returned error: " << nghttp2_strerror(stream_id) ++ << std::endl; ++ return -1; ++ } ++ ++ req->stream_id = stream_id; ++ client->request_done(req); ++ ++ req->req_nva = std::move(build_headers); ++ ++ if (expect_continue) { ++ auto timer = std::make_unique(client->verto_loop, req, client); ++ req->continue_timer = std::move(timer); ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++void verto_writecb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ auto rv = client->do_write(); ++ if (rv == HttpClient::ERR_CONNECT_FAIL) { ++ client->connect_fail(); ++ return; ++ } ++ if (rv != 0) { ++ client->disconnect(); ++ } ++} ++} // namespace ++ ++namespace { ++void verto_readcb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ ++ // In order to wait for the callback for the read event ++ // the write event observer was removed from the event loop in the previous step ++ // So now we need to add the read event observer into the event loop ++ verto_ev_flag write_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_WRITE); ++ client->verto_wev = verto_add_io(client->verto_loop, write_flag, verto_writecb, client->fd); ++ verto_set_private(client->verto_wev, client, NULL); ++ ++ if (client->do_read() != 0) { ++ client->disconnect(); ++ } ++} ++} // namespace ++ ++namespace { ++void verto_settings_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ verto_del(ev); ++ ++ nghttp2_session_terminate_session(client->session, NGHTTP2_SETTINGS_TIMEOUT); ++} ++} // namespace ++ ++ ++HttpClient::HttpClient(const nghttp2_session_callbacks *callbacks, ++ verto_ctx *verto_loop, SSL_CTX *ssl_ctx) ++ : wb(&mcpool), ++ session(nullptr), ++ callbacks(callbacks), ++ verto_loop(verto_loop), ++ ssl_ctx(ssl_ctx), ++ ssl(nullptr), ++ addrs(nullptr), ++ next_addr(nullptr), ++ cur_addr(nullptr), ++ complete(0), ++ success(0), ++ settings_payloadlen(0), ++ state(ClientState::IDLE), ++ upgrade_response_status_code(0), ++ fd(-1), ++ upgrade_response_complete(false) { ++} ++ ++HttpClient::~HttpClient() { ++ disconnect(); ++ ++ if (addrs) { ++ freeaddrinfo(addrs); ++ addrs = nullptr; ++ next_addr = nullptr; ++ } ++} ++ ++bool HttpClient::need_upgrade() const { ++ return config.upgrade && scheme == "http"; ++} ++ ++int HttpClient::resolve_host(const std::string &host, uint16_t port) { ++ int rv; ++ this->host = host; ++ addrinfo hints{}; ++ hints.ai_family = AF_UNSPEC; ++ hints.ai_socktype = SOCK_STREAM; ++ hints.ai_protocol = 0; ++ hints.ai_flags = AI_ADDRCONFIG; ++ rv = getaddrinfo(host.c_str(), util::utos(port).c_str(), &hints, &addrs); ++ if (rv != 0) { ++ std::cerr << "[ERROR] getaddrinfo() failed: " << gai_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ if (addrs == nullptr) { ++ std::cerr << "[ERROR] No address returned" << std::endl; ++ return -1; ++ } ++ next_addr = addrs; ++ return 0; ++} ++ ++namespace { ++// Just returns 1 to continue handshake. ++int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { return 1; } ++} // namespace ++ ++int HttpClient::initiate_connection() { ++ int rv; ++ ++ cur_addr = nullptr; ++ while (next_addr) { ++ cur_addr = next_addr; ++ next_addr = next_addr->ai_next; ++ fd = util::create_nonblock_socket(cur_addr->ai_family); ++ if (fd == -1) { ++ continue; ++ } ++ ++ if (ssl_ctx) { ++ // We are establishing TLS connection. ++ ssl = SSL_new(ssl_ctx); ++ if (!ssl) { ++ std::cerr << "[ERROR] SSL_new() failed: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ return -1; ++ } ++ ++ SSL_set_connect_state(ssl); ++ ++ // If the user overrode the :authority or host header, use that ++ // value for the SNI extension ++ const auto &host_string = ++ config.host_override.empty() ? host : config.host_override; ++ ++#if LIBRESSL_2_7_API || \ ++ (!LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L) || \ ++ defined(OPENSSL_IS_BORINGSSL) ++ auto param = SSL_get0_param(ssl); ++ X509_VERIFY_PARAM_set_hostflags(param, 0); ++ X509_VERIFY_PARAM_set1_host(param, host_string.c_str(), ++ host_string.size()); ++#endif // LIBRESSL_2_7_API || (!LIBRESSL_IN_USE && ++ // OPENSSL_VERSION_NUMBER >= 0x10002000L) || ++ // defined(OPENSSL_IS_BORINGSSL) ++ SSL_set_verify(ssl, SSL_VERIFY_PEER, verify_cb); ++ ++ if (!util::numeric_host(host_string.c_str())) { ++ SSL_set_tlsext_host_name(ssl, host_string.c_str()); ++ } ++ } ++ ++ rv = connect(fd, cur_addr->ai_addr, cur_addr->ai_addrlen); ++ ++ if (rv != 0 && errno != EINPROGRESS) { ++ if (ssl) { ++ SSL_free(ssl); ++ ssl = nullptr; ++ } ++ close(fd); ++ fd = -1; ++ continue; ++ } ++ break; ++ } ++ ++ if (fd == -1) { ++ return -1; ++ } ++ ++ writefn = &HttpClient::connected; ++ ++ if (need_upgrade()) { ++ on_readfn = &HttpClient::on_upgrade_read; ++ on_writefn = &HttpClient::on_upgrade_connect; ++ } else { ++ on_readfn = &HttpClient::on_read; ++ on_writefn = &HttpClient::on_write; ++ } ++ ++ // Add read event observers and write event observers to the event loop ++ verto_ev_flag read_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_READ); ++ verto_rev = verto_add_io(verto_loop, read_flag, verto_readcb, fd); ++ verto_set_private(verto_rev, this, NULL); ++ ++ verto_ev_flag write_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_WRITE); ++ verto_wev = verto_add_io(verto_loop, write_flag, verto_writecb, fd); ++ verto_set_private(verto_wev, this, NULL); ++ ++ return 0; ++} ++ ++void HttpClient::disconnect() { ++ state = ClientState::IDLE; ++ ++ for (auto req = std::begin(reqvec); req != std::end(reqvec); ++req) { ++ if ((*req)->continue_timer) { ++ (*req)->continue_timer->stop(); ++ } ++ } ++ ++ // Exit the event loop ++ verto_break(verto_loop); ++ ++ nghttp2_session_del(session); ++ session = nullptr; ++ ++ if (ssl) { ++ SSL_set_shutdown(ssl, SSL_get_shutdown(ssl) | SSL_RECEIVED_SHUTDOWN); ++ ERR_clear_error(); ++ SSL_shutdown(ssl); ++ SSL_free(ssl); ++ ssl = nullptr; ++ } ++ ++ if (fd != -1) { ++ shutdown(fd, SHUT_WR); ++ close(fd); ++ fd = -1; ++ } ++} ++ ++int HttpClient::read_clear() { ++ std::array buf; ++ ++ for (;;) { ++ ssize_t nread; ++ while ((nread = read(fd, buf.data(), buf.size())) == -1 && errno == EINTR) ++ ; ++ if (nread == -1) { ++ if (errno == EAGAIN || errno == EWOULDBLOCK) { ++ return 0; ++ } ++ return -1; ++ } ++ ++ if (nread == 0) { ++ return -1; ++ } ++ ++ if (on_readfn(*this, buf.data(), nread) != 0) { ++ return -1; ++ } ++ } ++ ++ return 0; ++} ++ ++int HttpClient::write_clear() { ++ std::array iov; ++ ++ for (;;) { ++ if (on_writefn(*this) != 0) { ++ return -1; ++ } ++ ++ auto iovcnt = wb.riovec(iov.data(), iov.size()); ++ ++ if (iovcnt == 0) { ++ break; ++ } ++ ++ ssize_t nwrite; ++ while ((nwrite = writev(fd, iov.data(), iovcnt)) == -1 && errno == EINTR) ++ ; ++ if (nwrite == -1) { ++ if (errno == EAGAIN || errno == EWOULDBLOCK) { ++ return 0; ++ } ++ return -1; ++ } ++ ++ wb.drain(nwrite); ++ } ++ ++ return 0; ++} ++ ++int HttpClient::noop() { return 0; } ++ ++void HttpClient::connect_fail() { ++ if (state == ClientState::IDLE) { ++ std::cerr << "[ERROR] Could not connect to the address " ++ << util::numeric_name(cur_addr->ai_addr, cur_addr->ai_addrlen) ++ << std::endl; ++ } ++ auto cur_state = state; ++ disconnect(); ++ if (cur_state == ClientState::IDLE) { ++ if (initiate_connection() == 0) { ++ std::cerr << "Trying next address " ++ << util::numeric_name(cur_addr->ai_addr, cur_addr->ai_addrlen) ++ << std::endl; ++ } ++ } ++} ++ ++int HttpClient::connected() { ++ if (!util::check_socket_connected(fd)) { ++ return ERR_CONNECT_FAIL; ++ } ++ ++ if (config.verbose) { ++ print_timer(); ++ std::cout << " Connected" << std::endl; ++ } ++ ++ state = ClientState::CONNECTED; ++ ++ if (ssl) { ++ SSL_set_fd(ssl, fd); ++ ++ readfn = &HttpClient::tls_handshake; ++ writefn = &HttpClient::tls_handshake; ++ ++ return do_write(); ++ } ++ ++ readfn = &HttpClient::read_clear; ++ writefn = &HttpClient::write_clear; ++ ++ if (need_upgrade()) { ++ htp = std::make_unique(); ++ llhttp_init(htp.get(), HTTP_RESPONSE, &htp_hooks); ++ htp->data = this; ++ ++ return do_write(); ++ } ++ ++ if (connection_made() != 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++namespace { ++size_t populate_settings(nghttp2_settings_entry *iv) { ++ size_t niv = 2; ++ ++ iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; ++ iv[0].value = config.max_concurrent_streams; ++ ++ iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; ++ if (config.window_bits != -1) { ++ iv[1].value = (1 << config.window_bits) - 1; ++ } else { ++ iv[1].value = NGHTTP2_INITIAL_WINDOW_SIZE; ++ } ++ ++ if (config.header_table_size >= 0) { ++ if (config.min_header_table_size < config.header_table_size) { ++ iv[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; ++ iv[niv].value = config.min_header_table_size; ++ ++niv; ++ } ++ ++ iv[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; ++ iv[niv].value = config.header_table_size; ++ ++niv; ++ } ++ ++ if (config.no_push) { ++ iv[niv].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; ++ iv[niv].value = 0; ++ ++niv; ++ } ++ ++ if (config.no_rfc7540_pri) { ++ iv[niv].settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; ++ iv[niv].value = 1; ++ ++niv; ++ } ++ ++ return niv; ++} ++} // namespace ++ ++int HttpClient::on_upgrade_connect() { ++ ssize_t rv; ++ record_connect_end_time(); ++ assert(!reqvec.empty()); ++ std::array iv; ++ size_t niv = populate_settings(iv.data()); ++ assert(settings_payload.size() >= 8 * niv); ++ rv = nghttp2_pack_settings_payload(settings_payload.data(), ++ settings_payload.size(), iv.data(), niv); ++ if (rv < 0) { ++ return -1; ++ } ++ settings_payloadlen = rv; ++ auto token68 = ++ base64::encode(std::begin(settings_payload), ++ std::begin(settings_payload) + settings_payloadlen); ++ util::to_token68(token68); ++ ++ std::string req; ++ if (reqvec[0]->data_prd) { ++ // If the request contains upload data, use OPTIONS * to upgrade ++ req = "OPTIONS *"; ++ } else { ++ auto meth = std::find_if( ++ std::begin(config.headers), std::end(config.headers), ++ [](const Header &kv) { return util::streq_l(":method", kv.name); }); ++ ++ if (meth == std::end(config.headers)) { ++ req = "GET "; ++ reqvec[0]->method = "GET"; ++ } else { ++ req = (*meth).value; ++ req += ' '; ++ reqvec[0]->method = (*meth).value; ++ } ++ req += reqvec[0]->make_reqpath(); ++ } ++ ++ auto headers = Headers{{"host", hostport}, ++ {"connection", "Upgrade, HTTP2-Settings"}, ++ {"upgrade", NGHTTP2_CLEARTEXT_PROTO_VERSION_ID}, ++ {"http2-settings", token68}, ++ {"accept", "*/*"}, ++ {"user-agent", "nghttp2/" NGHTTP2_VERSION}}; ++ auto initial_headerslen = headers.size(); ++ ++ for (auto &kv : config.headers) { ++ size_t i; ++ if (kv.name.empty() || kv.name[0] == ':') { ++ continue; ++ } ++ for (i = 0; i < initial_headerslen; ++i) { ++ if (kv.name == headers[i].name) { ++ headers[i].value = kv.value; ++ break; ++ } ++ } ++ if (i < initial_headerslen) { ++ continue; ++ } ++ headers.emplace_back(kv.name, kv.value, kv.no_index); ++ } ++ ++ req += " HTTP/1.1\r\n"; ++ ++ for (auto &kv : headers) { ++ req += kv.name; ++ req += ": "; ++ req += kv.value; ++ req += "\r\n"; ++ } ++ req += "\r\n"; ++ ++ wb.append(req); ++ ++ if (config.verbose) { ++ print_timer(); ++ std::cout << " HTTP Upgrade request\n" << req << std::endl; ++ } ++ ++ if (!reqvec[0]->data_prd) { ++ // record request time if this is a part of real request. ++ reqvec[0]->record_request_start_time(); ++ reqvec[0]->req_nva = std::move(headers); ++ } ++ ++ on_writefn = &HttpClient::noop; ++ ++ return 0; ++} ++ ++int HttpClient::on_upgrade_read(const uint8_t *data, size_t len) { ++ int rv; ++ ++ auto htperr = ++ llhttp_execute(htp.get(), reinterpret_cast(data), len); ++ auto nread = htperr == HPE_OK ++ ? len ++ : static_cast(reinterpret_cast( ++ llhttp_get_error_pos(htp.get())) - ++ data); ++ ++ if (config.verbose) { ++ std::cout.write(reinterpret_cast(data), nread); ++ } ++ ++ if (htperr != HPE_OK && htperr != HPE_PAUSED_UPGRADE) { ++ std::cerr << "[ERROR] Failed to parse HTTP Upgrade response header: " ++ << "(" << llhttp_errno_name(htperr) << ") " ++ << llhttp_get_error_reason(htp.get()) << std::endl; ++ return -1; ++ } ++ ++ if (!upgrade_response_complete) { ++ return 0; ++ } ++ ++ if (config.verbose) { ++ std::cout << std::endl; ++ } ++ ++ if (upgrade_response_status_code != 101) { ++ std::cerr << "[ERROR] HTTP Upgrade failed" << std::endl; ++ ++ return -1; ++ } ++ ++ if (config.verbose) { ++ print_timer(); ++ std::cout << " HTTP Upgrade success" << std::endl; ++ } ++ ++ on_readfn = &HttpClient::on_read; ++ on_writefn = &HttpClient::on_write; ++ ++ rv = connection_made(); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ // Read remaining data in the buffer because it is not notified ++ // callback anymore. ++ rv = on_readfn(*this, data + nread, len - nread); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ return 0; ++} ++ ++int HttpClient::do_read() { return readfn(*this); } ++int HttpClient::do_write() { return writefn(*this); } ++ ++int HttpClient::connection_made() { ++ int rv; ++ ++ if (!need_upgrade()) { ++ record_connect_end_time(); ++ } ++ ++ if (ssl) { ++ // Check NPN or ALPN result ++ const unsigned char *next_proto = nullptr; ++ unsigned int next_proto_len; ++#ifndef OPENSSL_NO_NEXTPROTONEG ++ SSL_get0_next_proto_negotiated(ssl, &next_proto, &next_proto_len); ++#endif // !OPENSSL_NO_NEXTPROTONEG ++ for (int i = 0; i < 2; ++i) { ++ if (next_proto) { ++ auto proto = StringRef{next_proto, next_proto_len}; ++ if (config.verbose) { ++ std::cout << "The negotiated protocol: " << proto << std::endl; ++ } ++ if (!util::check_h2_is_selected(proto)) { ++ next_proto = nullptr; ++ } ++ break; ++ } ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L ++ SSL_get0_alpn_selected(ssl, &next_proto, &next_proto_len); ++#else // OPENSSL_VERSION_NUMBER < 0x10002000L ++ break; ++#endif // OPENSSL_VERSION_NUMBER < 0x10002000L ++ } ++ if (!next_proto) { ++ print_protocol_nego_error(); ++ return -1; ++ } ++ } ++ ++ rv = nghttp2_session_client_new2(&session, callbacks, this, ++ config.http2_option); ++ ++ if (rv != 0) { ++ return -1; ++ } ++ if (need_upgrade()) { ++ // Adjust stream user-data depending on the existence of upload ++ // data ++ Request *stream_user_data = nullptr; ++ if (!reqvec[0]->data_prd) { ++ stream_user_data = reqvec[0].get(); ++ } ++ // If HEAD is used, that is only when user specified it with -H ++ // option. ++ auto head_request = stream_user_data && stream_user_data->method == "HEAD"; ++ rv = nghttp2_session_upgrade2(session, settings_payload.data(), ++ settings_payloadlen, head_request, ++ stream_user_data); ++ if (rv != 0) { ++ std::cerr << "[ERROR] nghttp2_session_upgrade() returned error: " ++ << nghttp2_strerror(rv) << std::endl; ++ return -1; ++ } ++ if (stream_user_data) { ++ stream_user_data->stream_id = 1; ++ request_done(stream_user_data); ++ } ++ } ++ // If upgrade succeeds, the SETTINGS value sent with ++ // HTTP2-Settings header field has already been submitted to ++ // session object. ++ if (!need_upgrade()) { ++ std::array iv; ++ auto niv = populate_settings(iv.data()); ++ rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv.data(), niv); ++ if (rv != 0) { ++ return -1; ++ } ++ } ++ if (!config.no_dep) { ++ // Create anchor stream nodes ++ nghttp2_priority_spec pri_spec; ++ ++ for (auto &anchor : anchors) { ++ nghttp2_priority_spec_init(&pri_spec, anchor.dep_stream_id, anchor.weight, ++ 0); ++ rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, anchor.stream_id, ++ &pri_spec); ++ if (rv != 0) { ++ return -1; ++ } ++ } ++ ++ rv = nghttp2_session_set_next_stream_id( ++ session, anchors[ANCHOR_FOLLOWERS].stream_id + 2); ++ if (rv != 0) { ++ return -1; ++ } ++ ++ if (need_upgrade() && !reqvec[0]->data_prd) { ++ // Amend the priority because we cannot send priority in ++ // HTTP/1.1 Upgrade. ++ auto &anchor = anchors[ANCHOR_FOLLOWERS]; ++ nghttp2_priority_spec_init(&pri_spec, anchor.stream_id, ++ reqvec[0]->pri_spec.weight, 0); ++ ++ rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec); ++ if (rv != 0) { ++ return -1; ++ } ++ } ++ } else if (need_upgrade() && !reqvec[0]->data_prd && ++ reqvec[0]->pri_spec.weight != NGHTTP2_DEFAULT_WEIGHT) { ++ // Amend the priority because we cannot send priority in HTTP/1.1 ++ // Upgrade. ++ nghttp2_priority_spec pri_spec; ++ ++ nghttp2_priority_spec_init(&pri_spec, 0, reqvec[0]->pri_spec.weight, 0); ++ ++ rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec); ++ if (rv != 0) { ++ return -1; ++ } ++ } ++ ++ // Add a settings timer and set the timeout to 10 seconds ++ verto_settings_timer = verto_add_timeout(verto_loop, VERTO_EV_FLAG_PERSIST, verto_settings_timeout_cb, 10 *1000); ++ verto_set_private(verto_settings_timer, this, NULL); ++ ++ if (config.connection_window_bits != -1) { ++ int32_t window_size = (1 << config.connection_window_bits) - 1; ++ rv = nghttp2_session_set_local_window_size(session, NGHTTP2_FLAG_NONE, 0, ++ window_size); ++ if (rv != 0) { ++ return -1; ++ } ++ } ++ // Adjust first request depending on the existence of the upload ++ // data ++ for (auto i = std::begin(reqvec) + (need_upgrade() && !reqvec[0]->data_prd); ++ i != std::end(reqvec); ++i) { ++ if (submit_request(this, config.headers, (*i).get()) != 0) { ++ return -1; ++ } ++ } ++ ++ return 0; ++} ++ ++ ++int HttpClient::on_read(const uint8_t *data, size_t len) { ++ if (config.hexdump) { ++ util::hexdump(stdout, data, len); ++ } ++ ++ auto rv = nghttp2_session_mem_recv(session, data, len); ++ if (rv < 0) { ++ std::cerr << "[ERROR] nghttp2_session_mem_recv() returned error: " ++ << nghttp2_strerror(rv) << std::endl; ++ return -1; ++ } ++ ++ assert(static_cast(rv) == len); ++ ++ if (nghttp2_session_want_read(session) == 0 && ++ nghttp2_session_want_write(session) == 0 && wb.rleft() == 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int HttpClient::on_write() { ++ if (session == NULL) ++ return 0; ++ ++ for (;;) { ++ if (wb.rleft() >= 16384) { ++ return 0; ++ } ++ ++ const uint8_t *data; ++ ++ auto len = nghttp2_session_mem_send(session, &data); ++ if (len < 0) { ++ std::cerr << "[ERROR] nghttp2_session_send() returned error: " ++ << nghttp2_strerror(len) << std::endl; ++ return -1; ++ } ++ ++ if (len == 0) { ++ break; ++ } ++ ++ wb.append(data, len); ++ } ++ ++ if (nghttp2_session_want_read(session) == 0 && ++ nghttp2_session_want_write(session) == 0 && wb.rleft() == 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int HttpClient::tls_handshake() { ++ ERR_clear_error(); ++ ++ auto rv = SSL_do_handshake(ssl); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: { ++ // Remove the write observer ++ // add it to the event loop inside the read event callback ++ verto_del(verto_wev); ++ return 0; ++ } ++ case SSL_ERROR_WANT_WRITE: ++ return 0; ++ default: ++ return -1; ++ } ++ } ++ ++ readfn = &HttpClient::read_tls; ++ writefn = &HttpClient::write_tls; ++ ++ if (config.verify_peer) { ++ auto verify_res = SSL_get_verify_result(ssl); ++ if (verify_res != X509_V_OK) { ++ std::cerr << "[WARNING] Certificate verification failed: " ++ << X509_verify_cert_error_string(verify_res) << std::endl; ++ } ++ } ++ ++ if (connection_made() != 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int HttpClient::read_tls() { ++ ERR_clear_error(); ++ ++ std::array buf; ++ for (;;) { ++ auto rv = SSL_read(ssl, buf.data(), buf.size()); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ return 0; ++ case SSL_ERROR_WANT_WRITE: ++ // renegotiation started ++ return -1; ++ default: ++ return -1; ++ } ++ } ++ ++ if (on_readfn(*this, buf.data(), rv) != 0) { ++ return -1; ++ } ++ } ++} ++ ++int HttpClient::write_tls() { ++ ERR_clear_error(); ++ ++ struct iovec iov; ++ ++ for (;;) { ++ if (on_writefn(*this) != 0) { ++ return -1; ++ } ++ ++ auto iovcnt = wb.riovec(&iov, 1); ++ ++ if (iovcnt == 0) { ++ // iovcnt == 0 indicates that the write event is no longer needed ++ verto_del(verto_wev); ++ break; ++ } ++ ++ auto rv = SSL_write(ssl, iov.iov_base, iov.iov_len); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ // renegotiation started ++ return -1; ++ case SSL_ERROR_WANT_WRITE: ++ return 0; ++ default: ++ return -1; ++ } ++ } ++ ++ wb.drain(rv); ++ } ++ return 0; ++} ++ ++bool HttpClient::all_requests_processed() const { ++ return complete == reqvec.size(); ++} ++ ++void HttpClient::update_hostport() { ++ if (reqvec.empty()) { ++ return; ++ } ++ scheme = util::get_uri_field(reqvec[0]->uri.c_str(), reqvec[0]->u, UF_SCHEMA) ++ .str(); ++ std::stringstream ss; ++ if (reqvec[0]->is_ipv6_literal_addr()) { ++ // we may have zone ID, which must start with "%25", or "%". RFC ++ // 6874 defines "%25" only, and just "%" is allowed for just ++ // convenience to end-user input. ++ auto host = ++ util::get_uri_field(reqvec[0]->uri.c_str(), reqvec[0]->u, UF_HOST); ++ auto end = std::find(std::begin(host), std::end(host), '%'); ++ ss << "["; ++ ss.write(host.c_str(), end - std::begin(host)); ++ ss << "]"; ++ } else { ++ util::write_uri_field(ss, reqvec[0]->uri.c_str(), reqvec[0]->u, UF_HOST); ++ } ++ if (util::has_uri_field(reqvec[0]->u, UF_PORT) && ++ reqvec[0]->u.port != ++ util::get_default_port(reqvec[0]->uri.c_str(), reqvec[0]->u)) { ++ ss << ":" << reqvec[0]->u.port; ++ } ++ hostport = ss.str(); ++} ++ ++bool HttpClient::add_request(const std::string &uri, ++ const nghttp2_data_provider *data_prd, ++ int64_t data_length, ++ const nghttp2_priority_spec &pri_spec, int level) { ++ http_parser_url u{}; ++ if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { ++ return false; ++ } ++ if (path_cache.count(uri)) { ++ return false; ++ } ++ ++ if (config.multiply == 1) { ++ path_cache.insert(uri); ++ } ++ ++ reqvec.push_back(std::make_unique(uri, u, data_prd, data_length, ++ pri_spec, level)); ++ return true; ++} ++ ++void HttpClient::record_start_time() { ++ timing.system_start_time = std::chrono::system_clock::now(); ++ timing.start_time = get_time(); ++} ++ ++void HttpClient::record_domain_lookup_end_time() { ++ timing.domain_lookup_end_time = get_time(); ++} ++ ++void HttpClient::record_connect_end_time() { ++ timing.connect_end_time = get_time(); ++} ++ ++void HttpClient::request_done(Request *req) { ++ if (req->stream_id % 2 == 0) { ++ return; ++ } ++} ++ ++#ifdef HAVE_JANSSON ++void HttpClient::output_har(FILE *outfile) { ++ static auto PAGE_ID = "page_0"; ++ ++ auto root = json_object(); ++ auto log = json_object(); ++ json_object_set_new(root, "log", log); ++ json_object_set_new(log, "version", json_string("1.2")); ++ ++ auto creator = json_object(); ++ json_object_set_new(log, "creator", creator); ++ ++ json_object_set_new(creator, "name", json_string("nghttp")); ++ json_object_set_new(creator, "version", json_string(NGHTTP2_VERSION)); ++ ++ auto pages = json_array(); ++ json_object_set_new(log, "pages", pages); ++ ++ auto page = json_object(); ++ json_array_append_new(pages, page); ++ ++ json_object_set_new( ++ page, "startedDateTime", ++ json_string(util::format_iso8601(timing.system_start_time).c_str())); ++ json_object_set_new(page, "id", json_string(PAGE_ID)); ++ json_object_set_new(page, "title", json_string("")); ++ ++ json_object_set_new(page, "pageTimings", json_object()); ++ ++ auto entries = json_array(); ++ json_object_set_new(log, "entries", entries); ++ ++ auto dns_delta = std::chrono::duration_cast( ++ timing.domain_lookup_end_time - timing.start_time) ++ .count() / ++ 1000.0; ++ auto connect_delta = ++ std::chrono::duration_cast( ++ timing.connect_end_time - timing.domain_lookup_end_time) ++ .count() / ++ 1000.0; ++ ++ for (size_t i = 0; i < reqvec.size(); ++i) { ++ auto &req = reqvec[i]; ++ ++ if (req->timing.state != RequestState::ON_COMPLETE) { ++ continue; ++ } ++ ++ auto entry = json_object(); ++ json_array_append_new(entries, entry); ++ ++ auto &req_timing = req->timing; ++ auto request_time = ++ (i == 0) ? timing.system_start_time ++ : timing.system_start_time + ++ std::chrono::duration_cast< ++ std::chrono::system_clock::duration>( ++ req_timing.request_start_time - timing.start_time); ++ ++ auto wait_delta = ++ std::chrono::duration_cast( ++ req_timing.response_start_time - req_timing.request_start_time) ++ .count() / ++ 1000.0; ++ auto receive_delta = ++ std::chrono::duration_cast( ++ req_timing.response_end_time - req_timing.response_start_time) ++ .count() / ++ 1000.0; ++ ++ auto time_sum = ++ std::chrono::duration_cast( ++ (i == 0) ? (req_timing.response_end_time - timing.start_time) ++ : (req_timing.response_end_time - ++ req_timing.request_start_time)) ++ .count() / ++ 1000.0; ++ ++ json_object_set_new( ++ entry, "startedDateTime", ++ json_string(util::format_iso8601(request_time).c_str())); ++ json_object_set_new(entry, "time", json_real(time_sum)); ++ ++ auto pushed = req->stream_id % 2 == 0; ++ ++ json_object_set_new(entry, "comment", ++ json_string(pushed ? "Pushed Object" : "")); ++ ++ auto request = json_object(); ++ json_object_set_new(entry, "request", request); ++ ++ auto req_headers = json_array(); ++ json_object_set_new(request, "headers", req_headers); ++ ++ for (auto &nv : req->req_nva) { ++ auto hd = json_object(); ++ json_array_append_new(req_headers, hd); ++ ++ json_object_set_new(hd, "name", json_string(nv.name.c_str())); ++ json_object_set_new(hd, "value", json_string(nv.value.c_str())); ++ } ++ ++ json_object_set_new(request, "method", json_string(req->method.c_str())); ++ json_object_set_new(request, "url", json_string(req->uri.c_str())); ++ json_object_set_new(request, "httpVersion", json_string("HTTP/2.0")); ++ json_object_set_new(request, "cookies", json_array()); ++ json_object_set_new(request, "queryString", json_array()); ++ json_object_set_new(request, "headersSize", json_integer(-1)); ++ json_object_set_new(request, "bodySize", json_integer(-1)); ++ ++ auto response = json_object(); ++ json_object_set_new(entry, "response", response); ++ ++ auto res_headers = json_array(); ++ json_object_set_new(response, "headers", res_headers); ++ ++ for (auto &nv : req->res_nva) { ++ auto hd = json_object(); ++ json_array_append_new(res_headers, hd); ++ ++ json_object_set_new(hd, "name", json_string(nv.name.c_str())); ++ json_object_set_new(hd, "value", json_string(nv.value.c_str())); ++ } ++ ++ json_object_set_new(response, "status", json_integer(req->status)); ++ json_object_set_new(response, "statusText", json_string("")); ++ json_object_set_new(response, "httpVersion", json_string("HTTP/2.0")); ++ json_object_set_new(response, "cookies", json_array()); ++ ++ auto content = json_object(); ++ json_object_set_new(response, "content", content); ++ ++ json_object_set_new(content, "size", json_integer(req->response_len)); ++ ++ auto content_type_ptr = http2::get_header(req->res_nva, "content-type"); ++ ++ const char *content_type = ""; ++ if (content_type_ptr) { ++ content_type = content_type_ptr->value.c_str(); ++ } ++ ++ json_object_set_new(content, "mimeType", json_string(content_type)); ++ ++ json_object_set_new(response, "redirectURL", json_string("")); ++ json_object_set_new(response, "headersSize", json_integer(-1)); ++ json_object_set_new(response, "bodySize", json_integer(-1)); ++ json_object_set_new(entry, "cache", json_object()); ++ ++ auto timings = json_object(); ++ json_object_set_new(entry, "timings", timings); ++ ++ auto dns_timing = (i == 0) ? dns_delta : 0; ++ auto connect_timing = (i == 0) ? connect_delta : 0; ++ ++ json_object_set_new(timings, "dns", json_real(dns_timing)); ++ json_object_set_new(timings, "connect", json_real(connect_timing)); ++ ++ json_object_set_new(timings, "blocked", json_real(0.0)); ++ json_object_set_new(timings, "send", json_real(0.0)); ++ json_object_set_new(timings, "wait", json_real(wait_delta)); ++ json_object_set_new(timings, "receive", json_real(receive_delta)); ++ ++ json_object_set_new(entry, "pageref", json_string(PAGE_ID)); ++ json_object_set_new(entry, "connection", ++ json_string(util::utos(req->stream_id).c_str())); ++ } ++ ++ json_dumpf(root, outfile, JSON_PRESERVE_ORDER | JSON_INDENT(2)); ++ json_decref(root); ++} ++#endif // HAVE_JANSSON ++ ++namespace { ++void update_html_parser(HttpClient *client, Request *req, const uint8_t *data, ++ size_t len, int fin) { ++ if (!req->html_parser) { ++ return; ++ } ++ req->update_html_parser(data, len, fin); ++ ++ auto scheme = req->get_real_scheme(); ++ auto host = req->get_real_host(); ++ auto port = req->get_real_port(); ++ ++ for (auto &p : req->html_parser->get_links()) { ++ auto uri = strip_fragment(p.first.c_str()); ++ auto res_type = p.second; ++ ++ http_parser_url u{}; ++ if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { ++ continue; ++ } ++ ++ if (!util::fieldeq(uri.c_str(), u, UF_SCHEMA, scheme) || ++ !util::fieldeq(uri.c_str(), u, UF_HOST, host)) { ++ continue; ++ } ++ ++ auto link_port = util::has_uri_field(u, UF_PORT) ? u.port ++ : scheme == "https" ? 443 ++ : 80; ++ ++ if (port != link_port) { ++ continue; ++ } ++ ++ // No POST data for assets ++ auto pri_spec = resolve_dep(res_type); ++ ++ if (client->add_request(uri, nullptr, 0, pri_spec, req->level + 1)) { ++ submit_request(client, config.headers, client->reqvec.back().get()); ++ } ++ } ++ req->html_parser->clear_links(); ++} ++} // namespace ++ ++namespace { ++HttpClient *get_client(void *user_data) { ++ return static_cast(user_data); ++} ++} // 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 client = get_client(user_data); ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, stream_id)); ++ ++ if (!req) { ++ return 0; ++ } ++ ++ if (config.verbose >= 2) { ++ verbose_on_data_chunk_recv_callback(session, flags, stream_id, data, len, ++ user_data); ++ } ++ ++ req->response_len += len; ++ ++ if (req->inflater) { ++ while (len > 0) { ++ const size_t MAX_OUTLEN = 4_k; ++ std::array out; ++ size_t outlen = MAX_OUTLEN; ++ size_t tlen = len; ++ int rv = ++ nghttp2_gzip_inflate(req->inflater, out.data(), &outlen, data, &tlen); ++ if (rv != 0) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, stream_id, ++ NGHTTP2_INTERNAL_ERROR); ++ break; ++ } ++ ++ if (!config.null_out) { ++ std::cout.write(reinterpret_cast(out.data()), outlen); ++ } ++ ++ update_html_parser(client, req, out.data(), outlen, 0); ++ data += tlen; ++ len -= tlen; ++ } ++ ++ return 0; ++ } ++ ++ if (!config.null_out) { ++ std::cout.write(reinterpret_cast(data), len); ++ } ++ ++ update_html_parser(client, req, data, len, 0); ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++ssize_t select_padding_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, size_t max_payload, ++ void *user_data) { ++ return std::min(max_payload, frame->hd.length + config.padding); ++} ++} // namespace ++ ++namespace { ++void check_response_header(nghttp2_session *session, Request *req) { ++ bool gzip = false; ++ ++ req->expect_final_response = false; ++ ++ auto status_hd = req->get_res_header(http2::HD__STATUS); ++ ++ if (!status_hd) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, req->stream_id, ++ NGHTTP2_PROTOCOL_ERROR); ++ return; ++ } ++ ++ auto status = http2::parse_http_status_code(StringRef{status_hd->value}); ++ if (status == -1) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, req->stream_id, ++ NGHTTP2_PROTOCOL_ERROR); ++ return; ++ } ++ ++ req->status = status; ++ ++ for (auto &nv : req->res_nva) { ++ if ("content-encoding" == nv.name) { ++ gzip = util::strieq_l("gzip", nv.value) || ++ util::strieq_l("deflate", nv.value); ++ continue; ++ } ++ } ++ ++ if (req->status / 100 == 1) { ++ if (req->continue_timer && (req->status == 100)) { ++ // If the request is waiting for a 100 Continue, complete the handshake. ++ req->continue_timer->dispatch_continue(); ++ } ++ ++ req->expect_final_response = true; ++ req->status = 0; ++ req->res_nva.clear(); ++ http2::init_hdidx(req->res_hdidx); ++ return; ++ } else if (req->continue_timer) { ++ // A final response stops any pending Expect/Continue handshake. ++ req->continue_timer->stop(); ++ } ++ ++ if (gzip) { ++ if (!req->inflater) { ++ req->init_inflater(); ++ } ++ } ++ if (config.get_assets && req->level == 0) { ++ if (!req->html_parser) { ++ req->init_html_parser(); ++ } ++ } ++} ++} // namespace ++ ++namespace { ++int on_begin_headers_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, void *user_data) { ++ auto client = get_client(user_data); ++ switch (frame->hd.type) { ++ case NGHTTP2_HEADERS: { ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); ++ if (!req) { ++ break; ++ } ++ ++ switch (frame->headers.cat) { ++ case NGHTTP2_HCAT_RESPONSE: ++ case NGHTTP2_HCAT_PUSH_RESPONSE: ++ req->record_response_start_time(); ++ break; ++ default: ++ break; ++ } ++ ++ break; ++ } ++ case NGHTTP2_PUSH_PROMISE: { ++ auto stream_id = frame->push_promise.promised_stream_id; ++ http_parser_url u{}; ++ // TODO Set pri and level ++ nghttp2_priority_spec pri_spec; ++ ++ nghttp2_priority_spec_default_init(&pri_spec); ++ ++ auto req = std::make_unique("", u, nullptr, 0, pri_spec); ++ req->stream_id = stream_id; ++ ++ nghttp2_session_set_stream_user_data(session, stream_id, req.get()); ++ ++ client->request_done(req.get()); ++ req->record_request_start_time(); ++ client->reqvec.push_back(std::move(req)); ++ ++ break; ++ } ++ } ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, ++ const uint8_t *name, size_t namelen, ++ const uint8_t *value, size_t valuelen, uint8_t flags, ++ void *user_data) { ++ if (config.verbose) { ++ verbose_on_header_callback(session, frame, name, namelen, value, valuelen, ++ flags, user_data); ++ } ++ ++ switch (frame->hd.type) { ++ case NGHTTP2_HEADERS: { ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); ++ ++ if (!req) { ++ break; ++ } ++ ++ /* ignore trailer header */ ++ if (frame->headers.cat == NGHTTP2_HCAT_HEADERS && ++ !req->expect_final_response) { ++ break; ++ } ++ ++ if (req->header_buffer_size + namelen + valuelen > 64_k) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, ++ NGHTTP2_INTERNAL_ERROR); ++ return 0; ++ } ++ ++ req->header_buffer_size += namelen + valuelen; ++ ++ auto token = http2::lookup_token(name, namelen); ++ ++ http2::index_header(req->res_hdidx, token, req->res_nva.size()); ++ http2::add_header(req->res_nva, name, namelen, value, valuelen, ++ flags & NGHTTP2_NV_FLAG_NO_INDEX, token); ++ break; ++ } ++ case NGHTTP2_PUSH_PROMISE: { ++ auto req = static_cast(nghttp2_session_get_stream_user_data( ++ session, frame->push_promise.promised_stream_id)); ++ ++ if (!req) { ++ break; ++ } ++ ++ if (req->header_buffer_size + namelen + valuelen > 64_k) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, ++ frame->push_promise.promised_stream_id, ++ NGHTTP2_INTERNAL_ERROR); ++ return 0; ++ } ++ ++ req->header_buffer_size += namelen + valuelen; ++ ++ auto token = http2::lookup_token(name, namelen); ++ ++ http2::index_header(req->req_hdidx, token, req->req_nva.size()); ++ http2::add_header(req->req_nva, name, namelen, value, valuelen, ++ flags & NGHTTP2_NV_FLAG_NO_INDEX, token); ++ break; ++ } ++ } ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_frame_recv_callback2(nghttp2_session *session, ++ const nghttp2_frame *frame, void *user_data) { ++ int rv = 0; ++ ++ if (config.verbose) { ++ verbose_on_frame_recv_callback(session, frame, user_data); ++ } ++ ++ auto client = get_client(user_data); ++ switch (frame->hd.type) { ++ case NGHTTP2_DATA: { ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); ++ if (!req) { ++ return 0; ++ ; ++ } ++ ++ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { ++ req->record_response_end_time(); ++ ++client->success; ++ } ++ ++ break; ++ } ++ case NGHTTP2_HEADERS: { ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); ++ // If this is the HTTP Upgrade with OPTIONS method to avoid POST, ++ // req is nullptr. ++ if (!req) { ++ return 0; ++ ; ++ } ++ ++ switch (frame->headers.cat) { ++ case NGHTTP2_HCAT_RESPONSE: ++ case NGHTTP2_HCAT_PUSH_RESPONSE: ++ check_response_header(session, req); ++ break; ++ case NGHTTP2_HCAT_HEADERS: ++ if (req->expect_final_response) { ++ check_response_header(session, req); ++ break; ++ } ++ if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, ++ frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); ++ return 0; ++ } ++ break; ++ default: ++ assert(0); ++ } ++ ++ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { ++ req->record_response_end_time(); ++ ++client->success; ++ } ++ ++ break; ++ } ++ case NGHTTP2_SETTINGS: ++ if ((frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { ++ break; ++ } ++ //ev_timer_stop(client->loop, &client->settings_timer); ++ //verto_del(client->verto_settings_timer); ++ break; ++ case NGHTTP2_PUSH_PROMISE: { ++ auto req = static_cast(nghttp2_session_get_stream_user_data( ++ session, frame->push_promise.promised_stream_id)); ++ if (!req) { ++ break; ++ } ++ ++ // Reset for response header field reception ++ req->header_buffer_size = 0; ++ ++ auto scheme = req->get_req_header(http2::HD__SCHEME); ++ auto authority = req->get_req_header(http2::HD__AUTHORITY); ++ auto path = req->get_req_header(http2::HD__PATH); ++ ++ if (!authority) { ++ authority = req->get_req_header(http2::HD_HOST); ++ } ++ ++ // libnghttp2 guarantees :scheme, :method, :path and (:authority | ++ // host) exist and non-empty. ++ if (path->value[0] != '/') { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, ++ frame->push_promise.promised_stream_id, ++ NGHTTP2_PROTOCOL_ERROR); ++ break; ++ } ++ std::string uri = scheme->value; ++ uri += "://"; ++ uri += authority->value; ++ uri += path->value; ++ http_parser_url u{}; ++ if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, ++ frame->push_promise.promised_stream_id, ++ NGHTTP2_PROTOCOL_ERROR); ++ break; ++ } ++ req->uri = uri; ++ req->u = u; ++ ++ if (client->path_cache.count(uri)) { ++ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, ++ frame->push_promise.promised_stream_id, ++ NGHTTP2_CANCEL); ++ break; ++ } ++ ++ if (config.multiply == 1) { ++ client->path_cache.insert(uri); ++ } ++ ++ break; ++ } ++ } ++ return rv; ++} ++} // namespace ++ ++namespace { ++int before_frame_send_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, void *user_data) { ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_REQUEST) { ++ return 0; ++ } ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); ++ assert(req); ++ req->record_request_start_time(); ++ return 0; ++} ++ ++} // namespace ++ ++namespace { ++int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, ++ void *user_data) { ++ if (config.verbose) { ++ verbose_on_frame_send_callback(session, frame, user_data); ++ } ++ ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_REQUEST) { ++ return 0; ++ } ++ ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); ++ if (!req) { ++ return 0; ++ } ++ ++ // If this request is using Expect/Continue, start its ContinueTimer. ++ if (req->continue_timer) { ++ req->continue_timer->start(); ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_frame_not_send_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, int lib_error_code, ++ void *user_data) { ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_REQUEST) { ++ return 0; ++ } ++ ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); ++ if (!req) { ++ return 0; ++ } ++ ++ std::cerr << "[ERROR] request " << req->uri ++ << " failed: " << nghttp2_strerror(lib_error_code) << std::endl; ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, ++ uint32_t error_code, void *user_data) { ++ auto client = get_client(user_data); ++ auto req = static_cast( ++ nghttp2_session_get_stream_user_data(session, stream_id)); ++ ++ if (!req) { ++ return 0; ++ } ++ ++ // If this request is using Expect/Continue, stop its ContinueTimer. ++ if (req->continue_timer) { ++ req->continue_timer->stop(); ++ } ++ ++ update_html_parser(client, req, nullptr, 0, 1); ++ ++client->complete; ++ ++ if (client->all_requests_processed()) { ++ nghttp2_session_terminate_session(session, NGHTTP2_NO_ERROR); ++ } ++ ++ return 0; ++} ++} // namespace ++ ++struct RequestResult { ++ std::chrono::microseconds time; ++}; ++ ++namespace { ++void print_stats(const HttpClient &client) { ++ std::cout << "***** Statistics *****" << std::endl; ++ ++ std::vector reqs; ++ reqs.reserve(client.reqvec.size()); ++ for (const auto &req : client.reqvec) { ++ if (req->timing.state == RequestState::ON_COMPLETE) { ++ reqs.push_back(req.get()); ++ } ++ } ++ ++ std::sort(std::begin(reqs), std::end(reqs), ++ [](const Request *lhs, const Request *rhs) { ++ const auto <iming = lhs->timing; ++ const auto &rtiming = rhs->timing; ++ return ltiming.response_end_time < rtiming.response_end_time || ++ (ltiming.response_end_time == rtiming.response_end_time && ++ ltiming.request_start_time < rtiming.request_start_time); ++ }); ++ ++ std::cout << R"( ++Request timing: ++ responseEnd: the time when last byte of response was received ++ relative to connectEnd ++ requestStart: the time just before first byte of request was sent ++ relative to connectEnd. If '*' is shown, this was ++ pushed by server. ++ process: responseEnd - requestStart ++ code: HTTP status code ++ size: number of bytes received as response body without ++ inflation. ++ URI: request URI ++ ++see http://www.w3.org/TR/resource-timing/#processing-model ++ ++sorted by 'complete' ++ ++id responseEnd requestStart process code size request path)" ++ << std::endl; ++ ++ const auto &base = client.timing.connect_end_time; ++ for (const auto &req : reqs) { ++ auto response_end = std::chrono::duration_cast( ++ req->timing.response_end_time - base); ++ auto request_start = std::chrono::duration_cast( ++ req->timing.request_start_time - base); ++ auto total = std::chrono::duration_cast( ++ req->timing.response_end_time - req->timing.request_start_time); ++ auto pushed = req->stream_id % 2 == 0; ++ ++ std::cout << std::setw(3) << req->stream_id << " " << std::setw(11) ++ << ("+" + util::format_duration(response_end)) << " " ++ << (pushed ? "*" : " ") << std::setw(11) ++ << ("+" + util::format_duration(request_start)) << " " ++ << std::setw(8) << util::format_duration(total) << " " ++ << std::setw(4) << req->status << " " << std::setw(4) ++ << util::utos_unit(req->response_len) << " " ++ << req->make_reqpath() << std::endl; ++ } ++} ++} // namespace ++ ++#ifndef OPENSSL_NO_NEXTPROTONEG ++namespace { ++int client_select_next_proto_cb(SSL *ssl, unsigned char **out, ++ unsigned char *outlen, const unsigned char *in, ++ unsigned int inlen, void *arg) { ++ if (config.verbose) { ++ print_timer(); ++ std::cout << "[NPN] server offers:" << std::endl; ++ } ++ for (unsigned int i = 0; i < inlen; i += in[i] + 1) { ++ if (config.verbose) { ++ std::cout << " * "; ++ std::cout.write(reinterpret_cast(&in[i + 1]), in[i]); ++ std::cout << std::endl; ++ } ++ } ++ if (!util::select_h2(const_cast(out), outlen, in, ++ inlen)) { ++ print_protocol_nego_error(); ++ return SSL_TLSEXT_ERR_NOACK; ++ } ++ return SSL_TLSEXT_ERR_OK; ++} ++} // namespace ++#endif // !OPENSSL_NO_NEXTPROTONEG ++ ++namespace { ++int communicate( ++ const std::string &scheme, const std::string &host, uint16_t port, ++ std::vector< ++ std::tuple> ++ requests, ++ const nghttp2_session_callbacks *callbacks) { ++ int result = 0; ++ // Create an event loop for libverto ++ auto loop = verto_new(NULL, VERTO_EV_TYPE_NONE); ++ ++ SSL_CTX *ssl_ctx = nullptr; ++ if (scheme == "https") { ++ ssl_ctx = SSL_CTX_new(TLS_client_method()); ++ if (!ssl_ctx) { ++ std::cerr << "[ERROR] Failed to create SSL_CTX: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ result = -1; ++ goto fin; ++ } ++ ++ auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | ++ SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | ++ SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; ++ ++#ifdef SSL_OP_ENABLE_KTLS ++ if (config.ktls) { ++ ssl_opts |= SSL_OP_ENABLE_KTLS; ++ } ++#endif // SSL_OP_ENABLE_KTLS ++ ++ SSL_CTX_set_options(ssl_ctx, ssl_opts); ++ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); ++ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); ++ ++ if (SSL_CTX_set_default_verify_paths(ssl_ctx) != 1) { ++ std::cerr << "[WARNING] Could not load system trusted CA certificates: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ } ++ ++ if (nghttp2::tls::ssl_ctx_set_proto_versions( ++ ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, ++ nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { ++ std::cerr << "[ERROR] Could not set TLS versions" << std::endl; ++ result = -1; ++ goto fin; ++ } ++ ++ if (SSL_CTX_set_cipher_list(ssl_ctx, tls::DEFAULT_CIPHER_LIST) == 0) { ++ std::cerr << "[ERROR] " << ERR_error_string(ERR_get_error(), nullptr) ++ << std::endl; ++ result = -1; ++ goto fin; ++ } ++ if (!config.keyfile.empty()) { ++ if (SSL_CTX_use_PrivateKey_file(ssl_ctx, config.keyfile.c_str(), ++ SSL_FILETYPE_PEM) != 1) { ++ std::cerr << "[ERROR] " << ERR_error_string(ERR_get_error(), nullptr) ++ << std::endl; ++ result = -1; ++ goto fin; ++ } ++ } ++ if (!config.certfile.empty()) { ++ if (SSL_CTX_use_certificate_chain_file(ssl_ctx, ++ config.certfile.c_str()) != 1) { ++ std::cerr << "[ERROR] " << ERR_error_string(ERR_get_error(), nullptr) ++ << std::endl; ++ result = -1; ++ goto fin; ++ } ++ } ++#ifndef OPENSSL_NO_NEXTPROTONEG ++ SSL_CTX_set_next_proto_select_cb(ssl_ctx, client_select_next_proto_cb, ++ nullptr); ++#endif // !OPENSSL_NO_NEXTPROTONEG ++ ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L ++ auto proto_list = util::get_default_alpn(); ++ ++ SSL_CTX_set_alpn_protos(ssl_ctx, proto_list.data(), proto_list.size()); ++#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L ++ } ++ { ++ HttpClient client{callbacks, loop, ssl_ctx}; ++ ++ int32_t dep_stream_id = 0; ++ ++ if (!config.no_dep) { ++ dep_stream_id = anchors[ANCHOR_FOLLOWERS].stream_id; ++ } ++ ++ for (auto &req : requests) { ++ nghttp2_priority_spec pri_spec; ++ ++ nghttp2_priority_spec_init(&pri_spec, dep_stream_id, std::get<3>(req), 0); ++ ++ for (int i = 0; i < config.multiply; ++i) { ++ client.add_request(std::get<0>(req), std::get<1>(req), std::get<2>(req), ++ pri_spec); ++ } ++ } ++ client.update_hostport(); ++ ++ client.record_start_time(); ++ ++ if (client.resolve_host(host, port) != 0) { ++ goto fin; ++ } ++ ++ client.record_domain_lookup_end_time(); ++ ++ if (client.initiate_connection() != 0) { ++ std::cerr << "[ERROR] Could not connect to " << host << ", port " << port ++ << std::endl; ++ goto fin; ++ } ++ ++ // Start running the event loop ++ verto_run(loop); ++ ++#ifdef HAVE_JANSSON ++ if (!config.harfile.empty()) { ++ FILE *outfile; ++ if (config.harfile == "-") { ++ outfile = stdout; ++ } else { ++ outfile = fopen(config.harfile.c_str(), "wb"); ++ } ++ ++ if (outfile) { ++ client.output_har(outfile); ++ ++ if (outfile != stdout) { ++ fclose(outfile); ++ } ++ } else { ++ std::cerr << "Cannot open file " << config.harfile << ". " ++ << "har file could not be created." << std::endl; ++ } ++ } ++#endif // HAVE_JANSSON ++ ++ if (client.success != client.reqvec.size()) { ++ std::cerr << "Some requests were not processed. total=" ++ << client.reqvec.size() << ", processed=" << client.success ++ << std::endl; ++ } ++ if (config.stat) { ++ print_stats(client); ++ } ++ } ++fin: ++ if (ssl_ctx) { ++ SSL_CTX_free(ssl_ctx); ++ } ++ return result; ++} ++} // namespace ++ ++namespace { ++ssize_t file_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 req = static_cast( ++ nghttp2_session_get_stream_user_data(session, stream_id)); ++ assert(req); ++ int fd = source->fd; ++ ssize_t nread; ++ ++ while ((nread = pread(fd, buf, length, req->data_offset)) == -1 && ++ errno == EINTR) ++ ; ++ ++ if (nread == -1) { ++ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; ++ } ++ ++ req->data_offset += nread; ++ ++ if (req->data_offset == req->data_length) { ++ *data_flags |= NGHTTP2_DATA_FLAG_EOF; ++ if (!config.trailer.empty()) { ++ std::vector nva; ++ nva.reserve(config.trailer.size()); ++ for (auto &kv : config.trailer) { ++ nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); ++ } ++ 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; ++ } ++ } ++ ++ return nread; ++ } ++ ++ if (req->data_offset > req->data_length || nread == 0) { ++ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; ++ } ++ ++ return nread; ++} ++} // namespace ++ ++namespace { ++int run(char **uris, int n) { ++ nghttp2_session_callbacks *callbacks; ++ ++ nghttp2_session_callbacks_new(&callbacks); ++ auto cbsdel = defer(nghttp2_session_callbacks_del, callbacks); ++ ++ 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_callback2); ++ ++ if (config.verbose) { ++ nghttp2_session_callbacks_set_on_invalid_frame_recv_callback( ++ callbacks, verbose_on_invalid_frame_recv_callback); ++ ++ nghttp2_session_callbacks_set_error_callback2(callbacks, ++ verbose_error_callback); ++ } ++ ++ nghttp2_session_callbacks_set_on_data_chunk_recv_callback( ++ callbacks, on_data_chunk_recv_callback); ++ ++ nghttp2_session_callbacks_set_on_begin_headers_callback( ++ callbacks, on_begin_headers_callback); ++ ++ nghttp2_session_callbacks_set_on_header_callback(callbacks, ++ on_header_callback); ++ ++ nghttp2_session_callbacks_set_before_frame_send_callback( ++ callbacks, before_frame_send_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); ++ ++ if (config.padding) { ++ nghttp2_session_callbacks_set_select_padding_callback( ++ callbacks, select_padding_callback); ++ } ++ ++ std::string prev_scheme; ++ std::string prev_host; ++ uint16_t prev_port = 0; ++ int failures = 0; ++ int data_fd = -1; ++ nghttp2_data_provider data_prd; ++ struct stat data_stat; ++ ++ if (!config.datafile.empty()) { ++ if (config.datafile == "-") { ++ if (fstat(0, &data_stat) == 0 && ++ (data_stat.st_mode & S_IFMT) == S_IFREG) { ++ // use STDIN if it is a regular file ++ data_fd = 0; ++ } else { ++ // copy the contents of STDIN to a temporary file ++ char tempfn[] = "/tmp/nghttp.temp.XXXXXX"; ++ data_fd = mkstemp(tempfn); ++ if (data_fd == -1) { ++ std::cerr << "[ERROR] Could not create a temporary file in /tmp" ++ << std::endl; ++ return 1; ++ } ++ if (unlink(tempfn) != 0) { ++ std::cerr << "[WARNING] failed to unlink temporary file:" << tempfn ++ << std::endl; ++ } ++ while (1) { ++ std::array buf; ++ ssize_t rret, wret; ++ while ((rret = read(0, buf.data(), buf.size())) == -1 && ++ errno == EINTR) ++ ; ++ if (rret == 0) ++ break; ++ if (rret == -1) { ++ std::cerr << "[ERROR] I/O error while reading from STDIN" ++ << std::endl; ++ return 1; ++ } ++ while ((wret = write(data_fd, buf.data(), rret)) == -1 && ++ errno == EINTR) ++ ; ++ if (wret != rret) { ++ std::cerr << "[ERROR] I/O error while writing to temporary file" ++ << std::endl; ++ return 1; ++ } ++ } ++ if (fstat(data_fd, &data_stat) == -1) { ++ close(data_fd); ++ std::cerr << "[ERROR] Could not stat temporary file" << std::endl; ++ return 1; ++ } ++ } ++ } else { ++ data_fd = open(config.datafile.c_str(), O_RDONLY | O_BINARY); ++ if (data_fd == -1) { ++ std::cerr << "[ERROR] Could not open file " << config.datafile ++ << std::endl; ++ return 1; ++ } ++ if (fstat(data_fd, &data_stat) == -1) { ++ close(data_fd); ++ std::cerr << "[ERROR] Could not stat file " << config.datafile ++ << std::endl; ++ return 1; ++ } ++ } ++ data_prd.source.fd = data_fd; ++ data_prd.read_callback = file_read_callback; ++ } ++ std::vector< ++ std::tuple> ++ requests; ++ ++ size_t next_weight_idx = 0; ++ ++ for (int i = 0; i < n; ++i) { ++ http_parser_url u{}; ++ auto uri = strip_fragment(uris[i]); ++ if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { ++ ++next_weight_idx; ++ std::cerr << "[ERROR] Could not parse URI " << uri << std::endl; ++ continue; ++ } ++ if (!util::has_uri_field(u, UF_SCHEMA)) { ++ ++next_weight_idx; ++ std::cerr << "[ERROR] URI " << uri << " does not have scheme part" ++ << std::endl; ++ continue; ++ } ++ auto port = util::has_uri_field(u, UF_PORT) ++ ? u.port ++ : util::get_default_port(uri.c_str(), u); ++ auto host = decode_host(util::get_uri_field(uri.c_str(), u, UF_HOST)); ++ if (!util::fieldeq(uri.c_str(), u, UF_SCHEMA, prev_scheme.c_str()) || ++ host != prev_host || port != prev_port) { ++ if (!requests.empty()) { ++ if (communicate(prev_scheme, prev_host, prev_port, std::move(requests), ++ callbacks) != 0) { ++ ++failures; ++ } ++ requests.clear(); ++ } ++ prev_scheme = util::get_uri_field(uri.c_str(), u, UF_SCHEMA).str(); ++ prev_host = std::move(host); ++ prev_port = port; ++ } ++ requests.emplace_back(uri, data_fd == -1 ? nullptr : &data_prd, ++ data_stat.st_size, config.weight[next_weight_idx++]); ++ } ++ if (!requests.empty()) { ++ if (communicate(prev_scheme, prev_host, prev_port, std::move(requests), ++ callbacks) != 0) { ++ ++failures; ++ } ++ } ++ return failures; ++} ++} // namespace ++ ++namespace { ++void print_version(std::ostream &out) { ++ out << "nghttp nghttp2/" NGHTTP2_VERSION << std::endl; ++} ++} // namespace ++ ++namespace { ++void print_usage(std::ostream &out) { ++ out << R"(Usage: nghttp [OPTIONS]... ... ++HTTP/2 client)" ++ << std::endl; ++} ++} // namespace ++ ++namespace { ++void print_help(std::ostream &out) { ++ print_usage(out); ++ out << R"( ++ Specify URI to access. ++Options: ++ -v, --verbose ++ Print debug information such as reception and ++ transmission of frames and name/value pairs. Specifying ++ this option multiple times increases verbosity. ++ -n, --null-out ++ Discard downloaded data. ++ -O, --remote-name ++ Save download data in the current directory. The ++ filename is derived from URI. If URI ends with '/', ++ 'index.html' is used as a filename. Not implemented ++ yet. ++ -t, --timeout= ++ Timeout each request after . Set 0 to disable ++ timeout. ++ -w, --window-bits= ++ Sets the stream level initial window size to 2**-1. ++ -W, --connection-window-bits= ++ Sets the connection level initial window size to ++ 2**-1. ++ -a, --get-assets ++ Download assets such as stylesheets, images and script ++ files linked from the downloaded resource. Only links ++ whose origins are the same with the linking resource ++ will be downloaded. nghttp prioritizes resources using ++ HTTP/2 dependency based priority. The priority order, ++ from highest to lowest, is html itself, css, javascript ++ and images. ++ -s, --stat Print statistics. ++ -H, --header=
++ Add a header to the requests. Example: -H':method: PUT' ++ --trailer=
++ Add a trailer header to the requests.
must not ++ include pseudo header field (header field name starting ++ with ':'). To send trailer, one must use -d option to ++ send request body. Example: --trailer 'foo: bar'. ++ --cert= ++ Use the specified client certificate file. The file ++ must be in PEM format. ++ --key= Use the client private key file. The file must be in ++ PEM format. ++ -d, --data= ++ Post FILE to server. If '-' is given, data will be read ++ from stdin. ++ -m, --multiply= ++ Request each URI times. By default, same URI is not ++ requested twice. This option disables it too. ++ -u, --upgrade ++ Perform HTTP Upgrade for HTTP/2. This option is ignored ++ if the request URI has https scheme. If -d is used, the ++ HTTP upgrade request is performed with OPTIONS method. ++ -p, --weight= ++ Sets weight of given URI. This option can be used ++ multiple times, and N-th -p option sets weight of N-th ++ URI in the command line. If the number of -p option is ++ less than the number of URI, the last -p option value is ++ repeated. If there is no -p option, default weight, 16, ++ is assumed. The valid value range is ++ [)" ++ << NGHTTP2_MIN_WEIGHT << ", " << NGHTTP2_MAX_WEIGHT << R"(], inclusive. ++ -M, --peer-max-concurrent-streams= ++ Use as SETTINGS_MAX_CONCURRENT_STREAMS value of ++ remote endpoint as if it is received in SETTINGS frame. ++ Default: 100 ++ -c, --header-table-size= ++ Specify decoder header table size. If this option is ++ used multiple times, and the minimum value among the ++ given values except for last one is strictly less than ++ the last value, that minimum value is set in SETTINGS ++ frame payload before the last value, to simulate ++ multiple header table size change. ++ --encoder-header-table-size= ++ Specify encoder header table size. The decoder (server) ++ specifies the maximum dynamic table size it accepts. ++ Then the negotiated dynamic table size is the minimum of ++ this option value and the value which server specified. ++ -b, --padding= ++ Add at most bytes to a frame payload as padding. ++ Specify 0 to disable padding. ++ -r, --har= ++ Output HTTP transactions in HAR format. If '-' ++ is given, data is written to stdout. ++ --color Force colored log output. ++ --continuation ++ Send large header to test CONTINUATION. ++ --no-content-length ++ Don't send content-length header field. ++ --no-dep Don't send dependency based priority hint to server. ++ --hexdump Display the incoming traffic in hexadecimal (Canonical ++ hex+ASCII display). If SSL/TLS is used, decrypted data ++ are used. ++ --no-push Disable server push. ++ --max-concurrent-streams= ++ The number of concurrent pushed streams this client ++ accepts. ++ --expect-continue ++ Perform an Expect/Continue handshake: wait to send DATA ++ (up to a short timeout) until the server sends a 100 ++ Continue interim response. This option is ignored unless ++ combined with the -d option. ++ -y, --no-verify-peer ++ Suppress warning on server certificate verification ++ failure. ++ --ktls Enable ktls. ++ --no-rfc7540-pri ++ Disable RFC7540 priorities. ++ --version Display version information and exit. ++ -h, --help Display this help and exit. ++ ++-- ++ ++ The argument is an integer and an optional unit (e.g., 10K is ++ 10 * 1024). Units are K, M and G (powers of 1024). ++ ++ The argument is an integer and an optional unit (e.g., 1s ++ is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms ++ (hours, minutes, seconds and milliseconds, respectively). If a unit ++ is omitted, a second is used as unit.)" ++ << std::endl; ++} ++} // namespace ++ ++int main(int argc, char **argv) { ++ tls::libssl_init(); ++ ++ bool color = false; ++ while (1) { ++ static int flag = 0; ++ constexpr static option long_options[] = { ++ {"verbose", no_argument, nullptr, 'v'}, ++ {"null-out", no_argument, nullptr, 'n'}, ++ {"remote-name", no_argument, nullptr, 'O'}, ++ {"timeout", required_argument, nullptr, 't'}, ++ {"window-bits", required_argument, nullptr, 'w'}, ++ {"connection-window-bits", required_argument, nullptr, 'W'}, ++ {"get-assets", no_argument, nullptr, 'a'}, ++ {"stat", no_argument, nullptr, 's'}, ++ {"help", no_argument, nullptr, 'h'}, ++ {"header", required_argument, nullptr, 'H'}, ++ {"data", required_argument, nullptr, 'd'}, ++ {"multiply", required_argument, nullptr, 'm'}, ++ {"upgrade", no_argument, nullptr, 'u'}, ++ {"weight", required_argument, nullptr, 'p'}, ++ {"peer-max-concurrent-streams", required_argument, nullptr, 'M'}, ++ {"header-table-size", required_argument, nullptr, 'c'}, ++ {"padding", required_argument, nullptr, 'b'}, ++ {"har", required_argument, nullptr, 'r'}, ++ {"no-verify-peer", no_argument, nullptr, 'y'}, ++ {"cert", required_argument, &flag, 1}, ++ {"key", required_argument, &flag, 2}, ++ {"color", no_argument, &flag, 3}, ++ {"continuation", no_argument, &flag, 4}, ++ {"version", no_argument, &flag, 5}, ++ {"no-content-length", no_argument, &flag, 6}, ++ {"no-dep", no_argument, &flag, 7}, ++ {"trailer", required_argument, &flag, 9}, ++ {"hexdump", no_argument, &flag, 10}, ++ {"no-push", no_argument, &flag, 11}, ++ {"max-concurrent-streams", required_argument, &flag, 12}, ++ {"expect-continue", no_argument, &flag, 13}, ++ {"encoder-header-table-size", required_argument, &flag, 14}, ++ {"ktls", no_argument, &flag, 15}, ++ {"no-rfc7540-pri", no_argument, &flag, 16}, ++ {nullptr, 0, nullptr, 0}}; ++ int option_index = 0; ++ int c = ++ getopt_long(argc, argv, "M:Oab:c:d:m:np:r:hH:vst:uw:yW:", long_options, ++ &option_index); ++ if (c == -1) { ++ break; ++ } ++ switch (c) { ++ case 'M': { ++ // peer-max-concurrent-streams option ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-M: Bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.peer_max_concurrent_streams = n; ++ break; ++ } ++ case 'O': ++ config.remote_name = true; ++ break; ++ case 'h': ++ print_help(std::cout); ++ exit(EXIT_SUCCESS); ++ case 'b': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-b: Bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.padding = n; ++ break; ++ } ++ case 'n': ++ config.null_out = true; ++ break; ++ case 'p': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1 || NGHTTP2_MIN_WEIGHT > n || n > NGHTTP2_MAX_WEIGHT) { ++ std::cerr << "-p: specify the integer in the range [" ++ << NGHTTP2_MIN_WEIGHT << ", " << NGHTTP2_MAX_WEIGHT ++ << "], inclusive" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.weight.push_back(n); ++ break; ++ } ++ case 'r': ++#ifdef HAVE_JANSSON ++ config.harfile = optarg; ++#else // !HAVE_JANSSON ++ std::cerr << "[WARNING]: -r, --har option is ignored because\n" ++ << "the binary was not compiled with libjansson." << std::endl; ++#endif // !HAVE_JANSSON ++ break; ++ case 'v': ++ ++config.verbose; ++ break; ++ case 't': ++ config.timeout = util::parse_duration_with_unit(optarg); ++ if (config.timeout == std::numeric_limits::infinity()) { ++ std::cerr << "-t: bad timeout value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 'u': ++ config.upgrade = true; ++ break; ++ case 'w': ++ case 'W': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1 || n > 30) { ++ std::cerr << "-" << static_cast(c) ++ << ": specify the integer in the range [0, 30], inclusive" ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (c == 'w') { ++ config.window_bits = n; ++ } else { ++ config.connection_window_bits = n; ++ } ++ break; ++ } ++ case 'H': { ++ char *header = optarg; ++ // Skip first possible ':' in the header name ++ char *value = strchr(optarg + 1, ':'); ++ if (!value || (header[0] == ':' && header + 1 == value)) { ++ std::cerr << "-H: invalid header: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ *value = 0; ++ value++; ++ while (isspace(*value)) { ++ value++; ++ } ++ if (*value == 0) { ++ // This could also be a valid case for suppressing a header ++ // similar to curl ++ std::cerr << "-H: invalid header - value missing: " << optarg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.headers.emplace_back(header, value, false); ++ util::inp_strlower(config.headers.back().name); ++ break; ++ } ++ case 'a': ++#ifdef HAVE_LIBXML2 ++ config.get_assets = true; ++#else // !HAVE_LIBXML2 ++ std::cerr << "[WARNING]: -a, --get-assets option is ignored because\n" ++ << "the binary was not compiled with libxml2." << std::endl; ++#endif // !HAVE_LIBXML2 ++ break; ++ case 's': ++ config.stat = true; ++ break; ++ case 'd': ++ config.datafile = optarg; ++ break; ++ case 'm': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-m: Bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.multiply = n; ++ break; ++ } ++ case 'c': { ++ auto n = util::parse_uint_with_unit(optarg); ++ if (n == -1) { ++ std::cerr << "-c: Bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (n > std::numeric_limits::max()) { ++ std::cerr << "-c: Value too large. It should be less than or equal to " ++ << std::numeric_limits::max() << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.header_table_size = n; ++ config.min_header_table_size = std::min(config.min_header_table_size, n); ++ break; ++ } ++ case 'y': ++ config.verify_peer = false; ++ break; ++ case '?': ++ util::show_candidates(argv[optind - 1], long_options); ++ exit(EXIT_FAILURE); ++ case 0: ++ switch (flag) { ++ case 1: ++ // cert option ++ config.certfile = optarg; ++ break; ++ case 2: ++ // key option ++ config.keyfile = optarg; ++ break; ++ case 3: ++ // color option ++ color = true; ++ break; ++ case 4: ++ // continuation option ++ config.continuation = true; ++ break; ++ case 5: ++ // version option ++ print_version(std::cout); ++ exit(EXIT_SUCCESS); ++ case 6: ++ // no-content-length option ++ config.no_content_length = true; ++ break; ++ case 7: ++ // no-dep option ++ config.no_dep = true; ++ break; ++ case 9: { ++ // trailer option ++ auto header = optarg; ++ auto value = strchr(optarg, ':'); ++ if (!value) { ++ std::cerr << "--trailer: invalid header: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ *value = 0; ++ value++; ++ while (isspace(*value)) { ++ value++; ++ } ++ if (*value == 0) { ++ // This could also be a valid case for suppressing a header ++ // similar to curl ++ std::cerr << "--trailer: invalid header - value missing: " << optarg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.trailer.emplace_back(header, value, false); ++ util::inp_strlower(config.trailer.back().name); ++ break; ++ } ++ case 10: ++ // hexdump option ++ config.hexdump = true; ++ break; ++ case 11: ++ // no-push option ++ config.no_push = true; ++ break; ++ case 12: { ++ // max-concurrent-streams option ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "--max-concurrent-streams: Bad option value: " << optarg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.max_concurrent_streams = n; ++ break; ++ } ++ case 13: ++ // expect-continue option ++ config.expect_continue = true; ++ break; ++ case 14: { ++ // encoder-header-table-size option ++ auto n = util::parse_uint_with_unit(optarg); ++ if (n == -1) { ++ std::cerr << "--encoder-header-table-size: Bad option value: " ++ << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (n > std::numeric_limits::max()) { ++ std::cerr << "--encoder-header-table-size: Value too large. It " ++ "should be less than or equal to " ++ << std::numeric_limits::max() << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.encoder_header_table_size = n; ++ break; ++ } ++ case 15: ++ // ktls option ++ config.ktls = true; ++ break; ++ case 16: ++ // no-rfc7540-pri option ++ config.no_rfc7540_pri = true; ++ break; ++ } ++ break; ++ default: ++ break; ++ } ++ } ++ ++ int32_t weight_to_fill; ++ if (config.weight.empty()) { ++ weight_to_fill = NGHTTP2_DEFAULT_WEIGHT; ++ } else { ++ weight_to_fill = config.weight.back(); ++ } ++ config.weight.insert(std::end(config.weight), argc - optind, weight_to_fill); ++ ++ // Find scheme overridden by extra header fields. ++ auto scheme_it = ++ std::find_if(std::begin(config.headers), std::end(config.headers), ++ [](const Header &nv) { return nv.name == ":scheme"; }); ++ if (scheme_it != std::end(config.headers)) { ++ config.scheme_override = (*scheme_it).value; ++ } ++ ++ // Find host and port overridden by extra header fields. ++ auto authority_it = ++ std::find_if(std::begin(config.headers), std::end(config.headers), ++ [](const Header &nv) { return nv.name == ":authority"; }); ++ if (authority_it == std::end(config.headers)) { ++ authority_it = ++ std::find_if(std::begin(config.headers), std::end(config.headers), ++ [](const Header &nv) { return nv.name == "host"; }); ++ } ++ ++ if (authority_it != std::end(config.headers)) { ++ // authority_it may looks like "host:port". ++ auto uri = "https://" + (*authority_it).value; ++ http_parser_url u{}; ++ if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { ++ std::cerr << "[ERROR] Could not parse authority in " ++ << (*authority_it).name << ": " << (*authority_it).value ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ config.host_override = util::get_uri_field(uri.c_str(), u, UF_HOST).str(); ++ if (util::has_uri_field(u, UF_PORT)) { ++ config.port_override = u.port; ++ } ++ } ++ ++ set_color_output(color || isatty(fileno(stdout))); ++ ++ nghttp2_option_set_peer_max_concurrent_streams( ++ config.http2_option, config.peer_max_concurrent_streams); ++ ++ if (config.encoder_header_table_size != -1) { ++ nghttp2_option_set_max_deflate_dynamic_table_size( ++ config.http2_option, config.encoder_header_table_size); ++ } ++ ++ struct sigaction act {}; ++ act.sa_handler = SIG_IGN; ++ sigaction(SIGPIPE, &act, nullptr); ++ reset_timer(); ++ return run(argv + optind, argc - optind); ++} ++ ++} // namespace nghttp2 ++ ++int main(int argc, char **argv) { ++ return nghttp2::run_app(nghttp2::main, argc, argv); ++} +diff --git a/src/nghttp_verto.h b/src/nghttp_verto.h +new file mode 100644 +index 0000000..3a80144 +--- /dev/null ++++ b/src/nghttp_verto.h +@@ -0,0 +1,315 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2015 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. ++ */ ++#ifndef NGHTTP_H ++#define NGHTTP_H ++ ++#include "nghttp2_config.h" ++ ++#include ++#ifdef HAVE_SYS_SOCKET_H ++# include ++#endif // HAVE_SYS_SOCKET_H ++#ifdef HAVE_NETDB_H ++# include ++#endif // HAVE_NETDB_H ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include ++ ++#include ++ ++#include "llhttp.h" ++ ++#include "memchunk.h" ++#include "http2.h" ++#include "nghttp2_gzip.h" ++#include "template.h" ++ ++namespace nghttp2 { ++ ++class HtmlParser; ++ ++struct Config { ++ Config(); ++ ~Config(); ++ ++ Headers headers; ++ Headers trailer; ++ std::vector weight; ++ std::string certfile; ++ std::string keyfile; ++ std::string datafile; ++ std::string harfile; ++ std::string scheme_override; ++ std::string host_override; ++ nghttp2_option *http2_option; ++ int64_t header_table_size; ++ int64_t min_header_table_size; ++ int64_t encoder_header_table_size; ++ size_t padding; ++ size_t max_concurrent_streams; ++ ssize_t peer_max_concurrent_streams; ++ int multiply; ++ // milliseconds ++ double timeout; ++ int window_bits; ++ int connection_window_bits; ++ int verbose; ++ uint16_t port_override; ++ bool null_out; ++ bool remote_name; ++ bool get_assets; ++ bool stat; ++ bool upgrade; ++ bool continuation; ++ bool no_content_length; ++ bool no_dep; ++ bool hexdump; ++ bool no_push; ++ bool expect_continue; ++ bool verify_peer; ++ bool ktls; ++ bool no_rfc7540_pri; ++}; ++ ++enum class RequestState { INITIAL, ON_REQUEST, ON_RESPONSE, ON_COMPLETE }; ++ ++struct RequestTiming { ++ // The point in time when request is started to be sent. ++ // Corresponds to requestStart in Resource Timing TR. ++ std::chrono::steady_clock::time_point request_start_time; ++ // The point in time when first byte of response is received. ++ // Corresponds to responseStart in Resource Timing TR. ++ std::chrono::steady_clock::time_point response_start_time; ++ // The point in time when last byte of response is received. ++ // Corresponds to responseEnd in Resource Timing TR. ++ std::chrono::steady_clock::time_point response_end_time; ++ RequestState state; ++ RequestTiming() : state(RequestState::INITIAL) {} ++}; ++ ++struct Request; // forward declaration for ContinueTimer ++struct HttpClient; ++ ++struct ContinueTimer { ++ ContinueTimer(verto_ctx *verto_loop, Request *req, HttpClient *client); ++ ~ContinueTimer(); ++ ++ void start(); ++ void stop(); ++ ++ // Schedules an immediate run of the continue callback on the loop, if the ++ // callback has not already been run ++ void dispatch_continue(); ++ ++ verto_ctx *verto_loop; ++ verto_ev *verto_timer; ++ ++ Request *req; ++}; ++ ++struct Request { ++ // For pushed request, |uri| is empty and |u| is zero-cleared. ++ Request(const std::string &uri, const http_parser_url &u, ++ const nghttp2_data_provider *data_prd, int64_t data_length, ++ const nghttp2_priority_spec &pri_spec, int level = 0); ++ ~Request(); ++ ++ void init_inflater(); ++ ++ void init_html_parser(); ++ int update_html_parser(const uint8_t *data, size_t len, int fin); ++ ++ std::string make_reqpath() const; ++ ++ bool is_ipv6_literal_addr() const; ++ ++ Headers::value_type *get_res_header(int32_t token); ++ Headers::value_type *get_req_header(int32_t token); ++ ++ void record_request_start_time(); ++ void record_response_start_time(); ++ void record_response_end_time(); ++ ++ // Returns scheme taking into account overridden scheme. ++ StringRef get_real_scheme() const; ++ // Returns request host, without port, taking into account ++ // overridden host. ++ StringRef get_real_host() const; ++ // Returns request port, taking into account overridden host, port, ++ // and scheme. ++ uint16_t get_real_port() const; ++ ++ Headers res_nva; ++ Headers req_nva; ++ std::string method; ++ // URI without fragment ++ std::string uri; ++ http_parser_url u; ++ nghttp2_priority_spec pri_spec; ++ RequestTiming timing; ++ int64_t data_length; ++ int64_t data_offset; ++ // Number of bytes received from server ++ int64_t response_len; ++ nghttp2_gzip *inflater; ++ std::unique_ptr html_parser; ++ const nghttp2_data_provider *data_prd; ++ size_t header_buffer_size; ++ int32_t stream_id; ++ int status; ++ // Recursion level: 0: first entity, 1: entity linked from first entity ++ int level; ++ http2::HeaderIndex res_hdidx; ++ // used for incoming PUSH_PROMISE ++ http2::HeaderIndex req_hdidx; ++ bool expect_final_response; ++ // only assigned if this request is using Expect/Continue ++ std::unique_ptr continue_timer; ++ // Pointer to the current Client ++ HttpClient *client; ++}; ++ ++struct SessionTiming { ++ // The point in time when operation was started. Corresponds to ++ // startTime in Resource Timing TR, but recorded in system clock time. ++ std::chrono::system_clock::time_point system_start_time; ++ // Same as above, but recorded in steady clock time. ++ std::chrono::steady_clock::time_point start_time; ++ // The point in time when DNS resolution was completed. Corresponds ++ // to domainLookupEnd in Resource Timing TR. ++ std::chrono::steady_clock::time_point domain_lookup_end_time; ++ // The point in time when connection was established or SSL/TLS ++ // handshake was completed. Corresponds to connectEnd in Resource ++ // Timing TR. ++ std::chrono::steady_clock::time_point connect_end_time; ++}; ++ ++enum class ClientState { IDLE, CONNECTED }; ++ ++struct HttpClient { ++ HttpClient(const nghttp2_session_callbacks *callbacks, verto_ctx *verto_loop, ++ SSL_CTX *ssl_ctx); ++ ~HttpClient(); ++ ++ bool need_upgrade() const; ++ int resolve_host(const std::string &host, uint16_t port); ++ int initiate_connection(); ++ void disconnect(); ++ ++ int noop(); ++ int read_clear(); ++ int write_clear(); ++ int connected(); ++ int tls_handshake(); ++ int read_tls(); ++ int write_tls(); ++ ++ int do_read(); ++ int do_write(); ++ ++ int on_upgrade_connect(); ++ int on_upgrade_read(const uint8_t *data, size_t len); ++ int on_read(const uint8_t *data, size_t len); ++ int on_write(); ++ ++ int connection_made(); ++ void connect_fail(); ++ void request_done(Request *req); ++ ++ bool all_requests_processed() const; ++ void update_hostport(); ++ bool add_request(const std::string &uri, ++ const nghttp2_data_provider *data_prd, int64_t data_length, ++ const nghttp2_priority_spec &pri_spec, int level = 0); ++ ++ void record_start_time(); ++ void record_domain_lookup_end_time(); ++ void record_connect_end_time(); ++ ++#ifdef HAVE_JANSSON ++ void output_har(FILE *outfile); ++#endif // HAVE_JANSSON ++ ++ MemchunkPool mcpool; ++ DefaultMemchunks wb; ++ std::vector> reqvec; ++ // Insert path already added in reqvec to prevent multiple request ++ // for 1 resource. ++ std::set path_cache; ++ std::string scheme; ++ std::string host; ++ std::string hostport; ++ // Used for parse the HTTP upgrade response from server ++ std::unique_ptr htp; ++ SessionTiming timing; ++ // An observer for reading events ++ verto_ev *verto_wev; ++ // An observer for writing events ++ verto_ev *verto_rev; ++ // An observer for timeout events ++ verto_ev *verto_settings_timer; ++ std::function readfn, writefn; ++ std::function on_readfn; ++ std::function on_writefn; ++ nghttp2_session *session; ++ const nghttp2_session_callbacks *callbacks; ++ // The event loop for the nghttp client ++ verto_ctx *verto_loop; ++ SSL_CTX *ssl_ctx; ++ SSL *ssl; ++ addrinfo *addrs; ++ addrinfo *next_addr; ++ addrinfo *cur_addr; ++ // The number of completed requests, including failed ones. ++ size_t complete; ++ // The number of requests that local endpoint received END_STREAM ++ // from peer. ++ size_t success; ++ // The length of settings_payload ++ size_t settings_payloadlen; ++ ClientState state; ++ // The HTTP status code of the response message of HTTP Upgrade. ++ unsigned int upgrade_response_status_code; ++ int fd; ++ // true if the response message of HTTP Upgrade request is fully ++ // received. It is not relevant the upgrade succeeds, or not. ++ bool upgrade_response_complete; ++ // SETTINGS payload sent as token68 in HTTP Upgrade ++ std::array settings_payload; ++ ++ enum { ERR_CONNECT_FAIL = -100 }; ++}; ++ ++} // namespace nghttp2 ++ ++#endif // NGHTTP_H +-- +2.33.0 + diff --git a/0002-nghttpd-server-implementation-based-on-libverto.patch b/0002-nghttpd-server-implementation-based-on-libverto.patch new file mode 100644 index 0000000..551c0f3 --- /dev/null +++ b/0002-nghttpd-server-implementation-based-on-libverto.patch @@ -0,0 +1,3086 @@ +From 53e23c8f7181f9d2e5f9ade54193524602303a02 Mon Sep 17 00:00:00 2001 +From: dengjie <1171276417@qq.com> +Date: Thu, 26 Oct 2023 23:58:06 +0800 +Subject: [PATCH 2/4] nghttpd server implementation based on libverto + +--- + src/HttpServer_verto.cc | 2280 +++++++++++++++++++++++++++++++++++++++ + src/HttpServer_verto.h | 263 +++++ + src/nghttpd_verto.cc | 508 +++++++++ + 3 files changed, 3051 insertions(+) + create mode 100644 src/HttpServer_verto.cc + create mode 100644 src/HttpServer_verto.h + create mode 100644 src/nghttpd_verto.cc + +diff --git a/src/HttpServer_verto.cc b/src/HttpServer_verto.cc +new file mode 100644 +index 0000000..e69107c +--- /dev/null ++++ b/src/HttpServer_verto.cc +@@ -0,0 +1,2280 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2013 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 "HttpServer_verto.h" ++ ++#include ++#ifdef HAVE_SYS_SOCKET_H ++# include ++#endif // HAVE_SYS_SOCKET_H ++#ifdef HAVE_NETDB_H ++# include ++#endif // HAVE_NETDB_H ++#ifdef HAVE_UNISTD_H ++# include ++#endif // HAVE_UNISTD_H ++#ifdef HAVE_FCNTL_H ++# include ++#endif // HAVE_FCNTL_H ++#ifdef HAVE_NETINET_IN_H ++# include ++#endif // HAVE_NETINET_IN_H ++#include ++#ifdef HAVE_ARPA_INET_H ++# include ++#endif // HAVE_ARPA_INET_H ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "ssl_compat.h" ++ ++#include ++#include ++#if OPENSSL_3_0_0_API ++# include ++#endif // OPENSSL_3_0_0_API ++ ++#include ++ ++#include "app_helper.h" ++#include "http2.h" ++#include "util.h" ++#include "tls.h" ++#include "template.h" ++#include ++ ++#ifndef O_BINARY ++# define O_BINARY (0) ++#endif // O_BINARY ++ ++using namespace std::chrono_literals; ++ ++namespace nghttp2 { ++ ++namespace { ++// TODO could be constexpr ++constexpr auto DEFAULT_HTML = StringRef::from_lit("index.html"); ++constexpr auto NGHTTPD_SERVER = ++ StringRef::from_lit("nghttpd nghttp2/" NGHTTP2_VERSION); ++} // namespace ++ ++namespace { ++void delete_handler(Http2Handler *handler) { ++ handler->remove_self(); ++ delete handler; ++} ++} // namespace ++ ++namespace { ++void print_session_id(int64_t id) { std::cout << "[id=" << id << "] "; } ++} // namespace ++ ++Config::Config() ++ : mime_types_file("/etc/mime.types"), ++ verto_stream_read_timeout(1_min), ++ verto_stream_write_timeout(1_min), ++ data_ptr(nullptr), ++ padding(0), ++ num_worker(1), ++ max_concurrent_streams(100), ++ header_table_size(-1), ++ encoder_header_table_size(-1), ++ window_bits(-1), ++ connection_window_bits(-1), ++ port(0), ++ verbose(false), ++ daemon(false), ++ verify_client(false), ++ no_tls(false), ++ error_gzip(false), ++ early_response(false), ++ hexdump(false), ++ echo_upload(false), ++ no_content_length(false), ++ ktls(false), ++ no_rfc7540_pri(false) {} ++ ++Config::~Config() {} ++ ++namespace { ++void verto_stream_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ int rv; ++ auto stream = static_cast(verto_get_private(ev)); ++ auto hd = stream->handler; ++ auto config = hd->get_config(); ++ ++ if (config->verbose) { ++ print_session_id(hd->session_id()); ++ print_timer(); ++ std::cout << " timeout stream_id=" << stream->stream_id << std::endl; ++ } ++ ++ hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); ++ ++ rv = hd->on_write(); ++ if (rv == -1) { ++ delete_handler(hd); ++ } ++} ++} // namespace ++ ++namespace { ++void add_stream_read_timeout(Stream *stream) { ++ auto hd = stream->handler; ++ auto config = hd->get_config(); ++ stream->verto_rtimer = verto_add_timeout(hd->get_loop(), VERTO_EV_FLAG_PERSIST, verto_stream_timeout_cb, config->verto_stream_read_timeout * 1000); ++ verto_set_private(stream->verto_rtimer, stream, NULL); ++} ++} // namespace ++ ++namespace { ++void add_stream_read_timeout_if_pending(Stream *stream) { ++ auto hd = stream->handler; ++ auto config = hd->get_config(); ++ stream->verto_rtimer = verto_add_timeout(hd->get_loop(), VERTO_EV_FLAG_PERSIST, verto_stream_timeout_cb, config->verto_stream_read_timeout * 1000); ++ verto_set_private(stream->verto_rtimer, stream, NULL); ++} ++} // namespace ++ ++namespace { ++void add_stream_write_timeout(Stream *stream) { ++ auto hd = stream->handler; ++ auto config = hd->get_config(); ++ stream->verto_wtimer = verto_add_timeout(hd->get_loop(), VERTO_EV_FLAG_PERSIST, verto_stream_timeout_cb, config->verto_stream_write_timeout * 1000); ++ verto_set_private(stream->verto_wtimer, stream, NULL); ++} ++} // namespace ++ ++namespace { ++void remove_stream_read_timeout(Stream *stream) { ++ auto hd = stream->handler; ++ verto_del(stream->verto_rtimer); ++} ++} // namespace ++ ++namespace { ++void remove_stream_write_timeout(Stream *stream) { ++ auto hd = stream->handler; ++ verto_del(stream->verto_wtimer); ++} ++} // namespace ++ ++namespace { ++void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config); ++} // namespace ++ ++namespace { ++constexpr double RELEASE_FD_TIMEOUT = 2.; ++} // namespace ++ ++namespace { ++void verto_release_fd_cb(verto_ctx *verto_loop, verto_ev *ev); ++} // namespace ++ ++namespace { ++constexpr auto FILE_ENTRY_MAX_AGE = 10s; ++} // namespace ++ ++namespace { ++constexpr size_t FILE_ENTRY_EVICT_THRES = 2048; ++} // namespace ++ ++namespace { ++bool need_validation_file_entry( ++ const FileEntry *ent, const std::chrono::steady_clock::time_point &now) { ++ return ent->last_valid + FILE_ENTRY_MAX_AGE < now; ++} ++} // namespace ++ ++namespace { ++bool validate_file_entry(FileEntry *ent, ++ const std::chrono::steady_clock::time_point &now) { ++ struct stat stbuf; ++ int rv; ++ ++ rv = fstat(ent->fd, &stbuf); ++ if (rv != 0) { ++ ent->stale = true; ++ return false; ++ } ++ ++ if (stbuf.st_nlink == 0 || ent->mtime != stbuf.st_mtime) { ++ ent->stale = true; ++ return false; ++ } ++ ++ ent->mtime = stbuf.st_mtime; ++ ent->last_valid = now; ++ ++ return true; ++} ++} // namespace ++ ++namespace { ++double getCurrentTimeInSeconds() { ++ struct timespec ts; ++ clock_gettime(CLOCK_REALTIME, &ts); ++ ++ // 计算time_t类型的时间戳,以秒为单位 ++ double timestamp = (double)ts.tv_sec + (double)ts.tv_nsec / 1.0e9; ++ ++ return timestamp; ++ } ++} ++ ++class Sessions { ++public: ++ Sessions(HttpServer *sv, verto_ctx *verto_loop, const Config *config, ++ SSL_CTX *ssl_ctx) ++ : sv_(sv), ++ verto_loop_(verto_loop), ++ config_(config), ++ ssl_ctx_(ssl_ctx), ++ callbacks_(nullptr), ++ option_(nullptr), ++ next_session_id_(1), ++ verto_tstamp_cached_(getCurrentTimeInSeconds()), ++ cached_date_(util::http_date((long)verto_tstamp_cached_)) { ++ nghttp2_session_callbacks_new(&callbacks_); ++ ++ fill_callback(callbacks_, config_); ++ ++ nghttp2_option_new(&option_); ++ ++ if (config_->encoder_header_table_size != -1) { ++ nghttp2_option_set_max_deflate_dynamic_table_size( ++ option_, config_->encoder_header_table_size); ++ } ++ } ++ ++ ~Sessions() { ++ verto_del(verto_release_fd_timer_); ++ for (auto handler : handlers_) { ++ delete handler; ++ } ++ nghttp2_option_del(option_); ++ nghttp2_session_callbacks_del(callbacks_); ++ } ++ ++ void add_handler(Http2Handler *handler) { handlers_.insert(handler); } ++ ++ void remove_handler(Http2Handler *handler) { ++ handlers_.erase(handler); ++ if (handlers_.empty() && !fd_cache_.empty()) { ++ // Add release_fd_timer to the event loop ++ verto_release_fd_timer_ = verto_add_timeout(verto_loop_, VERTO_EV_FLAG_PERSIST, verto_release_fd_cb, RELEASE_FD_TIMEOUT * 1000); ++ verto_set_private(verto_release_fd_timer_, this, NULL); ++ } ++ } ++ ++ SSL_CTX *get_ssl_ctx() const { return ssl_ctx_; } ++ ++ SSL *ssl_session_new(int fd) { ++ SSL *ssl = SSL_new(ssl_ctx_); ++ if (!ssl) { ++ std::cerr << "SSL_new() failed" << std::endl; ++ return nullptr; ++ } ++ if (SSL_set_fd(ssl, fd) == 0) { ++ std::cerr << "SSL_set_fd() failed" << std::endl; ++ SSL_free(ssl); ++ return nullptr; ++ } ++ return ssl; ++ } ++ ++ const Config *get_config() const { return config_; } ++ ++ verto_ctx *get_loop() const { ++ return verto_loop_; ++ } ++ ++ int64_t get_next_session_id() { ++ auto session_id = next_session_id_; ++ if (next_session_id_ == std::numeric_limits::max()) { ++ next_session_id_ = 1; ++ } else { ++ ++next_session_id_; ++ } ++ return session_id; ++ } ++ ++ const nghttp2_session_callbacks *get_callbacks() const { return callbacks_; } ++ ++ const nghttp2_option *get_option() const { return option_; } ++ ++ void accept_connection(int fd) { ++ util::make_socket_nodelay(fd); ++ SSL *ssl = nullptr; ++ if (ssl_ctx_) { ++ ssl = ssl_session_new(fd); ++ if (!ssl) { ++ close(fd); ++ return; ++ } ++ } ++ auto handler = ++ std::make_unique(this, fd, ssl, get_next_session_id()); ++ if (!ssl) { ++ if (handler->connection_made() != 0) { ++ return; ++ } ++ } ++ add_handler(handler.release()); ++ } ++ ++ void update_cached_date() { cached_date_ = util::http_date(verto_tstamp_cached_); } ++ ++ const std::string &get_cached_date() { ++ return cached_date_; ++ } ++ ++ FileEntry *get_cached_fd(const std::string &path) { ++ auto range = fd_cache_.equal_range(path); ++ if (range.first == range.second) { ++ return nullptr; ++ } ++ auto now = std::chrono::steady_clock::now(); ++ ++ for (auto it = range.first; it != range.second;) { ++ auto &ent = (*it).second; ++ if (ent->stale) { ++ ++it; ++ continue; ++ } ++ if (need_validation_file_entry(ent.get(), now) && ++ !validate_file_entry(ent.get(), now)) { ++ if (ent->usecount == 0) { ++ fd_cache_lru_.remove(ent.get()); ++ close(ent->fd); ++ it = fd_cache_.erase(it); ++ continue; ++ } ++ ++it; ++ continue; ++ } ++ ++ fd_cache_lru_.remove(ent.get()); ++ fd_cache_lru_.append(ent.get()); ++ ++ ++ent->usecount; ++ return ent.get(); ++ } ++ return nullptr; ++ } ++ ++ FileEntry *cache_fd(const std::string &path, const FileEntry &ent) { ++#ifdef HAVE_STD_MAP_EMPLACE ++ auto rv = fd_cache_.emplace(path, std::make_unique(ent)); ++#else // !HAVE_STD_MAP_EMPLACE ++ // for gcc-4.7 ++ auto rv = fd_cache_.insert( ++ std::make_pair(path, std::make_unique(ent))); ++#endif // !HAVE_STD_MAP_EMPLACE ++ auto &res = (*rv).second; ++ res->it = rv; ++ fd_cache_lru_.append(res.get()); ++ ++ while (fd_cache_.size() > FILE_ENTRY_EVICT_THRES) { ++ auto ent = fd_cache_lru_.head; ++ if (ent->usecount) { ++ break; ++ } ++ fd_cache_lru_.remove(ent); ++ close(ent->fd); ++ fd_cache_.erase(ent->it); ++ } ++ ++ return res.get(); ++ } ++ ++ void release_fd(FileEntry *target) { ++ --target->usecount; ++ ++ if (target->usecount == 0 && target->stale) { ++ fd_cache_lru_.remove(target); ++ close(target->fd); ++ fd_cache_.erase(target->it); ++ return; ++ } ++ ++ // We use timer to close file descriptor and delete the entry from ++ // cache. The timer will be started when there is no handler. ++ } ++ ++ void release_unused_fd() { ++ for (auto i = std::begin(fd_cache_); i != std::end(fd_cache_);) { ++ auto &ent = (*i).second; ++ if (ent->usecount != 0) { ++ ++i; ++ continue; ++ } ++ ++ fd_cache_lru_.remove(ent.get()); ++ close(ent->fd); ++ i = fd_cache_.erase(i); ++ } ++ } ++ ++ const HttpServer *get_server() const { return sv_; } ++ ++ bool handlers_empty() const { return handlers_.empty(); } ++ ++private: ++ std::set handlers_; ++ // cache for file descriptors to read file. ++ std::multimap> fd_cache_; ++ DList fd_cache_lru_; ++ HttpServer *sv_; ++ const Config *config_; ++ SSL_CTX *ssl_ctx_; ++ nghttp2_session_callbacks *callbacks_; ++ nghttp2_option *option_; ++ int64_t next_session_id_; ++ std::string cached_date_; ++ ++ verto_ctx *verto_loop_; ++ verto_ev *verto_release_fd_timer_; ++ double verto_tstamp_cached_; ++}; ++ ++namespace { ++void verto_release_fd_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto sessions = static_cast(verto_get_private(ev)); ++ ++ verto_del(ev); ++ ++ if (!sessions->handlers_empty()) { ++ return; ++ } ++ ++ sessions->release_unused_fd(); ++} ++} // namespace ++ ++Stream::Stream(Http2Handler *handler, int32_t stream_id) ++ : balloc(1024, 1024), ++ header{}, ++ handler(handler), ++ file_ent(nullptr), ++ body_length(0), ++ body_offset(0), ++ header_buffer_size(0), ++ stream_id(stream_id), ++ echo_upload(false) { ++} ++ ++Stream::~Stream() { ++ if (file_ent != nullptr) { ++ auto sessions = handler->get_sessions(); ++ sessions->release_fd(file_ent); ++ } ++ ++ auto &rcbuf = header.rcbuf; ++ nghttp2_rcbuf_decref(rcbuf.method); ++ nghttp2_rcbuf_decref(rcbuf.scheme); ++ nghttp2_rcbuf_decref(rcbuf.authority); ++ nghttp2_rcbuf_decref(rcbuf.host); ++ nghttp2_rcbuf_decref(rcbuf.path); ++ nghttp2_rcbuf_decref(rcbuf.ims); ++ nghttp2_rcbuf_decref(rcbuf.expect); ++ ++ // Remove the Stream's timeout ++ verto_del(verto_rtimer); ++ verto_del(verto_wtimer); ++} ++ ++namespace { ++void on_session_closed(Http2Handler *hd, int64_t session_id) { ++ if (hd->get_config()->verbose) { ++ print_session_id(session_id); ++ print_timer(); ++ std::cout << " closed" << std::endl; ++ } ++} ++} // namespace ++ ++namespace { ++void verto_settings_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ int rv; ++ auto hd = static_cast(verto_get_private(ev)); ++ hd->terminate_session(NGHTTP2_SETTINGS_TIMEOUT); ++ rv = hd->on_write(); ++ if (rv == -1) { ++ delete_handler(hd); ++ } ++} ++} // namespace ++ ++namespace { ++void verto_writecb(verto_ctx *verto_loop, verto_ev *ev) { ++ int rv; ++ auto handler = static_cast(verto_get_private(ev)); ++ ++ rv = handler->on_write(); ++ if (rv == -1) { ++ delete_handler(handler); ++ } ++} ++} // namespace ++ ++namespace { ++void verto_readcb(verto_ctx *verto_loop, verto_ev *ev) { ++ int rv; ++ auto handler = static_cast(verto_get_private(ev)); ++ ++ rv = handler->on_read(); ++ if (rv == -1) { ++ delete_handler(handler); ++ } ++} ++} // namespace ++ ++Http2Handler::Http2Handler(Sessions *sessions, int fd, SSL *ssl, int64_t session_id) ++ : session_id_(session_id), ++ session_(nullptr), ++ sessions_(sessions), ++ ssl_(ssl), ++ data_pending_(nullptr), ++ data_pendinglen_(0), ++ fd_(fd) { ++ auto verto_loop = sessions_->get_loop(); ++ ++ // Add read event observers and write event observers to the event loop ++ verto_ev_flag read_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_READ); ++ verto_ev_flag write_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_WRITE); ++ ++ verto_rev_ = verto_add_io(verto_loop, read_flag, verto_readcb, fd); ++ verto_wev_ = verto_add_io(verto_loop, write_flag, verto_writecb, fd); ++ ++ verto_set_private(verto_rev_, this, NULL); ++ verto_set_private(verto_wev_, this, NULL); ++ ++ if (ssl) { ++ SSL_set_accept_state(ssl); ++ read_ = &Http2Handler::tls_handshake; ++ write_ = &Http2Handler::tls_handshake; ++ } else { ++ read_ = &Http2Handler::read_clear; ++ write_ = &Http2Handler::write_clear; ++ } ++} ++ ++Http2Handler::~Http2Handler() { ++ on_session_closed(this, session_id_); ++ nghttp2_session_del(session_); ++ if (ssl_) { ++ SSL_set_shutdown(ssl_, SSL_get_shutdown(ssl_) | SSL_RECEIVED_SHUTDOWN); ++ ERR_clear_error(); ++ SSL_shutdown(ssl_); ++ } ++ ++ // Remove read/write event observers listening on the current client ++ verto_del(verto_rev_); ++ verto_del(verto_wev_); ++ ++ if (ssl_) { ++ SSL_free(ssl_); ++ } ++ shutdown(fd_, SHUT_WR); ++ close(fd_); ++} ++ ++void Http2Handler::remove_self() { sessions_->remove_handler(this); } ++ ++verto_ctx *Http2Handler::get_loop() { ++ // Get the event loop for the current session ++ return sessions_->get_loop(); ++} ++ ++Http2Handler::WriteBuf *Http2Handler::get_wb() { return &wb_; } ++ ++void Http2Handler::start_settings_timer() { ++ verto_settings_timerev_ = verto_add_timeout(sessions_->get_loop(), VERTO_EV_FLAG_NONE, verto_settings_timeout_cb, 10 * 1000); ++ verto_set_private(verto_settings_timerev_, this, NULL); ++ ++} ++ ++int Http2Handler::fill_wb() { ++ if (data_pending_) { ++ auto n = std::min(wb_.wleft(), data_pendinglen_); ++ wb_.write(data_pending_, n); ++ if (n < data_pendinglen_) { ++ data_pending_ += n; ++ data_pendinglen_ -= n; ++ return 0; ++ } ++ ++ data_pending_ = nullptr; ++ data_pendinglen_ = 0; ++ } ++ ++ for (;;) { ++ const uint8_t *data; ++ auto datalen = nghttp2_session_mem_send(session_, &data); ++ ++ if (datalen < 0) { ++ std::cerr << "nghttp2_session_mem_send() returned error: " ++ << nghttp2_strerror(datalen) << std::endl; ++ return -1; ++ } ++ if (datalen == 0) { ++ break; ++ } ++ auto n = wb_.write(data, datalen); ++ if (n < static_cast(datalen)) { ++ data_pending_ = data + n; ++ data_pendinglen_ = datalen - n; ++ break; ++ } ++ } ++ return 0; ++} ++ ++int Http2Handler::read_clear() { ++ int rv; ++ std::array buf; ++ ++ ssize_t nread; ++ while ((nread = read(fd_, buf.data(), buf.size())) == -1 && errno == EINTR) ++ ; ++ if (nread == -1) { ++ if (errno == EAGAIN || errno == EWOULDBLOCK) { ++ return write_(*this); ++ } ++ return -1; ++ } ++ if (nread == 0) { ++ return -1; ++ } ++ ++ if (get_config()->hexdump) { ++ util::hexdump(stdout, buf.data(), nread); ++ } ++ ++ rv = nghttp2_session_mem_recv(session_, buf.data(), nread); ++ if (rv < 0) { ++ if (rv != NGHTTP2_ERR_BAD_CLIENT_MAGIC) { ++ std::cerr << "nghttp2_session_mem_recv() returned error: " ++ << nghttp2_strerror(rv) << std::endl; ++ } ++ return -1; ++ } ++ ++ return write_(*this); ++} ++ ++int Http2Handler::write_clear() { ++ for (;;) { ++ if (wb_.rleft() > 0) { ++ ssize_t nwrite; ++ while ((nwrite = write(fd_, wb_.pos, wb_.rleft())) == -1 && ++ errno == EINTR) ++ ; ++ if (nwrite == -1) { ++ if (errno == EAGAIN || errno == EWOULDBLOCK) { ++ return 0; ++ } ++ return -1; ++ } ++ wb_.drain(nwrite); ++ continue; ++ } ++ wb_.reset(); ++ if (fill_wb() != 0) { ++ return -1; ++ } ++ if (wb_.rleft() == 0) { ++ break; ++ } ++ } ++ ++ if (nghttp2_session_want_read(session_) == 0 && ++ nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int Http2Handler::tls_handshake() { ++ ERR_clear_error(); ++ ++ auto rv = SSL_do_handshake(ssl_); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl_, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ return 0; ++ case SSL_ERROR_WANT_WRITE: ++ return 0; ++ default: ++ return -1; ++ } ++ } ++ ++ if (sessions_->get_config()->verbose) { ++ std::cerr << "SSL/TLS handshake completed" << std::endl; ++ } ++ ++ if (verify_npn_result() != 0) { ++ return -1; ++ } ++ ++ read_ = &Http2Handler::read_tls; ++ write_ = &Http2Handler::write_tls; ++ ++ if (connection_made() != 0) { ++ return -1; ++ } ++ ++ if (sessions_->get_config()->verbose) { ++ if (SSL_session_reused(ssl_)) { ++ std::cerr << "SSL/TLS session reused" << std::endl; ++ } ++ } ++ ++ return 0; ++} ++ ++int Http2Handler::read_tls() { ++ std::array buf; ++ ++ ERR_clear_error(); ++ ++ for (;;) { ++ auto rv = SSL_read(ssl_, buf.data(), buf.size()); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl_, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ return write_(*this); ++ case SSL_ERROR_WANT_WRITE: ++ // renegotiation started ++ return -1; ++ default: ++ return -1; ++ } ++ } ++ ++ auto nread = rv; ++ ++ if (get_config()->hexdump) { ++ util::hexdump(stdout, buf.data(), nread); ++ } ++ ++ rv = nghttp2_session_mem_recv(session_, buf.data(), nread); ++ if (rv < 0) { ++ if (rv != NGHTTP2_ERR_BAD_CLIENT_MAGIC) { ++ std::cerr << "nghttp2_session_mem_recv() returned error: " ++ << nghttp2_strerror(rv) << std::endl; ++ } ++ return -1; ++ } ++ } ++} ++ ++int Http2Handler::write_tls() { ++ ERR_clear_error(); ++ ++ for (;;) { ++ if (wb_.rleft() > 0) { ++ auto rv = SSL_write(ssl_, wb_.pos, wb_.rleft()); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl_, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ return -1; ++ case SSL_ERROR_WANT_WRITE: ++ return 0; ++ default: ++ return -1; ++ } ++ } ++ ++ wb_.drain(rv); ++ continue; ++ } ++ wb_.reset(); ++ if (fill_wb() != 0) { ++ return -1; ++ } ++ if (wb_.rleft() == 0) { ++ break; ++ } ++ } ++ ++ if (nghttp2_session_want_read(session_) == 0 && ++ nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int Http2Handler::on_read() { return read_(*this); } ++ ++int Http2Handler::on_write() { return write_(*this); } ++ ++int Http2Handler::connection_made() { ++ int r; ++ ++ r = nghttp2_session_server_new2(&session_, sessions_->get_callbacks(), this, ++ sessions_->get_option()); ++ ++ if (r != 0) { ++ return r; ++ } ++ ++ auto config = sessions_->get_config(); ++ std::array entry; ++ size_t niv = 1; ++ ++ entry[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; ++ entry[0].value = config->max_concurrent_streams; ++ ++ if (config->header_table_size >= 0) { ++ entry[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; ++ entry[niv].value = config->header_table_size; ++ ++niv; ++ } ++ ++ if (config->window_bits != -1) { ++ entry[niv].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; ++ entry[niv].value = (1 << config->window_bits) - 1; ++ ++niv; ++ } ++ ++ if (config->no_rfc7540_pri) { ++ entry[niv].settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; ++ entry[niv].value = 1; ++ ++niv; ++ } ++ ++ r = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry.data(), niv); ++ if (r != 0) { ++ return r; ++ } ++ ++ if (config->connection_window_bits != -1) { ++ r = nghttp2_session_set_local_window_size( ++ session_, NGHTTP2_FLAG_NONE, 0, ++ (1 << config->connection_window_bits) - 1); ++ if (r != 0) { ++ return r; ++ } ++ } ++ ++ if (ssl_ && !nghttp2::tls::check_http2_requirement(ssl_)) { ++ terminate_session(NGHTTP2_INADEQUATE_SECURITY); ++ } ++ ++ return on_write(); ++} ++ ++int Http2Handler::verify_npn_result() { ++ const unsigned char *next_proto = nullptr; ++ unsigned int next_proto_len; ++ // Check the negotiated protocol in NPN or ALPN ++#ifndef OPENSSL_NO_NEXTPROTONEG ++ SSL_get0_next_proto_negotiated(ssl_, &next_proto, &next_proto_len); ++#endif // !OPENSSL_NO_NEXTPROTONEG ++ for (int i = 0; i < 2; ++i) { ++ if (next_proto) { ++ auto proto = StringRef{next_proto, next_proto_len}; ++ if (sessions_->get_config()->verbose) { ++ std::cout << "The negotiated protocol: " << proto << std::endl; ++ } ++ if (util::check_h2_is_selected(proto)) { ++ return 0; ++ } ++ break; ++ } else { ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L ++ SSL_get0_alpn_selected(ssl_, &next_proto, &next_proto_len); ++#else // OPENSSL_VERSION_NUMBER < 0x10002000L ++ break; ++#endif // OPENSSL_VERSION_NUMBER < 0x10002000L ++ } ++ } ++ if (sessions_->get_config()->verbose) { ++ std::cerr << "Client did not advertise HTTP/2 protocol." ++ << " (nghttp2 expects " << NGHTTP2_PROTO_VERSION_ID << ")" ++ << std::endl; ++ } ++ return -1; ++} ++ ++int Http2Handler::submit_file_response(const StringRef &status, Stream *stream, ++ time_t last_modified, off_t file_length, ++ const std::string *content_type, ++ nghttp2_data_provider *data_prd) { ++ std::string last_modified_str; ++ auto nva = make_array(http2::make_nv_ls_nocopy(":status", status), ++ http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER), ++ http2::make_nv_ll("cache-control", "max-age=3600"), ++ http2::make_nv_ls("date", sessions_->get_cached_date()), ++ http2::make_nv_ll("", ""), http2::make_nv_ll("", ""), ++ http2::make_nv_ll("", ""), http2::make_nv_ll("", "")); ++ size_t nvlen = 4; ++ if (!get_config()->no_content_length) { ++ nva[nvlen++] = http2::make_nv_ls_nocopy( ++ "content-length", ++ util::make_string_ref_uint(stream->balloc, file_length)); ++ } ++ if (last_modified != 0) { ++ last_modified_str = util::http_date(last_modified); ++ nva[nvlen++] = http2::make_nv_ls("last-modified", last_modified_str); ++ } ++ if (content_type) { ++ nva[nvlen++] = http2::make_nv_ls("content-type", *content_type); ++ } ++ auto &trailer_names = get_config()->trailer_names; ++ if (!trailer_names.empty()) { ++ nva[nvlen++] = http2::make_nv_ls_nocopy("trailer", trailer_names); ++ } ++ return nghttp2_submit_response(session_, stream->stream_id, nva.data(), nvlen, ++ data_prd); ++} ++ ++int Http2Handler::submit_response(const StringRef &status, int32_t stream_id, ++ const HeaderRefs &headers, ++ nghttp2_data_provider *data_prd) { ++ auto nva = std::vector(); ++ nva.reserve(4 + headers.size()); ++ nva.push_back(http2::make_nv_ls_nocopy(":status", status)); ++ nva.push_back(http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER)); ++ nva.push_back(http2::make_nv_ls("date", sessions_->get_cached_date())); ++ ++ if (data_prd) { ++ auto &trailer_names = get_config()->trailer_names; ++ if (!trailer_names.empty()) { ++ nva.push_back(http2::make_nv_ls_nocopy("trailer", trailer_names)); ++ } ++ } ++ ++ for (auto &nv : headers) { ++ nva.push_back(http2::make_nv_nocopy(nv.name, nv.value, nv.no_index)); ++ } ++ int r = nghttp2_submit_response(session_, stream_id, nva.data(), nva.size(), ++ data_prd); ++ return r; ++} ++ ++int Http2Handler::submit_response(const StringRef &status, int32_t stream_id, ++ nghttp2_data_provider *data_prd) { ++ auto nva = make_array(http2::make_nv_ls_nocopy(":status", status), ++ http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER), ++ http2::make_nv_ls("date", sessions_->get_cached_date()), ++ http2::make_nv_ll("", "")); ++ size_t nvlen = 3; ++ ++ if (data_prd) { ++ auto &trailer_names = get_config()->trailer_names; ++ if (!trailer_names.empty()) { ++ nva[nvlen++] = http2::make_nv_ls_nocopy("trailer", trailer_names); ++ } ++ } ++ ++ return nghttp2_submit_response(session_, stream_id, nva.data(), nvlen, ++ data_prd); ++} ++ ++int Http2Handler::submit_non_final_response(const std::string &status, ++ int32_t stream_id) { ++ auto nva = make_array(http2::make_nv_ls(":status", status)); ++ return nghttp2_submit_headers(session_, NGHTTP2_FLAG_NONE, stream_id, nullptr, ++ nva.data(), nva.size(), nullptr); ++} ++ ++int Http2Handler::submit_push_promise(Stream *stream, ++ const StringRef &push_path) { ++ auto authority = stream->header.authority; ++ ++ if (authority.empty()) { ++ authority = stream->header.host; ++ } ++ ++ auto scheme = get_config()->no_tls ? StringRef::from_lit("http") ++ : StringRef::from_lit("https"); ++ ++ auto nva = make_array(http2::make_nv_ll(":method", "GET"), ++ http2::make_nv_ls_nocopy(":path", push_path), ++ http2::make_nv_ls_nocopy(":scheme", scheme), ++ http2::make_nv_ls_nocopy(":authority", authority)); ++ ++ auto promised_stream_id = nghttp2_submit_push_promise( ++ session_, NGHTTP2_FLAG_END_HEADERS, stream->stream_id, nva.data(), ++ nva.size(), nullptr); ++ ++ if (promised_stream_id < 0) { ++ return promised_stream_id; ++ } ++ ++ auto promised_stream = std::make_unique(this, promised_stream_id); ++ ++ auto &promised_header = promised_stream->header; ++ promised_header.method = StringRef::from_lit("GET"); ++ promised_header.path = push_path; ++ promised_header.scheme = scheme; ++ promised_header.authority = ++ make_string_ref(promised_stream->balloc, authority); ++ ++ add_stream(promised_stream_id, std::move(promised_stream)); ++ ++ return 0; ++} ++ ++int Http2Handler::submit_rst_stream(Stream *stream, uint32_t error_code) { ++ remove_stream_read_timeout(stream); ++ remove_stream_write_timeout(stream); ++ ++ return nghttp2_submit_rst_stream(session_, NGHTTP2_FLAG_NONE, ++ stream->stream_id, error_code); ++} ++ ++void Http2Handler::add_stream(int32_t stream_id, ++ std::unique_ptr stream) { ++ id2stream_[stream_id] = std::move(stream); ++} ++ ++void Http2Handler::remove_stream(int32_t stream_id) { ++ id2stream_.erase(stream_id); ++} ++ ++Stream *Http2Handler::get_stream(int32_t stream_id) { ++ auto itr = id2stream_.find(stream_id); ++ if (itr == std::end(id2stream_)) { ++ return nullptr; ++ } else { ++ return (*itr).second.get(); ++ } ++} ++ ++int64_t Http2Handler::session_id() const { return session_id_; } ++ ++Sessions *Http2Handler::get_sessions() const { return sessions_; } ++ ++const Config *Http2Handler::get_config() const { ++ return sessions_->get_config(); ++} ++ ++void Http2Handler::remove_settings_timer() { ++ verto_del(verto_settings_timerev_); ++} ++ ++void Http2Handler::terminate_session(uint32_t error_code) { ++ nghttp2_session_terminate_session(session_, error_code); ++} ++ ++ssize_t file_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 hd = static_cast(user_data); ++ auto stream = hd->get_stream(stream_id); ++ ++ auto nread = std::min(stream->body_length - stream->body_offset, ++ static_cast(length)); ++ ++ *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; ++ ++ if (nread == 0 || stream->body_length == stream->body_offset + nread) { ++ *data_flags |= NGHTTP2_DATA_FLAG_EOF; ++ ++ auto config = hd->get_config(); ++ if (!config->trailer.empty()) { ++ std::vector nva; ++ nva.reserve(config->trailer.size()); ++ for (auto &kv : config->trailer) { ++ nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); ++ } ++ 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 (nghttp2_session_get_stream_remote_close(session, stream_id) == 0) { ++ remove_stream_read_timeout(stream); ++ remove_stream_write_timeout(stream); ++ ++ hd->submit_rst_stream(stream, NGHTTP2_NO_ERROR); ++ } ++ } ++ ++ return nread; ++} ++ ++namespace { ++void prepare_status_response(Stream *stream, Http2Handler *hd, int status) { ++ auto sessions = hd->get_sessions(); ++ auto status_page = sessions->get_server()->get_status_page(status); ++ auto file_ent = &status_page->file_ent; ++ ++ // we don't set stream->file_ent since we don't want to expire it. ++ stream->body_length = file_ent->length; ++ nghttp2_data_provider data_prd; ++ data_prd.source.fd = file_ent->fd; ++ data_prd.read_callback = file_read_callback; ++ ++ HeaderRefs headers; ++ headers.reserve(2); ++ headers.emplace_back(StringRef::from_lit("content-type"), ++ StringRef::from_lit("text/html; charset=UTF-8")); ++ headers.emplace_back( ++ StringRef::from_lit("content-length"), ++ util::make_string_ref_uint(stream->balloc, file_ent->length)); ++ hd->submit_response(StringRef{status_page->status}, stream->stream_id, ++ headers, &data_prd); ++} ++} // namespace ++ ++namespace { ++void prepare_echo_response(Stream *stream, Http2Handler *hd) { ++ auto length = lseek(stream->file_ent->fd, 0, SEEK_END); ++ if (length == -1) { ++ hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); ++ return; ++ } ++ stream->body_length = length; ++ if (lseek(stream->file_ent->fd, 0, SEEK_SET) == -1) { ++ hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); ++ return; ++ } ++ nghttp2_data_provider data_prd; ++ data_prd.source.fd = stream->file_ent->fd; ++ data_prd.read_callback = file_read_callback; ++ ++ HeaderRefs headers; ++ headers.emplace_back(StringRef::from_lit("nghttpd-response"), ++ StringRef::from_lit("echo")); ++ if (!hd->get_config()->no_content_length) { ++ headers.emplace_back(StringRef::from_lit("content-length"), ++ util::make_string_ref_uint(stream->balloc, length)); ++ } ++ ++ hd->submit_response(StringRef::from_lit("200"), stream->stream_id, headers, ++ &data_prd); ++} ++} // namespace ++ ++namespace { ++bool prepare_upload_temp_store(Stream *stream, Http2Handler *hd) { ++ auto sessions = hd->get_sessions(); ++ ++ char tempfn[] = "/tmp/nghttpd.temp.XXXXXX"; ++ auto fd = mkstemp(tempfn); ++ if (fd == -1) { ++ return false; ++ } ++ unlink(tempfn); ++ // Ordinary request never start with "echo:". The length is 0 for ++ // now. We will update it when we get whole request body. ++ auto path = std::string("echo:") + tempfn; ++ stream->file_ent = ++ sessions->cache_fd(path, FileEntry(path, 0, 0, fd, nullptr, {}, true)); ++ stream->echo_upload = true; ++ return true; ++} ++} // namespace ++ ++namespace { ++void prepare_redirect_response(Stream *stream, Http2Handler *hd, ++ const StringRef &path, int status) { ++ auto scheme = stream->header.scheme; ++ ++ auto authority = stream->header.authority; ++ if (authority.empty()) { ++ authority = stream->header.host; ++ } ++ ++ auto location = concat_string_ref( ++ stream->balloc, scheme, StringRef::from_lit("://"), authority, path); ++ ++ auto headers = HeaderRefs{{StringRef::from_lit("location"), location}}; ++ ++ auto sessions = hd->get_sessions(); ++ auto status_page = sessions->get_server()->get_status_page(status); ++ ++ hd->submit_response(StringRef{status_page->status}, stream->stream_id, ++ headers, nullptr); ++} ++} // namespace ++ ++namespace { ++void prepare_response(Stream *stream, Http2Handler *hd, ++ bool allow_push = true) { ++ int rv; ++ auto reqpath = stream->header.path; ++ if (reqpath.empty()) { ++ prepare_status_response(stream, hd, 405); ++ return; ++ } ++ ++ auto ims = stream->header.ims; ++ ++ time_t last_mod = 0; ++ bool last_mod_found = false; ++ if (!ims.empty()) { ++ last_mod_found = true; ++ last_mod = util::parse_http_date(ims); ++ } ++ ++ StringRef raw_path, raw_query; ++ auto query_pos = std::find(std::begin(reqpath), std::end(reqpath), '?'); ++ if (query_pos != std::end(reqpath)) { ++ // Do not response to this request to allow clients to test timeouts. ++ if (util::streq_l("nghttpd_do_not_respond_to_req=yes", ++ StringRef{query_pos, std::end(reqpath)})) { ++ return; ++ } ++ raw_path = StringRef{std::begin(reqpath), query_pos}; ++ raw_query = StringRef{query_pos, std::end(reqpath)}; ++ } else { ++ raw_path = reqpath; ++ } ++ ++ auto sessions = hd->get_sessions(); ++ ++ StringRef path; ++ if (std::find(std::begin(raw_path), std::end(raw_path), '%') == ++ std::end(raw_path)) { ++ path = raw_path; ++ } else { ++ path = util::percent_decode(stream->balloc, raw_path); ++ } ++ ++ path = http2::path_join(stream->balloc, StringRef{}, StringRef{}, path, ++ StringRef{}); ++ ++ if (std::find(std::begin(path), std::end(path), '\\') != std::end(path)) { ++ if (stream->file_ent) { ++ sessions->release_fd(stream->file_ent); ++ stream->file_ent = nullptr; ++ } ++ prepare_status_response(stream, hd, 404); ++ return; ++ } ++ ++ if (!hd->get_config()->push.empty()) { ++ auto push_itr = hd->get_config()->push.find(path.str()); ++ if (allow_push && push_itr != std::end(hd->get_config()->push)) { ++ for (auto &push_path : (*push_itr).second) { ++ rv = hd->submit_push_promise(stream, StringRef{push_path}); ++ if (rv != 0) { ++ std::cerr << "nghttp2_submit_push_promise() returned error: " ++ << nghttp2_strerror(rv) << std::endl; ++ } ++ } ++ } ++ } ++ ++ std::string file_path; ++ { ++ auto len = hd->get_config()->htdocs.size() + path.size(); ++ ++ auto trailing_slash = path[path.size() - 1] == '/'; ++ if (trailing_slash) { ++ len += DEFAULT_HTML.size(); ++ } ++ ++ file_path.resize(len); ++ ++ auto p = &file_path[0]; ++ ++ auto &htdocs = hd->get_config()->htdocs; ++ p = std::copy(std::begin(htdocs), std::end(htdocs), p); ++ p = std::copy(std::begin(path), std::end(path), p); ++ if (trailing_slash) { ++ std::copy(std::begin(DEFAULT_HTML), std::end(DEFAULT_HTML), p); ++ } ++ } ++ ++ if (stream->echo_upload) { ++ assert(stream->file_ent); ++ prepare_echo_response(stream, hd); ++ return; ++ } ++ ++ auto file_ent = sessions->get_cached_fd(file_path); ++ ++ if (file_ent == nullptr) { ++ int file = open(file_path.c_str(), O_RDONLY | O_BINARY); ++ if (file == -1) { ++ prepare_status_response(stream, hd, 404); ++ ++ return; ++ } ++ ++ struct stat buf; ++ ++ if (fstat(file, &buf) == -1) { ++ close(file); ++ prepare_status_response(stream, hd, 404); ++ ++ return; ++ } ++ ++ if (buf.st_mode & S_IFDIR) { ++ close(file); ++ ++ auto reqpath = concat_string_ref(stream->balloc, raw_path, ++ StringRef::from_lit("/"), raw_query); ++ ++ prepare_redirect_response(stream, hd, reqpath, 301); ++ ++ return; ++ } ++ ++ const std::string *content_type = nullptr; ++ ++ auto ext = file_path.c_str() + file_path.size() - 1; ++ for (; file_path.c_str() < ext && *ext != '.' && *ext != '/'; --ext) ++ ; ++ if (*ext == '.') { ++ ++ext; ++ ++ const auto &mime_types = hd->get_config()->mime_types; ++ auto content_type_itr = mime_types.find(ext); ++ if (content_type_itr != std::end(mime_types)) { ++ content_type = &(*content_type_itr).second; ++ } ++ } ++ ++ file_ent = sessions->cache_fd( ++ file_path, FileEntry(file_path, buf.st_size, buf.st_mtime, file, ++ content_type, std::chrono::steady_clock::now())); ++ } ++ ++ stream->file_ent = file_ent; ++ ++ if (last_mod_found && file_ent->mtime <= last_mod) { ++ hd->submit_response(StringRef::from_lit("304"), stream->stream_id, nullptr); ++ ++ return; ++ } ++ ++ auto method = stream->header.method; ++ if (method == StringRef::from_lit("HEAD")) { ++ hd->submit_file_response(StringRef::from_lit("200"), stream, ++ file_ent->mtime, file_ent->length, ++ file_ent->content_type, nullptr); ++ return; ++ } ++ ++ stream->body_length = file_ent->length; ++ ++ nghttp2_data_provider data_prd; ++ ++ data_prd.source.fd = file_ent->fd; ++ data_prd.read_callback = file_read_callback; ++ ++ hd->submit_file_response(StringRef::from_lit("200"), stream, file_ent->mtime, ++ file_ent->length, file_ent->content_type, &data_prd); ++} ++} // namespace ++ ++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 hd = static_cast(user_data); ++ ++ auto namebuf = nghttp2_rcbuf_get_buf(name); ++ auto valuebuf = nghttp2_rcbuf_get_buf(value); ++ ++ if (hd->get_config()->verbose) { ++ print_session_id(hd->session_id()); ++ verbose_on_header_callback(session, frame, namebuf.base, namebuf.len, ++ valuebuf.base, valuebuf.len, flags, user_data); ++ } ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_REQUEST) { ++ return 0; ++ } ++ auto stream = hd->get_stream(frame->hd.stream_id); ++ if (!stream) { ++ return 0; ++ } ++ ++ if (stream->header_buffer_size + namebuf.len + valuebuf.len > 64_k) { ++ hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); ++ return 0; ++ } ++ ++ stream->header_buffer_size += namebuf.len + valuebuf.len; ++ ++ auto token = http2::lookup_token(namebuf.base, namebuf.len); ++ ++ auto &header = stream->header; ++ ++ switch (token) { ++ case http2::HD__METHOD: ++ header.method = StringRef{valuebuf.base, valuebuf.len}; ++ header.rcbuf.method = value; ++ nghttp2_rcbuf_incref(value); ++ break; ++ case http2::HD__SCHEME: ++ header.scheme = StringRef{valuebuf.base, valuebuf.len}; ++ header.rcbuf.scheme = value; ++ nghttp2_rcbuf_incref(value); ++ break; ++ case http2::HD__AUTHORITY: ++ header.authority = StringRef{valuebuf.base, valuebuf.len}; ++ header.rcbuf.authority = value; ++ nghttp2_rcbuf_incref(value); ++ break; ++ case http2::HD_HOST: ++ header.host = StringRef{valuebuf.base, valuebuf.len}; ++ header.rcbuf.host = value; ++ nghttp2_rcbuf_incref(value); ++ break; ++ case http2::HD__PATH: ++ header.path = StringRef{valuebuf.base, valuebuf.len}; ++ header.rcbuf.path = value; ++ nghttp2_rcbuf_incref(value); ++ break; ++ case http2::HD_IF_MODIFIED_SINCE: ++ header.ims = StringRef{valuebuf.base, valuebuf.len}; ++ header.rcbuf.ims = value; ++ nghttp2_rcbuf_incref(value); ++ break; ++ case http2::HD_EXPECT: ++ header.expect = StringRef{valuebuf.base, valuebuf.len}; ++ header.rcbuf.expect = value; ++ nghttp2_rcbuf_incref(value); ++ break; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_begin_headers_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, void *user_data) { ++ auto hd = static_cast(user_data); ++ ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_REQUEST) { ++ return 0; ++ } ++ ++ auto stream = std::make_unique(hd, frame->hd.stream_id); ++ ++ add_stream_read_timeout(stream.get()); ++ ++ hd->add_stream(frame->hd.stream_id, std::move(stream)); ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int hd_on_frame_recv_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, void *user_data) { ++ auto hd = static_cast(user_data); ++ if (hd->get_config()->verbose) { ++ print_session_id(hd->session_id()); ++ verbose_on_frame_recv_callback(session, frame, user_data); ++ } ++ switch (frame->hd.type) { ++ case NGHTTP2_DATA: { ++ // TODO Handle POST ++ auto stream = hd->get_stream(frame->hd.stream_id); ++ if (!stream) { ++ return 0; ++ } ++ ++ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { ++ remove_stream_read_timeout(stream); ++ if (stream->echo_upload || !hd->get_config()->early_response) { ++ prepare_response(stream, hd); ++ } ++ } else { ++ add_stream_read_timeout(stream); ++ } ++ ++ break; ++ } ++ case NGHTTP2_HEADERS: { ++ auto stream = hd->get_stream(frame->hd.stream_id); ++ if (!stream) { ++ return 0; ++ } ++ ++ if (frame->headers.cat == NGHTTP2_HCAT_REQUEST) { ++ ++ auto expect100 = stream->header.expect; ++ ++ if (util::strieq_l("100-continue", expect100)) { ++ hd->submit_non_final_response("100", frame->hd.stream_id); ++ } ++ ++ auto method = stream->header.method; ++ if (hd->get_config()->echo_upload && ++ (method == StringRef::from_lit("POST") || ++ method == StringRef::from_lit("PUT"))) { ++ if (!prepare_upload_temp_store(stream, hd)) { ++ hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); ++ return 0; ++ } ++ } else if (hd->get_config()->early_response) { ++ prepare_response(stream, hd); ++ } ++ } ++ ++ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { ++ remove_stream_read_timeout(stream); ++ if (stream->echo_upload || !hd->get_config()->early_response) { ++ prepare_response(stream, hd); ++ } ++ } else { ++ add_stream_read_timeout(stream); ++ } ++ ++ break; ++ } ++ case NGHTTP2_SETTINGS: ++ if (frame->hd.flags & NGHTTP2_FLAG_ACK) { ++ hd->remove_settings_timer(); ++ } ++ break; ++ default: ++ break; ++ } ++ return 0; ++} ++} // namespace ++ ++namespace { ++int hd_on_frame_send_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, void *user_data) { ++ auto hd = static_cast(user_data); ++ ++ if (hd->get_config()->verbose) { ++ print_session_id(hd->session_id()); ++ verbose_on_frame_send_callback(session, frame, user_data); ++ } ++ ++ switch (frame->hd.type) { ++ case NGHTTP2_DATA: ++ case NGHTTP2_HEADERS: { ++ auto stream = hd->get_stream(frame->hd.stream_id); ++ ++ if (!stream) { ++ return 0; ++ } ++ ++ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { ++ remove_stream_write_timeout(stream); ++ } else if (std::min(nghttp2_session_get_stream_remote_window_size( ++ session, frame->hd.stream_id), ++ nghttp2_session_get_remote_window_size(session)) <= 0) { ++ // If stream is blocked by flow control, enable write timeout. ++ add_stream_read_timeout_if_pending(stream); ++ add_stream_write_timeout(stream); ++ } else { ++ add_stream_read_timeout_if_pending(stream); ++ remove_stream_write_timeout(stream); ++ } ++ ++ break; ++ } ++ case NGHTTP2_SETTINGS: { ++ if (frame->hd.flags & NGHTTP2_FLAG_ACK) { ++ return 0; ++ } ++ ++ hd->start_settings_timer(); ++ ++ break; ++ } ++ case NGHTTP2_PUSH_PROMISE: { ++ auto promised_stream_id = frame->push_promise.promised_stream_id; ++ auto promised_stream = hd->get_stream(promised_stream_id); ++ auto stream = hd->get_stream(frame->hd.stream_id); ++ ++ if (!stream || !promised_stream) { ++ return 0; ++ } ++ ++ add_stream_read_timeout_if_pending(stream); ++ add_stream_write_timeout(stream); ++ ++ prepare_response(promised_stream, hd, /*allow_push */ false); ++ } ++ } ++ return 0; ++} ++} // namespace ++ ++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 hd = static_cast(user_data); ++ auto wb = hd->get_wb(); ++ auto padlen = frame->data.padlen; ++ auto stream = hd->get_stream(frame->hd.stream_id); ++ ++ if (wb->wleft() < 9 + length + padlen) { ++ return NGHTTP2_ERR_WOULDBLOCK; ++ } ++ ++ int fd = source->fd; ++ ++ auto p = wb->last; ++ ++ p = std::copy_n(framehd, 9, p); ++ ++ if (padlen) { ++ *p++ = padlen - 1; ++ } ++ ++ while (length) { ++ ssize_t nread; ++ while ((nread = pread(fd, p, length, stream->body_offset)) == -1 && ++ errno == EINTR) ++ ; ++ ++ if (nread == -1) { ++ remove_stream_read_timeout(stream); ++ remove_stream_write_timeout(stream); ++ ++ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; ++ } ++ ++ stream->body_offset += nread; ++ length -= nread; ++ p += nread; ++ } ++ ++ if (padlen) { ++ std::fill(p, p + padlen - 1, 0); ++ p += padlen - 1; ++ } ++ ++ wb->last = p; ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++ssize_t select_padding_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, size_t max_payload, ++ void *user_data) { ++ auto hd = static_cast(user_data); ++ return std::min(max_payload, frame->hd.length + hd->get_config()->padding); ++} ++} // 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 hd = static_cast(user_data); ++ auto stream = hd->get_stream(stream_id); ++ ++ if (!stream) { ++ return 0; ++ } ++ ++ if (stream->echo_upload) { ++ assert(stream->file_ent); ++ while (len) { ++ ssize_t n; ++ while ((n = write(stream->file_ent->fd, data, len)) == -1 && ++ errno == EINTR) ++ ; ++ if (n == -1) { ++ hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); ++ return 0; ++ } ++ len -= n; ++ data += n; ++ } ++ } ++ // TODO Handle POST ++ ++ add_stream_read_timeout(stream); ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, ++ uint32_t error_code, void *user_data) { ++ auto hd = static_cast(user_data); ++ hd->remove_stream(stream_id); ++ if (hd->get_config()->verbose) { ++ print_session_id(hd->session_id()); ++ print_timer(); ++ printf(" stream_id=%d closed\n", stream_id); ++ fflush(stdout); ++ } ++ return 0; ++} ++} // namespace ++ ++namespace { ++void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config) { ++ nghttp2_session_callbacks_set_on_stream_close_callback( ++ callbacks, on_stream_close_callback); ++ ++ nghttp2_session_callbacks_set_on_frame_recv_callback( ++ callbacks, hd_on_frame_recv_callback); ++ ++ nghttp2_session_callbacks_set_on_frame_send_callback( ++ callbacks, hd_on_frame_send_callback); ++ ++ if (config->verbose) { ++ nghttp2_session_callbacks_set_on_invalid_frame_recv_callback( ++ callbacks, verbose_on_invalid_frame_recv_callback); ++ ++ nghttp2_session_callbacks_set_error_callback2(callbacks, ++ verbose_error_callback); ++ } ++ ++ nghttp2_session_callbacks_set_on_data_chunk_recv_callback( ++ callbacks, on_data_chunk_recv_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); ++ ++ if (config->padding) { ++ nghttp2_session_callbacks_set_select_padding_callback( ++ callbacks, select_padding_callback); ++ } ++} ++} // namespace ++ ++struct ClientInfo { ++ int fd; ++}; ++ ++struct Worker { ++ std::unique_ptr sessions; ++ // protects q ++ std::mutex m; ++ std::deque q; ++}; ++ ++namespace { ++void run_worker(Worker *worker) { ++ auto verto_loop = worker->sessions->get_loop(); ++ ++ verto_run(verto_loop); ++} ++} // namespace ++ ++ ++class AcceptHandler { ++public: ++ AcceptHandler(HttpServer *sv, Sessions *sessions, const Config *config) ++ : sessions_(sessions), config_(config), next_worker_(0) { ++ if (config_->num_worker == 1) { ++ return; ++ } ++ for (size_t i = 0; i < config_->num_worker; ++i) { ++ if (config_->verbose) { ++ std::cerr << "spawning thread #" << i << std::endl; ++ } ++ auto worker = std::make_unique(); ++ auto verto_loop = verto_new(NULL, VERTO_EV_TYPE_NONE); ++ worker->sessions = std::make_unique(sv, verto_loop, config_, ++ sessions_->get_ssl_ctx()); ++ ++ auto t = std::thread(run_worker, worker.get()); ++ t.detach(); ++ workers_.push_back(std::move(worker)); ++ } ++ } ++ ++ void accept_connection(int fd) { ++ if (config_->num_worker == 1) { ++ sessions_->accept_connection(fd); ++ return; ++ } ++ ++ // Dispatch client to the one of the worker threads, in a round ++ // robin manner. ++ auto &worker = workers_[next_worker_]; ++ if (next_worker_ == config_->num_worker - 1) { ++ next_worker_ = 0; ++ } else { ++ ++next_worker_; ++ } ++ { ++ std::lock_guard lock(worker->m); ++ worker->q.push_back({fd}); ++ } ++ ++ auto &sessions = worker->sessions; ++ ++ std::deque q; ++ { ++ std::lock_guard lock(worker->m); ++ q.swap(worker->q); ++ } ++ ++ for (const auto &c : q) { ++ sessions->accept_connection(c.fd); ++ } ++ } ++ ++private: ++ std::vector> workers_; ++ Sessions *sessions_; ++ const Config *config_; ++ // In multi threading mode, this points to the next thread that ++ // client will be dispatched. ++ size_t next_worker_; ++}; ++ ++namespace { ++void verto_acceptcb(verto_ctx *verto_loop, verto_ev *ev); ++} // namespace ++ ++class ListenEventHandler { ++public: ++ ListenEventHandler(Sessions *sessions, int fd, ++ std::shared_ptr acceptor) ++ : acceptor_(std::move(acceptor)), sessions_(sessions), fd_(fd) { ++ ++ // Add read event observers, add requests from clients ++ verto_ev_flag read_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_READ); ++ verto_w_ = verto_add_io(sessions_->get_loop(), read_flag, verto_acceptcb, fd); ++ verto_set_private(verto_w_, this, NULL); ++ } ++ ++ void accept_connection() { ++ for (;;) { ++#ifdef HAVE_ACCEPT4 ++ auto fd = accept4(fd_, nullptr, nullptr, SOCK_NONBLOCK); ++#else // !HAVE_ACCEPT4 ++ auto fd = accept(fd_, nullptr, nullptr); ++#endif // !HAVE_ACCEPT4 ++ if (fd == -1) { ++ break; ++ } ++#ifndef HAVE_ACCEPT4 ++ util::make_socket_nonblocking(fd); ++#endif // !HAVE_ACCEPT4 ++ acceptor_->accept_connection(fd); ++ } ++ } ++ ++private: ++ verto_ev *verto_w_; ++ std::shared_ptr acceptor_; ++ Sessions *sessions_; ++ int fd_; ++}; ++ ++namespace { ++void verto_acceptcb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto handler = static_cast(verto_get_private(ev)); ++ handler->accept_connection(); ++} ++} // namespace ++ ++namespace { ++FileEntry make_status_body(int status, uint16_t port) { ++ BlockAllocator balloc(1024, 1024); ++ ++ auto status_string = http2::stringify_status(balloc, status); ++ auto reason_pharase = http2::get_reason_phrase(status); ++ ++ std::string body; ++ body = ""; ++ body += status_string; ++ body += ' '; ++ body += reason_pharase; ++ body += "

"; ++ body += status_string; ++ body += ' '; ++ body += reason_pharase; ++ body += "


"; ++ body += NGHTTPD_SERVER; ++ body += " at port "; ++ body += util::utos(port); ++ body += "
"; ++ body += ""; ++ ++ char tempfn[] = "/tmp/nghttpd.temp.XXXXXX"; ++ int fd = mkstemp(tempfn); ++ if (fd == -1) { ++ auto error = errno; ++ std::cerr << "Could not open status response body file: errno=" << error; ++ assert(0); ++ } ++ unlink(tempfn); ++ ssize_t nwrite; ++ while ((nwrite = write(fd, body.c_str(), body.size())) == -1 && ++ errno == EINTR) ++ ; ++ if (nwrite == -1) { ++ auto error = errno; ++ std::cerr << "Could not write status response body into file: errno=" ++ << error; ++ assert(0); ++ } ++ ++ return FileEntry(util::utos(status), nwrite, 0, fd, nullptr, {}); ++} ++} // namespace ++ ++// index into HttpServer::status_pages_ ++enum { ++ IDX_200, ++ IDX_301, ++ IDX_400, ++ IDX_404, ++ IDX_405, ++}; ++ ++HttpServer::HttpServer(const Config *config) : config_(config) { ++ status_pages_ = std::vector{ ++ {"200", make_status_body(200, config_->port)}, ++ {"301", make_status_body(301, config_->port)}, ++ {"400", make_status_body(400, config_->port)}, ++ {"404", make_status_body(404, config_->port)}, ++ {"405", make_status_body(405, config_->port)}, ++ }; ++} ++ ++#ifndef OPENSSL_NO_NEXTPROTONEG ++namespace { ++int next_proto_cb(SSL *s, const unsigned char **data, unsigned int *len, ++ void *arg) { ++ auto next_proto = static_cast *>(arg); ++ *data = next_proto->data(); ++ *len = next_proto->size(); ++ return SSL_TLSEXT_ERR_OK; ++} ++} // namespace ++#endif // !OPENSSL_NO_NEXTPROTONEG ++ ++namespace { ++int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { ++ // We don't verify the client certificate. Just request it for the ++ // testing purpose. ++ return 1; ++} ++} // namespace ++ ++namespace { ++int start_listen(HttpServer *sv, verto_ctx *verto_loop, Sessions *sessions, ++ const Config *config) { ++ int r; ++ bool ok = false; ++ const char *addr = nullptr; ++ ++ std::shared_ptr acceptor; ++ auto service = util::utos(config->port); ++ ++ addrinfo hints{}; ++ hints.ai_family = AF_UNSPEC; ++ hints.ai_socktype = SOCK_STREAM; ++ hints.ai_flags = AI_PASSIVE; ++#ifdef AI_ADDRCONFIG ++ hints.ai_flags |= AI_ADDRCONFIG; ++#endif // AI_ADDRCONFIG ++ ++ if (!config->address.empty()) { ++ addr = config->address.c_str(); ++ } ++ ++ addrinfo *res, *rp; ++ r = getaddrinfo(addr, service.c_str(), &hints, &res); ++ if (r != 0) { ++ std::cerr << "getaddrinfo() failed: " << gai_strerror(r) << std::endl; ++ return -1; ++ } ++ ++ for (rp = res; rp; rp = rp->ai_next) { ++ int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); ++ if (fd == -1) { ++ continue; ++ } ++ int val = 1; ++ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, ++ static_cast(sizeof(val))) == -1) { ++ close(fd); ++ continue; ++ } ++ (void)util::make_socket_nonblocking(fd); ++#ifdef IPV6_V6ONLY ++ if (rp->ai_family == AF_INET6) { ++ if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, ++ static_cast(sizeof(val))) == -1) { ++ close(fd); ++ continue; ++ } ++ } ++#endif // IPV6_V6ONLY ++ if (bind(fd, rp->ai_addr, rp->ai_addrlen) == 0 && listen(fd, 1000) == 0) { ++ if (!acceptor) { ++ acceptor = std::make_shared(sv, sessions, config); ++ } ++ new ListenEventHandler(sessions, fd, acceptor); ++ ++ if (config->verbose) { ++ std::string s = util::numeric_name(rp->ai_addr, rp->ai_addrlen); ++ std::cout << (rp->ai_family == AF_INET ? "IPv4" : "IPv6") << ": listen " ++ << s << ":" << config->port << std::endl; ++ } ++ ok = true; ++ continue; ++ } else { ++ std::cerr << strerror(errno) << std::endl; ++ } ++ close(fd); ++ } ++ freeaddrinfo(res); ++ ++ if (!ok) { ++ return -1; ++ } ++ return 0; ++} ++} // namespace ++ ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L ++namespace { ++int alpn_select_proto_cb(SSL *ssl, const unsigned char **out, ++ unsigned char *outlen, const unsigned char *in, ++ unsigned int inlen, void *arg) { ++ auto config = static_cast(arg)->get_config(); ++ if (config->verbose) { ++ std::cout << "[ALPN] client offers:" << std::endl; ++ } ++ if (config->verbose) { ++ for (unsigned int i = 0; i < inlen; i += in[i] + 1) { ++ std::cout << " * "; ++ std::cout.write(reinterpret_cast(&in[i + 1]), in[i]); ++ std::cout << std::endl; ++ } ++ } ++ if (!util::select_h2(out, outlen, in, inlen)) { ++ return SSL_TLSEXT_ERR_NOACK; ++ } ++ return SSL_TLSEXT_ERR_OK; ++} ++} // namespace ++#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L ++ ++int HttpServer::run() { ++ SSL_CTX *ssl_ctx = nullptr; ++ std::vector next_proto; ++ ++ if (!config_->no_tls) { ++ ssl_ctx = SSL_CTX_new(TLS_server_method()); ++ if (!ssl_ctx) { ++ std::cerr << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ return -1; ++ } ++ ++ auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | ++ SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | ++ SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION | ++ SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_TICKET | ++ SSL_OP_CIPHER_SERVER_PREFERENCE; ++ ++#ifdef SSL_OP_ENABLE_KTLS ++ if (config_->ktls) { ++ ssl_opts |= SSL_OP_ENABLE_KTLS; ++ } ++#endif // SSL_OP_ENABLE_KTLS ++ ++ SSL_CTX_set_options(ssl_ctx, ssl_opts); ++ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); ++ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); ++ ++ if (nghttp2::tls::ssl_ctx_set_proto_versions( ++ ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, ++ nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { ++ std::cerr << "Could not set TLS versions" << std::endl; ++ return -1; ++ } ++ ++ if (SSL_CTX_set_cipher_list(ssl_ctx, tls::DEFAULT_CIPHER_LIST) == 0) { ++ std::cerr << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ return -1; ++ } ++ ++ const unsigned char sid_ctx[] = "nghttpd"; ++ SSL_CTX_set_session_id_context(ssl_ctx, sid_ctx, sizeof(sid_ctx) - 1); ++ SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER); ++ ++#ifndef OPENSSL_NO_EC ++# if !LIBRESSL_LEGACY_API && OPENSSL_VERSION_NUMBER >= 0x10002000L ++ if (SSL_CTX_set1_curves_list(ssl_ctx, "P-256") != 1) { ++ std::cerr << "SSL_CTX_set1_curves_list failed: " ++ << ERR_error_string(ERR_get_error(), nullptr); ++ return -1; ++ } ++# else // !(!LIBRESSL_LEGACY_API && OPENSSL_VERSION_NUMBER >= 0x10002000L) ++ auto ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); ++ if (ecdh == nullptr) { ++ std::cerr << "EC_KEY_new_by_curv_name failed: " ++ << ERR_error_string(ERR_get_error(), nullptr); ++ return -1; ++ } ++ SSL_CTX_set_tmp_ecdh(ssl_ctx, ecdh); ++ EC_KEY_free(ecdh); ++# endif // !(!LIBRESSL_LEGACY_API && OPENSSL_VERSION_NUMBER >= 0x10002000L) ++#endif // OPENSSL_NO_EC ++ ++ if (!config_->dh_param_file.empty()) { ++ // Read DH parameters from file ++ auto bio = BIO_new_file(config_->dh_param_file.c_str(), "rb"); ++ if (bio == nullptr) { ++ std::cerr << "BIO_new_file() failed: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ return -1; ++ } ++ ++#if OPENSSL_3_0_0_API ++ EVP_PKEY *dh = nullptr; ++ auto dctx = OSSL_DECODER_CTX_new_for_pkey( ++ &dh, "PEM", nullptr, "DH", OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, ++ nullptr, nullptr); ++ ++ if (!OSSL_DECODER_from_bio(dctx, bio)) { ++ std::cerr << "OSSL_DECODER_from_bio() failed: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ return -1; ++ } ++ ++ if (SSL_CTX_set0_tmp_dh_pkey(ssl_ctx, dh) != 1) { ++ std::cerr << "SSL_CTX_set0_tmp_dh_pkey failed: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ return -1; ++ } ++#else // !OPENSSL_3_0_0_API ++ auto dh = PEM_read_bio_DHparams(bio, nullptr, nullptr, nullptr); ++ ++ if (dh == nullptr) { ++ std::cerr << "PEM_read_bio_DHparams() failed: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ return -1; ++ } ++ ++ SSL_CTX_set_tmp_dh(ssl_ctx, dh); ++ DH_free(dh); ++#endif // !OPENSSL_3_0_0_API ++ BIO_free(bio); ++ } ++ ++ if (SSL_CTX_use_PrivateKey_file(ssl_ctx, config_->private_key_file.c_str(), ++ SSL_FILETYPE_PEM) != 1) { ++ std::cerr << "SSL_CTX_use_PrivateKey_file failed." << std::endl; ++ return -1; ++ } ++ if (SSL_CTX_use_certificate_chain_file(ssl_ctx, ++ config_->cert_file.c_str()) != 1) { ++ std::cerr << "SSL_CTX_use_certificate_file failed." << std::endl; ++ return -1; ++ } ++ if (SSL_CTX_check_private_key(ssl_ctx) != 1) { ++ std::cerr << "SSL_CTX_check_private_key failed." << std::endl; ++ return -1; ++ } ++ if (config_->verify_client) { ++ SSL_CTX_set_verify(ssl_ctx, ++ SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | ++ SSL_VERIFY_FAIL_IF_NO_PEER_CERT, ++ verify_callback); ++ } ++ ++ next_proto = util::get_default_alpn(); ++ ++#ifndef OPENSSL_NO_NEXTPROTONEG ++ SSL_CTX_set_next_protos_advertised_cb(ssl_ctx, next_proto_cb, &next_proto); ++#endif // !OPENSSL_NO_NEXTPROTONEG ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L ++ // ALPN selection callback ++ SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, this); ++#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L ++ } ++ ++ auto verto_loop = verto_new(NULL, VERTO_EV_TYPE_NONE); ++ ++ Sessions sessions(this, verto_loop, config_, ssl_ctx); ++ if (start_listen(this, verto_loop, &sessions, config_) != 0) { ++ std::cerr << "Could not listen" << std::endl; ++ if (ssl_ctx) { ++ SSL_CTX_free(ssl_ctx); ++ } ++ return -1; ++ } ++ ++ verto_run(verto_loop); ++ return 0; ++} ++ ++const Config *HttpServer::get_config() const { return config_; } ++ ++const StatusPage *HttpServer::get_status_page(int status) const { ++ switch (status) { ++ case 200: ++ return &status_pages_[IDX_200]; ++ case 301: ++ return &status_pages_[IDX_301]; ++ case 400: ++ return &status_pages_[IDX_400]; ++ case 404: ++ return &status_pages_[IDX_404]; ++ case 405: ++ return &status_pages_[IDX_405]; ++ default: ++ assert(0); ++ } ++ return nullptr; ++} ++ ++} // namespace nghttp2 +diff --git a/src/HttpServer_verto.h b/src/HttpServer_verto.h +new file mode 100644 +index 0000000..c7dc7f1 +--- /dev/null ++++ b/src/HttpServer_verto.h +@@ -0,0 +1,263 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2013 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. ++ */ ++#ifndef HTTP_SERVER_H ++#define HTTP_SERVER_H ++ ++#include "nghttp2_config.h" ++ ++#include ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include ++ ++#include ++ ++#include "http2.h" ++#include "buffer.h" ++#include "template.h" ++#include "allocator.h" ++ ++namespace nghttp2 { ++ ++struct Config { ++ std::map> push; ++ std::map mime_types; ++ Headers trailer; ++ std::string trailer_names; ++ std::string htdocs; ++ std::string host; ++ std::string private_key_file; ++ std::string cert_file; ++ std::string dh_param_file; ++ std::string address; ++ std::string mime_types_file; ++ ++ double verto_stream_read_timeout; ++ double verto_stream_write_timeout; ++ ++ void *data_ptr; ++ size_t padding; ++ size_t num_worker; ++ size_t max_concurrent_streams; ++ ssize_t header_table_size; ++ ssize_t encoder_header_table_size; ++ int window_bits; ++ int connection_window_bits; ++ uint16_t port; ++ bool verbose; ++ bool daemon; ++ bool verify_client; ++ bool no_tls; ++ bool error_gzip; ++ bool early_response; ++ bool hexdump; ++ bool echo_upload; ++ bool no_content_length; ++ bool ktls; ++ bool no_rfc7540_pri; ++ Config(); ++ ~Config(); ++}; ++ ++class Http2Handler; ++ ++struct FileEntry { ++ FileEntry(std::string path, int64_t length, int64_t mtime, int fd, ++ const std::string *content_type, ++ const std::chrono::steady_clock::time_point &last_valid, ++ bool stale = false) ++ : path(std::move(path)), ++ length(length), ++ mtime(mtime), ++ last_valid(last_valid), ++ content_type(content_type), ++ dlnext(nullptr), ++ dlprev(nullptr), ++ fd(fd), ++ usecount(1), ++ stale(stale) {} ++ std::string path; ++ std::multimap>::iterator it; ++ int64_t length; ++ int64_t mtime; ++ std::chrono::steady_clock::time_point last_valid; ++ const std::string *content_type; ++ FileEntry *dlnext, *dlprev; ++ int fd; ++ int usecount; ++ bool stale; ++}; ++ ++struct RequestHeader { ++ StringRef method; ++ StringRef scheme; ++ StringRef authority; ++ StringRef host; ++ StringRef path; ++ StringRef ims; ++ StringRef expect; ++ ++ struct { ++ nghttp2_rcbuf *method; ++ nghttp2_rcbuf *scheme; ++ nghttp2_rcbuf *authority; ++ nghttp2_rcbuf *host; ++ nghttp2_rcbuf *path; ++ nghttp2_rcbuf *ims; ++ nghttp2_rcbuf *expect; ++ } rcbuf; ++}; ++ ++struct Stream { ++ BlockAllocator balloc; ++ RequestHeader header; ++ Http2Handler *handler; ++ FileEntry *file_ent; ++ ++ verto_ev *verto_rtimer; ++ verto_ev *verto_wtimer; ++ ++ int64_t body_length; ++ int64_t body_offset; ++ // Total amount of bytes (sum of name and value length) used in ++ // headers. ++ size_t header_buffer_size; ++ int32_t stream_id; ++ bool echo_upload; ++ Stream(Http2Handler *handler, int32_t stream_id); ++ ~Stream(); ++}; ++ ++class Sessions; ++ ++class Http2Handler { ++public: ++ Http2Handler(Sessions *sessions, int fd, SSL *ssl, int64_t session_id); ++ ~Http2Handler(); ++ ++ void remove_self(); ++ void start_settings_timer(); ++ int on_read(); ++ int on_write(); ++ int connection_made(); ++ int verify_npn_result(); ++ ++ int submit_file_response(const StringRef &status, Stream *stream, ++ time_t last_modified, off_t file_length, ++ const std::string *content_type, ++ nghttp2_data_provider *data_prd); ++ ++ int submit_response(const StringRef &status, int32_t stream_id, ++ nghttp2_data_provider *data_prd); ++ ++ int submit_response(const StringRef &status, int32_t stream_id, ++ const HeaderRefs &headers, ++ nghttp2_data_provider *data_prd); ++ ++ int submit_non_final_response(const std::string &status, int32_t stream_id); ++ ++ int submit_push_promise(Stream *stream, const StringRef &push_path); ++ ++ int submit_rst_stream(Stream *stream, uint32_t error_code); ++ ++ void add_stream(int32_t stream_id, std::unique_ptr stream); ++ void remove_stream(int32_t stream_id); ++ Stream *get_stream(int32_t stream_id); ++ int64_t session_id() const; ++ Sessions *get_sessions() const; ++ const Config *get_config() const; ++ void remove_settings_timer(); ++ void terminate_session(uint32_t error_code); ++ ++ int fill_wb(); ++ ++ int read_clear(); ++ int write_clear(); ++ int tls_handshake(); ++ int read_tls(); ++ int write_tls(); ++ ++ verto_ctx *get_loop(); ++ ++ using WriteBuf = Buffer<64_k>; ++ ++ WriteBuf *get_wb(); ++ ++ // An observer for writing events ++ verto_ev *verto_wev_; ++ // An observer for reading events ++ verto_ev *verto_rev_; ++ // An observer for timeout events ++ verto_ev *verto_settings_timerev_; ++ int fd_; ++ ++private: ++ std::map> id2stream_; ++ WriteBuf wb_; ++ ++ std::function read_, write_; ++ ++ int64_t session_id_; ++ nghttp2_session *session_; ++ Sessions *sessions_; ++ SSL *ssl_; ++ const uint8_t *data_pending_; ++ size_t data_pendinglen_; ++}; ++ ++struct StatusPage { ++ std::string status; ++ FileEntry file_ent; ++}; ++ ++class HttpServer { ++public: ++ HttpServer(const Config *config); ++ int listen(); ++ int run(); ++ const Config *get_config() const; ++ const StatusPage *get_status_page(int status) const; ++ ++private: ++ std::vector status_pages_; ++ const Config *config_; ++}; ++ ++ssize_t file_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); ++ ++} // namespace nghttp2 ++ ++#endif // HTTP_SERVER_H +diff --git a/src/nghttpd_verto.cc b/src/nghttpd_verto.cc +new file mode 100644 +index 0000000..c36a35b +--- /dev/null ++++ b/src/nghttpd_verto.cc +@@ -0,0 +1,508 @@ ++/* ++ * 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 "nghttp2_config.h" ++ ++#ifdef __sgi ++# define daemon _daemonize ++#endif ++ ++#ifdef HAVE_UNISTD_H ++# include ++#endif // HAVE_UNISTD_H ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++#include "app_helper.h" ++#include "HttpServer_verto.h" ++#include "util.h" ++#include "tls.h" ++ ++namespace nghttp2 { ++ ++namespace { ++int parse_push_config(Config &config, const char *optarg) { ++ const char *eq = strchr(optarg, '='); ++ if (eq == nullptr) { ++ return -1; ++ } ++ auto &paths = config.push[std::string(optarg, eq)]; ++ auto optarg_end = optarg + strlen(optarg); ++ auto i = eq + 1; ++ for (;;) { ++ const char *j = strchr(i, ','); ++ if (j == nullptr) { ++ j = optarg_end; ++ } ++ paths.emplace_back(i, j); ++ if (j == optarg_end) { ++ break; ++ } ++ i = j; ++ ++i; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++void print_version(std::ostream &out) { ++ out << "nghttpd nghttp2/" NGHTTP2_VERSION << std::endl; ++} ++} // namespace ++ ++namespace { ++void print_usage(std::ostream &out) { ++ out << "Usage: nghttpd [OPTION]... [ ]\n" ++ << "HTTP/2 server" << std::endl; ++} ++} // namespace ++ ++namespace { ++void print_help(std::ostream &out) { ++ Config config; ++ print_usage(out); ++ out << R"( ++ Specify listening port number. ++ ++ Set path to server's private key. Required unless ++ --no-tls is specified. ++ Set path to server's certificate. Required unless ++ --no-tls is specified. ++Options: ++ -a, --address= ++ The address to bind to. If not specified the default IP ++ address determined by getaddrinfo is used. ++ -D, --daemon ++ Run in a background. If -D is used, the current working ++ directory is changed to '/'. Therefore if this option ++ is used, -d option must be specified. ++ -V, --verify-client ++ The server sends a client certificate request. If the ++ client did not return a certificate, the handshake is ++ terminated. Currently, this option just requests a ++ client certificate and does not verify it. ++ -d, --htdocs= ++ Specify document root. If this option is not specified, ++ the document root is the current working directory. ++ -v, --verbose ++ Print debug information such as reception/ transmission ++ of frames and name/value pairs. ++ --no-tls Disable SSL/TLS. ++ -c, --header-table-size= ++ Specify decoder header table size. ++ --encoder-header-table-size= ++ Specify encoder header table size. The decoder (client) ++ specifies the maximum dynamic table size it accepts. ++ Then the negotiated dynamic table size is the minimum of ++ this option value and the value which client specified. ++ --color Force colored log output. ++ -p, --push== ++ Push resources s when is requested. ++ This option can be used repeatedly to specify multiple ++ push configurations. and s are ++ relative to document root. See --htdocs option. ++ Example: -p/=/foo.png -p/doc=/bar.css ++ -b, --padding= ++ Add at most bytes to a frame payload as padding. ++ Specify 0 to disable padding. ++ -m, --max-concurrent-streams= ++ Set the maximum number of the concurrent streams in one ++ HTTP/2 session. ++ Default: )" ++ << config.max_concurrent_streams << R"( ++ -n, --workers= ++ Set the number of worker threads. ++ Default: 1 ++ -e, --error-gzip ++ Make error response gzipped. ++ -w, --window-bits= ++ Sets the stream level initial window size to 2**-1. ++ -W, --connection-window-bits= ++ Sets the connection level initial window size to ++ 2**-1. ++ --dh-param-file= ++ Path to file that contains DH parameters in PEM format. ++ Without this option, DHE cipher suites are not ++ available. ++ --early-response ++ Start sending response when request HEADERS is received, ++ rather than complete request is received. ++ --trailer=
++ Add a trailer header to a response.
must not ++ include pseudo header field (header field name starting ++ with ':'). The trailer is sent only if a response has ++ body part. Example: --trailer 'foo: bar'. ++ --hexdump Display the incoming traffic in hexadecimal (Canonical ++ hex+ASCII display). If SSL/TLS is used, decrypted data ++ are used. ++ --echo-upload ++ Send back uploaded content if method is POST or PUT. ++ --mime-types-file= ++ Path to file that contains MIME media types and the ++ extensions that represent them. ++ Default: )" ++ << config.mime_types_file << R"( ++ --no-content-length ++ Don't send content-length header field. ++ --ktls Enable ktls. ++ --no-rfc7540-pri ++ Disable RFC7540 priorities. ++ --version Display version information and exit. ++ -h, --help Display this help and exit. ++ ++-- ++ ++ The argument is an integer and an optional unit (e.g., 10K is ++ 10 * 1024). Units are K, M and G (powers of 1024).)" ++ << std::endl; ++} ++} // namespace ++ ++int main(int argc, char **argv) { ++ tls::libssl_init(); ++ ++#ifndef NOTHREADS ++ tls::LibsslGlobalLock lock; ++#endif // NOTHREADS ++ ++ Config config; ++ bool color = false; ++ auto mime_types_file_set_manually = false; ++ ++ while (1) { ++ static int flag = 0; ++ constexpr static option long_options[] = { ++ {"address", required_argument, nullptr, 'a'}, ++ {"daemon", no_argument, nullptr, 'D'}, ++ {"htdocs", required_argument, nullptr, 'd'}, ++ {"help", no_argument, nullptr, 'h'}, ++ {"verbose", no_argument, nullptr, 'v'}, ++ {"verify-client", no_argument, nullptr, 'V'}, ++ {"header-table-size", required_argument, nullptr, 'c'}, ++ {"push", required_argument, nullptr, 'p'}, ++ {"padding", required_argument, nullptr, 'b'}, ++ {"max-concurrent-streams", required_argument, nullptr, 'm'}, ++ {"workers", required_argument, nullptr, 'n'}, ++ {"error-gzip", no_argument, nullptr, 'e'}, ++ {"window-bits", required_argument, nullptr, 'w'}, ++ {"connection-window-bits", required_argument, nullptr, 'W'}, ++ {"no-tls", no_argument, &flag, 1}, ++ {"color", no_argument, &flag, 2}, ++ {"version", no_argument, &flag, 3}, ++ {"dh-param-file", required_argument, &flag, 4}, ++ {"early-response", no_argument, &flag, 5}, ++ {"trailer", required_argument, &flag, 6}, ++ {"hexdump", no_argument, &flag, 7}, ++ {"echo-upload", no_argument, &flag, 8}, ++ {"mime-types-file", required_argument, &flag, 9}, ++ {"no-content-length", no_argument, &flag, 10}, ++ {"encoder-header-table-size", required_argument, &flag, 11}, ++ {"ktls", no_argument, &flag, 12}, ++ {"no-rfc7540-pri", no_argument, &flag, 13}, ++ {nullptr, 0, nullptr, 0}}; ++ int option_index = 0; ++ int c = getopt_long(argc, argv, "DVb:c:d:ehm:n:p:va:w:W:", long_options, ++ &option_index); ++ if (c == -1) { ++ break; ++ } ++ switch (c) { ++ case 'a': ++ config.address = optarg; ++ break; ++ case 'D': ++ config.daemon = true; ++ break; ++ case 'V': ++ config.verify_client = true; ++ break; ++ case 'b': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-b: Bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.padding = n; ++ break; ++ } ++ case 'd': ++ config.htdocs = optarg; ++ break; ++ case 'e': ++ config.error_gzip = true; ++ break; ++ case 'm': { ++ // max-concurrent-streams option ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-m: invalid argument: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.max_concurrent_streams = n; ++ break; ++ } ++ case 'n': { ++#ifdef NOTHREADS ++ std::cerr << "-n: WARNING: Threading disabled at build time, " ++ << "no threads created." << std::endl; ++#else ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-n: Bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.num_worker = n; ++#endif // NOTHREADS ++ break; ++ } ++ case 'h': ++ print_help(std::cout); ++ exit(EXIT_SUCCESS); ++ case 'v': ++ config.verbose = true; ++ break; ++ case 'c': { ++ auto n = util::parse_uint_with_unit(optarg); ++ if (n == -1) { ++ std::cerr << "-c: Bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (n > std::numeric_limits::max()) { ++ std::cerr << "-c: Value too large. It should be less than or equal to " ++ << std::numeric_limits::max() << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.header_table_size = n; ++ break; ++ } ++ case 'p': ++ if (parse_push_config(config, optarg) != 0) { ++ std::cerr << "-p: Bad option value: " << optarg << std::endl; ++ } ++ break; ++ case 'w': ++ case 'W': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1 || n > 30) { ++ std::cerr << "-" << static_cast(c) ++ << ": specify the integer in the range [0, 30], inclusive" ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (c == 'w') { ++ config.window_bits = n; ++ } else { ++ config.connection_window_bits = n; ++ } ++ ++ break; ++ } ++ case '?': ++ util::show_candidates(argv[optind - 1], long_options); ++ exit(EXIT_FAILURE); ++ case 0: ++ switch (flag) { ++ case 1: ++ // no-tls option ++ config.no_tls = true; ++ break; ++ case 2: ++ // color option ++ color = true; ++ break; ++ case 3: ++ // version ++ print_version(std::cout); ++ exit(EXIT_SUCCESS); ++ case 4: ++ // dh-param-file ++ config.dh_param_file = optarg; ++ break; ++ case 5: ++ // early-response ++ config.early_response = true; ++ break; ++ case 6: { ++ // trailer option ++ auto header = optarg; ++ auto value = strchr(optarg, ':'); ++ if (!value) { ++ std::cerr << "--trailer: invalid header: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ *value = 0; ++ value++; ++ while (isspace(*value)) { ++ value++; ++ } ++ if (*value == 0) { ++ // This could also be a valid case for suppressing a header ++ // similar to curl ++ std::cerr << "--trailer: invalid header - value missing: " << optarg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.trailer.emplace_back(header, value, false); ++ util::inp_strlower(config.trailer.back().name); ++ break; ++ } ++ case 7: ++ // hexdump option ++ config.hexdump = true; ++ break; ++ case 8: ++ // echo-upload option ++ config.echo_upload = true; ++ break; ++ case 9: ++ // mime-types-file option ++ mime_types_file_set_manually = true; ++ config.mime_types_file = optarg; ++ break; ++ case 10: ++ // no-content-length option ++ config.no_content_length = true; ++ break; ++ case 11: { ++ // encoder-header-table-size option ++ auto n = util::parse_uint_with_unit(optarg); ++ if (n == -1) { ++ std::cerr << "--encoder-header-table-size: Bad option value: " ++ << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (n > std::numeric_limits::max()) { ++ std::cerr << "--encoder-header-table-size: Value too large. It " ++ "should be less than or equal to " ++ << std::numeric_limits::max() << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.encoder_header_table_size = n; ++ break; ++ } ++ case 12: ++ // tls option ++ config.ktls = true; ++ break; ++ case 13: ++ // no-rfc7540-pri option ++ config.no_rfc7540_pri = true; ++ break; ++ } ++ break; ++ default: ++ break; ++ } ++ } ++ if (argc - optind < (config.no_tls ? 1 : 3)) { ++ print_usage(std::cerr); ++ std::cerr << "Too few arguments" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ { ++ auto portStr = argv[optind++]; ++ auto n = util::parse_uint(portStr); ++ if (n == -1 || n > std::numeric_limits::max()) { ++ std::cerr << ": Bad value: " << portStr << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.port = n; ++ } ++ ++ if (!config.no_tls) { ++ config.private_key_file = argv[optind++]; ++ config.cert_file = argv[optind++]; ++ } ++ ++ if (config.daemon) { ++ if (config.htdocs.empty()) { ++ print_usage(std::cerr); ++ std::cerr << "-d option must be specified when -D is used." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++#ifdef __sgi ++ if (daemon(0, 0, 0, 0) == -1) { ++#else ++ if (util::daemonize(0, 0) == -1) { ++#endif ++ perror("daemon"); ++ exit(EXIT_FAILURE); ++ } ++ } ++ if (config.htdocs.empty()) { ++ config.htdocs = "./"; ++ } ++ ++ if (util::read_mime_types(config.mime_types, ++ config.mime_types_file.c_str()) != 0) { ++ if (mime_types_file_set_manually) { ++ std::cerr << "--mime-types-file: Could not open mime types file: " ++ << config.mime_types_file << std::endl; ++ } ++ } ++ ++ auto &trailer_names = config.trailer_names; ++ for (auto &h : config.trailer) { ++ trailer_names += h.name; ++ trailer_names += ", "; ++ } ++ if (trailer_names.size() >= 2) { ++ trailer_names.resize(trailer_names.size() - 2); ++ } ++ ++ set_color_output(color || isatty(fileno(stdout))); ++ ++ struct sigaction act {}; ++ act.sa_handler = SIG_IGN; ++ sigaction(SIGPIPE, &act, nullptr); ++ ++ reset_timer(); ++ ++ HttpServer server(&config); ++ if (server.run() != 0) { ++ exit(EXIT_FAILURE); ++ } ++ return 0; ++ } ++ ++} // namespace nghttp2 ++ ++int main(int argc, char **argv) { ++ return nghttp2::run_app(nghttp2::main, argc, argv); ++} +-- +2.33.0 + diff --git a/0003-h2load-Benchmarking-implementation-based-on-libverto.patch b/0003-h2load-Benchmarking-implementation-based-on-libverto.patch new file mode 100644 index 0000000..636fc66 --- /dev/null +++ b/0003-h2load-Benchmarking-implementation-based-on-libverto.patch @@ -0,0 +1,6093 @@ +From cd0bdcc408031d72e1cd9dcb4da6c417c89a7c4f Mon Sep 17 00:00:00 2001 +From: dengjie <1171276417@qq.com> +Date: Thu, 26 Oct 2023 23:59:00 +0800 +Subject: [PATCH 3/4] h2load Benchmarking implementation based on libverto + +--- + src/h2load_http1_session_verto.cc | 306 +++ + src/h2load_http1_session_verto.h | 60 + + src/h2load_http2_session_verto.cc | 312 +++ + src/h2load_http2_session_verto.h | 54 + + src/h2load_http3_session_verto.cc | 457 ++++ + src/h2load_http3_session_verto.h | 83 + + src/h2load_quic_verto.cc | 838 ++++++++ + src/h2load_quic_verto.h | 36 + + src/h2load_session_verto.h | 60 + + src/h2load_verto.cc | 3277 +++++++++++++++++++++++++++++ + src/h2load_verto.h | 511 +++++ + 11 files changed, 5994 insertions(+) + create mode 100644 src/h2load_http1_session_verto.cc + create mode 100644 src/h2load_http1_session_verto.h + create mode 100644 src/h2load_http2_session_verto.cc + create mode 100644 src/h2load_http2_session_verto.h + create mode 100644 src/h2load_http3_session_verto.cc + create mode 100644 src/h2load_http3_session_verto.h + create mode 100644 src/h2load_quic_verto.cc + create mode 100644 src/h2load_quic_verto.h + create mode 100644 src/h2load_session_verto.h + create mode 100644 src/h2load_verto.cc + create mode 100644 src/h2load_verto.h + +diff --git a/src/h2load_http1_session_verto.cc b/src/h2load_http1_session_verto.cc +new file mode 100644 +index 0000000..d96befb +--- /dev/null ++++ b/src/h2load_http1_session_verto.cc +@@ -0,0 +1,306 @@ ++/* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2015 British Broadcasting Corporation ++ * ++ * 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 "h2load_http1_session_verto.h" ++ ++#include ++#include ++ ++#include "h2load_verto.h" ++#include "util.h" ++#include "template.h" ++ ++#include ++#include ++ ++using namespace nghttp2; ++ ++namespace h2load { ++ ++namespace { ++// HTTP response message begin ++int htp_msg_begincb(llhttp_t *htp) { ++ auto session = static_cast(htp->data); ++ ++ if (session->stream_resp_counter_ > session->stream_req_counter_) { ++ return -1; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++// HTTP response status code ++int htp_statuscb(llhttp_t *htp, const char *at, size_t length) { ++ auto session = static_cast(htp->data); ++ auto client = session->get_client(); ++ ++ if (htp->status_code / 100 == 1) { ++ return 0; ++ } ++ ++ client->on_status_code(session->stream_resp_counter_, htp->status_code); ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++// HTTP response message complete ++int htp_msg_completecb(llhttp_t *htp) { ++ auto session = static_cast(htp->data); ++ auto client = session->get_client(); ++ ++ if (htp->status_code / 100 == 1) { ++ return 0; ++ } ++ ++ client->final = llhttp_should_keep_alive(htp) == 0; ++ auto req_stat = client->get_req_stat(session->stream_resp_counter_); ++ ++ assert(req_stat); ++ ++ auto config = client->worker->config; ++ if (req_stat->data_offset >= config->data_length) { ++ client->on_stream_close(session->stream_resp_counter_, true, client->final); ++ } ++ ++ session->stream_resp_counter_ += 2; ++ ++ if (client->final) { ++ session->stream_req_counter_ = session->stream_resp_counter_; ++ ++ // Connection is going down. If we have still request to do, ++ // create new connection and keep on doing the job. ++ if (client->req_left) { ++ client->try_new_connection(); ++ } ++ ++ return HPE_PAUSED; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int htp_hdr_keycb(llhttp_t *htp, const char *data, size_t len) { ++ auto session = static_cast(htp->data); ++ auto client = session->get_client(); ++ ++ client->worker->stats.bytes_head += len; ++ client->worker->stats.bytes_head_decomp += len; ++ return 0; ++} ++} // namespace ++ ++namespace { ++int htp_hdr_valcb(llhttp_t *htp, const char *data, size_t len) { ++ auto session = static_cast(htp->data); ++ auto client = session->get_client(); ++ ++ client->worker->stats.bytes_head += len; ++ client->worker->stats.bytes_head_decomp += len; ++ return 0; ++} ++} // namespace ++ ++namespace { ++int htp_hdrs_completecb(llhttp_t *htp) { ++ return !http2::expect_response_body(htp->status_code); ++} ++} // namespace ++ ++namespace { ++int htp_body_cb(llhttp_t *htp, const char *data, size_t len) { ++ auto session = static_cast(htp->data); ++ auto client = session->get_client(); ++ ++ client->record_ttfb(); ++ client->worker->stats.bytes_body += len; ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++constexpr llhttp_settings_t htp_hooks = { ++ htp_msg_begincb, // llhttp_cb on_message_begin; ++ nullptr, // llhttp_data_cb on_url; ++ htp_statuscb, // llhttp_data_cb on_status; ++ nullptr, // llhttp_data_cb on_method; ++ nullptr, // llhttp_data_cb on_version; ++ htp_hdr_keycb, // llhttp_data_cb on_header_field; ++ htp_hdr_valcb, // llhttp_data_cb on_header_value; ++ nullptr, // llhttp_data_cb on_chunk_extension_name; ++ nullptr, // llhttp_data_cb on_chunk_extension_value; ++ htp_hdrs_completecb, // llhttp_cb on_headers_complete; ++ htp_body_cb, // llhttp_data_cb on_body; ++ htp_msg_completecb, // llhttp_cb on_message_complete; ++ nullptr, // llhttp_cb on_url_complete; ++ nullptr, // llhttp_cb on_status_complete; ++ nullptr, // llhttp_cb on_method_complete; ++ nullptr, // llhttp_cb on_version_complete; ++ nullptr, // llhttp_cb on_header_field_complete; ++ nullptr, // llhttp_cb on_header_value_complete; ++ nullptr, // llhttp_cb on_chunk_extension_name_complete; ++ nullptr, // llhttp_cb on_chunk_extension_value_complete; ++ nullptr, // llhttp_cb on_chunk_header; ++ nullptr, // llhttp_cb on_chunk_complete; ++ nullptr, // llhttp_cb on_reset; ++}; ++} // namespace ++ ++Http1Session::Http1Session(Client *client) ++ : stream_req_counter_(1), ++ stream_resp_counter_(1), ++ client_(client), ++ htp_(), ++ complete_(false) { ++ llhttp_init(&htp_, HTTP_RESPONSE, &htp_hooks); ++ htp_.data = this; ++} ++ ++Http1Session::~Http1Session() {} ++ ++void Http1Session::on_connect() { client_->signal_write(); } ++ ++int Http1Session::submit_request() { ++ auto config = client_->worker->config; ++ const auto &req = config->h1reqs[client_->reqidx]; ++ client_->reqidx++; ++ ++ if (client_->reqidx == config->h1reqs.size()) { ++ client_->reqidx = 0; ++ } ++ ++ client_->on_request(stream_req_counter_); ++ ++ auto req_stat = client_->get_req_stat(stream_req_counter_); ++ ++ client_->record_request_time(req_stat); ++ client_->wb.append(req); ++ ++ if (config->data_fd == -1 || config->data_length == 0) { ++ // increment for next request ++ stream_req_counter_ += 2; ++ ++ return 0; ++ } ++ ++ return on_write(); ++} ++ ++int Http1Session::on_read(const uint8_t *data, size_t len) { ++ auto htperr = ++ llhttp_execute(&htp_, reinterpret_cast(data), len); ++ auto nread = htperr == HPE_OK ++ ? len ++ : static_cast(reinterpret_cast( ++ llhttp_get_error_pos(&htp_)) - ++ data); ++ ++ if (client_->worker->config->verbose) { ++ std::cout.write(reinterpret_cast(data), nread); ++ } ++ ++ if (htperr == HPE_PAUSED) { ++ // pause is done only when connection: close is requested ++ return -1; ++ } ++ ++ if (htperr != HPE_OK) { ++ std::cerr << "[ERROR] HTTP parse error: " ++ << "(" << llhttp_errno_name(htperr) << ") " ++ << llhttp_get_error_reason(&htp_) << std::endl; ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int Http1Session::on_write() { ++ if (complete_) { ++ return -1; ++ } ++ ++ auto config = client_->worker->config; ++ auto req_stat = client_->get_req_stat(stream_req_counter_); ++ if (!req_stat) { ++ return 0; ++ } ++ ++ if (req_stat->data_offset < config->data_length) { ++ auto req_stat = client_->get_req_stat(stream_req_counter_); ++ auto &wb = client_->wb; ++ ++ // TODO unfortunately, wb has no interface to use with read(2) ++ // family functions. ++ std::array buf; ++ ++ ssize_t nread; ++ while ((nread = pread(config->data_fd, buf.data(), buf.size(), ++ req_stat->data_offset)) == -1 && ++ errno == EINTR) ++ ; ++ ++ if (nread == -1) { ++ return -1; ++ } ++ ++ req_stat->data_offset += nread; ++ ++ wb.append(buf.data(), nread); ++ ++ if (client_->worker->config->verbose) { ++ std::cout << "[send " << nread << " byte(s)]" << std::endl; ++ } ++ ++ if (req_stat->data_offset == config->data_length) { ++ // increment for next request ++ stream_req_counter_ += 2; ++ ++ if (stream_resp_counter_ == stream_req_counter_) { ++ // Response has already been received ++ client_->on_stream_close(stream_resp_counter_ - 2, true, ++ client_->final); ++ } ++ } ++ } ++ ++ return 0; ++} ++ ++void Http1Session::terminate() { complete_ = true; } ++ ++Client *Http1Session::get_client() { return client_; } ++ ++size_t Http1Session::max_concurrent_streams() { ++ auto config = client_->worker->config; ++ ++ return config->data_fd == -1 ? config->max_concurrent_streams : 1; ++} ++ ++} // namespace h2load +diff --git a/src/h2load_http1_session_verto.h b/src/h2load_http1_session_verto.h +new file mode 100644 +index 0000000..13a69d4 +--- /dev/null ++++ b/src/h2load_http1_session_verto.h +@@ -0,0 +1,60 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2015 British Broadcasting Corporation ++ * ++ * 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. ++ */ ++#ifndef H2LOAD_HTTP1_SESSION_H ++#define H2LOAD_HTTP1_SESSION_H ++ ++#include "h2load_session_verto.h" ++ ++#include ++ ++#include "llhttp.h" ++ ++namespace h2load { ++ ++struct Client; ++ ++class Http1Session : public Session { ++public: ++ Http1Session(Client *client); ++ virtual ~Http1Session(); ++ virtual void on_connect(); ++ virtual int submit_request(); ++ virtual int on_read(const uint8_t *data, size_t len); ++ virtual int on_write(); ++ virtual void terminate(); ++ virtual size_t max_concurrent_streams(); ++ Client *get_client(); ++ int32_t stream_req_counter_; ++ int32_t stream_resp_counter_; ++ ++private: ++ Client *client_; ++ llhttp_t htp_; ++ bool complete_; ++}; ++ ++} // namespace h2load ++ ++#endif // H2LOAD_HTTP1_SESSION_H +diff --git a/src/h2load_http2_session_verto.cc b/src/h2load_http2_session_verto.cc +new file mode 100644 +index 0000000..d846d52 +--- /dev/null ++++ b/src/h2load_http2_session_verto.cc +@@ -0,0 +1,312 @@ ++/* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2014 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 "h2load_http2_session_verto.h" ++ ++#include ++#include ++#include ++ ++#include "h2load_verto.h" ++#include "util.h" ++#include "template.h" ++ ++using namespace nghttp2; ++ ++namespace h2load { ++ ++Http2Session::Http2Session(Client *client) ++ : client_(client), session_(nullptr) { ++} ++ ++Http2Session::~Http2Session() { nghttp2_session_del(session_); } ++ ++namespace { ++int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, ++ const uint8_t *name, size_t namelen, ++ const uint8_t *value, size_t valuelen, uint8_t flags, ++ void *user_data) { ++ auto client = static_cast(user_data); ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_RESPONSE) { ++ return 0; ++ } ++ client->on_header(frame->hd.stream_id, name, namelen, value, valuelen); ++ client->worker->stats.bytes_head_decomp += namelen + valuelen; ++ ++ if (client->worker->config->verbose) { ++ std::cout << "[stream_id=" << frame->hd.stream_id << "] "; ++ std::cout.write(reinterpret_cast(name), namelen); ++ std::cout << ": "; ++ std::cout.write(reinterpret_cast(value), valuelen); ++ std::cout << "\n"; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, ++ void *user_data) { ++ auto client = static_cast(user_data); ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_RESPONSE) { ++ return 0; ++ } ++ client->worker->stats.bytes_head += ++ frame->hd.length - frame->headers.padlen - ++ ((frame->hd.flags & NGHTTP2_FLAG_PRIORITY) ? 5 : 0); ++ if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { ++ client->record_ttfb(); ++ } ++ 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 client = static_cast(user_data); ++ client->record_ttfb(); ++ client->worker->stats.bytes_body += len; ++ return 0; ++} ++} // namespace ++ ++namespace { ++int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, ++ uint32_t error_code, void *user_data) { ++ auto client = static_cast(user_data); ++ client->on_stream_close(stream_id, error_code == NGHTTP2_NO_ERROR); ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++int before_frame_send_callback(nghttp2_session *session, ++ const nghttp2_frame *frame, void *user_data) { ++ if (frame->hd.type != NGHTTP2_HEADERS || ++ frame->headers.cat != NGHTTP2_HCAT_REQUEST) { ++ return 0; ++ } ++ ++ auto client = static_cast(user_data); ++ auto req_stat = client->get_req_stat(frame->hd.stream_id); ++ assert(req_stat); ++ client->record_request_time(req_stat); ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++ssize_t file_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) { ++ auto client = static_cast(user_data); ++ auto config = client->worker->config; ++ auto req_stat = client->get_req_stat(stream_id); ++ assert(req_stat); ++ ssize_t nread; ++ while ((nread = pread(config->data_fd, buf, length, req_stat->data_offset)) == ++ -1 && ++ errno == EINTR) ++ ; ++ ++ if (nread == -1) { ++ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; ++ } ++ ++ req_stat->data_offset += nread; ++ ++ if (req_stat->data_offset == config->data_length) { ++ *data_flags |= NGHTTP2_DATA_FLAG_EOF; ++ return nread; ++ } ++ ++ if (req_stat->data_offset > config->data_length || nread == 0) { ++ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; ++ } ++ ++ return nread; ++} ++ ++} // namespace ++ ++namespace { ++ssize_t send_callback(nghttp2_session *session, const uint8_t *data, ++ size_t length, int flags, void *user_data) { ++ auto client = static_cast(user_data); ++ auto &wb = client->wb; ++ ++ if (wb.rleft() >= BACKOFF_WRITE_BUFFER_THRES) { ++ return NGHTTP2_ERR_WOULDBLOCK; ++ } ++ ++ return wb.append(data, length); ++} ++} // namespace ++ ++void Http2Session::on_connect() { ++ int rv; ++ ++ // This is required with --disable-assert. ++ (void)rv; ++ ++ nghttp2_session_callbacks *callbacks; ++ ++ nghttp2_session_callbacks_new(&callbacks); ++ ++ auto callbacks_deleter = defer(nghttp2_session_callbacks_del, callbacks); ++ ++ 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_stream_close_callback( ++ callbacks, on_stream_close_callback); ++ ++ nghttp2_session_callbacks_set_on_header_callback(callbacks, ++ on_header_callback); ++ ++ nghttp2_session_callbacks_set_before_frame_send_callback( ++ callbacks, before_frame_send_callback); ++ ++ nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); ++ ++ nghttp2_option *opt; ++ ++ rv = nghttp2_option_new(&opt); ++ ++ assert(rv == 0); ++ ++ auto config = client_->worker->config; ++ ++ if (config->encoder_header_table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { ++ nghttp2_option_set_max_deflate_dynamic_table_size( ++ opt, config->encoder_header_table_size); ++ } ++ ++ nghttp2_session_client_new2(&session_, callbacks, client_, opt); ++ ++ nghttp2_option_del(opt); ++ ++ std::array iv; ++ size_t niv = 2; ++ iv[0].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; ++ iv[0].value = 0; ++ iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; ++ iv[1].value = (1 << config->window_bits) - 1; ++ ++ if (config->header_table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { ++ iv[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; ++ iv[niv].value = config->header_table_size; ++ ++niv; ++ } ++ if (config->max_frame_size != 16_k) { ++ iv[niv].settings_id = NGHTTP2_SETTINGS_MAX_FRAME_SIZE; ++ iv[niv].value = config->max_frame_size; ++ ++niv; ++ } ++ ++ rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv.data(), niv); ++ ++ assert(rv == 0); ++ ++ auto connection_window = (1 << config->connection_window_bits) - 1; ++ nghttp2_session_set_local_window_size(session_, NGHTTP2_FLAG_NONE, 0, ++ connection_window); ++ ++} ++ ++int Http2Session::submit_request() { ++ if (nghttp2_session_check_request_allowed(session_) == 0) { ++ return -1; ++ } ++ ++ auto config = client_->worker->config; ++ auto &nva = config->nva[client_->reqidx++]; ++ ++ if (client_->reqidx == config->nva.size()) { ++ client_->reqidx = 0; ++ } ++ ++ nghttp2_data_provider prd{{0}, file_read_callback}; ++ ++ auto stream_id = ++ nghttp2_submit_request(session_, nullptr, nva.data(), nva.size(), ++ config->data_fd == -1 ? nullptr : &prd, nullptr); ++ if (stream_id < 0) { ++ return -1; ++ } ++ ++ client_->on_request(stream_id); ++ ++ return 0; ++} ++ ++int Http2Session::on_read(const uint8_t *data, size_t len) { ++ auto rv = nghttp2_session_mem_recv(session_, data, len); ++ if (rv < 0) { ++ return -1; ++ } ++ ++ assert(static_cast(rv) == len); ++ ++ if (nghttp2_session_want_read(session_) == 0 && ++ nghttp2_session_want_write(session_) == 0 && client_->wb.rleft() == 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int Http2Session::on_write() { ++ auto rv = nghttp2_session_send(session_); ++ if (rv != 0) { ++ return -1; ++ } ++ ++ if (nghttp2_session_want_read(session_) == 0 && ++ nghttp2_session_want_write(session_) == 0 && client_->wb.rleft() == 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++void Http2Session::terminate() { ++ nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); ++} ++ ++size_t Http2Session::max_concurrent_streams() { ++ return (size_t)client_->worker->config->max_concurrent_streams; ++} ++ ++} // namespace h2load +diff --git a/src/h2load_http2_session_verto.h b/src/h2load_http2_session_verto.h +new file mode 100644 +index 0000000..3e2bb44 +--- /dev/null ++++ b/src/h2load_http2_session_verto.h +@@ -0,0 +1,54 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2014 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. ++ */ ++#ifndef H2LOAD_HTTP2_SESSION_H ++#define H2LOAD_HTTP2_SESSION_H ++ ++#include "h2load_session_verto.h" ++ ++#include ++ ++namespace h2load { ++ ++struct Client; ++ ++class Http2Session : public Session { ++public: ++ Http2Session(Client *client); ++ virtual ~Http2Session(); ++ virtual void on_connect(); ++ virtual int submit_request(); ++ virtual int on_read(const uint8_t *data, size_t len); ++ virtual int on_write(); ++ virtual void terminate(); ++ virtual size_t max_concurrent_streams(); ++ ++private: ++ Client *client_; ++ nghttp2_session *session_; ++}; ++ ++} // namespace h2load ++ ++#endif // H2LOAD_HTTP2_SESSION_H +diff --git a/src/h2load_http3_session_verto.cc b/src/h2load_http3_session_verto.cc +new file mode 100644 +index 0000000..e2e43fc +--- /dev/null ++++ b/src/h2load_http3_session_verto.cc +@@ -0,0 +1,457 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2019 nghttp2 contributors ++ * ++ * 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 "h2load_http3_session_verto.h" ++ ++#include ++ ++#include ++ ++#include "h2load_verto.h" ++ ++namespace h2load { ++ ++Http3Session::Http3Session(Client *client) ++ : client_(client), conn_(nullptr), npending_request_(0), reqidx_(0) {} ++ ++Http3Session::~Http3Session() { nghttp3_conn_del(conn_); } ++ ++void Http3Session::on_connect() {} ++ ++int Http3Session::submit_request() { ++ if (npending_request_) { ++ ++npending_request_; ++ return 0; ++ } ++ ++ auto config = client_->worker->config; ++ reqidx_ = client_->reqidx; ++ ++ if (++client_->reqidx == config->nva.size()) { ++ client_->reqidx = 0; ++ } ++ ++ auto stream_id = submit_request_internal(); ++ if (stream_id < 0) { ++ if (stream_id == NGTCP2_ERR_STREAM_ID_BLOCKED) { ++ ++npending_request_; ++ return 0; ++ } ++ return -1; ++ } ++ ++ return 0; ++} ++ ++namespace { ++nghttp3_ssize read_data(nghttp3_conn *conn, int64_t stream_id, nghttp3_vec *vec, ++ size_t veccnt, uint32_t *pflags, void *user_data, ++ void *stream_user_data) { ++ auto s = static_cast(user_data); ++ ++ s->read_data(vec, veccnt, pflags); ++ ++ return 1; ++} ++} // namespace ++ ++void Http3Session::read_data(nghttp3_vec *vec, size_t veccnt, ++ uint32_t *pflags) { ++ assert(veccnt > 0); ++ ++ auto config = client_->worker->config; ++ ++ vec[0].base = config->data; ++ vec[0].len = config->data_length; ++ *pflags |= NGHTTP3_DATA_FLAG_EOF; ++} ++ ++int64_t Http3Session::submit_request_internal() { ++ int rv; ++ int64_t stream_id; ++ ++ auto config = client_->worker->config; ++ auto &nva = config->nva[reqidx_]; ++ ++ rv = ngtcp2_conn_open_bidi_stream(client_->quic.conn, &stream_id, nullptr); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ nghttp3_data_reader dr{}; ++ dr.read_data = h2load::read_data; ++ ++ rv = nghttp3_conn_submit_request( ++ conn_, stream_id, reinterpret_cast(nva.data()), nva.size(), ++ config->data_fd == -1 ? nullptr : &dr, nullptr); ++ if (rv != 0) { ++ return rv; ++ } ++ ++ client_->on_request(stream_id); ++ auto req_stat = client_->get_req_stat(stream_id); ++ assert(req_stat); ++ client_->record_request_time(req_stat); ++ ++ return stream_id; ++} ++ ++int Http3Session::on_read(const uint8_t *data, size_t len) { return -1; } ++ ++int Http3Session::on_write() { return -1; } ++ ++void Http3Session::terminate() {} ++ ++size_t Http3Session::max_concurrent_streams() { ++ return (size_t)client_->worker->config->max_concurrent_streams; ++} ++ ++namespace { ++int stream_close(nghttp3_conn *conn, int64_t stream_id, uint64_t app_error_code, ++ void *user_data, void *stream_user_data) { ++ auto s = static_cast(user_data); ++ if (s->stream_close(stream_id, app_error_code) != 0) { ++ return NGHTTP3_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Http3Session::stream_close(int64_t stream_id, uint64_t app_error_code) { ++ if (!ngtcp2_is_bidi_stream(stream_id)) { ++ assert(!ngtcp2_conn_is_local_stream(client_->quic.conn, stream_id)); ++ ngtcp2_conn_extend_max_streams_uni(client_->quic.conn, 1); ++ } ++ client_->on_stream_close(stream_id, app_error_code == NGHTTP3_H3_NO_ERROR); ++ return 0; ++} ++ ++namespace { ++int recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, ++ size_t datalen, void *user_data, void *stream_user_data) { ++ auto s = static_cast(user_data); ++ s->recv_data(stream_id, data, datalen); ++ return 0; ++} ++} // namespace ++ ++void Http3Session::recv_data(int64_t stream_id, const uint8_t *data, ++ size_t datalen) { ++ client_->record_ttfb(); ++ client_->worker->stats.bytes_body += datalen; ++ consume(stream_id, datalen); ++} ++ ++namespace { ++int deferred_consume(nghttp3_conn *conn, int64_t stream_id, size_t nconsumed, ++ void *user_data, void *stream_user_data) { ++ auto s = static_cast(user_data); ++ s->consume(stream_id, nconsumed); ++ return 0; ++} ++} // namespace ++ ++void Http3Session::consume(int64_t stream_id, size_t nconsumed) { ++ ngtcp2_conn_extend_max_stream_offset(client_->quic.conn, stream_id, ++ nconsumed); ++ ngtcp2_conn_extend_max_offset(client_->quic.conn, nconsumed); ++} ++ ++namespace { ++int begin_headers(nghttp3_conn *conn, int64_t stream_id, void *user_data, ++ void *stream_user_data) { ++ auto s = static_cast(user_data); ++ s->begin_headers(stream_id); ++ return 0; ++} ++} // namespace ++ ++void Http3Session::begin_headers(int64_t stream_id) { ++ auto payloadlen = nghttp3_conn_get_frame_payload_left(conn_, stream_id); ++ assert(payloadlen > 0); ++ ++ client_->worker->stats.bytes_head += payloadlen; ++} ++ ++namespace { ++int recv_header(nghttp3_conn *conn, int64_t stream_id, int32_t token, ++ nghttp3_rcbuf *name, nghttp3_rcbuf *value, uint8_t flags, ++ void *user_data, void *stream_user_data) { ++ auto s = static_cast(user_data); ++ auto k = nghttp3_rcbuf_get_buf(name); ++ auto v = nghttp3_rcbuf_get_buf(value); ++ s->recv_header(stream_id, &k, &v); ++ return 0; ++} ++} // namespace ++ ++void Http3Session::recv_header(int64_t stream_id, const nghttp3_vec *name, ++ const nghttp3_vec *value) { ++ client_->on_header(stream_id, name->base, name->len, value->base, value->len); ++ client_->worker->stats.bytes_head_decomp += name->len + value->len; ++} ++ ++namespace { ++int stop_sending(nghttp3_conn *conn, int64_t stream_id, uint64_t app_error_code, ++ void *user_data, void *stream_user_data) { ++ auto s = static_cast(user_data); ++ if (s->stop_sending(stream_id, app_error_code) != 0) { ++ return NGHTTP3_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Http3Session::stop_sending(int64_t stream_id, uint64_t app_error_code) { ++ auto rv = ngtcp2_conn_shutdown_stream_read(client_->quic.conn, 0, stream_id, ++ app_error_code); ++ if (rv != 0) { ++ std::cerr << "ngtcp2_conn_shutdown_stream_read: " << ngtcp2_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ return 0; ++} ++ ++namespace { ++int reset_stream(nghttp3_conn *conn, int64_t stream_id, uint64_t app_error_code, ++ void *user_data, void *stream_user_data) { ++ auto s = static_cast(user_data); ++ if (s->reset_stream(stream_id, app_error_code) != 0) { ++ return NGHTTP3_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Http3Session::reset_stream(int64_t stream_id, uint64_t app_error_code) { ++ auto rv = ngtcp2_conn_shutdown_stream_write(client_->quic.conn, 0, stream_id, ++ app_error_code); ++ if (rv != 0) { ++ std::cerr << "ngtcp2_conn_shutdown_stream_write: " << ngtcp2_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ return 0; ++} ++ ++int Http3Session::close_stream(int64_t stream_id, uint64_t app_error_code) { ++ auto rv = nghttp3_conn_close_stream(conn_, stream_id, app_error_code); ++ switch (rv) { ++ case 0: ++ return 0; ++ case NGHTTP3_ERR_STREAM_NOT_FOUND: ++ if (!ngtcp2_is_bidi_stream(stream_id)) { ++ assert(!ngtcp2_conn_is_local_stream(client_->quic.conn, stream_id)); ++ ngtcp2_conn_extend_max_streams_uni(client_->quic.conn, 1); ++ } ++ return 0; ++ default: ++ return -1; ++ } ++} ++ ++int Http3Session::shutdown_stream_read(int64_t stream_id) { ++ auto rv = nghttp3_conn_shutdown_stream_read(conn_, stream_id); ++ if (rv != 0) { ++ return -1; ++ } ++ return 0; ++} ++ ++int Http3Session::extend_max_local_streams() { ++ auto config = client_->worker->config; ++ ++ for (; npending_request_; --npending_request_) { ++ auto stream_id = submit_request_internal(); ++ if (stream_id < 0) { ++ if (stream_id == NGTCP2_ERR_STREAM_ID_BLOCKED) { ++ return 0; ++ } ++ return -1; ++ } ++ ++ if (++reqidx_ == config->nva.size()) { ++ reqidx_ = 0; ++ } ++ } ++ ++ return 0; ++} ++ ++int Http3Session::init_conn() { ++ int rv; ++ ++ assert(conn_ == nullptr); ++ ++ if (ngtcp2_conn_get_streams_uni_left(client_->quic.conn) < 3) { ++ return -1; ++ } ++ ++ nghttp3_callbacks callbacks{ ++ nullptr, // acked_stream_data ++ h2load::stream_close, ++ h2load::recv_data, ++ h2load::deferred_consume, ++ h2load::begin_headers, ++ h2load::recv_header, ++ nullptr, // end_headers ++ nullptr, // begin_trailers ++ h2load::recv_header, ++ nullptr, // end_trailers ++ h2load::stop_sending, ++ nullptr, // end_stream ++ h2load::reset_stream, ++ nullptr, // shutdown ++ }; ++ ++ auto config = client_->worker->config; ++ ++ nghttp3_settings settings; ++ nghttp3_settings_default(&settings); ++ settings.qpack_max_dtable_capacity = config->header_table_size; ++ settings.qpack_blocked_streams = 100; ++ ++ auto mem = nghttp3_mem_default(); ++ ++ rv = nghttp3_conn_client_new(&conn_, &callbacks, &settings, mem, this); ++ if (rv != 0) { ++ std::cerr << "nghttp3_conn_client_new: " << nghttp3_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ ++ int64_t ctrl_stream_id; ++ ++ rv = ++ ngtcp2_conn_open_uni_stream(client_->quic.conn, &ctrl_stream_id, nullptr); ++ if (rv != 0) { ++ std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ ++ rv = nghttp3_conn_bind_control_stream(conn_, ctrl_stream_id); ++ if (rv != 0) { ++ std::cerr << "nghttp3_conn_bind_control_stream: " << nghttp3_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ ++ int64_t qpack_enc_stream_id, qpack_dec_stream_id; ++ ++ rv = ngtcp2_conn_open_uni_stream(client_->quic.conn, &qpack_enc_stream_id, ++ nullptr); ++ if (rv != 0) { ++ std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ ++ rv = ngtcp2_conn_open_uni_stream(client_->quic.conn, &qpack_dec_stream_id, ++ nullptr); ++ if (rv != 0) { ++ std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ ++ rv = nghttp3_conn_bind_qpack_streams(conn_, qpack_enc_stream_id, ++ qpack_dec_stream_id); ++ if (rv != 0) { ++ std::cerr << "nghttp3_conn_bind_qpack_streams: " << nghttp3_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ ++ return 0; ++} ++ ++ssize_t Http3Session::read_stream(uint32_t flags, int64_t stream_id, ++ const uint8_t *data, size_t datalen) { ++ auto nconsumed = nghttp3_conn_read_stream( ++ conn_, stream_id, data, datalen, flags & NGTCP2_STREAM_DATA_FLAG_FIN); ++ if (nconsumed < 0) { ++ std::cerr << "nghttp3_conn_read_stream: " << nghttp3_strerror(nconsumed) ++ << std::endl; ++ ngtcp2_ccerr_set_application_error( ++ &client_->quic.last_error, ++ nghttp3_err_infer_quic_app_error_code(nconsumed), nullptr, 0); ++ return -1; ++ } ++ return nconsumed; ++} ++ ++ssize_t Http3Session::write_stream(int64_t &stream_id, int &fin, ++ nghttp3_vec *vec, size_t veccnt) { ++ auto sveccnt = ++ nghttp3_conn_writev_stream(conn_, &stream_id, &fin, vec, veccnt); ++ if (sveccnt < 0) { ++ ngtcp2_ccerr_set_application_error( ++ &client_->quic.last_error, ++ nghttp3_err_infer_quic_app_error_code(sveccnt), nullptr, 0); ++ return -1; ++ } ++ return sveccnt; ++} ++ ++void Http3Session::block_stream(int64_t stream_id) { ++ nghttp3_conn_block_stream(conn_, stream_id); ++} ++ ++int Http3Session::unblock_stream(int64_t stream_id) { ++ if (nghttp3_conn_unblock_stream(conn_, stream_id) != 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++void Http3Session::shutdown_stream_write(int64_t stream_id) { ++ nghttp3_conn_shutdown_stream_write(conn_, stream_id); ++} ++ ++int Http3Session::add_write_offset(int64_t stream_id, size_t ndatalen) { ++ auto rv = nghttp3_conn_add_write_offset(conn_, stream_id, ndatalen); ++ if (rv != 0) { ++ ngtcp2_ccerr_set_application_error( ++ &client_->quic.last_error, nghttp3_err_infer_quic_app_error_code(rv), ++ nullptr, 0); ++ return -1; ++ } ++ return 0; ++} ++ ++int Http3Session::add_ack_offset(int64_t stream_id, size_t datalen) { ++ auto rv = nghttp3_conn_add_ack_offset(conn_, stream_id, datalen); ++ if (rv != 0) { ++ ngtcp2_ccerr_set_application_error( ++ &client_->quic.last_error, nghttp3_err_infer_quic_app_error_code(rv), ++ nullptr, 0); ++ return -1; ++ } ++ return 0; ++} ++ ++} // namespace h2load +diff --git a/src/h2load_http3_session_verto.h b/src/h2load_http3_session_verto.h +new file mode 100644 +index 0000000..72e8c20 +--- /dev/null ++++ b/src/h2load_http3_session_verto.h +@@ -0,0 +1,83 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2019 nghttp2 contributors ++ * ++ * 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. ++ */ ++#ifndef H2LOAD_HTTP3_SESSION_H ++#define H2LOAD_HTTP3_SESSION_H ++ ++#include "h2load_session_verto.h" ++ ++#include ++ ++namespace h2load { ++ ++struct Client; ++ ++class Http3Session : public Session { ++public: ++ Http3Session(Client *client); ++ virtual ~Http3Session(); ++ virtual void on_connect(); ++ virtual int submit_request(); ++ virtual int on_read(const uint8_t *data, size_t len); ++ virtual int on_write(); ++ virtual void terminate(); ++ virtual size_t max_concurrent_streams(); ++ ++ int init_conn(); ++ int stream_close(int64_t stream_id, uint64_t app_error_code); ++ void recv_data(int64_t stream_id, const uint8_t *data, size_t datalen); ++ void consume(int64_t stream_id, size_t nconsumed); ++ void begin_headers(int64_t stream_id); ++ void recv_header(int64_t stream_id, const nghttp3_vec *name, ++ const nghttp3_vec *value); ++ int stop_sending(int64_t stream_id, uint64_t app_error_code); ++ int reset_stream(int64_t stream_id, uint64_t app_error_code); ++ ++ int close_stream(int64_t stream_id, uint64_t app_error_code); ++ int shutdown_stream_read(int64_t stream_id); ++ int extend_max_local_streams(); ++ int64_t submit_request_internal(); ++ ++ ssize_t read_stream(uint32_t flags, int64_t stream_id, const uint8_t *data, ++ size_t datalen); ++ ssize_t write_stream(int64_t &stream_id, int &fin, nghttp3_vec *vec, ++ size_t veccnt); ++ void block_stream(int64_t stream_id); ++ int unblock_stream(int64_t stream_id); ++ void shutdown_stream_write(int64_t stream_id); ++ int add_write_offset(int64_t stream_id, size_t ndatalen); ++ int add_ack_offset(int64_t stream_id, size_t datalen); ++ ++ void read_data(nghttp3_vec *vec, size_t veccnt, uint32_t *pflags); ++ ++private: ++ Client *client_; ++ nghttp3_conn *conn_; ++ size_t npending_request_; ++ size_t reqidx_; ++}; ++ ++} // namespace h2load ++ ++#endif // H2LOAD_HTTP3_SESSION_H +diff --git a/src/h2load_quic_verto.cc b/src/h2load_quic_verto.cc +new file mode 100644 +index 0000000..865d871 +--- /dev/null ++++ b/src/h2load_quic_verto.cc +@@ -0,0 +1,838 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2019 nghttp2 contributors ++ * ++ * 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 "h2load_quic_verto.h" ++ ++#include ++ ++#include ++ ++#ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS ++# include ++#endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS ++#ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL ++# include ++#endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL ++ ++#include ++#include ++ ++#include "h2load_http3_session_verto.h" ++ ++namespace h2load { ++ ++namespace { ++int handshake_completed(ngtcp2_conn *conn, void *user_data) { ++ auto c = static_cast(user_data); ++ ++ if (c->quic_handshake_completed() != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++int Client::quic_handshake_completed() { return connection_made(); } ++ ++namespace { ++int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, ++ uint64_t offset, const uint8_t *data, size_t datalen, ++ void *user_data, void *stream_user_data) { ++ auto c = static_cast(user_data); ++ if (c->quic_recv_stream_data(flags, stream_id, data, datalen) != 0) { ++ // TODO Better to do this gracefully rather than ++ // NGTCP2_ERR_CALLBACK_FAILURE. Perhaps, call ++ // ngtcp2_conn_write_application_close() ? ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Client::quic_recv_stream_data(uint32_t flags, int64_t stream_id, ++ const uint8_t *data, size_t datalen) { ++ if (worker->current_phase == Phase::MAIN_DURATION) { ++ worker->stats.bytes_total += datalen; ++ } ++ ++ auto s = static_cast(session.get()); ++ auto nconsumed = s->read_stream(flags, stream_id, data, datalen); ++ if (nconsumed == -1) { ++ return -1; ++ } ++ ++ ngtcp2_conn_extend_max_stream_offset(quic.conn, stream_id, nconsumed); ++ ngtcp2_conn_extend_max_offset(quic.conn, nconsumed); ++ ++ return 0; ++} ++ ++namespace { ++int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, ++ uint64_t offset, uint64_t datalen, void *user_data, ++ void *stream_user_data) { ++ auto c = static_cast(user_data); ++ if (c->quic_acked_stream_data_offset(stream_id, datalen) != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Client::quic_acked_stream_data_offset(int64_t stream_id, size_t datalen) { ++ auto s = static_cast(session.get()); ++ if (s->add_ack_offset(stream_id, datalen) != 0) { ++ return -1; ++ } ++ return 0; ++} ++ ++namespace { ++int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, ++ uint64_t app_error_code, void *user_data, ++ void *stream_user_data) { ++ auto c = static_cast(user_data); ++ ++ if (!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) { ++ app_error_code = NGHTTP3_H3_NO_ERROR; ++ } ++ ++ if (c->quic_stream_close(stream_id, app_error_code) != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Client::quic_stream_close(int64_t stream_id, uint64_t app_error_code) { ++ auto s = static_cast(session.get()); ++ if (s->close_stream(stream_id, app_error_code) != 0) { ++ return -1; ++ } ++ return 0; ++} ++ ++namespace { ++int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, ++ uint64_t app_error_code, void *user_data, ++ void *stream_user_data) { ++ auto c = static_cast(user_data); ++ if (c->quic_stream_reset(stream_id, app_error_code) != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Client::quic_stream_reset(int64_t stream_id, uint64_t app_error_code) { ++ auto s = static_cast(session.get()); ++ if (s->shutdown_stream_read(stream_id) != 0) { ++ return -1; ++ } ++ return 0; ++} ++ ++namespace { ++int stream_stop_sending(ngtcp2_conn *conn, int64_t stream_id, ++ uint64_t app_error_code, void *user_data, ++ void *stream_user_data) { ++ auto c = static_cast(user_data); ++ if (c->quic_stream_stop_sending(stream_id, app_error_code) != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++} // namespace ++ ++int Client::quic_stream_stop_sending(int64_t stream_id, ++ uint64_t app_error_code) { ++ auto s = static_cast(session.get()); ++ if (s->shutdown_stream_read(stream_id) != 0) { ++ return -1; ++ } ++ return 0; ++} ++ ++namespace { ++int extend_max_local_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, ++ void *user_data) { ++ auto c = static_cast(user_data); ++ ++ if (c->quic_extend_max_local_streams() != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++int Client::quic_extend_max_local_streams() { ++ auto s = static_cast(session.get()); ++ if (s->extend_max_local_streams() != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ return 0; ++} ++ ++namespace { ++int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, ++ uint64_t max_data, void *user_data, ++ void *stream_user_data) { ++ auto c = static_cast(user_data); ++ ++ if (c->quic_extend_max_stream_data(stream_id) != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++int Client::quic_extend_max_stream_data(int64_t stream_id) { ++ auto s = static_cast(session.get()); ++ if (s->unblock_stream(stream_id) != 0) { ++ return -1; ++ } ++ return 0; ++} ++ ++namespace { ++int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, ++ size_t cidlen, void *user_data) { ++ if (RAND_bytes(cid->data, cidlen) != 1) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ ++ cid->datalen = cidlen; ++ ++ if (RAND_bytes(token, NGTCP2_STATELESS_RESET_TOKENLEN) != 1) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++void debug_log_printf(void *user_data, const char *fmt, ...) { ++ va_list ap; ++ ++ va_start(ap, fmt); ++ vfprintf(stderr, fmt, ap); ++ va_end(ap); ++ ++ fprintf(stderr, "\n"); ++} ++} // namespace ++ ++namespace { ++int generate_cid(ngtcp2_cid &dest) { ++ dest.datalen = 8; ++ ++ if (RAND_bytes(dest.data, dest.datalen) != 1) { ++ return -1; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++ngtcp2_tstamp quic_timestamp() { ++ return std::chrono::duration_cast( ++ std::chrono::steady_clock::now().time_since_epoch()) ++ .count(); ++} ++} // namespace ++ ++// qlog write callback -- excerpted from ngtcp2/examples/client_base.cc ++namespace { ++void qlog_write_cb(void *user_data, uint32_t flags, const void *data, ++ size_t datalen) { ++ auto c = static_cast(user_data); ++ c->quic_write_qlog(data, datalen); ++} ++} // namespace ++ ++void Client::quic_write_qlog(const void *data, size_t datalen) { ++ assert(quic.qlog_file != nullptr); ++ fwrite(data, 1, datalen, quic.qlog_file); ++} ++ ++namespace { ++void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { ++ util::random_bytes(dest, dest + destlen, ++ *static_cast(rand_ctx->native_handle)); ++} ++} // namespace ++ ++namespace { ++int recv_rx_key(ngtcp2_conn *conn, ngtcp2_encryption_level level, ++ void *user_data) { ++ if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) { ++ return 0; ++ } ++ ++ auto c = static_cast(user_data); ++ ++ if (c->quic_make_http3_session() != 0) { ++ return NGTCP2_ERR_CALLBACK_FAILURE; ++ } ++ ++ return 0; ++} ++} // namespace ++ ++int Client::quic_make_http3_session() { ++ auto s = std::make_unique(this); ++ if (s->init_conn() == -1) { ++ return -1; ++ } ++ session = std::move(s); ++ ++ return 0; ++} ++ ++namespace { ++ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { ++ auto c = static_cast(conn_ref->user_data); ++ return c->quic.conn; ++} ++} // namespace ++ ++int Client::quic_init(const sockaddr *local_addr, socklen_t local_addrlen, ++ const sockaddr *remote_addr, socklen_t remote_addrlen) { ++ int rv; ++ ++ if (!ssl) { ++ ssl = SSL_new(worker->ssl_ctx); ++ ++ quic.conn_ref.get_conn = get_conn; ++ quic.conn_ref.user_data = this; ++ ++ SSL_set_app_data(ssl, &quic.conn_ref); ++ SSL_set_connect_state(ssl); ++ SSL_set_quic_use_legacy_codepoint(ssl, 0); ++ } ++ ++ auto callbacks = ngtcp2_callbacks{ ++ ngtcp2_crypto_client_initial_cb, ++ nullptr, // recv_client_initial ++ ngtcp2_crypto_recv_crypto_data_cb, ++ h2load::handshake_completed, ++ nullptr, // recv_version_negotiation ++ ngtcp2_crypto_encrypt_cb, ++ ngtcp2_crypto_decrypt_cb, ++ ngtcp2_crypto_hp_mask_cb, ++ h2load::recv_stream_data, ++ h2load::acked_stream_data_offset, ++ nullptr, // stream_open ++ h2load::stream_close, ++ nullptr, // recv_stateless_reset ++ ngtcp2_crypto_recv_retry_cb, ++ h2load::extend_max_local_streams_bidi, ++ nullptr, // extend_max_local_streams_uni ++ h2load::rand, ++ get_new_connection_id, ++ nullptr, // remove_connection_id ++ ngtcp2_crypto_update_key_cb, ++ nullptr, // path_validation ++ nullptr, // select_preferred_addr ++ h2load::stream_reset, ++ nullptr, // extend_max_remote_streams_bidi ++ nullptr, // extend_max_remote_streams_uni ++ h2load::extend_max_stream_data, ++ nullptr, // dcid_status ++ nullptr, // handshake_confirmed ++ nullptr, // recv_new_token ++ ngtcp2_crypto_delete_crypto_aead_ctx_cb, ++ ngtcp2_crypto_delete_crypto_cipher_ctx_cb, ++ nullptr, // recv_datagram ++ nullptr, // ack_datagram ++ nullptr, // lost_datagram ++ ngtcp2_crypto_get_path_challenge_data_cb, ++ h2load::stream_stop_sending, ++ nullptr, // version_negotiation ++ h2load::recv_rx_key, ++ nullptr, // recv_tx_key ++ }; ++ ++ ngtcp2_cid scid, dcid; ++ if (generate_cid(scid) != 0) { ++ return -1; ++ } ++ if (generate_cid(dcid) != 0) { ++ return -1; ++ } ++ ++ auto config = worker->config; ++ ++ ngtcp2_settings settings; ++ ngtcp2_settings_default(&settings); ++ if (config->verbose) { ++ settings.log_printf = debug_log_printf; ++ } ++ settings.initial_ts = quic_timestamp(); ++ settings.rand_ctx.native_handle = &worker->randgen; ++ if (!config->qlog_file_base.empty()) { ++ assert(quic.qlog_file == nullptr); ++ auto path = config->qlog_file_base; ++ path += '.'; ++ path += util::utos(worker->id); ++ path += '.'; ++ path += util::utos(id); ++ path += ".sqlog"; ++ quic.qlog_file = fopen(path.c_str(), "w"); ++ if (quic.qlog_file == nullptr) { ++ std::cerr << "Failed to open a qlog file: " << path << std::endl; ++ return -1; ++ } ++ settings.qlog_write = qlog_write_cb; ++ } ++ if (config->max_udp_payload_size) { ++ settings.max_tx_udp_payload_size = config->max_udp_payload_size; ++ settings.no_tx_udp_payload_size_shaping = 1; ++ } ++ ++ ngtcp2_transport_params params; ++ ngtcp2_transport_params_default(¶ms); ++ auto max_stream_data = ++ std::min((1 << 26) - 1, (1 << config->window_bits) - 1); ++ params.initial_max_stream_data_bidi_local = max_stream_data; ++ params.initial_max_stream_data_uni = max_stream_data; ++ params.initial_max_data = (1 << config->connection_window_bits) - 1; ++ params.initial_max_streams_bidi = 0; ++ params.initial_max_streams_uni = 100; ++ params.max_idle_timeout = 30 * NGTCP2_SECONDS; ++ ++ auto path = ngtcp2_path{ ++ { ++ const_cast(local_addr), ++ local_addrlen, ++ }, ++ { ++ const_cast(remote_addr), ++ remote_addrlen, ++ }, ++ }; ++ ++ assert(config->npn_list.size()); ++ ++ uint32_t quic_version; ++ ++ if (config->npn_list[0] == NGHTTP3_ALPN_H3) { ++ quic_version = NGTCP2_PROTO_VER_V1; ++ } else { ++ quic_version = NGTCP2_PROTO_VER_MIN; ++ } ++ ++ rv = ngtcp2_conn_client_new(&quic.conn, &dcid, &scid, &path, quic_version, ++ &callbacks, &settings, ¶ms, nullptr, this); ++ if (rv != 0) { ++ return -1; ++ } ++ ++ ngtcp2_conn_set_tls_native_handle(quic.conn, ssl); ++ ++ return 0; ++} ++ ++void Client::quic_free() { ++ ngtcp2_conn_del(quic.conn); ++ if (quic.qlog_file != nullptr) { ++ fclose(quic.qlog_file); ++ quic.qlog_file = nullptr; ++ } ++} ++ ++void Client::quic_close_connection() { ++ if (!quic.conn) { ++ return; ++ } ++ ++ std::array buf; ++ ngtcp2_path_storage ps; ++ ngtcp2_path_storage_zero(&ps); ++ ++ auto nwrite = ngtcp2_conn_write_connection_close( ++ quic.conn, &ps.path, nullptr, buf.data(), buf.size(), &quic.last_error, ++ quic_timestamp()); ++ ++ if (nwrite <= 0) { ++ return; ++ } ++ ++ write_udp(reinterpret_cast(ps.path.remote.addr), ++ ps.path.remote.addrlen, buf.data(), nwrite, 0); ++} ++ ++int Client::quic_write_client_handshake(ngtcp2_encryption_level level, ++ const uint8_t *data, size_t datalen) { ++ int rv; ++ ++ assert(level < 2); ++ ++ rv = ngtcp2_conn_submit_crypto_data(quic.conn, level, data, datalen); ++ if (rv != 0) { ++ std::cerr << "ngtcp2_conn_submit_crypto_data: " << ngtcp2_strerror(rv) ++ << std::endl; ++ return -1; ++ } ++ ++ return 0; ++} ++ ++void verto_quic_pkt_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto c = static_cast(verto_get_private(ev)); ++ ++ if (c->quic_pkt_timeout() != 0) { ++ c->fail(); ++ c->worker->free_client(c); ++ delete c; ++ return; ++ } ++} ++ ++int Client::quic_pkt_timeout() { ++ int rv; ++ auto now = quic_timestamp(); ++ ++ rv = ngtcp2_conn_handle_expiry(quic.conn, now); ++ if (rv != 0) { ++ ngtcp2_ccerr_set_liberr(&quic.last_error, rv, nullptr, 0); ++ return -1; ++ } ++ ++ return write_quic(); ++} ++ ++void Client::quic_restart_pkt_timer() { ++ auto expiry = ngtcp2_conn_get_expiry(quic.conn); ++ auto now = quic_timestamp(); ++ auto t = expiry > now ? static_cast(expiry - now) / NGTCP2_SECONDS ++ : 1e-9; ++ // quic.pkt_timer.repeat = t; ++ // ev_timer_again(worker->loop, &quic.pkt_timer); ++} ++ ++int Client::read_quic() { ++ std::array buf; ++ sockaddr_union su; ++ int rv; ++ size_t pktcnt = 0; ++ ngtcp2_pkt_info pi{}; ++ ++ iovec msg_iov; ++ msg_iov.iov_base = buf.data(); ++ msg_iov.iov_len = buf.size(); ++ ++ msghdr msg{}; ++ msg.msg_name = &su; ++ msg.msg_iov = &msg_iov; ++ msg.msg_iovlen = 1; ++ ++ uint8_t msg_ctrl[CMSG_SPACE(sizeof(uint16_t))]; ++ msg.msg_control = msg_ctrl; ++ ++ auto ts = quic_timestamp(); ++ ++ for (;;) { ++ msg.msg_namelen = sizeof(su); ++ msg.msg_controllen = sizeof(msg_ctrl); ++ ++ auto nread = recvmsg(fd, &msg, 0); ++ if (nread == -1) { ++ return 0; ++ } ++ ++ auto gso_size = util::msghdr_get_udp_gro(&msg); ++ if (gso_size == 0) { ++ gso_size = static_cast(nread); ++ } ++ ++ assert(quic.conn); ++ ++ ++worker->stats.udp_dgram_recv; ++ ++ auto path = ngtcp2_path{ ++ { ++ &local_addr.su.sa, ++ static_cast(local_addr.len), ++ }, ++ { ++ &su.sa, ++ msg.msg_namelen, ++ }, ++ }; ++ ++ auto data = buf.data(); ++ ++ for (;;) { ++ auto datalen = std::min(static_cast(nread), gso_size); ++ ++ ++pktcnt; ++ ++ rv = ngtcp2_conn_read_pkt(quic.conn, &path, &pi, data, datalen, ts); ++ if (rv != 0) { ++ if (!quic.last_error.error_code) { ++ if (rv == NGTCP2_ERR_CRYPTO) { ++ ngtcp2_ccerr_set_tls_alert(&quic.last_error, ++ ngtcp2_conn_get_tls_alert(quic.conn), ++ nullptr, 0); ++ } else { ++ ngtcp2_ccerr_set_liberr(&quic.last_error, rv, nullptr, 0); ++ } ++ } ++ ++ return -1; ++ } ++ ++ nread -= datalen; ++ if (nread == 0) { ++ break; ++ } ++ ++ data += datalen; ++ } ++ ++ if (pktcnt >= 100) { ++ break; ++ } ++ } ++ ++ return 0; ++} ++ ++int Client::write_quic() { ++ int rv; ++ ++ //ev_io_stop(worker->loop, &wev); ++ ++ if (quic.close_requested) { ++ return -1; ++ } ++ ++ if (quic.tx.send_blocked) { ++ rv = send_blocked_packet(); ++ if (rv != 0) { ++ return -1; ++ } ++ ++ if (quic.tx.send_blocked) { ++ return 0; ++ } ++ } ++ ++ std::array vec; ++ size_t pktcnt = 0; ++ auto max_udp_payload_size = ++ ngtcp2_conn_get_max_tx_udp_payload_size(quic.conn); ++#ifdef UDP_SEGMENT ++ auto path_max_udp_payload_size = ++ ngtcp2_conn_get_path_max_tx_udp_payload_size(quic.conn); ++#endif // UDP_SEGMENT ++ auto max_pktcnt = ++ ngtcp2_conn_get_send_quantum(quic.conn) / max_udp_payload_size; ++ uint8_t *bufpos = quic.tx.data.get(); ++ ngtcp2_path_storage ps; ++ size_t gso_size = 0; ++ ++ ngtcp2_path_storage_zero(&ps); ++ ++ auto s = static_cast(session.get()); ++ auto ts = quic_timestamp(); ++ ++ for (;;) { ++ int64_t stream_id = -1; ++ int fin = 0; ++ ssize_t sveccnt = 0; ++ ++ if (session && ngtcp2_conn_get_max_data_left(quic.conn)) { ++ sveccnt = s->write_stream(stream_id, fin, vec.data(), vec.size()); ++ if (sveccnt == -1) { ++ return -1; ++ } ++ } ++ ++ ngtcp2_ssize ndatalen; ++ auto v = vec.data(); ++ auto vcnt = static_cast(sveccnt); ++ ++ uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; ++ if (fin) { ++ flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; ++ } ++ ++ auto nwrite = ngtcp2_conn_writev_stream( ++ quic.conn, &ps.path, nullptr, bufpos, max_udp_payload_size, &ndatalen, ++ flags, stream_id, reinterpret_cast(v), vcnt, ts); ++ if (nwrite < 0) { ++ switch (nwrite) { ++ case NGTCP2_ERR_STREAM_DATA_BLOCKED: ++ assert(ndatalen == -1); ++ s->block_stream(stream_id); ++ continue; ++ case NGTCP2_ERR_STREAM_SHUT_WR: ++ assert(ndatalen == -1); ++ s->shutdown_stream_write(stream_id); ++ continue; ++ case NGTCP2_ERR_WRITE_MORE: ++ assert(ndatalen >= 0); ++ if (s->add_write_offset(stream_id, ndatalen) != 0) { ++ return -1; ++ } ++ continue; ++ } ++ ++ ngtcp2_ccerr_set_liberr(&quic.last_error, nwrite, nullptr, 0); ++ return -1; ++ } else if (ndatalen >= 0 && s->add_write_offset(stream_id, ndatalen) != 0) { ++ return -1; ++ } ++ ++ quic_restart_pkt_timer(); ++ ++ if (nwrite == 0) { ++ if (bufpos - quic.tx.data.get()) { ++ auto data = quic.tx.data.get(); ++ auto datalen = bufpos - quic.tx.data.get(); ++ rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, ++ datalen, gso_size); ++ if (rv == 1) { ++ on_send_blocked(ps.path.remote, data, datalen, gso_size); ++ return 0; ++ } ++ } ++ return 0; ++ } ++ ++ bufpos += nwrite; ++ ++#ifdef UDP_SEGMENT ++ if (worker->config->no_udp_gso) { ++#endif // UDP_SEGMENT ++ auto data = quic.tx.data.get(); ++ auto datalen = bufpos - quic.tx.data.get(); ++ rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, datalen, ++ 0); ++ if (rv == 1) { ++ on_send_blocked(ps.path.remote, data, datalen, 0); ++ return 0; ++ } ++ ++ if (++pktcnt == max_pktcnt) { ++ return 0; ++ } ++ ++ bufpos = quic.tx.data.get(); ++ ++#ifdef UDP_SEGMENT ++ continue; ++ } ++#endif // UDP_SEGMENT ++ ++#ifdef UDP_SEGMENT ++ if (pktcnt == 0) { ++ gso_size = nwrite; ++ } else if (static_cast(nwrite) > gso_size || ++ (gso_size > path_max_udp_payload_size && ++ static_cast(nwrite) != gso_size)) { ++ auto data = quic.tx.data.get(); ++ auto datalen = bufpos - quic.tx.data.get() - nwrite; ++ rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, datalen, ++ gso_size); ++ if (rv == 1) { ++ on_send_blocked(ps.path.remote, data, datalen, gso_size); ++ on_send_blocked(ps.path.remote, bufpos - nwrite, nwrite, 0); ++ } else { ++ auto data = bufpos - nwrite; ++ rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, ++ nwrite, 0); ++ if (rv == 1) { ++ on_send_blocked(ps.path.remote, data, nwrite, 0); ++ } ++ } ++ ++ return 0; ++ } ++ ++ // Assume that the path does not change. ++ if (++pktcnt == max_pktcnt || static_cast(nwrite) < gso_size) { ++ auto data = quic.tx.data.get(); ++ auto datalen = bufpos - quic.tx.data.get(); ++ rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, datalen, ++ gso_size); ++ if (rv == 1) { ++ on_send_blocked(ps.path.remote, data, datalen, gso_size); ++ } ++ return 0; ++ } ++#endif // UDP_SEGMENT ++ } ++} ++ ++void Client::on_send_blocked(const ngtcp2_addr &remote_addr, ++ const uint8_t *data, size_t datalen, ++ size_t gso_size) { ++ assert(quic.tx.num_blocked || !quic.tx.send_blocked); ++ assert(quic.tx.num_blocked < 2); ++ ++ quic.tx.send_blocked = true; ++ ++ auto &p = quic.tx.blocked[quic.tx.num_blocked++]; ++ ++ memcpy(&p.remote_addr.su, remote_addr.addr, remote_addr.addrlen); ++ ++ p.remote_addr.len = remote_addr.addrlen; ++ p.data = data; ++ p.datalen = datalen; ++ p.gso_size = gso_size; ++} ++ ++int Client::send_blocked_packet() { ++ int rv; ++ ++ assert(quic.tx.send_blocked); ++ ++ for (; quic.tx.num_blocked_sent < quic.tx.num_blocked; ++ ++quic.tx.num_blocked_sent) { ++ auto &p = quic.tx.blocked[quic.tx.num_blocked_sent]; ++ ++ rv = write_udp(&p.remote_addr.su.sa, p.remote_addr.len, p.data, p.datalen, ++ p.gso_size); ++ if (rv == 1) { ++ return 0; ++ } ++ } ++ ++ quic.tx.send_blocked = false; ++ quic.tx.num_blocked = 0; ++ quic.tx.num_blocked_sent = 0; ++ ++ return 0; ++} ++ ++} // namespace h2load ++ +diff --git a/src/h2load_quic_verto.h b/src/h2load_quic_verto.h +new file mode 100644 +index 0000000..32cf1e3 +--- /dev/null ++++ b/src/h2load_quic_verto.h +@@ -0,0 +1,36 @@ ++ /* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2019 nghttp2 contributors ++ * ++ * 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. ++ */ ++#ifndef H2LOAD_QUIC_H ++#define H2LOAD_QUIC_H ++ ++#include "nghttp2_config.h" ++ ++#include "h2load_verto.h" ++ ++namespace h2load { ++void verto_quic_pkt_timeout_cb(verto_ctx *verto_loop, verto_ev *ev); ++} // namespace h2load ++ ++#endif // H2LOAD_QUIC_H +diff --git a/src/h2load_session_verto.h b/src/h2load_session_verto.h +new file mode 100644 +index 0000000..083a360 +--- /dev/null ++++ b/src/h2load_session_verto.h +@@ -0,0 +1,60 @@ ++/* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2014 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. ++ */ ++#ifndef H2LOAD_SESSION_H ++#define H2LOAD_SESSION_H ++ ++#include "nghttp2_config.h" ++ ++#include ++ ++#include ++ ++#include "h2load_verto.h" ++ ++namespace h2load { ++ ++class Session { ++public: ++ virtual ~Session() {} ++ // Called when the connection was made. ++ virtual void on_connect() = 0; ++ // Called when one request must be issued. ++ virtual int submit_request() = 0; ++ // Called when incoming bytes are available. The subclass has to ++ // return the number of bytes read. ++ virtual int on_read(const uint8_t *data, size_t len) = 0; ++ // Called when write is available. Returns 0 on success, otherwise ++ // return -1. ++ virtual int on_write() = 0; ++ // Called when the underlying session must be terminated. ++ virtual void terminate() = 0; ++ // Return the maximum concurrency per connection ++ virtual size_t max_concurrent_streams() = 0; ++}; ++ ++} // namespace h2load ++ ++#endif // H2LOAD_SESSION_H ++ +diff --git a/src/h2load_verto.cc b/src/h2load_verto.cc +new file mode 100644 +index 0000000..51ca0d3 +--- /dev/null ++++ b/src/h2load_verto.cc +@@ -0,0 +1,3277 @@ ++/* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2014 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 "h2load_verto.h" ++ ++#include ++#include ++#ifdef HAVE_NETINET_IN_H ++# include ++#endif // HAVE_NETINET_IN_H ++#include ++#include ++#ifdef HAVE_FCNTL_H ++# include ++#endif // HAVE_FCNTL_H ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#ifdef ENABLE_HTTP3 ++# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS ++# include ++# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS ++# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL ++# include ++# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL ++#endif // ENABLE_HTTP3 ++ ++#include "url-parser/url_parser.h" ++ ++#include "h2load_http1_session_verto.h" ++#include "h2load_http2_session_verto.h" ++#ifdef ENABLE_HTTP3 ++# include "h2load_http3_session_verto.h" ++# include "h2load_quic_verto.h" ++#endif // ENABLE_HTTP3 ++#include "tls.h" ++#include "http2.h" ++#include "util.h" ++#include "template.h" ++ ++#ifndef O_BINARY ++# define O_BINARY (0) ++#endif // O_BINARY ++ ++using namespace nghttp2; ++ ++namespace h2load { ++ ++namespace { ++bool recorded(const std::chrono::steady_clock::time_point &t) { ++ return std::chrono::steady_clock::duration::zero() != t.time_since_epoch(); ++} ++} // namespace ++ ++#if OPENSSL_1_1_1_API ++namespace { ++std::ofstream keylog_file; ++void keylog_callback(const SSL *ssl, const char *line) { ++ keylog_file.write(line, strlen(line)); ++ keylog_file.put('\n'); ++ keylog_file.flush(); ++} ++} // namespace ++#endif // OPENSSL_1_1_1_API ++ ++Config::Config() ++ : ciphers(tls::DEFAULT_CIPHER_LIST), ++ tls13_ciphers("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_" ++ "CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256"), ++ groups("X25519:P-256:P-384:P-521"), ++ data_length(-1), ++ data(nullptr), ++ addrs(nullptr), ++ nreqs(1), ++ nclients(1), ++ nthreads(1), ++ max_concurrent_streams(1), ++ window_bits(30), ++ connection_window_bits(30), ++ max_frame_size(16_k), ++ rate(0), ++ rate_period(1.0), ++ duration(0.0), ++ warm_up_time(0.0), ++ conn_active_timeout(0.), ++ conn_inactivity_timeout(0.), ++ no_tls_proto(PROTO_HTTP2), ++ header_table_size(4_k), ++ encoder_header_table_size(4_k), ++ data_fd(-1), ++ log_fd(-1), ++ qlog_file_base(), ++ port(0), ++ default_port(0), ++ connect_to_port(0), ++ verbose(false), ++ timing_script(false), ++ base_uri_unix(false), ++ unix_addr{}, ++ rps(0.), ++ no_udp_gso(false), ++ max_udp_payload_size(0), ++ ktls(false) {} ++ ++Config::~Config() { ++ if (addrs) { ++ if (base_uri_unix) { ++ delete addrs; ++ } else { ++ freeaddrinfo(addrs); ++ } ++ } ++ ++ if (data_fd != -1) { ++ close(data_fd); ++ } ++} ++ ++bool Config::is_rate_mode() const { return (this->rate != 0); } ++bool Config::is_timing_based_mode() const { return (this->duration > 0); } ++bool Config::has_base_uri() const { return (!this->base_uri.empty()); } ++bool Config::rps_enabled() const { return this->rps > 0.0; } ++bool Config::is_quic() const { ++#ifdef ENABLE_HTTP3 ++ return !npn_list.empty() && ++ (npn_list[0] == NGHTTP3_ALPN_H3 || npn_list[0] == "\x5h3-29"); ++#else // !ENABLE_HTTP3 ++ return false; ++#endif // !ENABLE_HTTP3 ++} ++Config config; ++ ++namespace { ++constexpr size_t MAX_SAMPLES = 1000000; ++} // namespace ++ ++Stats::Stats(size_t req_todo, size_t nclients) ++ : req_todo(req_todo), ++ req_started(0), ++ req_done(0), ++ req_success(0), ++ req_status_success(0), ++ req_failed(0), ++ req_error(0), ++ req_timedout(0), ++ bytes_total(0), ++ bytes_head(0), ++ bytes_head_decomp(0), ++ bytes_body(0), ++ status(), ++ udp_dgram_recv(0), ++ udp_dgram_sent(0) {} ++ ++Stream::Stream() : req_stat{}, status_success(-1) {} ++ ++namespace { ++std::random_device rd; ++} // namespace ++ ++namespace { ++std::mt19937 gen(rd()); ++} // namespace ++ ++namespace { ++void sampling_init(Sampling &smp, size_t max_samples) { ++ smp.n = 0; ++ smp.max_samples = max_samples; ++} ++} // namespace ++ ++namespace { ++void verto_writecb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ client->restart_timeout(); ++ auto rv = client->do_write(); ++ if (rv == Client::ERR_CONNECT_FAIL) { ++ client->disconnect(); ++ // Try next address ++ client->current_addr = nullptr; ++ rv = client->connect(); ++ if (rv != 0) { ++ client->fail(); ++ client->worker->free_client(client); ++ delete client; ++ return; ++ } ++ return; ++ } ++ if (rv != 0) { ++ client->fail(); ++ client->worker->free_client(client); ++ delete client; ++ } ++} ++} // namespace ++ ++namespace { ++void verto_readcb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ ++ // In order to wait for the callback for the read event ++ // the write event observer was removed from the event loop in the previous step ++ // So now we need to add the read event observer into the event loop ++ verto_ev_flag write_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_WRITE); ++ client->verto_wev = verto_add_io(client->worker->verto_loop, write_flag, verto_writecb, client->fd); ++ verto_set_private(client->verto_wev, client, NULL); ++ ++ client->restart_timeout(); ++ if (client->do_read() != 0) { ++ if (client->try_again_or_fail() == 0) { ++ return; ++ } ++ client->worker->free_client(client); ++ delete client; ++ return; ++ } ++} ++} // namespace ++ ++namespace { ++// Called every rate_period when rate mode is being used ++void verto_rate_period_timeout_w_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto worker = static_cast(verto_get_private(ev)); ++ auto nclients_per_second = worker->rate; ++ auto conns_remaining = worker->nclients - worker->nconns_made; ++ auto nclients = std::min(nclients_per_second, conns_remaining); ++ ++ for (size_t i = 0; i < nclients; ++i) { ++ auto req_todo = worker->nreqs_per_client; ++ if (worker->nreqs_rem > 0) { ++ ++req_todo; ++ --worker->nreqs_rem; ++ } ++ ++ verto_loop = verto_new(NULL, VERTO_EV_TYPE_NONE); ++ auto client = ++ std::make_unique(worker->next_client_id++, worker, req_todo); ++ ++ ++worker->nconns_made; ++ ++ if (client->connect() != 0) { ++ std::cerr << "client could not connect to host" << std::endl; ++ client->fail(); ++ } else { ++ if (worker->config->is_timing_based_mode()) { ++ worker->clients.push_back(client.release()); ++ } else { ++ client.release(); ++ } ++ } ++ worker->report_rate_progress(); ++ verto_run(verto_loop); ++ verto_free(verto_loop); ++ } ++ if (!worker->config->is_timing_based_mode()) { ++ if (worker->nconns_made >= worker->nclients) { ++ verto_del(ev); ++ } ++ } else { ++ // To check whether all created clients are pushed correctly ++ assert(worker->nclients == worker->clients.size()); ++ } ++} ++} // namespace ++ ++namespace { ++// Called when the duration for infinite number of requests are over ++void verto_duration_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto worker = static_cast(verto_get_private(ev)); ++ ++ worker->current_phase = Phase::DURATION_OVER; ++ ++ std::cout << "Main benchmark duration is over for thread #" << worker->id ++ << ". Stopping all clients." << std::endl; ++ worker->stop_all_clients(); ++ std::cout << "Stopped all clients for thread #" << worker->id << std::endl; ++} ++} // namespace ++ ++namespace { ++// Called when the warmup duration for infinite number of requests are over ++void verto_warmup_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto worker = static_cast(verto_get_private(ev)); ++ ++ std::cout << "Warm-up phase is over for thread #" << worker->id << "." ++ << std::endl; ++ std::cout << "Main benchmark duration is started for thread #" << worker->id ++ << "." << std::endl; ++ assert(worker->stats.req_started == 0); ++ assert(worker->stats.req_done == 0); ++ ++ for (auto client : worker->clients) { ++ if (client) { ++ assert(client->req_todo == 0); ++ assert(client->req_left == 1); ++ assert(client->req_inflight == 0); ++ assert(client->req_started == 0); ++ assert(client->req_done == 0); ++ ++ client->record_client_start_time(); ++ client->clear_connect_times(); ++ client->record_connect_start_time(); ++ } ++ } ++ ++ worker->current_phase = Phase::MAIN_DURATION; ++ ++ worker->verto_duration_watcher = verto_add_timeout(worker->verto_loop, VERTO_EV_FLAG_NONE, verto_duration_timeout_cb, worker->config->duration); ++ verto_set_private(worker->verto_duration_watcher, worker, NULL); ++} ++} // namespace ++ ++namespace { ++void verto_rps_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ auto &session = client->session; ++ ++ assert(!config.timing_script); ++ ++ if (client->req_left == 0) { ++ verto_del(ev); ++ return; ++ } ++ ++ auto now = std::chrono::steady_clock::now(); ++ auto d = now - client->rps_duration_started; ++ auto n = static_cast( ++ round(std::chrono::duration(d).count() * config.rps)); ++ client->rps_req_pending += n; ++ client->rps_duration_started += ++ util::duration_from(static_cast(n) / config.rps); ++ ++ if (client->rps_req_pending == 0) { ++ return; ++ } ++ ++ auto nreq = session->max_concurrent_streams() - client->rps_req_inflight; ++ if (nreq == 0) { ++ return; ++ } ++ ++ nreq = config.is_timing_based_mode() ? std::max(nreq, client->req_left) ++ : std::min(nreq, client->req_left); ++ nreq = std::min(nreq, client->rps_req_pending); ++ ++ client->rps_req_inflight += nreq; ++ client->rps_req_pending -= nreq; ++ ++ for (; nreq > 0; --nreq) { ++ if (client->submit_request() != 0) { ++ client->process_request_failure(); ++ break; ++ } ++ } ++} ++} // namespace ++ ++namespace { ++// Called when an a connection has been inactive for a set period of time ++// or a fixed amount of time after all requests have been made on a ++// connection ++void verto_conn_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ ++ verto_del(client->verto_conn_inactivity_watcher); ++ verto_del(client->verto_conn_active_watcher); ++ ++ if (util::check_socket_connected(client->fd)) { ++ client->timeout(); ++ } ++} ++} // namespace ++ ++namespace { ++bool check_stop_client_request_timeout(Client *client, verto_ev *ev) { ++ if (client->req_left == 0) { ++ // no more requests to make, stop timer ++ verto_del(ev); ++ return true; ++ } ++ ++ return false; ++} ++} // namespace ++ ++namespace { ++void verto_client_request_timeout_cb(verto_ctx *verto_loop, verto_ev *ev) { ++ auto client = static_cast(verto_get_private(ev)); ++ ++ if (client->streams.size() >= (size_t)config.max_concurrent_streams) { ++ verto_del(ev); ++ return; ++ } ++ ++ if (client->submit_request() != 0) { ++ verto_del(ev); ++ client->process_request_failure(); ++ return; ++ } ++ ++ if (check_stop_client_request_timeout(client, ev)) { ++ return; ++ } ++ ++ auto duration = ++ config.timings[client->reqidx] - config.timings[client->reqidx - 1]; ++ ++ while (duration < std::chrono::duration(1e-9)) { ++ if (client->submit_request() != 0) { ++ verto_del(ev); ++ client->process_request_failure(); ++ return; ++ } ++ client->signal_write(); ++ if (check_stop_client_request_timeout(client, ev)) { ++ return; ++ } ++ ++ duration = ++ config.timings[client->reqidx] - config.timings[client->reqidx - 1]; ++ } ++ ++ verto_del(ev); ++ client->verto_request_timeout_watcher = verto_add_timeout(client->worker->verto_loop, VERTO_EV_FLAG_PERSIST, verto_client_request_timeout_cb, util::ev_tstamp_from(duration) * 1000); ++ verto_set_private(client->verto_request_timeout_watcher, client, NULL);} ++} // namespace ++ ++Client::Client(uint32_t id, Worker *worker, size_t req_todo) ++ : wb(&worker->mcpool), ++ cstat{}, ++ worker(worker), ++ ssl(nullptr), ++#ifdef ENABLE_HTTP3 ++ quic{}, ++#endif // ENABLE_HTTP3 ++ next_addr(config.addrs), ++ current_addr(nullptr), ++ reqidx(0), ++ state(CLIENT_IDLE), ++ req_todo(req_todo), ++ req_left(req_todo), ++ req_inflight(0), ++ req_started(0), ++ req_done(0), ++ id(id), ++ fd(-1), ++ local_addr{}, ++ new_connection_requested(false), ++ final(false), ++ rps_req_pending(0), ++ rps_req_inflight(0) { ++ if (req_todo == 0) { // this means infinite number of requests are to be made ++ // This ensures that number of requests are unbounded ++ // Just a positive number is fine, we chose the first positive number ++ req_left = 1; ++ } ++ ++#ifdef ENABLE_HTTP3 ++ if (config.is_quic()) { ++ quic.tx.data = std::make_unique(64_k); ++ } ++ ++ ngtcp2_ccerr_default(&quic.last_error); ++#endif // ENABLE_HTTP3 ++} ++ ++Client::~Client() { ++ disconnect(); ++ ++#ifdef ENABLE_HTTP3 ++ if (config.is_quic()) { ++ quic_free(); ++ } ++#endif // ENABLE_HTTP3 ++ ++ if (ssl) { ++ SSL_free(ssl); ++ } ++ ++ worker->sample_client_stat(&cstat); ++ ++worker->client_smp.n; ++} ++ ++int Client::do_read() { return readfn(*this); } ++int Client::do_write() { return writefn(*this); } ++ ++int Client::make_socket(addrinfo *addr) { ++ int rv; ++ ++ if (config.is_quic()) { ++#ifdef ENABLE_HTTP3 ++ fd = util::create_nonblock_udp_socket(addr->ai_family); ++ if (fd == -1) { ++ return -1; ++ } ++ ++# ifdef UDP_GRO ++ int val = 1; ++ if (setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, sizeof(val)) != 0) { ++ std::cerr << "setsockopt UDP_GRO failed" << std::endl; ++ return -1; ++ } ++# endif // UDP_GRO ++ ++ rv = util::bind_any_addr_udp(fd, addr->ai_family); ++ if (rv != 0) { ++ close(fd); ++ fd = -1; ++ return -1; ++ } ++ ++ socklen_t addrlen = sizeof(local_addr.su.storage); ++ rv = getsockname(fd, &local_addr.su.sa, &addrlen); ++ if (rv == -1) { ++ return -1; ++ } ++ local_addr.len = addrlen; ++ ++ if (quic_init(&local_addr.su.sa, local_addr.len, addr->ai_addr, ++ addr->ai_addrlen) != 0) { ++ std::cerr << "quic_init failed" << std::endl; ++ return -1; ++ } ++#endif // ENABLE_HTTP3 ++ } else { ++ fd = util::create_nonblock_socket(addr->ai_family); ++ if (fd == -1) { ++ return -1; ++ } ++ if (config.scheme == "https") { ++ if (!ssl) { ++ ssl = SSL_new(worker->ssl_ctx); ++ } ++ ++ SSL_set_connect_state(ssl); ++ } ++ } ++ ++ if (ssl && !util::numeric_host(config.host.c_str())) { ++ SSL_set_tlsext_host_name(ssl, config.host.c_str()); ++ } ++ ++ if (config.is_quic()) { ++ return 0; ++ } ++ ++ rv = ::connect(fd, addr->ai_addr, addr->ai_addrlen); ++ if (rv != 0 && errno != EINPROGRESS) { ++ if (ssl) { ++ SSL_free(ssl); ++ ssl = nullptr; ++ } ++ close(fd); ++ fd = -1; ++ return -1; ++ } ++ return 0; ++} ++ ++int Client::connect() { ++ int rv; ++ ++ if (!worker->config->is_timing_based_mode() || ++ worker->current_phase == Phase::MAIN_DURATION) { ++ record_client_start_time(); ++ clear_connect_times(); ++ record_connect_start_time(); ++ } else if (worker->current_phase == Phase::INITIAL_IDLE) { ++ worker->current_phase = Phase::WARM_UP; ++ std::cout << "Warm-up started for thread #" << worker->id << "." ++ << std::endl; ++ worker->verto_warmup_watcher = verto_add_timeout(worker->verto_loop, VERTO_EV_FLAG_NONE, verto_warmup_timeout_cb, worker->config->warm_up_time); ++ verto_set_private(worker->verto_warmup_watcher, worker, NULL); ++ } ++ ++ if (worker->config->conn_inactivity_timeout > 0.) { ++ verto_conn_inactivity_watcher = verto_add_timeout(worker->verto_loop, VERTO_EV_FLAG_PERSIST, verto_conn_timeout_cb, worker->config->conn_inactivity_timeout * 1000); ++ verto_set_private(verto_conn_inactivity_watcher, this, NULL); ++ } ++ ++ if (current_addr) { ++ rv = make_socket(current_addr); ++ if (rv == -1) { ++ return -1; ++ } ++ } else { ++ addrinfo *addr = nullptr; ++ while (next_addr) { ++ addr = next_addr; ++ next_addr = next_addr->ai_next; ++ rv = make_socket(addr); ++ if (rv == 0) { ++ break; ++ } ++ } ++ ++ if (fd == -1) { ++ return -1; ++ } ++ ++ assert(addr); ++ ++ current_addr = addr; ++ } ++ ++ // Add read event observers and write event observers to the event loop ++ verto_ev_flag read_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_READ); ++ verto_rev = verto_add_io(worker->verto_loop, read_flag, verto_readcb, fd); ++ verto_set_private(verto_rev, this, NULL); ++ ++ verto_ev_flag write_flag = (verto_ev_flag)(VERTO_EV_FLAG_PERSIST | VERTO_EV_FLAG_IO_WRITE); ++ verto_wev = verto_add_io(worker->verto_loop, write_flag, verto_writecb, fd); ++ verto_set_private(verto_wev, this, NULL); ++ ++ if (config.is_quic()) { ++#ifdef ENABLE_HTTP3 ++ readfn = &Client::read_quic; ++ writefn = &Client::write_quic; ++#endif // ENABLE_HTTP3 ++ } else { ++ writefn = &Client::connected; ++ } ++ ++ return 0; ++} ++ ++void Client::timeout() { ++ process_timedout_streams(); ++ ++ disconnect(); ++} ++ ++void Client::restart_timeout() { ++ if (worker->config->conn_inactivity_timeout > 0.) { ++ verto_conn_inactivity_watcher = verto_add_timeout(worker->verto_loop, VERTO_EV_FLAG_PERSIST, verto_conn_timeout_cb, worker->config->conn_inactivity_timeout * 1000); ++ verto_set_private(verto_conn_inactivity_watcher, this, NULL); ++ } ++} ++ ++int Client::try_again_or_fail() { ++ disconnect(); ++ ++ if (new_connection_requested) { ++ new_connection_requested = false; ++ ++ if (req_left) { ++ ++ if (worker->current_phase == Phase::MAIN_DURATION) { ++ // At the moment, we don't have a facility to re-start request ++ // already in in-flight. Make them fail. ++ worker->stats.req_failed += req_inflight; ++ worker->stats.req_error += req_inflight; ++ ++ req_inflight = 0; ++ } ++ ++ // Keep using current address ++ if (connect() == 0) { ++ return 0; ++ } ++ std::cerr << "client could not connect to host" << std::endl; ++ } ++ } ++ ++ process_abandoned_streams(); ++ ++ return -1; ++} ++ ++void Client::fail() { ++ disconnect(); ++ ++ process_abandoned_streams(); ++} ++ ++void Client::disconnect() { ++ record_client_end_time(); ++ ++#ifdef ENABLE_HTTP3 ++ if (config.is_quic()) { ++ quic_close_connection(); ++ } ++#endif // ENABLE_HTTP3 ++ streams.clear(); ++ session.reset(); ++ wb.reset(); ++ state = CLIENT_IDLE; ++ if (ssl) { ++ if (config.is_quic()) { ++ SSL_free(ssl); ++ ssl = nullptr; ++ } else { ++ SSL_set_shutdown(ssl, SSL_get_shutdown(ssl) | SSL_RECEIVED_SHUTDOWN); ++ ERR_clear_error(); ++ ++ if (SSL_shutdown(ssl) != 1) { ++ SSL_free(ssl); ++ ssl = nullptr; ++ } ++ } ++ } ++ if (fd != -1) { ++ shutdown(fd, SHUT_WR); ++ close(fd); ++ fd = -1; ++ } ++ ++ final = false; ++ // Exit the event loop ++ verto_break(worker->verto_loop); ++} ++ ++int Client::submit_request() { ++ if (session->submit_request() != 0) { ++ return -1; ++ } ++ ++ if (worker->current_phase != Phase::MAIN_DURATION) { ++ return 0; ++ } ++ ++ ++worker->stats.req_started; ++ ++req_started; ++ ++req_inflight; ++ if (!worker->config->is_timing_based_mode()) { ++ --req_left; ++ } ++ // if an active timeout is set and this is the last request to be submitted ++ // on this connection, start the active timeout. ++ if (worker->config->conn_active_timeout > 0. && req_left == 0) { ++ verto_conn_active_watcher = verto_add_timeout(worker->verto_loop, VERTO_EV_FLAG_NONE, verto_conn_timeout_cb, worker->config->conn_active_timeout); ++ verto_set_private(verto_conn_active_watcher, this, NULL); } ++ ++ return 0; ++} ++ ++void Client::process_timedout_streams() { ++ if (worker->current_phase != Phase::MAIN_DURATION) { ++ return; ++ } ++ ++ for (auto &p : streams) { ++ auto &req_stat = p.second.req_stat; ++ if (!req_stat.completed) { ++ req_stat.stream_close_time = std::chrono::steady_clock::now(); ++ } ++ } ++ ++ worker->stats.req_timedout += req_inflight; ++ ++ process_abandoned_streams(); ++} ++ ++void Client::process_abandoned_streams() { ++ if (worker->current_phase != Phase::MAIN_DURATION) { ++ return; ++ } ++ ++ auto req_abandoned = req_inflight + req_left; ++ ++ worker->stats.req_failed += req_abandoned; ++ worker->stats.req_error += req_abandoned; ++ ++ req_inflight = 0; ++ req_left = 0; ++} ++ ++void Client::process_request_failure() { ++ if (worker->current_phase != Phase::MAIN_DURATION) { ++ return; ++ } ++ ++ worker->stats.req_failed += req_left; ++ worker->stats.req_error += req_left; ++ ++ req_left = 0; ++ ++ if (req_inflight == 0) { ++ terminate_session(); ++ } ++ std::cout << "Process Request Failure:" << worker->stats.req_failed ++ << std::endl; ++} ++ ++namespace { ++void print_server_tmp_key(SSL *ssl) { ++// libressl does not have SSL_get_server_tmp_key ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L && defined(SSL_get_server_tmp_key) ++ EVP_PKEY *key; ++ ++ if (!SSL_get_server_tmp_key(ssl, &key)) { ++ return; ++ } ++ ++ auto key_del = defer(EVP_PKEY_free, key); ++ ++ std::cout << "Server Temp Key: "; ++ ++ auto pkey_id = EVP_PKEY_id(key); ++ switch (pkey_id) { ++ case EVP_PKEY_RSA: ++ std::cout << "RSA " << EVP_PKEY_bits(key) << " bits" << std::endl; ++ break; ++ case EVP_PKEY_DH: ++ std::cout << "DH " << EVP_PKEY_bits(key) << " bits" << std::endl; ++ break; ++ case EVP_PKEY_EC: { ++# if OPENSSL_3_0_0_API ++ std::array curve_name; ++ const char *cname; ++ if (!EVP_PKEY_get_utf8_string_param(key, "group", curve_name.data(), ++ curve_name.size(), nullptr)) { ++ cname = ""; ++ } else { ++ cname = curve_name.data(); ++ } ++# else // !OPENSSL_3_0_0_API ++ auto ec = EVP_PKEY_get1_EC_KEY(key); ++ auto ec_del = defer(EC_KEY_free, ec); ++ auto nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); ++ auto cname = EC_curve_nid2nist(nid); ++ if (!cname) { ++ cname = OBJ_nid2sn(nid); ++ } ++# endif // !OPENSSL_3_0_0_API ++ ++ std::cout << "ECDH " << cname << " " << EVP_PKEY_bits(key) << " bits" ++ << std::endl; ++ break; ++ } ++ default: ++ std::cout << OBJ_nid2sn(pkey_id) << " " << EVP_PKEY_bits(key) << " bits" ++ << std::endl; ++ break; ++ } ++#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L ++} ++} // namespace ++ ++void Client::report_tls_info() { ++ if (worker->id == 0 && !worker->tls_info_report_done) { ++ worker->tls_info_report_done = true; ++ auto cipher = SSL_get_current_cipher(ssl); ++ std::cout << "TLS Protocol: " << tls::get_tls_protocol(ssl) << "\n" ++ << "Cipher: " << SSL_CIPHER_get_name(cipher) << std::endl; ++ print_server_tmp_key(ssl); ++ } ++} ++ ++void Client::report_app_info() { ++ if (worker->id == 0 && !worker->app_info_report_done) { ++ worker->app_info_report_done = true; ++ std::cout << "Application protocol: " << selected_proto << std::endl; ++ } ++} ++ ++void Client::terminate_session() { ++#ifdef ENABLE_HTTP3 ++ if (config.is_quic()) { ++ quic.close_requested = true; ++ } ++#endif // ENABLE_HTTP3 ++ if (session) { ++ session->terminate(); ++ } ++} ++ ++void Client::on_request(int32_t stream_id) { streams[stream_id] = Stream(); } ++ ++void Client::on_header(int32_t stream_id, const uint8_t *name, size_t namelen, ++ const uint8_t *value, size_t valuelen) { ++ auto itr = streams.find(stream_id); ++ if (itr == std::end(streams)) { ++ return; ++ } ++ auto &stream = (*itr).second; ++ ++ if (worker->current_phase != Phase::MAIN_DURATION) { ++ // If the stream is for warm-up phase, then mark as a success ++ // But we do not update the count for 2xx, 3xx, etc status codes ++ // Same has been done in on_status_code function ++ stream.status_success = 1; ++ return; ++ } ++ ++ if (stream.status_success == -1 && namelen == 7 && ++ util::streq_l(":status", name, namelen)) { ++ int status = 0; ++ for (size_t i = 0; i < valuelen; ++i) { ++ if ('0' <= value[i] && value[i] <= '9') { ++ status *= 10; ++ status += value[i] - '0'; ++ if (status > 999) { ++ stream.status_success = 0; ++ return; ++ } ++ } else { ++ break; ++ } ++ } ++ ++ stream.req_stat.status = status; ++ if (status >= 200 && status < 300) { ++ ++worker->stats.status[2]; ++ stream.status_success = 1; ++ } else if (status < 400) { ++ ++worker->stats.status[3]; ++ stream.status_success = 1; ++ } else if (status < 600) { ++ ++worker->stats.status[status / 100]; ++ stream.status_success = 0; ++ } else { ++ stream.status_success = 0; ++ } ++ } ++} ++ ++void Client::on_status_code(int32_t stream_id, uint16_t status) { ++ auto itr = streams.find(stream_id); ++ if (itr == std::end(streams)) { ++ return; ++ } ++ auto &stream = (*itr).second; ++ ++ if (worker->current_phase != Phase::MAIN_DURATION) { ++ stream.status_success = 1; ++ return; ++ } ++ ++ stream.req_stat.status = status; ++ if (status >= 200 && status < 300) { ++ ++worker->stats.status[2]; ++ stream.status_success = 1; ++ } else if (status < 400) { ++ ++worker->stats.status[3]; ++ stream.status_success = 1; ++ } else if (status < 600) { ++ ++worker->stats.status[status / 100]; ++ stream.status_success = 0; ++ } else { ++ stream.status_success = 0; ++ } ++} ++ ++void Client::on_stream_close(int32_t stream_id, bool success, bool final) { ++ if (worker->current_phase == Phase::MAIN_DURATION) { ++ if (req_inflight > 0) { ++ --req_inflight; ++ } ++ auto req_stat = get_req_stat(stream_id); ++ if (!req_stat) { ++ return; ++ } ++ ++ req_stat->stream_close_time = std::chrono::steady_clock::now(); ++ if (success) { ++ req_stat->completed = true; ++ ++worker->stats.req_success; ++ ++cstat.req_success; ++ ++ if (streams[stream_id].status_success == 1) { ++ ++worker->stats.req_status_success; ++ } else { ++ ++worker->stats.req_failed; ++ } ++ ++ worker->sample_req_stat(req_stat); ++ ++ // Count up in successful cases only ++ ++worker->request_times_smp.n; ++ } else { ++ ++worker->stats.req_failed; ++ ++worker->stats.req_error; ++ } ++ ++worker->stats.req_done; ++ ++req_done; ++ ++ if (worker->config->log_fd != -1) { ++ auto start = std::chrono::duration_cast( ++ req_stat->request_wall_time.time_since_epoch()); ++ auto delta = std::chrono::duration_cast( ++ req_stat->stream_close_time - req_stat->request_time); ++ ++ std::array buf; ++ auto p = std::begin(buf); ++ p = util::utos(p, start.count()); ++ *p++ = '\t'; ++ if (success) { ++ p = util::utos(p, req_stat->status); ++ } else { ++ *p++ = '-'; ++ *p++ = '1'; ++ } ++ *p++ = '\t'; ++ p = util::utos(p, delta.count()); ++ *p++ = '\n'; ++ ++ auto nwrite = static_cast(std::distance(std::begin(buf), p)); ++ assert(nwrite <= buf.size()); ++ while (write(worker->config->log_fd, buf.data(), nwrite) == -1 && ++ errno == EINTR) ++ ; ++ } ++ } ++ ++ worker->report_progress(); ++ streams.erase(stream_id); ++ if (req_left == 0 && req_inflight == 0) { ++ terminate_session(); ++ return; ++ } ++ ++ if (!final && req_left > 0) { ++ if (!config.rps_enabled()) { ++ if (submit_request() != 0) { ++ process_request_failure(); ++ } ++ } else if (rps_req_pending) { ++ --rps_req_pending; ++ if (submit_request() != 0) { ++ process_request_failure(); ++ } ++ } else { ++ assert(rps_req_inflight); ++ --rps_req_inflight; ++ } ++ } ++} ++ ++RequestStat *Client::get_req_stat(int32_t stream_id) { ++ auto it = streams.find(stream_id); ++ if (it == std::end(streams)) { ++ return nullptr; ++ } ++ ++ return &(*it).second.req_stat; ++} ++ ++int Client::connection_made() { ++ if (ssl) { ++ report_tls_info(); ++ ++ const unsigned char *next_proto = nullptr; ++ unsigned int next_proto_len; ++ ++#ifndef OPENSSL_NO_NEXTPROTONEG ++ SSL_get0_next_proto_negotiated(ssl, &next_proto, &next_proto_len); ++#endif // !OPENSSL_NO_NEXTPROTONEG ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L ++ if (next_proto == nullptr) { ++ SSL_get0_alpn_selected(ssl, &next_proto, &next_proto_len); ++ } ++#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L ++ ++ if (next_proto) { ++ auto proto = StringRef{next_proto, next_proto_len}; ++ if (config.is_quic()) { ++#ifdef ENABLE_HTTP3 ++ assert(session); ++ if (!util::streq(StringRef{&NGHTTP3_ALPN_H3[1]}, proto) && ++ !util::streq_l("h3-29", proto)) { ++ return -1; ++ } ++#endif // ENABLE_HTTP3 ++ } else if (util::check_h2_is_selected(proto)) { ++ session = std::make_unique(this); ++ } else if (util::streq(NGHTTP2_H1_1, proto)) { ++ session = std::make_unique(this); ++ } ++ ++ // Just assign next_proto to selected_proto anyway to show the ++ // negotiation result. ++ selected_proto = proto.str(); ++ } else if (config.is_quic()) { ++ std::cerr << "QUIC requires ALPN negotiation" << std::endl; ++ return -1; ++ } else { ++ std::cout << "No protocol negotiated. Fallback behaviour may be activated" ++ << std::endl; ++ ++ for (const auto &proto : config.npn_list) { ++ if (util::streq(NGHTTP2_H1_1_ALPN, StringRef{proto})) { ++ std::cout ++ << "Server does not support NPN/ALPN. Falling back to HTTP/1.1." ++ << std::endl; ++ session = std::make_unique(this); ++ selected_proto = NGHTTP2_H1_1.str(); ++ break; ++ } ++ } ++ } ++ ++ if (!selected_proto.empty()) { ++ report_app_info(); ++ } ++ ++ if (!session) { ++ std::cout ++ << "No supported protocol was negotiated. Supported protocols were:" ++ << std::endl; ++ for (const auto &proto : config.npn_list) { ++ std::cout << proto.substr(1) << std::endl; ++ } ++ disconnect(); ++ return -1; ++ } ++ } else { ++ switch (config.no_tls_proto) { ++ case Config::PROTO_HTTP2: ++ session = std::make_unique(this); ++ selected_proto = NGHTTP2_CLEARTEXT_PROTO_VERSION_ID; ++ break; ++ case Config::PROTO_HTTP1_1: ++ session = std::make_unique(this); ++ selected_proto = NGHTTP2_H1_1.str(); ++ break; ++ default: ++ // unreachable ++ assert(0); ++ } ++ ++ report_app_info(); ++ } ++ ++ state = CLIENT_CONNECTED; ++ ++ session->on_connect(); ++ ++ record_connect_time(); ++ ++ if (config.rps_enabled()) { ++ verto_rps_watcher = verto_add_timeout(worker->verto_loop, VERTO_EV_FLAG_PERSIST, verto_rps_cb, std::max(0.01, 1. / config.rps) * 1000); ++ verto_set_private(verto_rps_watcher, this, NULL); ++ rps_duration_started = std::chrono::steady_clock::now(); ++ } ++ ++ if (config.rps_enabled()) { ++ assert(req_left); ++ ++ ++rps_req_inflight; ++ ++ if (submit_request() != 0) { ++ process_request_failure(); ++ } ++ } else if (!config.timing_script) { ++ auto nreq = config.is_timing_based_mode() ++ ? std::max(req_left, session->max_concurrent_streams()) ++ : std::min(req_left, session->max_concurrent_streams()); ++ ++ for (; nreq > 0; --nreq) { ++ if (submit_request() != 0) { ++ process_request_failure(); ++ break; ++ } ++ } ++ } else { ++ ++ auto duration = config.timings[reqidx]; ++ ++ while (duration < std::chrono::duration(1e-9)) { ++ if (submit_request() != 0) { ++ process_request_failure(); ++ break; ++ } ++ duration = config.timings[reqidx]; ++ if (reqidx == 0) { ++ // if reqidx wraps around back to 0, we uses up all lines and ++ // should break ++ break; ++ } ++ } ++ ++ if (duration >= std::chrono::duration(1e-9)) { ++ // double check since we may have break due to reqidx wraps ++ // around back to 0 ++ verto_request_timeout_watcher = verto_add_timeout(worker->verto_loop, VERTO_EV_FLAG_PERSIST, verto_client_request_timeout_cb, util::ev_tstamp_from(duration) * 1000); ++ verto_set_private(verto_request_timeout_watcher, this, NULL); ++ } ++ } ++ ++ return 0; ++} ++ ++int Client::on_read(const uint8_t *data, size_t len) { ++ auto rv = session->on_read(data, len); ++ if (rv != 0) { ++ return -1; ++ } ++ if (worker->current_phase == Phase::MAIN_DURATION) { ++ worker->stats.bytes_total += len; ++ } ++ return 0; ++} ++ ++int Client::on_write() { ++ if (wb.rleft() >= BACKOFF_WRITE_BUFFER_THRES) { ++ return 0; ++ } ++ ++ if (session->on_write() != 0) { ++ return -1; ++ } ++ return 0; ++} ++ ++int Client::read_clear() { ++ uint8_t buf[8_k]; ++ ++ for (;;) { ++ ssize_t nread; ++ while ((nread = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR) ++ ; ++ if (nread == -1) { ++ if (errno == EAGAIN || errno == EWOULDBLOCK) { ++ return 0; ++ } ++ return -1; ++ } ++ ++ if (nread == 0) { ++ return -1; ++ } ++ ++ if (on_read(buf, nread) != 0) { ++ return -1; ++ } ++ } ++ ++ return 0; ++} ++ ++int Client::write_clear() { ++ std::array iov; ++ ++ for (;;) { ++ if (on_write() != 0) { ++ return -1; ++ } ++ ++ auto iovcnt = wb.riovec(iov.data(), iov.size()); ++ ++ if (iovcnt == 0) { ++ break; ++ } ++ ++ ssize_t nwrite; ++ while ((nwrite = writev(fd, iov.data(), iovcnt)) == -1 && errno == EINTR) ++ ; ++ ++ if (nwrite == -1) { ++ if (errno == EAGAIN || errno == EWOULDBLOCK) { ++ return 0; ++ } ++ return -1; ++ } ++ ++ wb.drain(nwrite); ++ } ++ ++ return 0; ++} ++ ++int Client::connected() { ++ if (!util::check_socket_connected(fd)) { ++ return ERR_CONNECT_FAIL; ++ } ++ ++ if (ssl) { ++ SSL_set_fd(ssl, fd); ++ ++ readfn = &Client::tls_handshake; ++ writefn = &Client::tls_handshake; ++ ++ return do_write(); ++ } ++ ++ readfn = &Client::read_clear; ++ writefn = &Client::write_clear; ++ ++ if (connection_made() != 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int Client::tls_handshake() { ++ ERR_clear_error(); ++ ++ auto rv = SSL_do_handshake(ssl); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ // Remove the write observer ++ // add it to the event loop inside the read event callback ++ verto_del(verto_wev); ++ return 0; ++ case SSL_ERROR_WANT_WRITE: ++ return 0; ++ default: ++ return -1; ++ } ++ } ++ ++ readfn = &Client::read_tls; ++ writefn = &Client::write_tls; ++ ++ if (connection_made() != 0) { ++ return -1; ++ } ++ ++ return 0; ++} ++ ++int Client::read_tls() { ++ uint8_t buf[8_k]; ++ ++ ERR_clear_error(); ++ ++ for (;;) { ++ auto rv = SSL_read(ssl, buf, sizeof(buf)); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ return 0; ++ case SSL_ERROR_WANT_WRITE: ++ // renegotiation started ++ return -1; ++ default: ++ return -1; ++ } ++ } ++ ++ if (on_read(buf, rv) != 0) { ++ return -1; ++ } ++ } ++} ++ ++int Client::write_tls() { ++ ERR_clear_error(); ++ ++ struct iovec iov; ++ ++ for (;;) { ++ if (on_write() != 0) { ++ return -1; ++ } ++ ++ auto iovcnt = wb.riovec(&iov, 1); ++ ++ if (iovcnt == 0) { ++ // iovcnt == 0 indicates that the write event is no longer needed ++ verto_del(verto_wev); ++ break; ++ } ++ ++ auto rv = SSL_write(ssl, iov.iov_base, iov.iov_len); ++ ++ if (rv <= 0) { ++ auto err = SSL_get_error(ssl, rv); ++ switch (err) { ++ case SSL_ERROR_WANT_READ: ++ // renegotiation started ++ return -1; ++ case SSL_ERROR_WANT_WRITE: ++ return 0; ++ default: ++ return -1; ++ } ++ } ++ ++ wb.drain(rv); ++ } ++ ++ return 0; ++} ++ ++#ifdef ENABLE_HTTP3 ++// Returns 1 if sendmsg is blocked. ++int Client::write_udp(const sockaddr *addr, socklen_t addrlen, ++ const uint8_t *data, size_t datalen, size_t gso_size) { ++ iovec msg_iov; ++ msg_iov.iov_base = const_cast(data); ++ msg_iov.iov_len = datalen; ++ ++ msghdr msg{}; ++ msg.msg_name = const_cast(addr); ++ msg.msg_namelen = addrlen; ++ msg.msg_iov = &msg_iov; ++ msg.msg_iovlen = 1; ++ ++# ifdef UDP_SEGMENT ++ std::array msg_ctrl{}; ++ if (gso_size && datalen > gso_size) { ++ msg.msg_control = msg_ctrl.data(); ++ msg.msg_controllen = msg_ctrl.size(); ++ ++ auto cm = CMSG_FIRSTHDR(&msg); ++ cm->cmsg_level = SOL_UDP; ++ cm->cmsg_type = UDP_SEGMENT; ++ cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); ++ *(reinterpret_cast(CMSG_DATA(cm))) = gso_size; ++ } ++# endif // UDP_SEGMENT ++ ++ auto nwrite = sendmsg(fd, &msg, 0); ++ if (nwrite < 0) { ++ if (errno == EAGAIN || errno == EWOULDBLOCK) { ++ return 1; ++ } ++ ++ std::cerr << "sendmsg: errno=" << errno << std::endl; ++ } else { ++ ++worker->stats.udp_dgram_sent; ++ } ++ ++ return 0; ++} ++#endif // ENABLE_HTTP3 ++ ++void Client::record_request_time(RequestStat *req_stat) { ++ req_stat->request_time = std::chrono::steady_clock::now(); ++ req_stat->request_wall_time = std::chrono::system_clock::now(); ++} ++ ++void Client::record_connect_start_time() { ++ cstat.connect_start_time = std::chrono::steady_clock::now(); ++} ++ ++void Client::record_connect_time() { ++ cstat.connect_time = std::chrono::steady_clock::now(); ++} ++ ++void Client::record_ttfb() { ++ if (recorded(cstat.ttfb)) { ++ return; ++ } ++ ++ cstat.ttfb = std::chrono::steady_clock::now(); ++} ++ ++void Client::clear_connect_times() { ++ cstat.connect_start_time = std::chrono::steady_clock::time_point(); ++ cstat.connect_time = std::chrono::steady_clock::time_point(); ++ cstat.ttfb = std::chrono::steady_clock::time_point(); ++} ++ ++void Client::record_client_start_time() { ++ // Record start time only once at the very first connection is going ++ // to be made. ++ if (recorded(cstat.client_start_time)) { ++ return; ++ } ++ ++ cstat.client_start_time = std::chrono::steady_clock::now(); ++} ++ ++void Client::record_client_end_time() { ++ // Unlike client_start_time, we overwrite client_end_time. This ++ // handles multiple connect/disconnect for HTTP/1.1 benchmark. ++ cstat.client_end_time = std::chrono::steady_clock::now(); ++} ++ ++void Client::signal_write() { ++ // This function has no effect on libverto-based projects, ++ // but it is used in some files and needs to be retained ++} ++ ++void Client::try_new_connection() { ++ new_connection_requested = true; ++} ++ ++ ++Worker::Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t req_todo, size_t nclients, ++ size_t rate, size_t max_samples, Config *config) ++ : randgen(util::make_mt19937()), ++ stats(req_todo, nclients), ++ ssl_ctx(ssl_ctx), ++ config(config), ++ id(id), ++ tls_info_report_done(false), ++ app_info_report_done(false), ++ nconns_made(0), ++ nclients(nclients), ++ nreqs_per_client(req_todo / nclients), ++ nreqs_rem(req_todo % nclients), ++ rate(rate), ++ max_samples(max_samples), ++ next_client_id(0) { ++ if (!config->is_rate_mode() && !config->is_timing_based_mode()) { ++ progress_interval = std::max(static_cast(1), req_todo / 10); ++ } else { ++ progress_interval = std::max(static_cast(1), nclients / 10); ++ } ++ ++ if (config->is_timing_based_mode()) { ++ stats.req_stats.reserve(std::max(req_todo, max_samples)); ++ stats.client_stats.reserve(std::max(nclients, max_samples)); ++ } else { ++ stats.req_stats.reserve(std::min(req_todo, max_samples)); ++ stats.client_stats.reserve(std::min(nclients, max_samples)); ++ } ++ ++ sampling_init(request_times_smp, max_samples); ++ sampling_init(client_smp, max_samples); ++ ++ if (config->is_timing_based_mode()) { ++ current_phase = Phase::INITIAL_IDLE; ++ } else { ++ current_phase = Phase::MAIN_DURATION; ++ } ++} ++ ++Worker::~Worker() { ++ // Clear all event loops ++ verto_cleanup(); ++} ++ ++void Worker::stop_all_clients() { ++ for (auto client : clients) { ++ if (client) { ++ client->terminate_session(); ++ } ++ } ++} ++ ++void Worker::free_client(Client *deleted_client) { ++ for (auto &client : clients) { ++ if (client == deleted_client) { ++ client->req_todo = client->req_done; ++ stats.req_todo += client->req_todo; ++ auto index = &client - &clients[0]; ++ clients[index] = nullptr; ++ return; ++ } ++ } ++} ++ ++void Worker::run() { ++ ++ if (!config->is_rate_mode() && !config->is_timing_based_mode()) { ++ for (size_t i = 0; i < nclients; ++i) { ++ auto req_todo = nreqs_per_client; ++ if (nreqs_rem > 0) { ++ ++req_todo; ++ --nreqs_rem; ++ } ++ ++ verto_loop = verto_new(NULL, VERTO_EV_TYPE_NONE); ++ auto client = std::make_unique(next_client_id++, this, req_todo); ++ if (client->connect() != 0) { ++ std::cerr << "client could not connect to host" << std::endl; ++ client->fail(); ++ } else { ++ client.release(); ++ } ++ verto_run(verto_loop); ++ verto_free(verto_loop); ++ ++ } ++ } else { ++ // call the callback to start for one single time ++ verto_timeout_watcher = verto_add_timeout(verto_new(NULL, VERTO_EV_TYPE_NONE), VERTO_EV_FLAG_NONE, verto_rate_period_timeout_w_cb, 0); ++ verto_set_private(verto_timeout_watcher, this, NULL); ++ verto_rate_period_timeout_w_cb(verto_new(NULL, VERTO_EV_TYPE_NONE), verto_timeout_watcher); ++ } ++} ++ ++namespace { ++template ++void sample(Sampling &smp, Stats &stats, Stat *s) { ++ ++smp.n; ++ if (stats.size() < smp.max_samples) { ++ stats.push_back(*s); ++ return; ++ } ++ auto d = std::uniform_int_distribution(0, smp.n - 1); ++ auto i = d(gen); ++ if (i < smp.max_samples) { ++ stats[i] = *s; ++ } ++} ++} // namespace ++ ++void Worker::sample_req_stat(RequestStat *req_stat) { ++ sample(request_times_smp, stats.req_stats, req_stat); ++} ++ ++void Worker::sample_client_stat(ClientStat *cstat) { ++ sample(client_smp, stats.client_stats, cstat); ++} ++ ++void Worker::report_progress() { ++ if (id != 0 || config->is_rate_mode() || stats.req_done % progress_interval || ++ config->is_timing_based_mode()) { ++ return; ++ } ++ ++ std::cout << "progress: " << stats.req_done * 100 / stats.req_todo << "% done" ++ << std::endl; ++} ++ ++void Worker::report_rate_progress() { ++ if (id != 0 || nconns_made % progress_interval) { ++ return; ++ } ++ ++ std::cout << "progress: " << nconns_made * 100 / nclients ++ << "% of clients started" << std::endl; ++} ++ ++namespace { ++// Returns percentage of number of samples within mean +/- sd. ++double within_sd(const std::vector &samples, double mean, double sd) { ++ if (samples.size() == 0) { ++ return 0.0; ++ } ++ auto lower = mean - sd; ++ auto upper = mean + sd; ++ auto m = std::count_if( ++ std::begin(samples), std::end(samples), ++ [&lower, &upper](double t) { return lower <= t && t <= upper; }); ++ return (m / static_cast(samples.size())) * 100; ++} ++} // namespace ++ ++namespace { ++// Computes statistics using |samples|. The min, max, mean, sd, and ++// percentage of number of samples within mean +/- sd are computed. ++// If |sampling| is true, this computes sample variance. Otherwise, ++// population variance. ++SDStat compute_time_stat(const std::vector &samples, ++ bool sampling = false) { ++ if (samples.empty()) { ++ return {0.0, 0.0, 0.0, 0.0, 0.0}; ++ } ++ // standard deviation calculated using Rapid calculation method: ++ // https://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods ++ double a = 0, q = 0; ++ size_t n = 0; ++ double sum = 0; ++ auto res = SDStat{std::numeric_limits::max(), ++ std::numeric_limits::min()}; ++ for (const auto &t : samples) { ++ ++n; ++ res.min = std::min(res.min, t); ++ res.max = std::max(res.max, t); ++ sum += t; ++ ++ auto na = a + (t - a) / n; ++ q += (t - a) * (t - na); ++ a = na; ++ } ++ ++ assert(n > 0); ++ res.mean = sum / n; ++ res.sd = sqrt(q / (sampling && n > 1 ? n - 1 : n)); ++ res.within_sd = within_sd(samples, res.mean, res.sd); ++ ++ return res; ++} ++} // namespace ++ ++namespace { ++SDStats ++process_time_stats(const std::vector> &workers) { ++ auto request_times_sampling = false; ++ auto client_times_sampling = false; ++ size_t nrequest_times = 0; ++ size_t nclient_times = 0; ++ for (const auto &w : workers) { ++ nrequest_times += w->stats.req_stats.size(); ++ request_times_sampling = w->request_times_smp.n > w->stats.req_stats.size(); ++ ++ nclient_times += w->stats.client_stats.size(); ++ client_times_sampling = w->client_smp.n > w->stats.client_stats.size(); ++ } ++ ++ std::vector request_times; ++ request_times.reserve(nrequest_times); ++ ++ std::vector connect_times, ttfb_times, rps_values; ++ connect_times.reserve(nclient_times); ++ ttfb_times.reserve(nclient_times); ++ rps_values.reserve(nclient_times); ++ ++ for (const auto &w : workers) { ++ for (const auto &req_stat : w->stats.req_stats) { ++ if (!req_stat.completed) { ++ continue; ++ } ++ request_times.push_back( ++ std::chrono::duration_cast>( ++ req_stat.stream_close_time - req_stat.request_time) ++ .count()); ++ } ++ ++ const auto &stat = w->stats; ++ ++ for (const auto &cstat : stat.client_stats) { ++ if (recorded(cstat.client_start_time) && ++ recorded(cstat.client_end_time)) { ++ auto t = std::chrono::duration_cast>( ++ cstat.client_end_time - cstat.client_start_time) ++ .count(); ++ if (t > 1e-9) { ++ rps_values.push_back(cstat.req_success / t); ++ } ++ } ++ ++ // We will get connect event before FFTB. ++ if (!recorded(cstat.connect_start_time) || ++ !recorded(cstat.connect_time)) { ++ continue; ++ } ++ ++ connect_times.push_back( ++ std::chrono::duration_cast>( ++ cstat.connect_time - cstat.connect_start_time) ++ .count()); ++ ++ if (!recorded(cstat.ttfb)) { ++ continue; ++ } ++ ++ ttfb_times.push_back( ++ std::chrono::duration_cast>( ++ cstat.ttfb - cstat.connect_start_time) ++ .count()); ++ } ++ } ++ ++ return {compute_time_stat(request_times, request_times_sampling), ++ compute_time_stat(connect_times, client_times_sampling), ++ compute_time_stat(ttfb_times, client_times_sampling), ++ compute_time_stat(rps_values, client_times_sampling)}; ++} ++} // namespace ++ ++namespace { ++void resolve_host() { ++ if (config.base_uri_unix) { ++ auto res = std::make_unique(); ++ res->ai_family = config.unix_addr.sun_family; ++ res->ai_socktype = SOCK_STREAM; ++ res->ai_addrlen = sizeof(config.unix_addr); ++ res->ai_addr = ++ static_cast(static_cast(&config.unix_addr)); ++ ++ config.addrs = res.release(); ++ return; ++ }; ++ ++ int rv; ++ addrinfo hints{}, *res; ++ ++ hints.ai_family = AF_UNSPEC; ++ hints.ai_socktype = SOCK_STREAM; ++ hints.ai_protocol = 0; ++ hints.ai_flags = AI_ADDRCONFIG; ++ ++ const auto &resolve_host = ++ config.connect_to_host.empty() ? config.host : config.connect_to_host; ++ auto port = ++ config.connect_to_port == 0 ? config.port : config.connect_to_port; ++ ++ rv = ++ getaddrinfo(resolve_host.c_str(), util::utos(port).c_str(), &hints, &res); ++ if (rv != 0) { ++ std::cerr << "getaddrinfo() failed: " << gai_strerror(rv) << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (res == nullptr) { ++ std::cerr << "No address returned" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.addrs = res; ++} ++} // namespace ++ ++namespace { ++std::string get_reqline(const char *uri, const http_parser_url &u) { ++ std::string reqline; ++ ++ if (util::has_uri_field(u, UF_PATH)) { ++ reqline = util::get_uri_field(uri, u, UF_PATH).str(); ++ } else { ++ reqline = "/"; ++ } ++ ++ if (util::has_uri_field(u, UF_QUERY)) { ++ reqline += '?'; ++ reqline += util::get_uri_field(uri, u, UF_QUERY); ++ } ++ ++ return reqline; ++} ++} // namespace ++ ++#ifndef OPENSSL_NO_NEXTPROTONEG ++namespace { ++int client_select_next_proto_cb(SSL *ssl, unsigned char **out, ++ unsigned char *outlen, const unsigned char *in, ++ unsigned int inlen, void *arg) { ++ if (util::select_protocol(const_cast(out), outlen, in, ++ inlen, config.npn_list)) { ++ return SSL_TLSEXT_ERR_OK; ++ } ++ ++ // OpenSSL will terminate handshake with fatal alert if we return ++ // NOACK. So there is no way to fallback. ++ return SSL_TLSEXT_ERR_NOACK; ++} ++} // namespace ++#endif // !OPENSSL_NO_NEXTPROTONEG ++ ++namespace { ++constexpr char UNIX_PATH_PREFIX[] = "unix:"; ++} // namespace ++ ++namespace { ++bool parse_base_uri(const StringRef &base_uri) { ++ http_parser_url u{}; ++ if (http_parser_parse_url(base_uri.c_str(), base_uri.size(), 0, &u) != 0 || ++ !util::has_uri_field(u, UF_SCHEMA) || !util::has_uri_field(u, UF_HOST)) { ++ return false; ++ } ++ ++ config.scheme = util::get_uri_field(base_uri.c_str(), u, UF_SCHEMA).str(); ++ config.host = util::get_uri_field(base_uri.c_str(), u, UF_HOST).str(); ++ config.default_port = util::get_default_port(base_uri.c_str(), u); ++ if (util::has_uri_field(u, UF_PORT)) { ++ config.port = u.port; ++ } else { ++ config.port = config.default_port; ++ } ++ ++ return true; ++} ++} // namespace ++namespace { ++// Use std::vector::iterator explicitly, without that, ++// http_parser_url u{} fails with clang-3.4. ++std::vector parse_uris(std::vector::iterator first, ++ std::vector::iterator last) { ++ std::vector reqlines; ++ ++ if (first == last) { ++ std::cerr << "no URI available" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (!config.has_base_uri()) { ++ ++ if (!parse_base_uri(StringRef{*first})) { ++ std::cerr << "invalid URI: " << *first << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ config.base_uri = *first; ++ } ++ ++ for (; first != last; ++first) { ++ http_parser_url u{}; ++ ++ auto uri = (*first).c_str(); ++ ++ if (http_parser_parse_url(uri, (*first).size(), 0, &u) != 0) { ++ std::cerr << "invalid URI: " << uri << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ reqlines.push_back(get_reqline(uri, u)); ++ } ++ ++ return reqlines; ++} ++} // namespace ++ ++namespace { ++std::vector read_uri_from_file(std::istream &infile) { ++ std::vector uris; ++ std::string line_uri; ++ while (std::getline(infile, line_uri)) { ++ uris.push_back(line_uri); ++ } ++ ++ return uris; ++} ++} // namespace ++ ++namespace { ++void read_script_from_file( ++ std::istream &infile, ++ std::vector &timings, ++ std::vector &uris) { ++ std::string script_line; ++ int line_count = 0; ++ while (std::getline(infile, script_line)) { ++ line_count++; ++ if (script_line.empty()) { ++ std::cerr << "Empty line detected at line " << line_count ++ << ". Ignoring and continuing." << std::endl; ++ continue; ++ } ++ ++ std::size_t pos = script_line.find("\t"); ++ if (pos == std::string::npos) { ++ std::cerr << "Invalid line format detected, no tab character at line " ++ << line_count << ". \n\t" << script_line << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ const char *start = script_line.c_str(); ++ char *end; ++ auto v = std::strtod(start, &end); ++ ++ errno = 0; ++ if (v < 0.0 || !std::isfinite(v) || end == start || errno != 0) { ++ auto error = errno; ++ std::cerr << "Time value error at line " << line_count << ". \n\t" ++ << "value = " << script_line.substr(0, pos) << std::endl; ++ if (error != 0) { ++ std::cerr << "\t" << strerror(error) << std::endl; ++ } ++ exit(EXIT_FAILURE); ++ } ++ ++ timings.emplace_back( ++ std::chrono::duration_cast( ++ std::chrono::duration(v))); ++ uris.push_back(script_line.substr(pos + 1, script_line.size())); ++ } ++} ++} // namespace ++ ++namespace { ++std::unique_ptr create_worker(uint32_t id, SSL_CTX *ssl_ctx, ++ size_t nreqs, size_t nclients, ++ size_t rate, size_t max_samples) { ++ std::stringstream rate_report; ++ if (config.is_rate_mode() && nclients > rate) { ++ rate_report << "Up to " << rate << " client(s) will be created every " ++ << util::duration_str(config.rate_period) << " "; ++ } ++ ++ if (config.is_timing_based_mode()) { ++ std::cout << "spawning thread #" << id << ": " << nclients ++ << " total client(s). Timing-based test with " ++ << config.warm_up_time << "s of warm-up time and " ++ << config.duration << "s of main duration for measurements." ++ << std::endl; ++ } else { ++ std::cout << "spawning thread #" << id << ": " << nclients ++ << " total client(s). " << rate_report.str() << nreqs ++ << " total requests" << std::endl; ++ } ++ ++ if (config.is_rate_mode()) { ++ return std::make_unique(id, ssl_ctx, nreqs, nclients, rate, ++ max_samples, &config); ++ } else { ++ // Here rate is same as client because the rate_timeout callback ++ // will be called only once ++ return std::make_unique(id, ssl_ctx, nreqs, nclients, nclients, ++ max_samples, &config); ++ } ++} ++} // namespace ++ ++namespace { ++int parse_header_table_size(uint32_t &dst, const char *opt, ++ const char *optarg) { ++ auto n = util::parse_uint_with_unit(optarg); ++ if (n == -1) { ++ std::cerr << "--" << opt << ": Bad option value: " << optarg << std::endl; ++ return -1; ++ } ++ if (n > std::numeric_limits::max()) { ++ std::cerr << "--" << opt ++ << ": Value too large. It should be less than or equal to " ++ << std::numeric_limits::max() << std::endl; ++ return -1; ++ } ++ ++ dst = n; ++ ++ return 0; ++} ++} // namespace ++ ++namespace { ++void print_version(std::ostream &out) { ++ out << "h2load nghttp2/" NGHTTP2_VERSION << std::endl; ++} ++} // namespace ++ ++namespace { ++void print_usage(std::ostream &out) { ++ out << R"(Usage: h2load [OPTIONS]... [URI]... ++benchmarking tool for HTTP/2 server)" ++ << std::endl; ++} ++} // namespace ++ ++namespace { ++constexpr char DEFAULT_NPN_LIST[] = "h2,h2-16,h2-14,http/1.1"; ++} // namespace ++ ++namespace { ++void print_help(std::ostream &out) { ++ print_usage(out); ++ ++ auto config = Config(); ++ ++ out << R"( ++ Specify URI to access. Multiple URIs can be specified. ++ URIs are used in this order for each client. All URIs ++ are used, then first URI is used and then 2nd URI, and ++ so on. The scheme, host and port in the subsequent ++ URIs, if present, are ignored. Those in the first URI ++ are used solely. Definition of a base URI overrides all ++ scheme, host or port values. ++Options: ++ -n, --requests= ++ Number of requests across all clients. If it is used ++ with --timing-script-file option, this option specifies ++ the number of requests each client performs rather than ++ the number of requests across all clients. This option ++ is ignored if timing-based benchmarking is enabled (see ++ --duration option). ++ Default: )" ++ << config.nreqs << R"( ++ -c, --clients= ++ Number of concurrent clients. With -r option, this ++ specifies the maximum number of connections to be made. ++ Default: )" ++ << config.nclients << R"( ++ -t, --threads= ++ Number of native threads. ++ Default: )" ++ << config.nthreads << R"( ++ -i, --input-file= ++ Path of a file with multiple URIs are separated by EOLs. ++ This option will disable URIs getting from command-line. ++ If '-' is given as , URIs will be read from stdin. ++ URIs are used in this order for each client. All URIs ++ are used, then first URI is used and then 2nd URI, and ++ so on. The scheme, host and port in the subsequent ++ URIs, if present, are ignored. Those in the first URI ++ are used solely. Definition of a base URI overrides all ++ scheme, host or port values. ++ -m, --max-concurrent-streams= ++ Max concurrent streams to issue per session. When ++ http/1.1 is used, this specifies the number of HTTP ++ pipelining requests in-flight. ++ Default: 1 ++ -f, --max-frame-size= ++ Maximum frame size that the local endpoint is willing to ++ receive. ++ Default: )" ++ << util::utos_unit(config.max_frame_size) << R"( ++ -w, --window-bits= ++ Sets the stream level initial window size to (2**)-1. ++ For QUIC, is capped to 26 (roughly 64MiB). ++ Default: )" ++ << config.window_bits << R"( ++ -W, --connection-window-bits= ++ Sets the connection level initial window size to ++ (2**)-1. ++ Default: )" ++ << config.connection_window_bits << R"( ++ -H, --header=
++ Add/Override a header to the requests. ++ --ciphers= ++ Set allowed cipher list for TLSv1.2 or earlier. The ++ format of the string is described in OpenSSL ciphers(1). ++ Default: )" ++ << config.ciphers << R"( ++ --tls13-ciphers= ++ Set allowed cipher list for TLSv1.3. The format of the ++ string is described in OpenSSL ciphers(1). ++ Default: )" ++ << config.tls13_ciphers << R"( ++ -p, --no-tls-proto= ++ Specify ALPN identifier of the protocol to be used when ++ accessing http URI without SSL/TLS. ++ Available protocols: )" ++ << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( and )" << NGHTTP2_H1_1 << R"( ++ Default: )" ++ << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( ++ -d, --data= ++ Post FILE to server. The request method is changed to ++ POST. For http/1.1 connection, if -d is used, the ++ maximum number of in-flight pipelined requests is set to ++ 1. ++ -r, --rate= ++ Specifies the fixed rate at which connections are ++ created. The rate must be a positive integer, ++ representing the number of connections to be made per ++ rate period. The maximum number of connections to be ++ made is given in -c option. This rate will be ++ distributed among threads as evenly as possible. For ++ example, with -t2 and -r4, each thread gets 2 ++ connections per period. When the rate is 0, the program ++ will run as it normally does, creating connections at ++ whatever variable rate it wants. The default value for ++ this option is 0. -r and -D are mutually exclusive. ++ --rate-period= ++ Specifies the time period between creating connections. ++ The period must be a positive number, representing the ++ length of the period in time. This option is ignored if ++ the rate option is not used. The default value for this ++ option is 1s. ++ -D, --duration= ++ Specifies the main duration for the measurements in case ++ of timing-based benchmarking. -D and -r are mutually ++ exclusive. ++ --warm-up-time= ++ Specifies the time period before starting the actual ++ measurements, in case of timing-based benchmarking. ++ Needs to provided along with -D option. ++ -T, --connection-active-timeout= ++ Specifies the maximum time that h2load is willing to ++ keep a connection open, regardless of the activity on ++ said connection. must be a positive integer, ++ specifying the amount of time to wait. When no timeout ++ value is set (either active or inactive), h2load will ++ keep a connection open indefinitely, waiting for a ++ response. ++ -N, --connection-inactivity-timeout= ++ Specifies the amount of time that h2load is willing to ++ wait to see activity on a given connection. ++ must be a positive integer, specifying the amount of ++ time to wait. When no timeout value is set (either ++ active or inactive), h2load will keep a connection open ++ indefinitely, waiting for a response. ++ --timing-script-file= ++ Path of a file containing one or more lines separated by ++ EOLs. Each script line is composed of two tab-separated ++ fields. The first field represents the time offset from ++ the start of execution, expressed as a positive value of ++ milliseconds with microsecond resolution. The second ++ field represents the URI. This option will disable URIs ++ getting from command-line. If '-' is given as , ++ script lines will be read from stdin. Script lines are ++ used in order for each client. If -n is given, it must ++ be less than or equal to the number of script lines, ++ larger values are clamped to the number of script lines. ++ If -n is not given, the number of requests will default ++ to the number of script lines. The scheme, host and ++ port defined in the first URI are used solely. Values ++ contained in other URIs, if present, are ignored. ++ Definition of a base URI overrides all scheme, host or ++ port values. --timing-script-file and --rps are ++ mutually exclusive. ++ -B, --base-uri=(|unix:) ++ Specify URI from which the scheme, host and port will be ++ used for all requests. The base URI overrides all ++ values defined either at the command line or inside ++ input files. If argument starts with "unix:", then the ++ rest of the argument will be treated as UNIX domain ++ socket path. The connection is made through that path ++ instead of TCP. In this case, scheme is inferred from ++ the first URI appeared in the command line or inside ++ input files as usual. ++ --npn-list= ++ Comma delimited list of ALPN protocol identifier sorted ++ in the order of preference. That means most desirable ++ protocol comes first. This is used in both ALPN and ++ NPN. The parameter must be delimited by a single comma ++ only and any white spaces are treated as a part of ++ protocol string. ++ Default: )" ++ << DEFAULT_NPN_LIST << R"( ++ --h1 Short hand for --npn-list=http/1.1 ++ --no-tls-proto=http/1.1, which effectively force ++ http/1.1 for both http and https URI. ++ --header-table-size= ++ Specify decoder header table size. ++ Default: )" ++ << util::utos_unit(config.header_table_size) << R"( ++ --encoder-header-table-size= ++ Specify encoder header table size. The decoder (server) ++ specifies the maximum dynamic table size it accepts. ++ Then the negotiated dynamic table size is the minimum of ++ this option value and the value which server specified. ++ Default: )" ++ << util::utos_unit(config.encoder_header_table_size) << R"( ++ --log-file= ++ Write per-request information to a file as tab-separated ++ columns: start time as microseconds since epoch; HTTP ++ status code; microseconds until end of response. More ++ columns may be added later. Rows are ordered by end-of- ++ response time when using one worker thread, but may ++ appear slightly out of order with multiple threads due ++ to buffering. Status code is -1 for failed streams. ++ --qlog-file-base= ++ Enable qlog output and specify base file name for qlogs. ++ Qlog is emitted for each connection. For a given base ++ name "base", each output file name becomes ++ "base.M.N.sqlog" where M is worker ID and N is client ID ++ (e.g. "base.0.3.sqlog"). Only effective in QUIC runs. ++ --connect-to=[:] ++ Host and port to connect instead of using the authority ++ in . ++ --rps= Specify request per second for each client. --rps and ++ --timing-script-file are mutually exclusive. ++ --groups= ++ Specify the supported groups. ++ Default: )" ++ << config.groups << R"( ++ --no-udp-gso ++ Disable UDP GSO. ++ --max-udp-payload-size= ++ Specify the maximum outgoing UDP datagram payload size. ++ --ktls Enable ktls. ++ -v, --verbose ++ Output debug information. ++ --version Display version information and exit. ++ -h, --help Display this help and exit. ++ ++-- ++ ++ The argument is an integer and an optional unit (e.g., 10K is ++ 10 * 1024). Units are K, M and G (powers of 1024). ++ ++ The argument is an integer and an optional unit (e.g., 1s ++ is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms ++ (hours, minutes, seconds and milliseconds, respectively). If a unit ++ is omitted, a second is used as unit.)" ++ << std::endl; ++} ++} // namespace ++ ++int main(int argc, char **argv) { ++ tls::libssl_init(); ++ ++#ifndef NOTHREADS ++ tls::LibsslGlobalLock lock; ++#endif // NOTHREADS ++ ++ std::string datafile; ++ std::string logfile; ++ std::string qlog_base; ++ bool nreqs_set_manually = false; ++ while (1) { ++ static int flag = 0; ++ constexpr static option long_options[] = { ++ {"requests", required_argument, nullptr, 'n'}, ++ {"clients", required_argument, nullptr, 'c'}, ++ {"data", required_argument, nullptr, 'd'}, ++ {"threads", required_argument, nullptr, 't'}, ++ {"max-concurrent-streams", required_argument, nullptr, 'm'}, ++ {"window-bits", required_argument, nullptr, 'w'}, ++ {"max-frame-size", required_argument, nullptr, 'f'}, ++ {"connection-window-bits", required_argument, nullptr, 'W'}, ++ {"input-file", required_argument, nullptr, 'i'}, ++ {"header", required_argument, nullptr, 'H'}, ++ {"no-tls-proto", required_argument, nullptr, 'p'}, ++ {"verbose", no_argument, nullptr, 'v'}, ++ {"help", no_argument, nullptr, 'h'}, ++ {"version", no_argument, &flag, 1}, ++ {"ciphers", required_argument, &flag, 2}, ++ {"rate", required_argument, nullptr, 'r'}, ++ {"connection-active-timeout", required_argument, nullptr, 'T'}, ++ {"connection-inactivity-timeout", required_argument, nullptr, 'N'}, ++ {"duration", required_argument, nullptr, 'D'}, ++ {"timing-script-file", required_argument, &flag, 3}, ++ {"base-uri", required_argument, nullptr, 'B'}, ++ {"npn-list", required_argument, &flag, 4}, ++ {"rate-period", required_argument, &flag, 5}, ++ {"h1", no_argument, &flag, 6}, ++ {"header-table-size", required_argument, &flag, 7}, ++ {"encoder-header-table-size", required_argument, &flag, 8}, ++ {"warm-up-time", required_argument, &flag, 9}, ++ {"log-file", required_argument, &flag, 10}, ++ {"connect-to", required_argument, &flag, 11}, ++ {"rps", required_argument, &flag, 12}, ++ {"groups", required_argument, &flag, 13}, ++ {"tls13-ciphers", required_argument, &flag, 14}, ++ {"no-udp-gso", no_argument, &flag, 15}, ++ {"qlog-file-base", required_argument, &flag, 16}, ++ {"max-udp-payload-size", required_argument, &flag, 17}, ++ {"ktls", no_argument, &flag, 18}, ++ {nullptr, 0, nullptr, 0}}; ++ int option_index = 0; ++ auto c = getopt_long(argc, argv, ++ "hvW:c:d:m:n:p:t:w:f:H:i:r:T:N:D:B:", long_options, ++ &option_index); ++ if (c == -1) { ++ break; ++ } ++ switch (c) { ++ case 'n': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-n: bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.nreqs = n; ++ nreqs_set_manually = true; ++ break; ++ } ++ case 'c': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-c: bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.nclients = n; ++ break; ++ } ++ case 'd': ++ datafile = optarg; ++ break; ++ case 't': { ++#ifdef NOTHREADS ++ std::cerr << "-t: WARNING: Threading disabled at build time, " ++ << "no threads created." << std::endl; ++#else ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-t: bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.nthreads = n; ++#endif // NOTHREADS ++ break; ++ } ++ case 'm': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-m: bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.max_concurrent_streams = n; ++ break; ++ } ++ case 'w': ++ case 'W': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1 || n > 30) { ++ std::cerr << "-" << static_cast(c) ++ << ": specify the integer in the range [0, 30], inclusive" ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (c == 'w') { ++ config.window_bits = n; ++ } else { ++ config.connection_window_bits = n; ++ } ++ break; ++ } ++ case 'f': { ++ auto n = util::parse_uint_with_unit(optarg); ++ if (n == -1) { ++ std::cerr << "--max-frame-size: bad option value: " << optarg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (static_cast(n) < 16_k) { ++ std::cerr << "--max-frame-size: minimum 16384" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (static_cast(n) > 16_m - 1) { ++ std::cerr << "--max-frame-size: maximum 16777215" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.max_frame_size = n; ++ break; ++ } ++ case 'H': { ++ char *header = optarg; ++ // Skip first possible ':' in the header name ++ char *value = strchr(optarg + 1, ':'); ++ if (!value || (header[0] == ':' && header + 1 == value)) { ++ std::cerr << "-H: invalid header: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ *value = 0; ++ value++; ++ while (isspace(*value)) { ++ value++; ++ } ++ if (*value == 0) { ++ // This could also be a valid case for suppressing a header ++ // similar to curl ++ std::cerr << "-H: invalid header - value missing: " << optarg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ // Note that there is no processing currently to handle multiple ++ // message-header fields with the same field name ++ config.custom_headers.emplace_back(header, value); ++ util::inp_strlower(config.custom_headers.back().name); ++ break; ++ } ++ case 'i': ++ config.ifile = optarg; ++ break; ++ case 'p': { ++ auto proto = StringRef{optarg}; ++ if (util::strieq(StringRef::from_lit(NGHTTP2_CLEARTEXT_PROTO_VERSION_ID), ++ proto)) { ++ config.no_tls_proto = Config::PROTO_HTTP2; ++ } else if (util::strieq(NGHTTP2_H1_1, proto)) { ++ config.no_tls_proto = Config::PROTO_HTTP1_1; ++ } else { ++ std::cerr << "-p: unsupported protocol " << proto << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ break; ++ } ++ case 'r': { ++ auto n = util::parse_uint(optarg); ++ if (n == -1) { ++ std::cerr << "-r: bad option value: " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (n == 0) { ++ std::cerr << "-r: the rate at which connections are made " ++ << "must be positive." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.rate = n; ++ break; ++ } ++ case 'T': ++ config.conn_active_timeout = util::parse_duration_with_unit(optarg); ++ if (!std::isfinite(config.conn_active_timeout)) { ++ std::cerr << "-T: bad value for the conn_active_timeout wait time: " ++ << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 'N': ++ config.conn_inactivity_timeout = util::parse_duration_with_unit(optarg); ++ if (!std::isfinite(config.conn_inactivity_timeout)) { ++ std::cerr << "-N: bad value for the conn_inactivity_timeout wait time: " ++ << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 'B': { ++ auto arg = StringRef{optarg}; ++ config.base_uri = ""; ++ config.base_uri_unix = false; ++ ++ if (util::istarts_with_l(arg, UNIX_PATH_PREFIX)) { ++ // UNIX domain socket path ++ sockaddr_un un; ++ ++ auto path = StringRef{std::begin(arg) + str_size(UNIX_PATH_PREFIX), ++ std::end(arg)}; ++ ++ if (path.size() == 0 || path.size() + 1 > sizeof(un.sun_path)) { ++ std::cerr << "--base-uri: invalid UNIX domain socket path: " << arg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ config.base_uri_unix = true; ++ ++ auto &unix_addr = config.unix_addr; ++ std::copy(std::begin(path), std::end(path), unix_addr.sun_path); ++ unix_addr.sun_path[path.size()] = '\0'; ++ unix_addr.sun_family = AF_UNIX; ++ ++ break; ++ } ++ ++ if (!parse_base_uri(arg)) { ++ std::cerr << "--base-uri: invalid base URI: " << arg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ config.base_uri = arg.str(); ++ break; ++ } ++ case 'D': ++ config.duration = util::parse_duration_with_unit(optarg); ++ if (!std::isfinite(config.duration)) { ++ std::cerr << "-D: value error " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 'v': ++ config.verbose = true; ++ break; ++ case 'h': ++ print_help(std::cout); ++ exit(EXIT_SUCCESS); ++ case '?': ++ util::show_candidates(argv[optind - 1], long_options); ++ exit(EXIT_FAILURE); ++ case 0: ++ switch (flag) { ++ case 1: ++ // version option ++ print_version(std::cout); ++ exit(EXIT_SUCCESS); ++ case 2: ++ // ciphers option ++ config.ciphers = optarg; ++ break; ++ case 3: ++ // timing-script option ++ config.ifile = optarg; ++ config.timing_script = true; ++ break; ++ case 4: ++ // npn-list option ++ config.npn_list = util::parse_config_str_list(StringRef{optarg}); ++ break; ++ case 5: ++ // rate-period ++ config.rate_period = util::parse_duration_with_unit(optarg); ++ if (!std::isfinite(config.rate_period)) { ++ std::cerr << "--rate-period: value error " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 6: ++ // --h1 ++ config.npn_list = ++ util::parse_config_str_list(StringRef::from_lit("http/1.1")); ++ config.no_tls_proto = Config::PROTO_HTTP1_1; ++ break; ++ case 7: ++ // --header-table-size ++ if (parse_header_table_size(config.header_table_size, ++ "header-table-size", optarg) != 0) { ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 8: ++ // --encoder-header-table-size ++ if (parse_header_table_size(config.encoder_header_table_size, ++ "encoder-header-table-size", optarg) != 0) { ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 9: ++ // --warm-up-time ++ config.warm_up_time = util::parse_duration_with_unit(optarg); ++ if (!std::isfinite(config.warm_up_time)) { ++ std::cerr << "--warm-up-time: value error " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ break; ++ case 10: ++ // --log-file ++ logfile = optarg; ++ break; ++ case 11: { ++ // --connect-to ++ auto p = util::split_hostport(StringRef{optarg}); ++ int64_t port = 0; ++ if (p.first.empty() || ++ (!p.second.empty() && (port = util::parse_uint(p.second)) == -1)) { ++ std::cerr << "--connect-to: Invalid value " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.connect_to_host = p.first.str(); ++ config.connect_to_port = port; ++ break; ++ } ++ case 12: { ++ char *end; ++ auto v = std::strtod(optarg, &end); ++ if (end == optarg || *end != '\0' || !std::isfinite(v) || ++ 1. / v < 1e-6) { ++ std::cerr << "--rps: Invalid value " << optarg << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.rps = v; ++ break; ++ } ++ case 13: ++ // --groups ++ config.groups = optarg; ++ break; ++ case 14: ++ // --tls13-ciphers ++ config.tls13_ciphers = optarg; ++ break; ++ case 15: ++ // --no-udp-gso ++ config.no_udp_gso = true; ++ break; ++ case 16: ++ // --qlog-file-base ++ qlog_base = optarg; ++ break; ++ case 17: { ++ // --max-udp-payload-size ++ auto n = util::parse_uint_with_unit(optarg); ++ if (n == -1) { ++ std::cerr << "--max-udp-payload-size: bad option value: " << optarg ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ if (static_cast(n) > 64_k) { ++ std::cerr << "--max-udp-payload-size: must not exceed 65536" ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.max_udp_payload_size = n; ++ break; ++ } ++ case 18: ++ // --ktls ++ config.ktls = true; ++ break; ++ } ++ break; ++ default: ++ break; ++ } ++ } ++ ++ if (argc == optind) { ++ if (config.ifile.empty()) { ++ std::cerr << "no URI or input file given" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ } ++ ++ if (config.nclients == 0) { ++ std::cerr << "-c: the number of clients must be strictly greater than 0." ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.npn_list.empty()) { ++ config.npn_list = ++ util::parse_config_str_list(StringRef::from_lit(DEFAULT_NPN_LIST)); ++ } ++ ++ // serialize the APLN tokens ++ for (auto &proto : config.npn_list) { ++ proto.insert(proto.begin(), static_cast(proto.size())); ++ } ++ ++ std::vector reqlines; ++ ++ if (config.ifile.empty()) { ++ std::vector uris; ++ std::copy(&argv[optind], &argv[argc], std::back_inserter(uris)); ++ reqlines = parse_uris(std::begin(uris), std::end(uris)); ++ } else { ++ std::vector uris; ++ if (!config.timing_script) { ++ if (config.ifile == "-") { ++ uris = read_uri_from_file(std::cin); ++ } else { ++ std::ifstream infile(config.ifile); ++ if (!infile) { ++ std::cerr << "cannot read input file: " << config.ifile << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ uris = read_uri_from_file(infile); ++ } ++ } else { ++ if (config.ifile == "-") { ++ read_script_from_file(std::cin, config.timings, uris); ++ } else { ++ std::ifstream infile(config.ifile); ++ if (!infile) { ++ std::cerr << "cannot read input file: " << config.ifile << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ read_script_from_file(infile, config.timings, uris); ++ } ++ ++ if (nreqs_set_manually) { ++ if (config.nreqs > uris.size()) { ++ std::cerr << "-n: the number of requests must be less than or equal " ++ "to the number of timing script entries. Setting number " ++ "of requests to " ++ << uris.size() << std::endl; ++ ++ config.nreqs = uris.size(); ++ } ++ } else { ++ config.nreqs = uris.size(); ++ } ++ } ++ ++ reqlines = parse_uris(std::begin(uris), std::end(uris)); ++ } ++ ++ if (reqlines.empty()) { ++ std::cerr << "No URI given" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.is_timing_based_mode() && config.is_rate_mode()) { ++ std::cerr << "-r, -D: they are mutually exclusive." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.timing_script && config.rps_enabled()) { ++ std::cerr << "--timing-script-file, --rps: they are mutually exclusive." ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.nreqs == 0 && !config.is_timing_based_mode()) { ++ std::cerr << "-n: the number of requests must be strictly greater than 0 " ++ "if timing-based test is not being run." ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.max_concurrent_streams == 0) { ++ std::cerr << "-m: the max concurrent streams must be strictly greater " ++ << "than 0." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.nthreads == 0) { ++ std::cerr << "-t: the number of threads must be strictly greater than 0." ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.nthreads > std::thread::hardware_concurrency()) { ++ std::cerr << "-t: warning: the number of threads is greater than hardware " ++ << "cores." << std::endl; ++ } ++ ++ // With timing script, we don't distribute config.nreqs to each ++ // client or thread. ++ if (!config.timing_script && config.nreqs < config.nclients && ++ !config.is_timing_based_mode()) { ++ std::cerr << "-n, -c: the number of requests must be greater than or " ++ << "equal to the clients." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.nclients < config.nthreads) { ++ std::cerr << "-c, -t: the number of clients must be greater than or equal " ++ << "to the number of threads." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.is_timing_based_mode()) { ++ config.nreqs = 0; ++ } ++ ++ if (config.is_rate_mode()) { ++ if (config.rate < config.nthreads) { ++ std::cerr << "-r, -t: the connection rate must be greater than or equal " ++ << "to the number of threads." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (config.rate > config.nclients) { ++ std::cerr << "-r, -c: the connection rate must be smaller than or equal " ++ "to the number of clients." ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ } ++ ++ if (!datafile.empty()) { ++ config.data_fd = open(datafile.c_str(), O_RDONLY | O_BINARY); ++ if (config.data_fd == -1) { ++ std::cerr << "-d: Could not open file " << datafile << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ struct stat data_stat; ++ if (fstat(config.data_fd, &data_stat) == -1) { ++ std::cerr << "-d: Could not stat file " << datafile << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.data_length = data_stat.st_size; ++ auto addr = mmap(nullptr, config.data_length, PROT_READ, MAP_SHARED, ++ config.data_fd, 0); ++ if (addr == MAP_FAILED) { ++ std::cerr << "-d: Could not mmap file " << datafile << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ config.data = static_cast(addr); ++ } ++ ++ if (!logfile.empty()) { ++ config.log_fd = open(logfile.c_str(), O_WRONLY | O_CREAT | O_APPEND, ++ S_IRUSR | S_IWUSR | S_IRGRP); ++ if (config.log_fd == -1) { ++ std::cerr << "--log-file: Could not open file " << logfile << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ } ++ ++ if (!qlog_base.empty()) { ++ if (!config.is_quic()) { ++ std::cerr ++ << "Warning: --qlog-file-base: only effective in quic, ignoring." ++ << std::endl; ++ } else { ++#ifdef ENABLE_HTTP3 ++ config.qlog_file_base = qlog_base; ++#endif // ENABLE_HTTP3 ++ } ++ } ++ ++ struct sigaction act {}; ++ act.sa_handler = SIG_IGN; ++ sigaction(SIGPIPE, &act, nullptr); ++ ++ auto ssl_ctx = SSL_CTX_new(TLS_client_method()); ++ if (!ssl_ctx) { ++ std::cerr << "Failed to create SSL_CTX: " ++ << ERR_error_string(ERR_get_error(), nullptr) << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | ++ SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | ++ SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; ++ ++#ifdef SSL_OP_ENABLE_KTLS ++ if (config.ktls) { ++ ssl_opts |= SSL_OP_ENABLE_KTLS; ++ } ++#endif // SSL_OP_ENABLE_KTLS ++ ++ SSL_CTX_set_options(ssl_ctx, ssl_opts); ++ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); ++ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); ++ ++ if (config.is_quic()) { ++#ifdef ENABLE_HTTP3 ++# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS ++ if (ngtcp2_crypto_quictls_configure_client_context(ssl_ctx) != 0) { ++ std::cerr << "ngtcp2_crypto_quictls_configure_client_context failed" ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS ++# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL ++ if (ngtcp2_crypto_boringssl_configure_client_context(ssl_ctx) != 0) { ++ std::cerr << "ngtcp2_crypto_boringssl_configure_client_context failed" ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL ++#endif // ENABLE_HTTP3 ++ } else if (nghttp2::tls::ssl_ctx_set_proto_versions( ++ ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, ++ nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { ++ std::cerr << "Could not set TLS versions" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ if (SSL_CTX_set_cipher_list(ssl_ctx, config.ciphers.c_str()) == 0) { ++ std::cerr << "SSL_CTX_set_cipher_list with " << config.ciphers ++ << " failed: " << ERR_error_string(ERR_get_error(), nullptr) ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++#if OPENSSL_1_1_1_API && !defined(OPENSSL_IS_BORINGSSL) ++ if (SSL_CTX_set_ciphersuites(ssl_ctx, config.tls13_ciphers.c_str()) == 0) { ++ std::cerr << "SSL_CTX_set_ciphersuites with " << config.tls13_ciphers ++ << " failed: " << ERR_error_string(ERR_get_error(), nullptr) ++ << std::endl; ++ exit(EXIT_FAILURE); ++ } ++#endif // OPENSSL_1_1_1_API && !defined(OPENSSL_IS_BORINGSSL) ++ ++#if OPENSSL_1_1_1_API && !defined(OPENSSL_IS_BORINGSSL) ++ if (SSL_CTX_set1_groups_list(ssl_ctx, config.groups.c_str()) != 1) { ++ std::cerr << "SSL_CTX_set1_groups_list failed" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++#else // !(OPENSSL_1_1_1_API && !defined(OPENSSL_IS_BORINGSSL)) ++ if (SSL_CTX_set1_curves_list(ssl_ctx, config.groups.c_str()) != 1) { ++ std::cerr << "SSL_CTX_set1_curves_list failed" << std::endl; ++ exit(EXIT_FAILURE); ++ } ++#endif // !(OPENSSL_1_1_1_API && !defined(OPENSSL_IS_BORINGSSL)) ++ ++#ifndef OPENSSL_NO_NEXTPROTONEG ++ SSL_CTX_set_next_proto_select_cb(ssl_ctx, client_select_next_proto_cb, ++ nullptr); ++#endif // !OPENSSL_NO_NEXTPROTONEG ++ ++#if OPENSSL_VERSION_NUMBER >= 0x10002000L ++ std::vector proto_list; ++ for (const auto &proto : config.npn_list) { ++ std::copy_n(proto.c_str(), proto.size(), std::back_inserter(proto_list)); ++ } ++ ++ SSL_CTX_set_alpn_protos(ssl_ctx, proto_list.data(), proto_list.size()); ++#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L ++ ++#if OPENSSL_1_1_1_API ++ auto keylog_filename = getenv("SSLKEYLOGFILE"); ++ if (keylog_filename) { ++ keylog_file.open(keylog_filename, std::ios_base::app); ++ if (keylog_file) { ++ SSL_CTX_set_keylog_callback(ssl_ctx, keylog_callback); ++ } ++ } ++#endif // OPENSSL_1_1_1_API ++ ++ std::string user_agent = "h2load nghttp2/" NGHTTP2_VERSION; ++ Headers shared_nva; ++ shared_nva.emplace_back(":scheme", config.scheme); ++ if (config.port != config.default_port) { ++ shared_nva.emplace_back(":authority", ++ config.host + ":" + util::utos(config.port)); ++ } else { ++ shared_nva.emplace_back(":authority", config.host); ++ } ++ shared_nva.emplace_back(":method", config.data_fd == -1 ? "GET" : "POST"); ++ shared_nva.emplace_back("user-agent", user_agent); ++ ++ // list header fields that can be overridden. ++ auto override_hdrs = make_array(":authority", ":host", ":method", ++ ":scheme", "user-agent"); ++ ++ for (auto &kv : config.custom_headers) { ++ if (std::find(std::begin(override_hdrs), std::end(override_hdrs), ++ kv.name) != std::end(override_hdrs)) { ++ // override header ++ for (auto &nv : shared_nva) { ++ if ((nv.name == ":authority" && kv.name == ":host") || ++ (nv.name == kv.name)) { ++ nv.value = kv.value; ++ } ++ } ++ } else { ++ // add additional headers ++ shared_nva.push_back(kv); ++ } ++ } ++ ++ std::string content_length_str; ++ if (config.data_fd != -1) { ++ content_length_str = util::utos(config.data_length); ++ } ++ ++ auto method_it = ++ std::find_if(std::begin(shared_nva), std::end(shared_nva), ++ [](const Header &nv) { return nv.name == ":method"; }); ++ assert(method_it != std::end(shared_nva)); ++ ++ config.h1reqs.reserve(reqlines.size()); ++ config.nva.reserve(reqlines.size()); ++ ++ for (auto &req : reqlines) { ++ // For HTTP/1.1 ++ auto h1req = (*method_it).value; ++ h1req += ' '; ++ h1req += req; ++ h1req += " HTTP/1.1\r\n"; ++ for (auto &nv : shared_nva) { ++ if (nv.name == ":authority") { ++ h1req += "Host: "; ++ h1req += nv.value; ++ h1req += "\r\n"; ++ continue; ++ } ++ if (nv.name[0] == ':') { ++ continue; ++ } ++ h1req += nv.name; ++ h1req += ": "; ++ h1req += nv.value; ++ h1req += "\r\n"; ++ } ++ ++ if (!content_length_str.empty()) { ++ h1req += "Content-Length: "; ++ h1req += content_length_str; ++ h1req += "\r\n"; ++ } ++ h1req += "\r\n"; ++ ++ config.h1reqs.push_back(std::move(h1req)); ++ ++ // For nghttp2 ++ std::vector nva; ++ // 2 for :path, and possible content-length ++ nva.reserve(2 + shared_nva.size()); ++ ++ nva.push_back(http2::make_nv_ls(":path", req)); ++ ++ for (auto &nv : shared_nva) { ++ nva.push_back(http2::make_nv(nv.name, nv.value, false)); ++ } ++ ++ if (!content_length_str.empty()) { ++ nva.push_back(http2::make_nv(StringRef::from_lit("content-length"), ++ StringRef{content_length_str})); ++ } ++ ++ config.nva.push_back(std::move(nva)); ++ } ++ ++ // Don't DOS our server! ++ if (config.host == "nghttp2.org") { ++ std::cerr << "Using h2load against public server " << config.host ++ << " should be prohibited." << std::endl; ++ exit(EXIT_FAILURE); ++ } ++ ++ resolve_host(); ++ ++ std::cout << "starting benchmark..." << std::endl; ++ ++ std::vector> workers; ++ workers.reserve(config.nthreads); ++ ++#ifndef NOTHREADS ++ size_t nreqs_per_thread = 0; ++ ssize_t nreqs_rem = 0; ++ ++ if (!config.timing_script) { ++ nreqs_per_thread = config.nreqs / config.nthreads; ++ nreqs_rem = config.nreqs % config.nthreads; ++ } ++ ++ size_t nclients_per_thread = config.nclients / config.nthreads; ++ ssize_t nclients_rem = config.nclients % config.nthreads; ++ ++ size_t rate_per_thread = config.rate / config.nthreads; ++ ssize_t rate_per_thread_rem = config.rate % config.nthreads; ++ ++ size_t max_samples_per_thread = ++ std::max(static_cast(256), MAX_SAMPLES / config.nthreads); ++ ++ std::mutex mu; ++ std::condition_variable cv; ++ auto ready = false; ++ ++ std::vector> futures; ++ for (size_t i = 0; i < config.nthreads; ++i) { ++ auto rate = rate_per_thread; ++ if (rate_per_thread_rem > 0) { ++ --rate_per_thread_rem; ++ ++rate; ++ } ++ auto nclients = nclients_per_thread; ++ if (nclients_rem > 0) { ++ --nclients_rem; ++ ++nclients; ++ } ++ ++ size_t nreqs; ++ if (config.timing_script) { ++ // With timing script, each client issues config.nreqs requests. ++ // We divide nreqs by number of clients in Worker ctor to ++ // distribute requests to those clients evenly, so multiply ++ // config.nreqs here by config.nclients. ++ nreqs = config.nreqs * nclients; ++ } else { ++ nreqs = nreqs_per_thread; ++ if (nreqs_rem > 0) { ++ --nreqs_rem; ++ ++nreqs; ++ } ++ } ++ ++ workers.push_back(create_worker(i, ssl_ctx, nreqs, nclients, rate, ++ max_samples_per_thread)); ++ auto &worker = workers.back(); ++ futures.push_back( ++ std::async(std::launch::async, [&worker, &mu, &cv, &ready]() { ++ { ++ std::unique_lock ulk(mu); ++ cv.wait(ulk, [&ready] { return ready; }); ++ } ++ worker->run(); ++ })); ++ } ++ ++ { ++ std::lock_guard lg(mu); ++ ready = true; ++ cv.notify_all(); ++ } ++ ++ auto start = std::chrono::steady_clock::now(); ++ ++ for (auto &fut : futures) { ++ fut.get(); ++ } ++ ++#else // NOTHREADS ++ auto rate = config.rate; ++ auto nclients = config.nclients; ++ auto nreqs = ++ config.timing_script ? config.nreqs * config.nclients : config.nreqs; ++ ++ workers.push_back( ++ create_worker(0, ssl_ctx, nreqs, nclients, rate, MAX_SAMPLES)); ++ ++ auto start = std::chrono::steady_clock::now(); ++ ++ workers.back()->run(); ++#endif // NOTHREADS ++ ++ auto end = std::chrono::steady_clock::now(); ++ auto duration = ++ std::chrono::duration_cast(end - start); ++ ++ Stats stats(0, 0); ++ for (const auto &w : workers) { ++ const auto &s = w->stats; ++ ++ stats.req_todo += s.req_todo; ++ stats.req_started += s.req_started; ++ stats.req_done += s.req_done; ++ stats.req_timedout += s.req_timedout; ++ stats.req_success += s.req_success; ++ stats.req_status_success += s.req_status_success; ++ stats.req_failed += s.req_failed; ++ stats.req_error += s.req_error; ++ stats.bytes_total += s.bytes_total; ++ stats.bytes_head += s.bytes_head; ++ stats.bytes_head_decomp += s.bytes_head_decomp; ++ stats.bytes_body += s.bytes_body; ++ stats.udp_dgram_recv += s.udp_dgram_recv; ++ stats.udp_dgram_sent += s.udp_dgram_sent; ++ ++ for (size_t i = 0; i < stats.status.size(); ++i) { ++ stats.status[i] += s.status[i]; ++ } ++ } ++ ++ auto ts = process_time_stats(workers); ++ ++ // Requests which have not been issued due to connection errors, are ++ // counted towards req_failed and req_error. ++ auto req_not_issued = ++ (stats.req_todo - stats.req_status_success - stats.req_failed); ++ stats.req_failed += req_not_issued; ++ stats.req_error += req_not_issued; ++ ++ // UI is heavily inspired by weighttp[1] and wrk[2] ++ // ++ // [1] https://github.com/lighttpd/weighttp ++ // [2] https://github.com/wg/wrk ++ double rps = 0; ++ int64_t bps = 0; ++ if (duration.count() > 0) { ++ if (config.is_timing_based_mode()) { ++ // we only want to consider the main duration if warm-up is given ++ rps = stats.req_success / config.duration; ++ bps = stats.bytes_total / config.duration; ++ } else { ++ auto secd = std::chrono::duration_cast< ++ std::chrono::duration>( ++ duration); ++ rps = stats.req_success / secd.count(); ++ bps = stats.bytes_total / secd.count(); ++ } ++ } ++ ++ double header_space_savings = 0.; ++ if (stats.bytes_head_decomp > 0) { ++ header_space_savings = ++ 1. - static_cast(stats.bytes_head) / stats.bytes_head_decomp; ++ } ++ ++ std::cout << std::fixed << std::setprecision(2) << R"( ++finished in )" ++ << util::format_duration(duration) << ", " << rps << " req/s, " ++ << util::utos_funit(bps) << R"(B/s ++requests: )" << stats.req_todo ++ << " total, " << stats.req_started << " started, " << stats.req_done ++ << " done, " << stats.req_status_success << " succeeded, " ++ << stats.req_failed << " failed, " << stats.req_error ++ << " errored, " << stats.req_timedout << R"( timeout ++status codes: )" ++ << stats.status[2] << " 2xx, " << stats.status[3] << " 3xx, " ++ << stats.status[4] << " 4xx, " << stats.status[5] << R"( 5xx ++traffic: )" << util::utos_funit(stats.bytes_total) ++ << "B (" << stats.bytes_total << ") total, " ++ << util::utos_funit(stats.bytes_head) << "B (" << stats.bytes_head ++ << ") headers (space savings " << header_space_savings * 100 ++ << "%), " << util::utos_funit(stats.bytes_body) << "B (" ++ << stats.bytes_body << R"() data)" << std::endl; ++#ifdef ENABLE_HTTP3 ++ if (config.is_quic()) { ++ std::cout << "UDP datagram: " << stats.udp_dgram_sent << " sent, " ++ << stats.udp_dgram_recv << " received" << std::endl; ++ } ++#endif // ENABLE_HTTP3 ++ std::cout ++ << R"( min max mean sd +/- sd ++time for request: )" ++ << std::setw(10) << util::format_duration(ts.request.min) << " " ++ << std::setw(10) << util::format_duration(ts.request.max) << " " ++ << std::setw(10) << util::format_duration(ts.request.mean) << " " ++ << std::setw(10) << util::format_duration(ts.request.sd) << std::setw(9) ++ << util::dtos(ts.request.within_sd) << "%" ++ << "\ntime for connect: " << std::setw(10) ++ << util::format_duration(ts.connect.min) << " " << std::setw(10) ++ << util::format_duration(ts.connect.max) << " " << std::setw(10) ++ << util::format_duration(ts.connect.mean) << " " << std::setw(10) ++ << util::format_duration(ts.connect.sd) << std::setw(9) ++ << util::dtos(ts.connect.within_sd) << "%" ++ << "\ntime to 1st byte: " << std::setw(10) ++ << util::format_duration(ts.ttfb.min) << " " << std::setw(10) ++ << util::format_duration(ts.ttfb.max) << " " << std::setw(10) ++ << util::format_duration(ts.ttfb.mean) << " " << std::setw(10) ++ << util::format_duration(ts.ttfb.sd) << std::setw(9) ++ << util::dtos(ts.ttfb.within_sd) << "%" ++ << "\nreq/s : " << std::setw(10) << ts.rps.min << " " ++ << std::setw(10) << ts.rps.max << " " << std::setw(10) << ts.rps.mean ++ << " " << std::setw(10) << ts.rps.sd << std::setw(9) ++ << util::dtos(ts.rps.within_sd) << "%" << std::endl; ++ ++ SSL_CTX_free(ssl_ctx); ++ ++ if (config.log_fd != -1) { ++ close(config.log_fd); ++ } ++ ++ return 0; ++} ++ ++} // namespace h2load ++ ++int main(int argc, char **argv) { ++ return h2load::main(argc, argv); ++} ++ +diff --git a/src/h2load_verto.h b/src/h2load_verto.h +new file mode 100644 +index 0000000..84b2789 +--- /dev/null ++++ b/src/h2load_verto.h +@@ -0,0 +1,511 @@ ++/* ++ * nghttp2 - HTTP/2 C Library ++ * ++ * Copyright (c) 2014 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. ++ */ ++#ifndef H2LOAD_H ++#define H2LOAD_H ++ ++#include "nghttp2_config.h" ++ ++#include ++#ifdef HAVE_SYS_SOCKET_H ++# include ++#endif // HAVE_SYS_SOCKET_H ++#ifdef HAVE_NETDB_H ++# include ++#endif // HAVE_NETDB_H ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#ifdef ENABLE_HTTP3 ++# include ++# include ++#endif // ENABLE_HTTP3 ++ ++#include ++ ++#include ++ ++#include "http2.h" ++#ifdef ENABLE_HTTP3 ++# include "quic.h" ++#endif // ENABLE_HTTP3 ++#include "memchunk.h" ++#include "template.h" ++ ++using namespace nghttp2; ++ ++namespace h2load { ++ ++constexpr auto BACKOFF_WRITE_BUFFER_THRES = 16_k; ++ ++class Session; ++struct Worker; ++ ++struct Config { ++ std::vector> nva; ++ std::vector h1reqs; ++ std::vector timings; ++ nghttp2::Headers custom_headers; ++ std::string scheme; ++ std::string host; ++ std::string connect_to_host; ++ std::string ifile; ++ std::string ciphers; ++ std::string tls13_ciphers; ++ // supported groups (or curves). ++ std::string groups; ++ // length of upload data ++ int64_t data_length; ++ // memory mapped upload data ++ uint8_t *data; ++ addrinfo *addrs; ++ size_t nreqs; ++ size_t nclients; ++ size_t nthreads; ++ // The maximum number of concurrent streams per session. ++ ssize_t max_concurrent_streams; ++ size_t window_bits; ++ size_t connection_window_bits; ++ size_t max_frame_size; ++ // rate at which connections should be made ++ size_t rate; ++ double rate_period; ++ // amount of time for main measurements in timing-based test ++ double duration; ++ // amount of time to wait before starting measurements in timing-based test ++ double warm_up_time; ++ // amount of time to wait for activity on a given connection ++ double conn_active_timeout; ++ // amount of time to wait after the last request is made on a connection ++ double conn_inactivity_timeout; ++ enum { PROTO_HTTP2, PROTO_HTTP1_1 } no_tls_proto; ++ uint32_t header_table_size; ++ uint32_t encoder_header_table_size; ++ // file descriptor for upload data ++ int data_fd; ++ // file descriptor to write per-request stats to. ++ int log_fd; ++ // base file name of qlog output files ++ std::string qlog_file_base; ++ uint16_t port; ++ uint16_t default_port; ++ uint16_t connect_to_port; ++ bool verbose; ++ bool timing_script; ++ std::string base_uri; ++ // true if UNIX domain socket is used. In this case, base_uri is ++ // not used in usual way. ++ bool base_uri_unix; ++ // used when UNIX domain socket is used (base_uri_unix is true). ++ sockaddr_un unix_addr; ++ // list of supported NPN/ALPN protocol strings in the order of ++ // preference. ++ std::vector npn_list; ++ // The number of request per second for each client. ++ double rps; ++ // Disables GSO for UDP connections. ++ bool no_udp_gso; ++ // The maximum UDP datagram payload size to send. ++ size_t max_udp_payload_size; ++ // Enable ktls. ++ bool ktls; ++ ++ Config(); ++ ~Config(); ++ ++ bool is_rate_mode() const; ++ bool is_timing_based_mode() const; ++ bool has_base_uri() const; ++ bool rps_enabled() const; ++ bool is_quic() const; ++}; ++ ++struct RequestStat { ++ // time point when request was sent ++ std::chrono::steady_clock::time_point request_time; ++ // same, but in wall clock reference frame ++ std::chrono::system_clock::time_point request_wall_time; ++ // time point when stream was closed ++ std::chrono::steady_clock::time_point stream_close_time; ++ // upload data length sent so far ++ int64_t data_offset; ++ // HTTP status code ++ int status; ++ // true if stream was successfully closed. This means stream was ++ // not reset, but it does not mean HTTP level error (e.g., 404). ++ bool completed; ++}; ++ ++struct ClientStat { ++ // time client started (i.e., first connect starts) ++ std::chrono::steady_clock::time_point client_start_time; ++ // time client end (i.e., client somehow processed all requests it ++ // is responsible for, and disconnected) ++ std::chrono::steady_clock::time_point client_end_time; ++ // The number of requests completed successful, but not necessarily ++ // means successful HTTP status code. ++ size_t req_success; ++ ++ // The following 3 numbers are overwritten each time when connection ++ // is made. ++ ++ // time connect starts ++ std::chrono::steady_clock::time_point connect_start_time; ++ // time to connect ++ std::chrono::steady_clock::time_point connect_time; ++ // time to first byte (TTFB) ++ std::chrono::steady_clock::time_point ttfb; ++}; ++ ++struct SDStat { ++ // min, max, mean and sd (standard deviation) ++ double min, max, mean, sd; ++ // percentage of samples inside mean -/+ sd ++ double within_sd; ++}; ++ ++struct SDStats { ++ // time for request ++ SDStat request; ++ // time for connect ++ SDStat connect; ++ // time to first byte (TTFB) ++ SDStat ttfb; ++ // request per second for each client ++ SDStat rps; ++}; ++ ++struct Stats { ++ Stats(size_t req_todo, size_t nclients); ++ // The total number of requests ++ size_t req_todo; ++ // The number of requests issued so far ++ size_t req_started; ++ // The number of requests finished ++ size_t req_done; ++ // The number of requests completed successful, but not necessarily ++ // means successful HTTP status code. ++ size_t req_success; ++ // The number of requests marked as success. HTTP status code is ++ // also considered as success. This is subset of req_done. ++ size_t req_status_success; ++ // The number of requests failed. This is subset of req_done. ++ size_t req_failed; ++ // The number of requests failed due to network errors. This is ++ // subset of req_failed. ++ size_t req_error; ++ // The number of requests that failed due to timeout. ++ size_t req_timedout; ++ // The number of bytes received on the "wire". If SSL/TLS is used, ++ // this is the number of decrypted bytes the application received. ++ int64_t bytes_total; ++ // The number of bytes received for header fields. This is ++ // compressed version. ++ int64_t bytes_head; ++ // The number of bytes received for header fields after they are ++ // decompressed. ++ int64_t bytes_head_decomp; ++ // The number of bytes received in DATA frame. ++ int64_t bytes_body; ++ // The number of each HTTP status category, status[i] is status code ++ // in the range [i*100, (i+1)*100). ++ std::array status; ++ // The statistics per request ++ std::vector req_stats; ++ // The statistics per client ++ std::vector client_stats; ++ // The number of UDP datagrams received. ++ size_t udp_dgram_recv; ++ // The number of UDP datagrams sent. ++ size_t udp_dgram_sent; ++}; ++ ++enum ClientState { CLIENT_IDLE, CLIENT_CONNECTED }; ++ ++// This type tells whether the client is in warmup phase or not or is over ++enum class Phase { ++ INITIAL_IDLE, // Initial idle state before warm-up phase ++ WARM_UP, // Warm up phase when no measurements are done ++ MAIN_DURATION, // Main measurement phase; if timing-based ++ // test is not run, this is the default phase ++ DURATION_OVER // This phase occurs after the measurements are over ++}; ++ ++struct Client; ++ ++// We use reservoir sampling method ++struct Sampling { ++ // maximum number of samples ++ size_t max_samples; ++ // number of samples seen, including discarded samples. ++ size_t n; ++}; ++ ++struct Worker { ++ MemchunkPool mcpool; ++ std::mt19937 randgen; ++ Stats stats; ++ Sampling request_times_smp; ++ Sampling client_smp; ++ verto_ctx *verto_loop; ++ SSL_CTX *ssl_ctx; ++ Config *config; ++ size_t progress_interval; ++ uint32_t id; ++ bool tls_info_report_done; ++ bool app_info_report_done; ++ size_t nconns_made; ++ // number of clients this worker handles ++ size_t nclients; ++ // number of requests each client issues ++ size_t nreqs_per_client; ++ // at most nreqs_rem clients get an extra request ++ size_t nreqs_rem; ++ size_t rate; ++ // maximum number of samples in this worker thread ++ size_t max_samples; ++ verto_ev *verto_timeout_watcher; ++ // The next client ID this worker assigns ++ uint32_t next_client_id; ++ // Keeps track of the current phase (for timing-based experiment) for the ++ // worker ++ Phase current_phase; ++ // We need to keep track of the clients in order to stop them when needed ++ std::vector clients; ++ // This is only active when there is not a bounded number of requests ++ // specified ++ verto_ev *verto_duration_watcher; ++ verto_ev *verto_warmup_watcher; ++ ++ Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t nreq_todo, size_t nclients, ++ size_t rate, size_t max_samples, Config *config); ++ ~Worker(); ++ Worker(Worker &&o) = default; ++ void run(); ++ void sample_req_stat(RequestStat *req_stat); ++ void sample_client_stat(ClientStat *cstat); ++ void report_progress(); ++ void report_rate_progress(); ++ // This function calls the destructors of all the clients. ++ void stop_all_clients(); ++ // This function frees a client from the list of clients for this Worker. ++ void free_client(Client *); ++}; ++ ++struct Stream { ++ RequestStat req_stat; ++ int status_success; ++ Stream(); ++}; ++ ++struct Client { ++ DefaultMemchunks wb; ++ std::unordered_map streams; ++ ClientStat cstat; ++ std::unique_ptr session; ++ verto_ev *verto_wev; ++ verto_ev *verto_rev; ++ std::function readfn, writefn; ++ Worker *worker; ++ SSL *ssl; ++#ifdef ENABLE_HTTP3 ++ struct { ++ ngtcp2_crypto_conn_ref conn_ref; ++ verto_ev *verto_pkt_timer; ++ ngtcp2_conn *conn; ++ ngtcp2_ccerr last_error; ++ bool close_requested; ++ FILE *qlog_file; ++ ++ struct { ++ bool send_blocked; ++ size_t num_blocked; ++ size_t num_blocked_sent; ++ struct { ++ Address remote_addr; ++ const uint8_t *data; ++ size_t datalen; ++ size_t gso_size; ++ } blocked[2]; ++ std::unique_ptr data; ++ } tx; ++ } quic; ++#endif // ENABLE_HTTP3 ++ verto_ev *verto_request_timeout_watcher; ++ addrinfo *next_addr; ++ // Address for the current address. When try_new_connection() is ++ // used and current_addr is not nullptr, it is used instead of ++ // trying next address though next_addr. To try new address, set ++ // nullptr to current_addr before calling connect(). ++ addrinfo *current_addr; ++ size_t reqidx; ++ ClientState state; ++ // The number of requests this client has to issue. ++ size_t req_todo; ++ // The number of requests left to issue ++ size_t req_left; ++ // The number of requests currently have started, but not abandoned ++ // or finished. ++ size_t req_inflight; ++ // The number of requests this client has issued so far. ++ size_t req_started; ++ // The number of requests this client has done so far. ++ size_t req_done; ++ // The client id per worker ++ uint32_t id; ++ int fd; ++ Address local_addr; ++ verto_ev *verto_conn_active_watcher; ++ verto_ev *verto_conn_inactivity_watcher; ++ std::string selected_proto; ++ bool new_connection_requested; ++ // true if the current connection will be closed, and no more new ++ // request cannot be processed. ++ bool final; ++ // rps_watcher is a timer to invoke callback periodically to ++ // generate a new request. ++ verto_ev *verto_rps_watcher; ++ // The timestamp that starts the period which contributes to the ++ // next request generation. ++ std::chrono::steady_clock::time_point rps_duration_started; ++ // The number of requests allowed by rps, but limited by stream ++ // concurrency. ++ size_t rps_req_pending; ++ // The number of in-flight streams. req_inflight has similar value ++ // but it only measures requests made during Phase::MAIN_DURATION. ++ // rps_req_inflight measures the number of requests in all phases, ++ // and it is only used if --rps is given. ++ size_t rps_req_inflight; ++ ++ enum { ERR_CONNECT_FAIL = -100 }; ++ ++ Client(uint32_t id, Worker *worker, size_t req_todo); ++ ~Client(); ++ int make_socket(addrinfo *addr); ++ int connect(); ++ void disconnect(); ++ void fail(); ++ // Call this function when do_read() returns -1. This function ++ // tries to connect to the remote host again if it is requested. If ++ // so, this function returns 0, and this object should be retained. ++ // Otherwise, this function returns -1, and this object should be ++ // deleted. ++ int try_again_or_fail(); ++ void timeout(); ++ void restart_timeout(); ++ int submit_request(); ++ void process_request_failure(); ++ void process_timedout_streams(); ++ void process_abandoned_streams(); ++ void report_tls_info(); ++ void report_app_info(); ++ void terminate_session(); ++ // Asks client to create new connection, instead of just fail. ++ void try_new_connection(); ++ ++ int do_read(); ++ int do_write(); ++ ++ // low-level I/O callback functions called by do_read/do_write ++ int connected(); ++ int read_clear(); ++ int write_clear(); ++ int tls_handshake(); ++ int read_tls(); ++ int write_tls(); ++ ++ int on_read(const uint8_t *data, size_t len); ++ int on_write(); ++ ++ int connection_made(); ++ ++ void on_request(int32_t stream_id); ++ void on_header(int32_t stream_id, const uint8_t *name, size_t namelen, ++ const uint8_t *value, size_t valuelen); ++ void on_status_code(int32_t stream_id, uint16_t status); ++ // |success| == true means that the request/response was exchanged ++ // |successfully, but it does not mean response carried successful ++ // |HTTP status code. ++ void on_stream_close(int32_t stream_id, bool success, bool final = false); ++ // Returns RequestStat for |stream_id|. This function must be ++ // called after on_request(stream_id), and before ++ // on_stream_close(stream_id, ...). Otherwise, this will return ++ // nullptr. ++ RequestStat *get_req_stat(int32_t stream_id); ++ ++ void record_request_time(RequestStat *req_stat); ++ void record_connect_start_time(); ++ void record_connect_time(); ++ void record_ttfb(); ++ void clear_connect_times(); ++ void record_client_start_time(); ++ void record_client_end_time(); ++ ++ void signal_write(); ++ ++#ifdef ENABLE_HTTP3 ++ // QUIC ++ int quic_init(const sockaddr *local_addr, socklen_t local_addrlen, ++ const sockaddr *remote_addr, socklen_t remote_addrlen); ++ void quic_free(); ++ int read_quic(); ++ int write_quic(); ++ int write_udp(const sockaddr *addr, socklen_t addrlen, const uint8_t *data, ++ size_t datalen, size_t gso_size); ++ void on_send_blocked(const ngtcp2_addr &remote_addr, const uint8_t *data, ++ size_t datalen, size_t gso_size); ++ int send_blocked_packet(); ++ void quic_close_connection(); ++ ++ int quic_handshake_completed(); ++ int quic_recv_stream_data(uint32_t flags, int64_t stream_id, ++ const uint8_t *data, size_t datalen); ++ int quic_acked_stream_data_offset(int64_t stream_id, size_t datalen); ++ int quic_stream_close(int64_t stream_id, uint64_t app_error_code); ++ int quic_stream_reset(int64_t stream_id, uint64_t app_error_code); ++ int quic_stream_stop_sending(int64_t stream_id, uint64_t app_error_code); ++ int quic_extend_max_local_streams(); ++ int quic_extend_max_stream_data(int64_t stream_id); ++ ++ int quic_write_client_handshake(ngtcp2_encryption_level level, ++ const uint8_t *data, size_t datalen); ++ int quic_pkt_timeout(); ++ void quic_restart_pkt_timer(); ++ void quic_write_qlog(const void *data, size_t datalen); ++ int quic_make_http3_session(); ++#endif // ENABLE_HTTP3 ++}; ++ ++} // namespace h2load ++ ++#endif // H2LOAD_H ++ +-- +2.33.0 + diff --git a/0004-updata-build-file.patch b/0004-updata-build-file.patch new file mode 100644 index 0000000..d0bc6d5 --- /dev/null +++ b/0004-updata-build-file.patch @@ -0,0 +1,213 @@ +From dd85ebd9c84df155190fb7ec4b0e2ee922c5354d Mon Sep 17 00:00:00 2001 +From: dengjie <1171276417@qq.com> +Date: Fri, 27 Oct 2023 00:09:20 +0800 +Subject: [PATCH 4/4] updata build file + +--- + configure.ac | 43 +++++++++++++++++++++++++++++++++++++++++++ + src/Makefile.am | 48 +++++++++++++++++++++++++++++++++++++++++++++++- + 2 files changed, 90 insertions(+), 1 deletion(-) + +diff --git a/configure.ac b/configure.ac +index 5163b03..6487455 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -102,6 +102,12 @@ AC_ARG_ENABLE([http3], + [(EXPERIMENTAL) Enable HTTP/3. This requires ngtcp2, nghttp3, and a custom OpenSSL.])], + [request_http3=$enableval], [request_http3=no]) + ++AC_ARG_ENABLE([libverto], ++ [AS_HELP_STRING([--enable-libverto], ++ [Enable libverto.])], ++ [request_libverto=$enableval], [request_libverto=no]) ++ ++ + AC_ARG_WITH([libxml2], + [AS_HELP_STRING([--with-libxml2], + [Use libxml2 [default=check]])], +@@ -177,10 +183,19 @@ AC_ARG_WITH([libbpf], + [Use libbpf [default=no]])], + [request_libbpf=$withval], [request_libbpf=no]) + ++AC_ARG_WITH([libverto], ++ [AS_HELP_STRING([--with-libverto], ++ [Use libverto [default=no]])], ++ [request_libverto=$withval], [request_libverto=no]) ++ ++ + dnl Define variables + AC_ARG_VAR([LIBEV_CFLAGS], [C compiler flags for libev, skipping any checks]) + AC_ARG_VAR([LIBEV_LIBS], [linker flags for libev, skipping any checks]) + ++AC_ARG_VAR([LIBVERTO_CFLAGS], [C compiler flags for libverto, skipping any checks]) ++AC_ARG_VAR([LIBVERTO_LIBS], [linker flags for libverto, skipping any checks]) ++ + AC_ARG_VAR([JEMALLOC_CFLAGS], + [C compiler flags for jemalloc, skipping any checks]) + AC_ARG_VAR([JEMALLOC_LIBS], [linker flags for jemalloc, skipping any checks]) +@@ -208,6 +223,7 @@ if [test "x$request_lib_only" = "xyes"]; then + request_hpack_tools=no + request_examples=no + request_http3=no ++ request_libverto=no + request_libxml2=no + request_jansson=no + request_zlib=no +@@ -444,6 +460,19 @@ if test "x${request_libev}" = "xyes" && + AC_MSG_ERROR([libev was requested (--with-libev) but not found]) + fi + ++# libverto (for src) ++have_libverto=no ++if test "x${request_libverto}" != "xno"; then ++ have_libverto=yes ++ LIBVERTO_LIBS=-lverto ++ LIBVERTO_CFLAGS= ++ if test "x${have_libverto}" = "xyes"; then ++ AC_DEFINE([HAVE_LIBVERTO], [1], [Define to 1 if you have `libverto` library.]) ++ fi ++ ++fi ++ ++ + # openssl (for src) + have_openssl=no + if test "x${request_openssl}" != "xno"; then +@@ -770,6 +799,17 @@ fi + AM_CONDITIONAL([ENABLE_HTTP3], [ test "x${enable_http3}" = "xyes" ]) + + enable_hpack_tools=no ++ ++#Check libverto support ++enable_libverto=no ++if test "x${request_libverto}" != "xno"; then ++ enable_libverto=yes ++ AC_DEFINE([ENABLE_LIBVERTO], [1], [Define to 1 if libverto is enable.]) ++fi ++ ++AM_CONDITIONAL([ENABLE_LIBVERTO], [ test "x${enable_libverto}" = "xyes" ]) ++ ++ + # HPACK tools requires jansson + if test "x${request_hpack_tools}" != "xno" && + test "x${have_jansson}" = "xyes"; then +@@ -1142,6 +1182,7 @@ AC_MSG_NOTICE([summary of build options: + OpenSSL: ${have_openssl} (CFLAGS='${OPENSSL_CFLAGS}' LIBS='${OPENSSL_LIBS}') + Libxml2: ${have_libxml2} (CFLAGS='${LIBXML2_CFLAGS}' LIBS='${LIBXML2_LIBS}') + Libev: ${have_libev} (CFLAGS='${LIBEV_CFLAGS}' LIBS='${LIBEV_LIBS}') ++ Libverto: ${have_libverto} (CFLAGS='${LIBVERTO_CFLAGS}' LIBS='${LIBVERTO_LIBS}') + Libc-ares: ${have_libcares} (CFLAGS='${LIBCARES_CFLAGS}' LIBS='${LIBCARES_LIBS}') + libngtcp2: ${have_libngtcp2} (CFLAGS='${LIBNGTCP2_CFLAGS}' LIBS='${LIBNGTCP2_LIBS}') + libngtcp2_crypto_quictls: ${have_libngtcp2_crypto_quictls} (CFLAGS='${LIBNGTCP2_CRYPTO_QUICTLS_CFLAGS}' LIBS='${LIBNGTCP2_CRYPTO_QUICTLS_LIBS}') +@@ -1163,4 +1204,6 @@ AC_MSG_NOTICE([summary of build options: + Examples: ${enable_examples} + Threading: ${enable_threads} + HTTP/3 (EXPERIMENTAL): ${enable_http3} ++ Enable_libverto: ${enable_libverto} ++ + ]) +diff --git a/src/Makefile.am b/src/Makefile.am +index f112ac2..a49c09f 100644 +--- a/src/Makefile.am ++++ b/src/Makefile.am +@@ -44,6 +44,7 @@ AM_CPPFLAGS = \ + @JEMALLOC_CFLAGS@ \ + @LIBXML2_CFLAGS@ \ + @LIBEV_CFLAGS@ \ ++ @LIBVERTO_CFLAGS@ \ + @LIBNGHTTP3_CFLAGS@ \ + @LIBNGTCP2_CRYPTO_QUICTLS_CFLAGS@ \ + @LIBNGTCP2_CRYPTO_BORINGSSL_CFLAGS@ \ +@@ -63,6 +64,7 @@ LDADD = $(top_builddir)/lib/libnghttp2.la \ + @JEMALLOC_LIBS@ \ + @LIBXML2_LIBS@ \ + @LIBEV_LIBS@ \ ++ @LIBVERTO_LIBS@ \ + @LIBNGHTTP3_LIBS@ \ + @LIBNGTCP2_CRYPTO_QUICTLS_LIBS@ \ + @LIBNGTCP2_CRYPTO_BORINGSSL_LIBS@ \ +@@ -77,6 +79,11 @@ LDADD = $(top_builddir)/lib/libnghttp2.la \ + + if ENABLE_APP + ++ ++if ENABLE_LIBVERTO ++bin_PROGRAMS += nghttp_verto nghttpd_verto ++endif # ENABLE_LIBVERTO ++ + bin_PROGRAMS += nghttp nghttpd nghttpx + + HELPER_OBJECTS = util.cc \ +@@ -92,25 +99,62 @@ if HAVE_LIBXML2 + HTML_PARSER_OBJECTS += HtmlParser.cc + endif # HAVE_LIBXML2 + ++ ++if ENABLE_LIBVERTO ++nghttp_verto_SOURCES = ${HELPER_OBJECTS} ${HELPER_HFILES} nghttp_verto.cc nghttp_verto.h \ ++ ${HTML_PARSER_OBJECTS} ${HTML_PARSER_HFILES} \ ++ tls.cc tls.h ssl_compat.h ++endif # ENABLE_LIBVERTO ++ + nghttp_SOURCES = ${HELPER_OBJECTS} ${HELPER_HFILES} nghttp.cc nghttp.h \ + ${HTML_PARSER_OBJECTS} ${HTML_PARSER_HFILES} \ + tls.cc tls.h ssl_compat.h + ++ ++if ENABLE_LIBVERTO ++nghttpd_verto_SOURCES = ${HELPER_OBJECTS} ${HELPER_HFILES} nghttpd_verto.cc \ ++ tls.cc tls.h ssl_compat.h \ ++ HttpServer_verto.cc HttpServer_verto.h ++endif # ENABLE_LIBVERTO ++ + nghttpd_SOURCES = ${HELPER_OBJECTS} ${HELPER_HFILES} nghttpd.cc \ + tls.cc tls.h ssl_compat.h \ + HttpServer.cc HttpServer.h + ++ ++if ENABLE_LIBVERTO ++bin_PROGRAMS += h2load_verto ++endif # ENABLE_LIBVERTO ++ + bin_PROGRAMS += h2load + ++if ENABLE_LIBVERTO ++h2load_verto_SOURCES = util.cc util.h \ ++ http2.cc http2.h h2load_verto.cc h2load_verto.h \ ++ timegm.c timegm.h \ ++ tls.cc tls.h ssl_compat.h \ ++ h2load_session_verto.h \ ++ h2load_http2_session_verto.cc h2load_http2_session_verto.h \ ++ h2load_http1_session_verto.cc h2load_http1_session_verto.h ++endif # ENABLE_LIBVERTO ++ + h2load_SOURCES = util.cc util.h \ + http2.cc http2.h h2load.cc h2load.h \ + timegm.c timegm.h \ + tls.cc tls.h ssl_compat.h \ + h2load_session.h \ + h2load_http2_session.cc h2load_http2_session.h \ +- h2load_http1_session.cc h2load_http1_session.h ++ h2load_http1_session.cc h2load_http1_session.h ++ + + if ENABLE_HTTP3 ++if ENABLE_LIBVERTO ++h2load_verto_SOURCES += \ ++ h2load_http3_session_verto.cc h2load_http3_session_verto.h \ ++ h2load_quic_verto.cc h2load_quic_verto.h \ ++ quic.cc quic.h ++endif # ENABLE_LIBVERTO ++ + h2load_SOURCES += \ + h2load_http3_session.cc h2load_http3_session.h \ + h2load_quic.cc h2load_quic.h \ +@@ -255,3 +299,5 @@ inflatehd_SOURCES = inflatehd.cc $(HPACK_TOOLS_COMMON_SRCS) + deflatehd_SOURCES = deflatehd.cc $(HPACK_TOOLS_COMMON_SRCS) + + endif # ENABLE_HPACK_TOOLS ++ ++ +-- +2.33.0 + diff --git a/nghttp2-1.55.1 b/nghttp2-1.55.1 new file mode 160000 index 0000000..8c49278 --- /dev/null +++ b/nghttp2-1.55.1 @@ -0,0 +1 @@ +Subproject commit 8c49278f3709e18121923982520d9f7f3b2d05bd diff --git a/nghttp2.spec b/nghttp2.spec index e077374..1aa1524 100644 --- a/nghttp2.spec +++ b/nghttp2.spec @@ -1,6 +1,8 @@ +%bcond_with verto + Name: nghttp2 Version: 1.55.1 -Release: 2 +Release: 3 Summary: Contains the HTTP/2 client, server and proxy programs. License: MIT URL: https://nghttp2.org/ @@ -8,10 +10,17 @@ Source0: https://github.com/nghttp2/nghttp2/releases/download/v%{version} Patch0: backport-CVE-2023-44487.patch Patch1: backport-Fix-build-error-when-both-clock_gettime-and-GetTickCount64.patch +Patch2: 0001-nghttp-client-implementation-based-on-libverto.patch +Patch3: 0002-nghttpd-server-implementation-based-on-libverto.patch +Patch4: 0003-h2load-Benchmarking-implementation-based-on-libverto.patch +Patch5: 0004-updata-build-file.patch + BuildRequires: CUnit-devel c-ares-devel gcc-c++ libev-devel openssl-devel automake BuildRequires: python3-devel systemd-devel zlib-devel make libxml2-devel - +%if %{with verto} +BuildRequires: libverto-devel +%endif Requires: libnghttp2 = %{version}-%{release} %{?systemd_requires} @@ -42,8 +51,16 @@ sed -e '1 s|^#!/.*python|&3|' -i script/fetch-ocsp-response %build autoreconf -%configure PYTHON=%{__python3} --disable-hpack-tools --disable-python-bindings\ - --with-libxml2 --disable-static +automake +%configure PYTHON=%{__python3} \ + --disable-hpack-tools \ + --disable-python-bindings \ + --with-libxml2 \ +%if %{with verto} + --with-libverto \ +%endif + --disable-static + %disable_rpath %make_build V=1 @@ -70,6 +87,9 @@ export "LD_LIBRARY_PATH=$RPM_BUILD_ROOT%{_libdir}:$LD_LIBRARY_PATH" %license COPYING %{_bindir}/h2load %{_bindir}/nghttp* +%if %{with verto} +%{_bindir}/*_verto +%endif %{_datadir}/nghttp2 %{_datadir}/doc/nghttp2/README.rst %{_unitdir}/nghttpx.service @@ -92,6 +112,12 @@ export "LD_LIBRARY_PATH=$RPM_BUILD_ROOT%{_libdir}:$LD_LIBRARY_PATH" %{_mandir}/man1/* %changelog +* Fri Oct 27 2023 dengjie <1171276417@qq.com> - 1.55.1-3 +- Type:requirements +- ID:NA +- SUG:NA +- DESC:add libverto support + * Thu Oct 19 2023 xingwei - 1.55.1-2 - Type:CVE - ID:CVE-2023-44487 -- Gitee