diff --git a/doc/Makefile.am b/doc/Makefile.am index de438968..b24cede6 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -57,6 +57,7 @@ APIDOCS= \ nghttp2_option_new.rst \ nghttp2_option_set_builtin_recv_extension_type.rst \ nghttp2_option_set_max_reserved_remote_streams.rst \ + nghttp2_option_set_max_send_header_block_length.rst \ nghttp2_option_set_no_auto_ping_ack.rst \ nghttp2_option_set_no_auto_window_update.rst \ nghttp2_option_set_no_http_messaging.rst \ diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index c545c31f..a17ff120 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -2412,6 +2412,21 @@ nghttp2_option_set_builtin_recv_extension_type(nghttp2_option *option, NGHTTP2_EXTERN void nghttp2_option_set_no_auto_ping_ack(nghttp2_option *option, int val); +/** + * @function + * + * This option sets the maximum length of header block (a set of + * header fields per one HEADERS frame) to send. The length of a + * given set of header fields is calculated using + * `nghttp2_hd_deflate_bound()`. The default value is 64KiB. If + * application attempts to send header fields larger than this limit, + * the transmission of the frame fails with error code + * :enum:`NGHTTP2_ERR_FRAME_SIZE_ERROR`. + */ +NGHTTP2_EXTERN void +nghttp2_option_set_max_send_header_block_length(nghttp2_option *option, + size_t val); + /** * @function * diff --git a/lib/nghttp2_frame.h b/lib/nghttp2_frame.h index 77d8c087..58d3d134 100644 --- a/lib/nghttp2_frame.h +++ b/lib/nghttp2_frame.h @@ -52,14 +52,12 @@ #define NGHTTP2_FRAMEBUF_CHUNKLEN \ (NGHTTP2_FRAME_HDLEN + 1 + NGHTTP2_MAX_PAYLOADLEN) -/* Number of inbound buffer */ -#define NGHTTP2_FRAMEBUF_MAX_NUM 5 - /* The default length of DATA frame payload. */ #define NGHTTP2_DATA_PAYLOADLEN NGHTTP2_MAX_FRAME_SIZE_MIN -/* Maximum headers payload length, calculated in compressed form. - This applies to transmission only. */ +/* Maximum headers block size to send, calculated using + nghttp2_hd_deflate_bound(). This is the default value, and can be + overridden by nghttp2_option_set_max_send_header_block_size(). */ #define NGHTTP2_MAX_HEADERSLEN 65536 /* The number of bytes for each SETTINGS entry */ diff --git a/lib/nghttp2_option.c b/lib/nghttp2_option.c index e8d6ed16..860c9f17 100644 --- a/lib/nghttp2_option.c +++ b/lib/nghttp2_option.c @@ -95,3 +95,9 @@ void nghttp2_option_set_no_auto_ping_ack(nghttp2_option *option, int val) { option->opt_set_mask |= NGHTTP2_OPT_NO_AUTO_PING_ACK; option->no_auto_ping_ack = val; } + +void nghttp2_option_set_max_send_header_block_length(nghttp2_option *option, + size_t val) { + option->opt_set_mask |= NGHTTP2_OPT_MAX_SEND_HEADER_BLOCK_LENGTH; + option->max_send_header_block_length = val; +} diff --git a/lib/nghttp2_option.h b/lib/nghttp2_option.h index 87ef4e04..fb79b920 100644 --- a/lib/nghttp2_option.h +++ b/lib/nghttp2_option.h @@ -62,13 +62,18 @@ typedef enum { NGHTTP2_OPT_MAX_RESERVED_REMOTE_STREAMS = 1 << 4, NGHTTP2_OPT_USER_RECV_EXT_TYPES = 1 << 5, NGHTTP2_OPT_NO_AUTO_PING_ACK = 1 << 6, - NGHTTP2_OPT_BUILTIN_RECV_EXT_TYPES = 1 << 7 + NGHTTP2_OPT_BUILTIN_RECV_EXT_TYPES = 1 << 7, + NGHTTP2_OPT_MAX_SEND_HEADER_BLOCK_LENGTH = 1 << 8, } nghttp2_option_flag; /** * Struct to store option values for nghttp2_session. */ struct nghttp2_option { + /** + * NGHTTP2_OPT_MAX_SEND_HEADER_BLOCK_LENGTH + */ + size_t max_send_header_block_length; /** * Bitwise OR of nghttp2_option_flag to determine that which fields * are specified. diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index b498d968..da105819 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -389,6 +389,7 @@ static int session_new(nghttp2_session **session_ptr, void *user_data, int server, const nghttp2_option *option, nghttp2_mem *mem) { int rv; + size_t nbuffer; if (mem == NULL) { mem = nghttp2_mem_default(); @@ -441,16 +442,6 @@ static int session_new(nghttp2_session **session_ptr, (*session_ptr)->server = 1; } - /* 1 for Pad Field. */ - rv = nghttp2_bufs_init3(&(*session_ptr)->aob.framebufs, - NGHTTP2_FRAMEBUF_CHUNKLEN, NGHTTP2_FRAMEBUF_MAX_NUM, - 1, NGHTTP2_FRAME_HDLEN + 1, mem); - if (rv != 0) { - goto fail_aob_framebuf; - } - - active_outbound_item_reset(&(*session_ptr)->aob, mem); - init_settings(&(*session_ptr)->remote_settings); init_settings(&(*session_ptr)->local_settings); @@ -460,6 +451,8 @@ static int session_new(nghttp2_session **session_ptr, /* Limit max outgoing concurrent streams to sensible value */ (*session_ptr)->remote_settings.max_concurrent_streams = 100; + (*session_ptr)->max_send_header_block_length = NGHTTP2_MAX_HEADERSLEN; + if (option) { if ((option->opt_set_mask & NGHTTP2_OPT_NO_AUTO_WINDOW_UPDATE) && option->no_auto_window_update) { @@ -504,8 +497,31 @@ static int session_new(nghttp2_session **session_ptr, option->no_auto_ping_ack) { (*session_ptr)->opt_flags |= NGHTTP2_OPTMASK_NO_AUTO_PING_ACK; } + + if (option->opt_set_mask & NGHTTP2_OPT_MAX_SEND_HEADER_BLOCK_LENGTH) { + (*session_ptr)->max_send_header_block_length = + option->max_send_header_block_length; + } } + nbuffer = ((*session_ptr)->max_send_header_block_length + + NGHTTP2_FRAMEBUF_CHUNKLEN - 1) / + NGHTTP2_FRAMEBUF_CHUNKLEN; + + if (nbuffer == 0) { + nbuffer = 1; + } + + /* 1 for Pad Field. */ + rv = nghttp2_bufs_init3(&(*session_ptr)->aob.framebufs, + NGHTTP2_FRAMEBUF_CHUNKLEN, nbuffer, 1, + NGHTTP2_FRAME_HDLEN + 1, mem); + if (rv != 0) { + goto fail_aob_framebuf; + } + + active_outbound_item_reset(&(*session_ptr)->aob, mem); + (*session_ptr)->callbacks = *callbacks; (*session_ptr)->user_data = user_data; @@ -1951,7 +1967,7 @@ static int session_prep_frame(nghttp2_session *session, session, frame->headers.nva, frame->headers.nvlen, NGHTTP2_PRIORITY_SPECLEN); - if (estimated_payloadlen > NGHTTP2_MAX_HEADERSLEN) { + if (estimated_payloadlen > session->max_send_header_block_length) { return NGHTTP2_ERR_FRAME_SIZE_ERROR; } @@ -1970,7 +1986,7 @@ static int session_prep_frame(nghttp2_session *session, session, frame->headers.nva, frame->headers.nvlen, NGHTTP2_PRIORITY_SPECLEN); - if (estimated_payloadlen > NGHTTP2_MAX_HEADERSLEN) { + if (estimated_payloadlen > session->max_send_header_block_length) { return NGHTTP2_ERR_FRAME_SIZE_ERROR; } @@ -2089,7 +2105,7 @@ static int session_prep_frame(nghttp2_session *session, estimated_payloadlen = session_estimate_headers_payload( session, frame->push_promise.nva, frame->push_promise.nvlen, 0); - if (estimated_payloadlen > NGHTTP2_MAX_HEADERSLEN) { + if (estimated_payloadlen > session->max_send_header_block_length) { return NGHTTP2_ERR_FRAME_SIZE_ERROR; } diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index 58f1aba4..6dfa0a3f 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -256,6 +256,9 @@ struct nghttp2_session { size_t nvbuflen; /* Counter for detecting flooding in outbound queue */ size_t obq_flood_counter_; + /* The maximum length of header block to send. Calculated by the + same way as nghttp2_hd_deflate_bound() does. */ + size_t max_send_header_block_length; /* Next Stream ID. Made unsigned int to detect >= (1 << 31). */ uint32_t next_stream_id; /* The last stream ID this session initiated. For client session, diff --git a/tests/main.c b/tests/main.c index 02fbe415..b4e638b1 100644 --- a/tests/main.c +++ b/tests/main.c @@ -183,6 +183,8 @@ int main(int argc _U_, char *argv[] _U_) { !CU_add_test(pSuite, "submit_headers", test_nghttp2_submit_headers) || !CU_add_test(pSuite, "submit_headers_continuation", test_nghttp2_submit_headers_continuation) || + !CU_add_test(pSuite, "submit_headers_continuation_extra_large", + test_nghttp2_submit_headers_continuation_extra_large) || !CU_add_test(pSuite, "submit_priority", test_nghttp2_submit_priority) || !CU_add_test(pSuite, "session_submit_settings", test_nghttp2_submit_settings) || diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index 6af7f536..35b51435 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -4842,6 +4842,51 @@ void test_nghttp2_submit_headers_continuation(void) { nghttp2_session_del(session); } +void test_nghttp2_submit_headers_continuation_extra_large(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_nv nv[] = { + MAKE_NV("h1", ""), MAKE_NV("h1", ""), MAKE_NV("h1", ""), + MAKE_NV("h1", ""), MAKE_NV("h1", ""), MAKE_NV("h1", ""), + }; + nghttp2_outbound_item *item; + uint8_t data[16384]; + size_t i; + my_user_data ud; + nghttp2_option *opt; + + memset(data, '0', sizeof(data)); + for (i = 0; i < ARRLEN(nv); ++i) { + nv[i].valuelen = sizeof(data); + nv[i].value = data; + } + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + callbacks.on_frame_send_callback = on_frame_send_callback; + + /* The default size of max send header block length is too small to + send these header fields. Expand it. */ + nghttp2_option_new(&opt); + nghttp2_option_set_max_send_header_block_length(opt, 102400); + + CU_ASSERT(0 == nghttp2_session_client_new2(&session, &callbacks, &ud, opt)); + CU_ASSERT(1 == nghttp2_submit_headers(session, NGHTTP2_FLAG_END_STREAM, -1, + NULL, nv, ARRLEN(nv), NULL)); + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_HEADERS == item->frame.hd.type); + CU_ASSERT((NGHTTP2_FLAG_END_STREAM | NGHTTP2_FLAG_END_HEADERS) == + item->frame.hd.flags); + CU_ASSERT(0 == (item->frame.hd.flags & NGHTTP2_FLAG_PRIORITY)); + + ud.frame_send_cb_called = 0; + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(1 == ud.frame_send_cb_called); + + nghttp2_session_del(session); + nghttp2_option_del(opt); +} + void test_nghttp2_submit_priority(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index a1898a27..3e2e7738 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -87,6 +87,7 @@ void test_nghttp2_submit_headers_reply(void); void test_nghttp2_submit_headers_push_reply(void); void test_nghttp2_submit_headers(void); void test_nghttp2_submit_headers_continuation(void); +void test_nghttp2_submit_headers_continuation_extra_large(void); void test_nghttp2_submit_priority(void); void test_nghttp2_submit_settings(void); void test_nghttp2_submit_settings_update_local_window_size(void);