From 2f2a535113ae314c4cbc0adf25a327e445232bb9 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 7 Mar 2015 17:02:00 +0900 Subject: [PATCH 1/4] Add a way to send trailer with nghttp2_submit_request/nghttp2_submit_response nghttp2_submit_request and nghttp2_submit_response will set NGHTTP2_FLAG_END_STREAM after all given data is sent (data could be 0). This means we have no way to send trailers. In this commit, we added NGHTTP2_DATA_FLAG_NO_END_STREAM flag. The application can set this flag in *data_flags inside nghttp2_data_source_read_callback. If NGHTTP2_DATA_FLAG_EOF is set, library automatically set NGHTTP2_FLAG_END_STREAM. But if both NGHTTP2_DATA_FLAG_EOF and NGHTTP2_DATA_FLAG_NO_END_STREAM are set, NGHTTP2_FLAG_END_STREAM will not set by library. Then application can use new nghttp2_submit_trailer() to send trailers. nghttp2_submit_trailer() will set NGHTTP2_FLAG_END_STREAM and it is actually thing wrapper of nghttp2_submit_headers(). --- lib/includes/nghttp2/nghttp2.h | 56 +++++++++++++++++++++++++- lib/nghttp2_session.c | 5 ++- lib/nghttp2_submit.c | 6 +++ tests/main.c | 1 + tests/nghttp2_session_test.c | 72 ++++++++++++++++++++++++++++++++++ tests/nghttp2_session_test.h | 1 + 6 files changed, 139 insertions(+), 2 deletions(-) diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index 91380ca5..6d1d360a 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -682,7 +682,14 @@ typedef enum { /** * Indicates EOF was sensed. */ - NGHTTP2_DATA_FLAG_EOF = 0x01 + NGHTTP2_DATA_FLAG_EOF = 0x01, + /** + * Indicates that END_STREAM flag must not be set even if + * NGHTTP2_DATA_FLAG_EOF is set. Usually this flag is used to send + * trailer header fields with `nghttp2_submit_request()` or + * `nghttp2_submit_response()`. + */ + NGHTTP2_DATA_FLAG_NO_END_STREAM = 0x02, } nghttp2_data_flag; /** @@ -2834,6 +2841,53 @@ int nghttp2_submit_response(nghttp2_session *session, int32_t stream_id, const nghttp2_nv *nva, size_t nvlen, const nghttp2_data_provider *data_prd); +/** + * @function + * + * Submits trailer HEADERS against the stream |stream_id|. + * + * The |nva| is an array of name/value pair :type:`nghttp2_nv` with + * |nvlen| elements. The application is responsible not to include + * required pseudo-header fields (header field whose name starts with + * ":") in |nva|. + * + * This function creates copies of all name/value pairs in |nva|. It + * also lower-cases all names in |nva|. The order of elements in + * |nva| is preserved. + * + * For server, trailer must be followed by response HEADERS or + * response DATA. The library does not check that response HEADERS + * has already sent and if `nghttp2_submit_trailer()` is called before + * any response HEADERS submission (usually by + * `nghttp2_submit_response()`), the content of |nva| will be sent as + * reponse headers, which will result in error. + * + * This function has the same effect with `nghttp2_submit_headers()`, + * with flags = :enum:`NGHTTP2_FLAG_END_HEADERS` and both pri_spec and + * stream_user_data to NULL. + * + * To submit trailer after `nghttp2_submit_response()` is called, the + * application has to specify :type:`nghttp2_data_provider` to + * `nghttp2_submit_response()`. In side + * :type:`nghttp2_data_source_read_callback`, when setting + * :enum:`NGHTTP2_DATA_FLAG_EOF`, also set + * :enum:`NGHTTP2_DATA_FLAG_NO_END_STREAM`. After that, the + * application can send trailer using `nghttp2_submit_trailer()`. + * `nghttp2_submit_trailer()` can be used inside + * :type:`nghttp2_data_source_read_callback`. + * + * This function returns 0 if it succeeds and |stream_id| is -1. + * Otherwise, this function returns 0 if it succeeds, or one of the + * following negative error codes: + * + * :enum:`NGHTTP2_ERR_NOMEM` + * Out of memory. + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * The |stream_id| is 0. + */ +int nghttp2_submit_trailer(nghttp2_session *session, int32_t stream_id, + const nghttp2_nv *nva, size_t nvlen); + /** * @function * diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 0e5170bf..5ef2b746 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -6222,7 +6222,10 @@ int nghttp2_session_pack_data(nghttp2_session *session, nghttp2_bufs *bufs, if (data_flags & NGHTTP2_DATA_FLAG_EOF) { aux_data->eof = 1; - if (aux_data->flags & NGHTTP2_FLAG_END_STREAM) { + /* If NGHTTP2_DATA_FLAG_NO_END_STREAM is set, don't set + NGHTTP2_FLAG_END_STREAM */ + if ((aux_data->flags & NGHTTP2_FLAG_END_STREAM) && + (data_flags & NGHTTP2_DATA_FLAG_NO_END_STREAM) == 0) { frame->hd.flags |= NGHTTP2_FLAG_END_STREAM; } } diff --git a/lib/nghttp2_submit.c b/lib/nghttp2_submit.c index c61982ec..bde78a20 100644 --- a/lib/nghttp2_submit.c +++ b/lib/nghttp2_submit.c @@ -155,6 +155,12 @@ static int32_t submit_headers_shared_nva(nghttp2_session *session, attach_stream); } +int32_t nghttp2_submit_trailer(nghttp2_session *session, int32_t stream_id, + const nghttp2_nv *nva, size_t nvlen) { + return submit_headers_shared_nva(session, NGHTTP2_FLAG_END_STREAM, stream_id, + NULL, nva, nvlen, NULL, NULL, 0); +} + int32_t nghttp2_submit_headers(nghttp2_session *session, uint8_t flags, int32_t stream_id, const nghttp2_priority_spec *pri_spec, diff --git a/tests/main.c b/tests/main.c index e5656d66..b359d41e 100644 --- a/tests/main.c +++ b/tests/main.c @@ -154,6 +154,7 @@ int main(int argc _U_, char *argv[] _U_) { test_nghttp2_submit_response_with_data) || !CU_add_test(pSuite, "submit_response_without_data", test_nghttp2_submit_response_without_data) || + !CU_add_test(pSuite, "submit_trailer", test_nghttp2_submit_trailer) || !CU_add_test(pSuite, "submit_headers_start_stream", test_nghttp2_submit_headers_start_stream) || !CU_add_test(pSuite, "submit_headers_reply", diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index ae8f7f5a..f1debab9 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -88,6 +88,13 @@ static const nghttp2_nv resnv[] = { MAKE_NV(":status", "200"), }; +static const nghttp2_nv trailernv[] = { + // from http://tools.ietf.org/html/rfc6249#section-7 + MAKE_NV("digest", "SHA-256=" + "MWVkMWQxYTRiMzk5MDQ0MzI3NGU5NDEyZTk5OWY1ZGFmNzgyZTJlODYz" + "YjRjYzFhOTlmNTQwYzI2M2QwM2U2MQ=="), +}; + static void scripted_data_feed_init2(scripted_data_feed *df, nghttp2_bufs *bufs) { nghttp2_buf_chain *ci; @@ -279,6 +286,14 @@ static ssize_t fail_data_source_read_callback(nghttp2_session *session _U_, return NGHTTP2_ERR_CALLBACK_FAILURE; } +static ssize_t no_end_stream_data_source_read_callback( + nghttp2_session *session _U_, int32_t stream_id _U_, uint8_t *buf _U_, + size_t len _U_, uint32_t *data_flags, nghttp2_data_source *source _U_, + void *user_data _U_) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF | NGHTTP2_DATA_FLAG_NO_END_STREAM; + return 0; +} + /* static void no_stream_user_data_stream_close_callback */ /* (nghttp2_session *session, */ /* int32_t stream_id, */ @@ -3472,6 +3487,63 @@ void test_nghttp2_submit_response_without_data(void) { nghttp2_session_del(session); } +void test_nghttp2_submit_trailer(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + accumulator acc; + nghttp2_data_provider data_prd; + nghttp2_outbound_item *item; + my_user_data ud; + nghttp2_frame frame; + nghttp2_hd_inflater inflater; + nva_out out; + nghttp2_bufs bufs; + nghttp2_mem *mem; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + data_prd.read_callback = no_end_stream_data_source_read_callback; + nva_out_init(&out); + acc.length = 0; + ud.acc = &acc; + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + CU_ASSERT(0 == nghttp2_session_server_new(&session, &callbacks, &ud)); + + nghttp2_hd_inflate_init(&inflater, mem); + nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_END_STREAM, + &pri_spec_default, NGHTTP2_STREAM_OPENING, NULL); + CU_ASSERT(0 == nghttp2_submit_response(session, 1, resnv, ARRLEN(resnv), + &data_prd)); + CU_ASSERT(0 == nghttp2_session_send(session)); + + CU_ASSERT(0 == + nghttp2_submit_trailer(session, 1, trailernv, ARRLEN(trailernv))); + + session->callbacks.send_callback = accumulator_send_callback; + + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_HEADERS == item->frame.hd.type); + CU_ASSERT(NGHTTP2_HCAT_HEADERS == item->frame.headers.cat); + CU_ASSERT(item->frame.hd.flags & NGHTTP2_FLAG_END_STREAM); + + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(0 == unpack_frame(&frame, acc.buf, acc.length)); + + nghttp2_bufs_add(&bufs, acc.buf, acc.length); + inflate_hd(&inflater, &out, &bufs, NGHTTP2_FRAME_HDLEN, mem); + + CU_ASSERT(ARRLEN(trailernv) == out.nvlen); + assert_nv_equal(trailernv, out.nva, out.nvlen, mem); + + nva_out_reset(&out, mem); + nghttp2_bufs_free(&bufs); + nghttp2_frame_headers_free(&frame.headers, mem); + nghttp2_hd_inflate_free(&inflater); + nghttp2_session_del(session); +} + void test_nghttp2_submit_headers_start_stream(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index 65230d63..d2767a60 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -69,6 +69,7 @@ void test_nghttp2_submit_request_with_data(void); void test_nghttp2_submit_request_without_data(void); void test_nghttp2_submit_response_with_data(void); void test_nghttp2_submit_response_without_data(void); +void test_nghttp2_submit_trailer(void); void test_nghttp2_submit_headers_start_stream(void); void test_nghttp2_submit_headers_reply(void); void test_nghttp2_submit_headers_push_reply(void); From 4c55a2340ba454b8d0565d14abf2ede7399fb731 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 7 Mar 2015 17:39:31 +0900 Subject: [PATCH 2/4] nghttp: Add --trailer optiont to send trailer header fields with -d option --- src/nghttp.cc | 43 +++++++++++++++++++++++++++++++++++++++++++ src/nghttp.h | 1 + 2 files changed, 44 insertions(+) diff --git a/src/nghttp.cc b/src/nghttp.cc index cff6a84e..faa9c649 100644 --- a/src/nghttp.cc +++ b/src/nghttp.cc @@ -2110,6 +2110,7 @@ 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); @@ -2126,6 +2127,18 @@ ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, if (nread == 0) { *data_flags |= NGHTTP2_DATA_FLAG_EOF; + if (!config.trailer.empty()) { + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + 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) { + *data_flags &= ~NGHTTP2_DATA_FLAG_NO_END_STREAM; + } + } } else { req->data_offset += nread; } @@ -2347,6 +2360,11 @@ Options: -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. @@ -2425,6 +2443,7 @@ int main(int argc, char **argv) { {"no-content-length", no_argument, &flag, 6}, {"no-dep", no_argument, &flag, 7}, {"dep-idle", no_argument, &flag, 8}, + {"trailer", required_argument, &flag, 9}, {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:", @@ -2587,6 +2606,30 @@ int main(int argc, char **argv) { // dep-idle option config.dep_idle = 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; + } } break; default: diff --git a/src/nghttp.h b/src/nghttp.h index 3c8f8649..a4d85ef5 100644 --- a/src/nghttp.h +++ b/src/nghttp.h @@ -58,6 +58,7 @@ struct Config { ~Config(); Headers headers; + Headers trailer; std::string certfile; std::string keyfile; std::string datafile; From 80a7523b49ff2388b05e973beb96b6e4b0c41ddc Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 7 Mar 2015 17:50:03 +0900 Subject: [PATCH 3/4] nghttpd: Add --trailer to send trailer header fields --- src/HttpServer.cc | 15 +++++++++++++++ src/HttpServer.h | 1 + src/nghttpd.cc | 30 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/HttpServer.cc b/src/HttpServer.cc index ff13b6c4..3044567f 100644 --- a/src/HttpServer.cc +++ b/src/HttpServer.cc @@ -809,6 +809,7 @@ void Http2Handler::terminate_session(uint32_t 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); @@ -829,6 +830,20 @@ ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, if (nread == 0 || stream->body_left <= 0) { *data_flags |= NGHTTP2_DATA_FLAG_EOF; + auto config = hd->get_config(); + if (!config->trailer.empty()) { + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + 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) { + *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); diff --git a/src/HttpServer.h b/src/HttpServer.h index 3723afd1..5b8de7ef 100644 --- a/src/HttpServer.h +++ b/src/HttpServer.h @@ -50,6 +50,7 @@ namespace nghttp2 { struct Config { std::map> push; + Headers trailer; std::string htdocs; std::string host; std::string private_key_file; diff --git a/src/nghttpd.cc b/src/nghttpd.cc index fc1f70a9..275a4827 100644 --- a/src/nghttpd.cc +++ b/src/nghttpd.cc @@ -140,6 +140,11 @@ Options: --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'. --version Display version information and exit. -h, --help Display this help and exit. @@ -170,6 +175,7 @@ int main(int argc, char **argv) { {"version", no_argument, &flag, 3}, {"dh-param-file", required_argument, &flag, 4}, {"early-response", no_argument, &flag, 5}, + {"trailer", required_argument, &flag, 6}, {nullptr, 0, nullptr, 0}}; int option_index = 0; int c = getopt_long(argc, argv, "DVb:c:d:ehn:p:va:", long_options, @@ -254,6 +260,30 @@ int main(int argc, char **argv) { // 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; + } } break; default: From dd9e829ee1582455ff41329d406d1681080f1109 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 7 Mar 2015 17:59:58 +0900 Subject: [PATCH 4/4] Document NGHTTP2_DATA_FLAG_NO_END_STREAM in nghttp2_data_source_read_callback --- lib/includes/nghttp2/nghttp2.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index 6d1d360a..51a4a30a 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -702,6 +702,21 @@ typedef enum { * them in |buf| and return number of data stored in |buf|. If EOF is * reached, set :enum:`NGHTTP2_DATA_FLAG_EOF` flag in |*data_flags|. * + * If this callback is set by `nghttp2_submit_request()`, + * `nghttp2_submit_response()` or `nghttp2_submit_headers()` and + * `nghttp2_submit_data()` with flag parameter + * :enum:`NGHTTP2_FLAG_END_STREAM` set, and + * :enum:`NGHTTP2_DATA_FLAG_EOF` flag is set to |*data_flags|, DATA + * frame will have END_STREAM flag set. Usually, this is expected + * behaviour and all are fine. One exception is send trailer header + * fields. You cannot send trailers after sending frame with + * END_STREAM set. To avoid this problem, one can set + * :enum:`NGHTTP2_DATA_FLAG_NO_END_STREAM` along with + * :enum:`NGHTTP2_DATA_FLAG_EOF` to signal the library not to set + * END_STREAM in DATA frame. Then application can use + * `nghttp2_submit_trailer()` to send trailers. + * `nghttp2_submit_trailer()` can be called inside this callback. + * * If the application wants to postpone DATA frames (e.g., * asynchronous I/O, or reading data blocks for long time), it is * achieved by returning :enum:`NGHTTP2_ERR_DEFERRED` without reading