diff --git a/src/nghttp.cc b/src/nghttp.cc index dad9652f..c9f219ac 100644 --- a/src/nghttp.cc +++ b/src/nghttp.cc @@ -113,7 +113,8 @@ Config::Config() no_content_length(false), no_dep(false), hexdump(false), - no_push(false) { + no_push(false), + expect_continue(false) { nghttp2_option_new(&http2_option); nghttp2_option_set_peer_max_concurrent_streams(http2_option, peer_max_concurrent_streams); @@ -303,6 +304,51 @@ void Request::record_response_end_time() { timing.response_end_time = get_time(); } +namespace { +void continue_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast(ev_userdata(loop)); + auto req = static_cast(w->data); + 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); + } + + client->signal_write(); +} +} // namespace + +ContinueTimer::ContinueTimer(struct ev_loop *loop, Request *req) + : loop(loop) { + ev_timer_init(&timer, continue_timeout_cb, 1., 0.); + timer.data = req; +} + +ContinueTimer::~ContinueTimer() { + stop(); +} + +void ContinueTimer::start() { + ev_timer_start(loop, &timer); +} + +void ContinueTimer::stop() { + ev_timer_stop(loop, &timer); +} + +void ContinueTimer::dispatch_continue() { + // Only dispatch the timeout callback if it hasn't already been called. + if (ev_is_active(&timer)) { + ev_feed_event(loop, &timer, 0); + } +} + namespace { int htp_msg_begincb(http_parser *htp) { if (config.verbose) { @@ -353,16 +399,27 @@ int submit_request(HttpClient *client, const Headers &headers, Request *req) { {"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 (!config.no_content_length && req->data_prd) { - build_headers.emplace_back("content-length", util::utos(req->data_length)); + + 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) { @@ -400,11 +457,21 @@ int submit_request(HttpClient *client, const Headers &headers, Request *req) { 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); + 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_request() returned error: " + std::cerr << "[ERROR] nghttp2_submit_" + << (expect_continue ? "headers" : "request") + << "() returned error: " << nghttp2_strerror(stream_id) << std::endl; return -1; } @@ -414,6 +481,11 @@ int submit_request(HttpClient *client, const Headers &headers, Request *req) { req->req_nva = std::move(build_headers); + if (expect_continue) { + auto timer = make_unique(client->loop, req); + req->continue_timer = std::move(timer); + } + return 0; } } // namespace @@ -614,6 +686,12 @@ int HttpClient::initiate_connection() { 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(); + } + } + ev_timer_stop(loop, &settings_timer); ev_timer_stop(loop, &rt); @@ -1616,11 +1694,19 @@ void check_response_header(nghttp2_session *session, Request *req) { } 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) { @@ -1895,6 +1981,34 @@ int before_frame_send_callback(nghttp2_session *session, } // 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, @@ -1928,6 +2042,11 @@ int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, 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; @@ -2138,7 +2257,10 @@ int communicate( << std::endl; goto fin; } + + ev_set_userdata(loop, &client); ev_run(loop, 0); + ev_set_userdata(loop, nullptr); #ifdef HAVE_JANSSON if (!config.harfile.empty()) { @@ -2251,9 +2373,6 @@ int run(char **uris, int n) { 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); @@ -2273,6 +2392,9 @@ int run(char **uris, int n) { 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); @@ -2503,6 +2625,11 @@ Options: --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. --version Display version information and exit. -h, --help Display this help and exit. @@ -2554,6 +2681,7 @@ int main(int argc, char **argv) { {"hexdump", no_argument, &flag, 10}, {"no-push", no_argument, &flag, 11}, {"max-concurrent-streams", required_argument, &flag, 12}, + {"expect-continue", no_argument, &flag, 13}, {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:", @@ -2751,6 +2879,10 @@ int main(int argc, char **argv) { // max-concurrent-streams option config.max_concurrent_streams = strtoul(optarg, nullptr, 10); break; + case 13: + // expect-continue option + config.expect_continue = true; + break; } break; default: diff --git a/src/nghttp.h b/src/nghttp.h index df4ca685..fd433b84 100644 --- a/src/nghttp.h +++ b/src/nghttp.h @@ -91,6 +91,7 @@ struct Config { bool no_dep; bool hexdump; bool no_push; + bool expect_continue; }; enum class RequestState { INITIAL, ON_REQUEST, ON_RESPONSE, ON_COMPLETE }; @@ -109,6 +110,23 @@ struct RequestTiming { RequestTiming() : state(RequestState::INITIAL) {} }; +struct Request; // forward declaration for ContinueTimer + +struct ContinueTimer { + ContinueTimer(struct ev_loop *loop, Request *req); + ~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(); + + struct ev_loop *loop; + ev_timer timer; +}; + struct Request { // For pushed request, |uri| is empty and |u| is zero-cleared. Request(const std::string &uri, const http_parser_url &u, @@ -156,6 +174,8 @@ struct Request { // 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; }; struct SessionTiming {