2708 lines
77 KiB
C++
2708 lines
77 KiB
C++
/*
|
|
* 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.h"
|
|
|
|
#include <sys/stat.h>
|
|
#ifdef HAVE_UNISTD_H
|
|
#include <unistd.h>
|
|
#endif // HAVE_UNISTD_H
|
|
#ifdef HAVE_FCNTL_H
|
|
#include <fcntl.h>
|
|
#endif // HAVE_FCNTL_H
|
|
#ifdef HAVE_NETINET_IN_H
|
|
#include <netinet/in.h>
|
|
#endif // HAVE_NETINET_IN_H
|
|
#include <netinet/tcp.h>
|
|
#include <getopt.h>
|
|
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <tuple>
|
|
|
|
#include <openssl/err.h>
|
|
#include <openssl/conf.h>
|
|
|
|
#ifdef HAVE_JANSSON
|
|
#include <jansson.h>
|
|
#endif // HAVE_JANSSON
|
|
|
|
#include "app_helper.h"
|
|
#include "HtmlParser.h"
|
|
#include "util.h"
|
|
#include "base64.h"
|
|
#include "ssl.h"
|
|
#include "template.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<Anchor, 5>{{
|
|
{3, 0, 201}, {5, 0, 101}, {7, 0, 1}, {9, 7, 1}, {11, 3, 1},
|
|
}};
|
|
} // namespace
|
|
|
|
Config::Config()
|
|
: padding(0), max_concurrent_streams(100),
|
|
peer_max_concurrent_streams(NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS),
|
|
header_table_size(-1), weight(NGHTTP2_DEFAULT_WEIGHT), multiply(1),
|
|
timeout(0.), window_bits(-1), connection_window_bits(-1), verbose(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) {
|
|
nghttp2_option_new(&http2_option);
|
|
nghttp2_option_set_peer_max_concurrent_streams(http2_option,
|
|
peer_max_concurrent_streams);
|
|
}
|
|
|
|
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), html_parser(nullptr),
|
|
data_prd(data_prd), 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);
|
|
delete html_parser;
|
|
}
|
|
|
|
void Request::init_inflater() {
|
|
int rv;
|
|
rv = nghttp2_gzip_inflate_new(&inflater);
|
|
assert(rv == 0);
|
|
}
|
|
|
|
void Request::init_html_parser() { html_parser = new HtmlParser(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<const char *>(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)
|
|
: "/";
|
|
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(std::string 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 std::move(host);
|
|
}
|
|
// case: ::1%
|
|
if (zone_start + 1 == std::end(host)) {
|
|
host.pop_back();
|
|
return std::move(host);
|
|
}
|
|
// case: ::1%12 or ::1%1
|
|
if (zone_start + 3 >= std::end(host)) {
|
|
return std::move(host);
|
|
}
|
|
// 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::percentDecode(zone_id_src, std::end(host));
|
|
host.erase(zone_start + 1, std::end(host));
|
|
host += zone_id;
|
|
return std::move(host);
|
|
}
|
|
} // 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 = 2;
|
|
break;
|
|
case REQ_UNBLOCK_JS:
|
|
anchor_id = anchors[ANCHOR_UNBLOCKED].stream_id;
|
|
weight = 2;
|
|
break;
|
|
case REQ_IMG:
|
|
anchor_id = anchors[ANCHOR_FOLLOWERS].stream_id;
|
|
weight = 12;
|
|
break;
|
|
default:
|
|
anchor_id = anchors[ANCHOR_FOLLOWERS].stream_id;
|
|
weight = 2;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
bool Request::response_pseudo_header_allowed(int16_t token) const {
|
|
if (!res_nva.empty() && res_nva.back().name.c_str()[0] != ':') {
|
|
return false;
|
|
}
|
|
switch (token) {
|
|
case http2::HD__STATUS:
|
|
return res_hdidx[token] == -1;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Request::push_request_pseudo_header_allowed(int16_t token) const {
|
|
if (!req_nva.empty() && req_nva.back().name.c_str()[0] != ':') {
|
|
return false;
|
|
}
|
|
switch (token) {
|
|
case http2::HD__AUTHORITY:
|
|
case http2::HD__METHOD:
|
|
case http2::HD__PATH:
|
|
case http2::HD__SCHEME:
|
|
return req_hdidx[token] == -1;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Headers::value_type *Request::get_res_header(int16_t token) {
|
|
auto idx = res_hdidx[token];
|
|
if (idx == -1) {
|
|
return nullptr;
|
|
}
|
|
return &res_nva[idx];
|
|
}
|
|
|
|
Headers::value_type *Request::get_req_header(int16_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 {
|
|
int htp_msg_begincb(http_parser *htp) {
|
|
if (config.verbose) {
|
|
print_timer();
|
|
std::cout << " HTTP Upgrade response" << std::endl;
|
|
}
|
|
return 0;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
int htp_statuscb(http_parser *htp, const char *at, size_t length) {
|
|
auto client = static_cast<HttpClient *>(htp->data);
|
|
client->upgrade_response_status_code = htp->status_code;
|
|
return 0;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
int htp_msg_completecb(http_parser *htp) {
|
|
auto client = static_cast<HttpClient *>(htp->data);
|
|
client->upgrade_response_complete = true;
|
|
return 0;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
http_parser_settings htp_hooks = {
|
|
htp_msg_begincb, // http_cb on_message_begin;
|
|
nullptr, // http_data_cb on_url;
|
|
htp_statuscb, // http_data_cb on_status;
|
|
nullptr, // http_data_cb on_header_field;
|
|
nullptr, // http_data_cb on_header_value;
|
|
nullptr, // http_cb on_headers_complete;
|
|
nullptr, // http_data_cb on_body;
|
|
htp_msg_completecb // http_cb on_message_complete;
|
|
};
|
|
} // namespace
|
|
|
|
namespace {
|
|
int submit_request(HttpClient *client, const Headers &headers, Request *req) {
|
|
auto path = req->make_reqpath();
|
|
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", path},
|
|
{":scheme", scheme},
|
|
{":authority", client->hostport},
|
|
{"accept", "*/*"},
|
|
{"accept-encoding", "gzip, deflate"},
|
|
{"user-agent", "nghttp2/" NGHTTP2_VERSION}};
|
|
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 (!config.no_content_length && req->data_prd) {
|
|
build_headers.emplace_back("content-length", util::utos(req->data_length));
|
|
}
|
|
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<nghttp2_nv>();
|
|
nva.reserve(build_headers.size());
|
|
|
|
for (auto &kv : build_headers) {
|
|
nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index));
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
auto 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_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);
|
|
|
|
return 0;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
void readcb(struct ev_loop *loop, ev_io *w, int revents) {
|
|
auto client = static_cast<HttpClient *>(w->data);
|
|
if (client->do_read() != 0) {
|
|
client->disconnect();
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
void writecb(struct ev_loop *loop, ev_io *w, int revents) {
|
|
auto client = static_cast<HttpClient *>(w->data);
|
|
auto rv = client->do_write();
|
|
if (rv == HttpClient::ERR_CONNECT_FAIL) {
|
|
client->connect_fail();
|
|
return;
|
|
}
|
|
if (rv != 0) {
|
|
client->disconnect();
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) {
|
|
auto client = static_cast<HttpClient *>(w->data);
|
|
std::cerr << "[ERROR] Timeout" << std::endl;
|
|
client->disconnect();
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) {
|
|
auto client = static_cast<HttpClient *>(w->data);
|
|
ev_timer_stop(loop, w);
|
|
|
|
nghttp2_session_terminate_session(client->session, NGHTTP2_SETTINGS_TIMEOUT);
|
|
|
|
client->signal_write();
|
|
}
|
|
} // namespace
|
|
|
|
HttpClient::HttpClient(const nghttp2_session_callbacks *callbacks,
|
|
struct ev_loop *loop, SSL_CTX *ssl_ctx)
|
|
: session(nullptr), callbacks(callbacks), loop(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) {
|
|
ev_io_init(&wev, writecb, 0, EV_WRITE);
|
|
ev_io_init(&rev, readcb, 0, EV_READ);
|
|
|
|
wev.data = this;
|
|
rev.data = this;
|
|
|
|
ev_timer_init(&wt, timeoutcb, 0., config.timeout);
|
|
ev_timer_init(&rt, timeoutcb, 0., config.timeout);
|
|
|
|
wt.data = this;
|
|
rt.data = this;
|
|
|
|
ev_timer_init(&settings_timer, settings_timeout_cb, 0., 10.);
|
|
|
|
settings_timer.data = this;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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_fd(ssl, fd);
|
|
SSL_set_connect_state(ssl);
|
|
|
|
// If the user overrode the :authority or host header, use that
|
|
// value for the SNI extension
|
|
const char *host_string = nullptr;
|
|
auto i = std::find_if(std::begin(config.headers),
|
|
std::end(config.headers), [](const Header &nv) {
|
|
return ":authority" == nv.name || "host" == nv.name;
|
|
});
|
|
if (i != std::end(config.headers)) {
|
|
host_string = (*i).value.c_str();
|
|
} else {
|
|
host_string = host.c_str();
|
|
}
|
|
|
|
if (!util::numeric_host(host_string)) {
|
|
SSL_set_tlsext_host_name(ssl, host_string);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ev_io_set(&rev, fd, EV_READ);
|
|
ev_io_set(&wev, fd, EV_WRITE);
|
|
|
|
ev_io_start(loop, &wev);
|
|
|
|
ev_timer_again(loop, &wt);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void HttpClient::disconnect() {
|
|
state = ClientState::IDLE;
|
|
|
|
ev_timer_stop(loop, &settings_timer);
|
|
|
|
ev_timer_stop(loop, &rt);
|
|
ev_timer_stop(loop, &wt);
|
|
|
|
ev_io_stop(loop, &rev);
|
|
ev_io_stop(loop, &wev);
|
|
|
|
nghttp2_session_del(session);
|
|
session = nullptr;
|
|
|
|
if (ssl) {
|
|
SSL_set_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() {
|
|
ev_timer_again(loop, &rt);
|
|
|
|
std::array<uint8_t, 8_k> 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() {
|
|
ev_timer_again(loop, &rt);
|
|
|
|
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) {
|
|
ev_io_start(loop, &wev);
|
|
ev_timer_again(loop, &wt);
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
wb.drain(nwrite);
|
|
continue;
|
|
}
|
|
wb.reset();
|
|
if (on_writefn(*this) != 0) {
|
|
return -1;
|
|
}
|
|
if (wb.rleft() == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
ev_io_stop(loop, &wev);
|
|
ev_timer_stop(loop, &wt);
|
|
|
|
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;
|
|
|
|
ev_io_start(loop, &rev);
|
|
ev_io_stop(loop, &wev);
|
|
|
|
ev_timer_again(loop, &rt);
|
|
ev_timer_stop(loop, &wt);
|
|
|
|
if (ssl) {
|
|
readfn = &HttpClient::tls_handshake;
|
|
writefn = &HttpClient::tls_handshake;
|
|
|
|
return do_write();
|
|
}
|
|
|
|
readfn = &HttpClient::read_clear;
|
|
writefn = &HttpClient::write_clear;
|
|
|
|
if (need_upgrade()) {
|
|
htp = make_unique<http_parser>();
|
|
http_parser_init(htp.get(), HTTP_RESPONSE);
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
return niv;
|
|
}
|
|
} // namespace
|
|
|
|
int HttpClient::on_upgrade_connect() {
|
|
ssize_t rv;
|
|
record_connect_end_time();
|
|
assert(!reqvec.empty());
|
|
std::array<nghttp2_settings_entry, 16> 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 {
|
|
req = "GET ";
|
|
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;
|
|
for (i = 0; i < initial_headerslen; ++i) {
|
|
if (util::strieq(kv.name, headers[i].name)) {
|
|
headers[i].value = kv.value;
|
|
break;
|
|
}
|
|
}
|
|
if (i < initial_headerslen) {
|
|
continue;
|
|
}
|
|
if (kv.name.size() != 0 && kv.name[0] != ':') {
|
|
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.write(req.c_str(), req.size());
|
|
|
|
if (config.verbose) {
|
|
print_timer();
|
|
std::cout << " HTTP Upgrade request\n" << req << std::endl;
|
|
}
|
|
|
|
// record request time if this is GET request
|
|
if (!reqvec[0]->data_prd) {
|
|
reqvec[0]->record_request_start_time();
|
|
}
|
|
|
|
on_writefn = &HttpClient::noop;
|
|
|
|
signal_write();
|
|
|
|
return 0;
|
|
}
|
|
|
|
int HttpClient::on_upgrade_read(const uint8_t *data, size_t len) {
|
|
int rv;
|
|
|
|
auto nread = http_parser_execute(htp.get(), &htp_hooks,
|
|
reinterpret_cast<const char *>(data), len);
|
|
|
|
if (config.verbose) {
|
|
std::cout.write(reinterpret_cast<const char *>(data), nread);
|
|
}
|
|
|
|
auto htperr = HTTP_PARSER_ERRNO(htp.get());
|
|
|
|
if (htperr != HPE_OK) {
|
|
std::cerr << "[ERROR] Failed to parse HTTP Upgrade response header: "
|
|
<< "(" << http_errno_name(htperr) << ") "
|
|
<< http_errno_description(htperr) << 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;
|
|
SSL_get0_next_proto_negotiated(ssl, &next_proto, &next_proto_len);
|
|
for (int i = 0; i < 2; ++i) {
|
|
if (next_proto) {
|
|
if (config.verbose) {
|
|
std::cout << "The negotiated protocol: ";
|
|
std::cout.write(reinterpret_cast<const char *>(next_proto),
|
|
next_proto_len);
|
|
std::cout << std::endl;
|
|
}
|
|
if (!util::check_h2_is_selected(next_proto, next_proto_len)) {
|
|
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();
|
|
}
|
|
rv = nghttp2_session_upgrade(session, settings_payload.data(),
|
|
settings_payloadlen, 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<nghttp2_settings_entry, 16> 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()) {
|
|
// 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, config.weight, 0);
|
|
|
|
rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec);
|
|
if (rv != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
} else if (need_upgrade() && config.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, config.weight, 0);
|
|
|
|
rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec);
|
|
if (rv != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
ev_timer_again(loop, &settings_timer);
|
|
|
|
if (config.connection_window_bits != -1) {
|
|
int32_t wininc = (1 << config.connection_window_bits) - 1 -
|
|
NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE;
|
|
rv = nghttp2_submit_window_update(session, NGHTTP2_FLAG_NONE, 0, wininc);
|
|
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;
|
|
}
|
|
}
|
|
|
|
signal_write();
|
|
|
|
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<size_t>(rv) == len);
|
|
|
|
if (nghttp2_session_want_read(session) == 0 &&
|
|
nghttp2_session_want_write(session) == 0 && wb.rleft() == 0) {
|
|
return -1;
|
|
}
|
|
|
|
signal_write();
|
|
|
|
return 0;
|
|
}
|
|
|
|
int HttpClient::on_write() {
|
|
auto rv = nghttp2_session_send(session);
|
|
if (rv != 0) {
|
|
std::cerr << "[ERROR] nghttp2_session_send() returned error: "
|
|
<< nghttp2_strerror(rv) << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
if (nghttp2_session_want_read(session) == 0 &&
|
|
nghttp2_session_want_write(session) == 0 && wb.rleft() == 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int HttpClient::tls_handshake() {
|
|
ev_timer_again(loop, &rt);
|
|
|
|
ERR_clear_error();
|
|
|
|
auto rv = SSL_do_handshake(ssl);
|
|
|
|
if (rv == 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (rv < 0) {
|
|
auto err = SSL_get_error(ssl, rv);
|
|
switch (err) {
|
|
case SSL_ERROR_WANT_READ:
|
|
ev_io_stop(loop, &wev);
|
|
ev_timer_stop(loop, &wt);
|
|
return 0;
|
|
case SSL_ERROR_WANT_WRITE:
|
|
ev_io_start(loop, &wev);
|
|
ev_timer_again(loop, &wt);
|
|
return 0;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
ev_io_stop(loop, &wev);
|
|
ev_timer_stop(loop, &wt);
|
|
|
|
readfn = &HttpClient::read_tls;
|
|
writefn = &HttpClient::write_tls;
|
|
|
|
if (connection_made() != 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int HttpClient::read_tls() {
|
|
ev_timer_again(loop, &rt);
|
|
|
|
ERR_clear_error();
|
|
|
|
std::array<uint8_t, 8_k> buf;
|
|
for (;;) {
|
|
auto rv = SSL_read(ssl, buf.data(), buf.size());
|
|
|
|
if (rv == 0) {
|
|
return -1;
|
|
}
|
|
|
|
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() {
|
|
ev_timer_again(loop, &rt);
|
|
|
|
ERR_clear_error();
|
|
|
|
for (;;) {
|
|
if (wb.rleft() > 0) {
|
|
auto rv = SSL_write(ssl, wb.pos, wb.rleft());
|
|
|
|
if (rv == 0) {
|
|
return -1;
|
|
}
|
|
|
|
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:
|
|
ev_io_start(loop, &wev);
|
|
ev_timer_again(loop, &wt);
|
|
return 0;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
wb.drain(rv);
|
|
|
|
continue;
|
|
}
|
|
wb.reset();
|
|
if (on_writefn(*this) != 0) {
|
|
return -1;
|
|
}
|
|
if (wb.rleft() == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
ev_io_stop(loop, &wev);
|
|
ev_timer_stop(loop, &wt);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void HttpClient::signal_write() { ev_io_start(loop, &wev); }
|
|
|
|
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);
|
|
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(
|
|
make_unique<Request>(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<std::chrono::microseconds>(
|
|
timing.domain_lookup_end_time - timing.start_time).count() /
|
|
1000.0;
|
|
auto connect_delta =
|
|
std::chrono::duration_cast<std::chrono::microseconds>(
|
|
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<std::chrono::microseconds>(
|
|
req_timing.response_start_time -
|
|
req_timing.request_start_time).count() /
|
|
1000.0;
|
|
auto receive_delta = std::chrono::duration_cast<std::chrono::microseconds>(
|
|
req_timing.response_end_time -
|
|
req_timing.response_start_time).count() /
|
|
1000.0;
|
|
|
|
auto time_sum =
|
|
std::chrono::duration_cast<std::chrono::microseconds>(
|
|
(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 method_ptr = http2::get_header(req->req_nva, ":method");
|
|
|
|
const char *method = "GET";
|
|
if (method_ptr) {
|
|
method = (*method_ptr).value.c_str();
|
|
}
|
|
|
|
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(method));
|
|
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);
|
|
|
|
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 &&
|
|
util::fieldeq(uri.c_str(), u, req->uri.c_str(), req->u, UF_SCHEMA) &&
|
|
util::fieldeq(uri.c_str(), u, req->uri.c_str(), req->u, UF_HOST) &&
|
|
util::porteq(uri.c_str(), u, req->uri.c_str(), req->u)) {
|
|
// 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<HttpClient *>(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<Request *>(
|
|
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<uint8_t, MAX_OUTLEN> 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<const char *>(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<const char *>(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(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) {
|
|
req->expect_final_response = true;
|
|
req->status = 0;
|
|
req->res_nva.clear();
|
|
http2::init_hdidx(req->res_hdidx);
|
|
return;
|
|
}
|
|
|
|
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<Request *>(
|
|
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 = make_unique<Request>("", 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<Request *>(
|
|
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;
|
|
}
|
|
|
|
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<Request *>(nghttp2_session_get_stream_user_data(
|
|
session, frame->push_promise.promised_stream_id));
|
|
|
|
if (!req) {
|
|
break;
|
|
}
|
|
|
|
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<Request *>(
|
|
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<Request *>(
|
|
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);
|
|
break;
|
|
case NGHTTP2_PUSH_PROMISE: {
|
|
auto req = static_cast<Request *>(nghttp2_session_get_stream_user_data(
|
|
session, frame->push_promise.promised_stream_id));
|
|
if (!req) {
|
|
break;
|
|
}
|
|
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<Request *>(
|
|
nghttp2_session_get_stream_user_data(session, frame->hd.stream_id));
|
|
assert(req);
|
|
req->record_request_start_time();
|
|
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<Request *>(
|
|
nghttp2_session_get_stream_user_data(session, stream_id));
|
|
|
|
if (!req) {
|
|
return 0;
|
|
}
|
|
|
|
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<Request *> 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<std::chrono::microseconds>(
|
|
req->timing.response_end_time - base);
|
|
auto request_start = std::chrono::duration_cast<std::chrono::microseconds>(
|
|
req->timing.request_start_time - base);
|
|
auto total = std::chrono::duration_cast<std::chrono::microseconds>(
|
|
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_with_unit(req->response_len) << " "
|
|
<< req->make_reqpath() << std::endl;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
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<const char *>(&in[i + 1]), in[i]);
|
|
std::cout << std::endl;
|
|
}
|
|
}
|
|
if (!util::select_h2(const_cast<const unsigned char **>(out), outlen, in,
|
|
inlen)) {
|
|
print_protocol_nego_error();
|
|
return SSL_TLSEXT_ERR_NOACK;
|
|
}
|
|
return SSL_TLSEXT_ERR_OK;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
// Recommended general purpose "Intermediate compatibility" cipher by
|
|
// mozilla.
|
|
//
|
|
// https://wiki.mozilla.org/Security/Server_Side_TLS
|
|
const char *const CIPHER_LIST =
|
|
"ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-"
|
|
"AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:"
|
|
"DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-"
|
|
"AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-"
|
|
"AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-"
|
|
"AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:"
|
|
"DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-"
|
|
"SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-"
|
|
"SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!"
|
|
"aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA";
|
|
} // namespace
|
|
|
|
namespace {
|
|
int communicate(
|
|
const std::string &scheme, const std::string &host, uint16_t port,
|
|
std::vector<std::tuple<std::string, nghttp2_data_provider *, int64_t>>
|
|
requests, const nghttp2_session_callbacks *callbacks) {
|
|
int result = 0;
|
|
auto loop = EV_DEFAULT;
|
|
SSL_CTX *ssl_ctx = nullptr;
|
|
if (scheme == "https") {
|
|
ssl_ctx = SSL_CTX_new(SSLv23_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;
|
|
|
|
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_cipher_list(ssl_ctx, 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;
|
|
}
|
|
}
|
|
SSL_CTX_set_next_proto_select_cb(ssl_ctx, client_select_next_proto_cb,
|
|
nullptr);
|
|
|
|
#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};
|
|
|
|
nghttp2_priority_spec pri_spec;
|
|
int32_t dep_stream_id = 0;
|
|
|
|
if (!config.no_dep) {
|
|
dep_stream_id = anchors[ANCHOR_FOLLOWERS].stream_id;
|
|
}
|
|
|
|
nghttp2_priority_spec_init(&pri_spec, dep_stream_id, config.weight, 0);
|
|
|
|
for (auto req : requests) {
|
|
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;
|
|
}
|
|
ev_run(loop, 0);
|
|
|
|
#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<Request *>(
|
|
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;
|
|
}
|
|
|
|
if (nread == 0) {
|
|
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
|
if (!config.trailer.empty()) {
|
|
std::vector<nghttp2_nv> 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;
|
|
}
|
|
}
|
|
} else {
|
|
req->data_offset += nread;
|
|
}
|
|
|
|
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<HttpClient *>(user_data);
|
|
auto &wb = client->wb;
|
|
|
|
if (wb.wleft() == 0) {
|
|
return NGHTTP2_ERR_WOULDBLOCK;
|
|
}
|
|
|
|
return wb.write(data, length);
|
|
}
|
|
} // 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_frame_send_callback(
|
|
callbacks, verbose_on_frame_send_callback);
|
|
|
|
nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(
|
|
callbacks, verbose_on_invalid_frame_recv_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_send_callback(callbacks, 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<char, 1_k> 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<std::string, nghttp2_data_provider *, int64_t>>
|
|
requests;
|
|
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) {
|
|
std::cerr << "[ERROR] Could not parse URI " << uri << std::endl;
|
|
continue;
|
|
}
|
|
if (!util::has_uri_field(u, UF_SCHEMA)) {
|
|
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);
|
|
prev_host = std::move(host);
|
|
prev_port = port;
|
|
}
|
|
requests.emplace_back(uri, data_fd == -1 ? nullptr : &data_prd,
|
|
data_stat.st_size);
|
|
}
|
|
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]... <URI>...
|
|
HTTP/2 experimental client)" << std::endl;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
void print_help(std::ostream &out) {
|
|
print_usage(out);
|
|
out << R"(
|
|
<URI> 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 dereived from URI. If URI ends with '/',
|
|
'index.html' is used as a filename. Not implemented
|
|
yet.
|
|
-t, --timeout=<DURATION>
|
|
Timeout each request after <DURATION>. Set 0 to disable
|
|
timeout.
|
|
-w, --window-bits=<N>
|
|
Sets the stream level initial window size to 2**<N>-1.
|
|
-W, --connection-window-bits=<N>
|
|
Sets the connection level initial window size to
|
|
2**<N>-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=<HEADER>
|
|
Add a header to the requests. Example: -H':method: PUT'
|
|
--trailer=<HEADER>
|
|
Add a trailer header to the requests. <HEADER> 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=<CERT>
|
|
Use the specified client certificate file. The file
|
|
must be in PEM format.
|
|
--key=<KEY> Use the client private key file. The file must be in
|
|
PEM format.
|
|
-d, --data=<PATH>
|
|
Post FILE to server. If '-' is given, data will be read
|
|
from stdin.
|
|
-m, --multiply=<N>
|
|
Request each URI <N> 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=<WEIGHT>
|
|
Sets priority group weight. The valid value range is
|
|
[)" << NGHTTP2_MIN_WEIGHT << ", " << NGHTTP2_MAX_WEIGHT
|
|
<< R"(], inclusive.
|
|
Default: )" << NGHTTP2_DEFAULT_WEIGHT << R"(
|
|
-M, --peer-max-concurrent-streams=<N>
|
|
Use <N> as SETTINGS_MAX_CONCURRENT_STREAMS value of
|
|
remote endpoint as if it is received in SETTINGS frame.
|
|
The default is large enough as it is seen as unlimited.
|
|
-c, --header-table-size=<SIZE>
|
|
Specify decoder header table size.
|
|
-b, --padding=<N>
|
|
Add at most <N> bytes to a frame payload as padding.
|
|
Specify 0 to disable padding.
|
|
-r, --har=<PATH>
|
|
Output HTTP transactions <PATH> 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=<N>
|
|
The number of concurrent pushed streams this client
|
|
accepts.
|
|
--version Display version information and exit.
|
|
-h, --help Display this help and exit.
|
|
|
|
--
|
|
|
|
The <SIZE> argument is an integer and an optional unit (e.g., 10K is
|
|
10 * 1024). Units are K, M and G (powers of 1024).
|
|
|
|
The <DURATION> 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) {
|
|
SSL_load_error_strings();
|
|
SSL_library_init();
|
|
OpenSSL_add_all_algorithms();
|
|
OPENSSL_config(nullptr);
|
|
|
|
bool color = false;
|
|
while (1) {
|
|
static int flag = 0;
|
|
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'},
|
|
{"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},
|
|
{nullptr, 0, nullptr, 0}};
|
|
int option_index = 0;
|
|
int c = getopt_long(argc, argv, "M:Oab:c:d:gm:np:r:hH:vst:uw:W:",
|
|
long_options, &option_index);
|
|
if (c == -1) {
|
|
break;
|
|
}
|
|
switch (c) {
|
|
case 'M':
|
|
// peer-max-concurrent-streams option
|
|
config.peer_max_concurrent_streams = strtoul(optarg, nullptr, 10);
|
|
break;
|
|
case 'O':
|
|
config.remote_name = true;
|
|
break;
|
|
case 'h':
|
|
print_help(std::cout);
|
|
exit(EXIT_SUCCESS);
|
|
case 'b':
|
|
config.padding = strtol(optarg, nullptr, 10);
|
|
break;
|
|
case 'n':
|
|
config.null_out = true;
|
|
break;
|
|
case 'p': {
|
|
errno = 0;
|
|
auto n = strtoul(optarg, nullptr, 10);
|
|
if (errno == 0 && NGHTTP2_MIN_WEIGHT <= n && n <= NGHTTP2_MAX_WEIGHT) {
|
|
config.weight = n;
|
|
} else {
|
|
std::cerr << "-p: specify the integer in the range ["
|
|
<< NGHTTP2_MIN_WEIGHT << ", " << NGHTTP2_MAX_WEIGHT
|
|
<< "], inclusive" << std::endl;
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
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<double>::infinity()) {
|
|
std::cerr << "-t: bad timeout value: " << optarg << std::endl;
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
break;
|
|
case 'u':
|
|
config.upgrade = true;
|
|
break;
|
|
case 'w':
|
|
case 'W': {
|
|
errno = 0;
|
|
char *endptr = nullptr;
|
|
unsigned long int n = strtoul(optarg, &endptr, 10);
|
|
if (errno == 0 && *endptr == '\0' && n < 31) {
|
|
if (c == 'w') {
|
|
config.window_bits = n;
|
|
} else {
|
|
config.connection_window_bits = n;
|
|
}
|
|
} else {
|
|
std::cerr << "-" << static_cast<char>(c)
|
|
<< ": specify the integer in the range [0, 30], inclusive"
|
|
<< std::endl;
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
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':
|
|
config.multiply = strtoul(optarg, nullptr, 10);
|
|
break;
|
|
case 'c':
|
|
errno = 0;
|
|
config.header_table_size = util::parse_uint_with_unit(optarg);
|
|
if (config.header_table_size == -1) {
|
|
std::cerr << "-c: Bad option value: " << optarg << std::endl;
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
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
|
|
config.max_concurrent_streams = strtoul(optarg, nullptr, 10);
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
set_color_output(color || isatty(fileno(stdout)));
|
|
|
|
nghttp2_option_set_peer_max_concurrent_streams(
|
|
config.http2_option, config.peer_max_concurrent_streams);
|
|
|
|
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::main(argc, argv); }
|