From 271f7fbbb6e12ce6b6fedc32f2501b889a5f7d3b Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Wed, 3 Aug 2016 23:49:49 +0900 Subject: [PATCH] Add nghttp2_on_invalid_header_callback nghttp2_on_invalid_header_callback is similar to nghttp2_on_header_callback, but the former is only called when the invalid header field is received which is silently ignored when the callback is not set. With this callback, application inspects the incoming invalid field, and it also can reset stream from this callback by returning NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE, or using nghttp2_submit_rst_stream() directly with the error code of choice. We also added nghttp2_on_invalid_header_callback2, which uses reference counted header fields. --- doc/Makefile.am | 2 + lib/includes/nghttp2/nghttp2.h | 95 +++++++++++++++++++++++++++++--- lib/nghttp2_callbacks.c | 12 +++++ lib/nghttp2_callbacks.h | 7 +++ lib/nghttp2_session.c | 33 ++++++++++++ tests/nghttp2_session_test.c | 98 +++++++++++++++++++++++++++++++++- 6 files changed, 240 insertions(+), 7 deletions(-) diff --git a/doc/Makefile.am b/doc/Makefile.am index b24cede6..ce9eef5b 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -87,6 +87,8 @@ APIDOCS= \ nghttp2_session_callbacks_set_on_header_callback.rst \ nghttp2_session_callbacks_set_on_header_callback2.rst \ nghttp2_session_callbacks_set_on_invalid_frame_recv_callback.rst \ + nghttp2_session_callbacks_set_on_invalid_header_callback.rst \ + nghttp2_session_callbacks_set_on_invalid_header_callback2.rst \ nghttp2_session_callbacks_set_on_stream_close_callback.rst \ nghttp2_session_callbacks_set_pack_extension_callback.rst \ nghttp2_session_callbacks_set_recv_callback.rst \ diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index 0f368c14..cad1558f 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -1717,6 +1717,65 @@ typedef int (*nghttp2_on_header_callback2)(nghttp2_session *session, nghttp2_rcbuf *value, uint8_t flags, void *user_data); +/** + * @functypedef + * + * Callback function invoked when a invalid header name/value pair is + * received for the |frame|. + * + * The parameter and behaviour are similar to + * :type:`nghttp2_on_header_callback`. The difference is that this + * callback is only invoked when a invalid header name/value pair is + * received which is silently ignored if this callback is not set. + * Only invalid regular header field are passed to this callback. In + * other words, invalid pseudo header field is not passed to this + * callback. Also header fields which includes upper cased latter are + * also treated as error without passing them to this callback. + * + * This callback is only considered if HTTP messaging validation is + * turned on (which is on by default, see + * `nghttp2_option_set_no_http_messaging()`). + * + * With this callback, application inspects the incoming invalid + * field, and it also can reset stream from this callback by returning + * :enum:`NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE`, or using + * `nghttp2_submit_rst_stream()` directly with the error code of + * choice. + */ +typedef int (*nghttp2_on_invalid_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); + +/** + * @functypedef + * + * Callback function invoked when a invalid header name/value pair is + * received for the |frame|. + * + * The parameter and behaviour are similar to + * :type:`nghttp2_on_header_callback2`. The difference is that this + * callback is only invoked when a invalid header name/value pair is + * received which is silently ignored if this callback is not set. + * Only invalid regular header field are passed to this callback. In + * other words, invalid pseudo header field is not passed to this + * callback. Also header fields which includes upper cased latter are + * also treated as error without passing them to this callback. + * + * This callback is only considered if HTTP messaging validation is + * turned on (which is on by default, see + * `nghttp2_option_set_no_http_messaging()`). + * + * With this callback, application inspects the incoming invalid + * field, and it also can reset stream from this callback by returning + * :enum:`NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE`, or using + * `nghttp2_submit_rst_stream()` directly with the error code of + * choice. + */ +typedef int (*nghttp2_on_invalid_header_callback2)( + nghttp2_session *session, const nghttp2_frame *frame, nghttp2_rcbuf *name, + nghttp2_rcbuf *value, uint8_t flags, void *user_data); + /** * @functypedef * @@ -2075,6 +2134,29 @@ NGHTTP2_EXTERN void nghttp2_session_callbacks_set_on_header_callback2( nghttp2_session_callbacks *cbs, nghttp2_on_header_callback2 on_header_callback2); +/** + * @function + * + * Sets callback function invoked when a invalid header name/value + * pair is received. If both + * `nghttp2_session_callbacks_set_on_invalid_header_callback()` and + * `nghttp2_session_callbacks_set_on_invalid_header_callback2()` are + * used to set callbacks, the latter takes the precedence. + */ +NGHTTP2_EXTERN void nghttp2_session_callbacks_set_on_invalid_header_callback( + nghttp2_session_callbacks *cbs, + nghttp2_on_invalid_header_callback on_invalid_header_callback); + +/** + * @function + * + * Sets callback function invoked when a invalid header name/value + * pair is received. + */ +NGHTTP2_EXTERN void nghttp2_session_callbacks_set_on_invalid_header_callback2( + nghttp2_session_callbacks *cbs, + nghttp2_on_invalid_header_callback2 on_invalid_header_callback2); + /** * @function * @@ -2734,12 +2816,13 @@ nghttp2_session_mem_send(nghttp2_session *session, const uint8_t **data_ptr); * taken. If the frame is either HEADERS or PUSH_PROMISE, * :type:`nghttp2_on_begin_headers_callback` is invoked. Then * :type:`nghttp2_on_header_callback` is invoked for each header - * name/value pair. After all name/value pairs are emitted - * successfully, :type:`nghttp2_on_frame_recv_callback` is - * invoked. For other frames, - * :type:`nghttp2_on_frame_recv_callback` is invoked. If the - * reception of the frame triggers the closure of the stream, - * :type:`nghttp2_on_stream_close_callback` is invoked. + * name/value pair. For invalid header field, + * :type:`nghttp2_on_invalid_header_callback` is called. After + * all name/value pairs are emitted successfully, + * :type:`nghttp2_on_frame_recv_callback` is invoked. For other + * frames, :type:`nghttp2_on_frame_recv_callback` is invoked. + * If the reception of the frame triggers the closure of the + * stream, :type:`nghttp2_on_stream_close_callback` is invoked. * * 3. If the received frame is unpacked but is interpreted as * invalid, :type:`nghttp2_on_invalid_frame_recv_callback` is diff --git a/lib/nghttp2_callbacks.c b/lib/nghttp2_callbacks.c index 4d5211a1..b6cf5957 100644 --- a/lib/nghttp2_callbacks.c +++ b/lib/nghttp2_callbacks.c @@ -110,6 +110,18 @@ void nghttp2_session_callbacks_set_on_header_callback2( cbs->on_header_callback2 = on_header_callback2; } +void nghttp2_session_callbacks_set_on_invalid_header_callback( + nghttp2_session_callbacks *cbs, + nghttp2_on_invalid_header_callback on_invalid_header_callback) { + cbs->on_invalid_header_callback = on_invalid_header_callback; +} + +void nghttp2_session_callbacks_set_on_invalid_header_callback2( + nghttp2_session_callbacks *cbs, + nghttp2_on_invalid_header_callback2 on_invalid_header_callback2) { + cbs->on_invalid_header_callback2 = on_invalid_header_callback2; +} + void nghttp2_session_callbacks_set_select_padding_callback( nghttp2_session_callbacks *cbs, nghttp2_select_padding_callback select_padding_callback) { diff --git a/lib/nghttp2_callbacks.h b/lib/nghttp2_callbacks.h index 5f08474a..5967524e 100644 --- a/lib/nghttp2_callbacks.h +++ b/lib/nghttp2_callbacks.h @@ -92,6 +92,13 @@ struct nghttp2_session_callbacks { */ nghttp2_on_header_callback on_header_callback; nghttp2_on_header_callback2 on_header_callback2; + /** + * Callback function invoked when a invalid header name/value pair + * is received which is silently ignored if these callbacks are not + * set. + */ + nghttp2_on_invalid_header_callback on_invalid_header_callback; + nghttp2_on_invalid_header_callback2 on_invalid_header_callback2; /** * Callback function invoked when the library asks application how * many padding bytes are required for the transmission of the given diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 3e885ee3..25402d8b 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -3343,6 +3343,31 @@ static int session_call_on_header(nghttp2_session *session, return 0; } +static int session_call_on_invalid_header(nghttp2_session *session, + const nghttp2_frame *frame, + const nghttp2_hd_nv *nv) { + int rv; + if (session->callbacks.on_invalid_header_callback2) { + rv = session->callbacks.on_invalid_header_callback2( + session, frame, nv->name, nv->value, nv->flags, session->user_data); + } else if (session->callbacks.on_invalid_header_callback) { + rv = session->callbacks.on_invalid_header_callback( + session, frame, nv->name->base, nv->name->len, nv->value->base, + nv->value->len, nv->flags, session->user_data); + } else { + return 0; + } + + if (rv == NGHTTP2_ERR_PAUSE || rv == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) { + return rv; + } + if (rv != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + static int session_call_on_extension_chunk_recv_callback(nghttp2_session *session, const uint8_t *data, size_t len) { @@ -3606,6 +3631,14 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame, if (rv == NGHTTP2_ERR_IGN_HTTP_HEADER) { /* Don't overwrite rv here */ int rv2; + + rv2 = session_call_on_invalid_header(session, frame, &nv); + /* This handles NGHTTP2_ERR_PAUSE and + NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE as well */ + if (rv2 != 0) { + return rv2; + } + /* header is ignored */ DEBUGF(fprintf( stderr, "recv: HTTP ignored: type=%u, id=%d, header %.*s: %.*s\n", diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index 211a7b0c..d3df4adc 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -70,6 +70,7 @@ typedef struct { const nghttp2_frame *frame; size_t fixed_sendlen; int header_cb_called; + int invalid_header_cb_called; int begin_headers_cb_called; nghttp2_nv nv; size_t data_chunk_len; @@ -395,6 +396,44 @@ static int temporal_failure_on_header_callback( return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } +static int on_invalid_header_callback(nghttp2_session *session _U_, + const nghttp2_frame *frame, + const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, + uint8_t flags _U_, void *user_data) { + my_user_data *ud = (my_user_data *)user_data; + ++ud->invalid_header_cb_called; + ud->nv.name = (uint8_t *)name; + ud->nv.namelen = namelen; + ud->nv.value = (uint8_t *)value; + ud->nv.valuelen = valuelen; + + ud->frame = frame; + return 0; +} + +static int pause_on_invalid_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) { + on_invalid_header_callback(session, frame, name, namelen, value, valuelen, + flags, user_data); + return NGHTTP2_ERR_PAUSE; +} + +static int reset_on_invalid_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) { + on_invalid_header_callback(session, frame, name, namelen, value, valuelen, + flags, user_data); + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; +} + static int on_begin_headers_callback(nghttp2_session *session _U_, const nghttp2_frame *frame _U_, void *user_data) { @@ -10363,6 +10402,7 @@ void test_nghttp2_http_ignore_regular_header(void) { MAKE_NV(":path", "/"), MAKE_NV(":method", "GET"), MAKE_NV("bar", "buzz")}; size_t proclen; size_t i; + nghttp2_outbound_item *item; mem = nghttp2_mem_default(); frame_pack_bufs_init(&bufs); @@ -10380,6 +10420,8 @@ void test_nghttp2_http_ignore_regular_header(void) { CU_ASSERT_FATAL(0 == rv); + nghttp2_hd_deflate_free(&deflater); + proclen = 0; for (i = 0; i < 4; ++i) { @@ -10400,7 +10442,61 @@ void test_nghttp2_http_ignore_regular_header(void) { CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == proclen); - nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + + /* use on_invalid_header_callback */ + callbacks.on_invalid_header_callback = pause_on_invalid_header_callback; + + nghttp2_session_server_new(&session, &callbacks, &ud); + + proclen = 0; + + ud.invalid_header_cb_called = 0; + + for (i = 0; i < 4; ++i) { + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos + proclen, + nghttp2_buf_len(&bufs.head->buf) - proclen); + CU_ASSERT_FATAL(rv > 0); + proclen += (size_t)rv; + CU_ASSERT(nghttp2_nv_equal(&bad_ansnv[i], &ud.nv)); + } + + CU_ASSERT(0 == ud.invalid_header_cb_called); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos + proclen, + nghttp2_buf_len(&bufs.head->buf) - proclen); + + CU_ASSERT_FATAL(rv > 0); + CU_ASSERT(1 == ud.invalid_header_cb_called); + CU_ASSERT(nghttp2_nv_equal(&bad_reqnv[4], &ud.nv)); + + proclen += (size_t)rv; + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos + proclen, + nghttp2_buf_len(&bufs.head->buf) - proclen); + + CU_ASSERT(rv > 0); + CU_ASSERT(nghttp2_nv_equal(&bad_ansnv[4], &ud.nv)); + + nghttp2_session_del(session); + + /* make sure that we can reset stream from + on_invalid_header_callback */ + callbacks.on_header_callback = on_header_callback; + callbacks.on_invalid_header_callback = reset_on_invalid_header_callback; + + nghttp2_session_server_new(&session, &callbacks, &ud); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(rv == (ssize_t)nghttp2_buf_len(&bufs.head->buf)); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + CU_ASSERT(1 == item->frame.hd.stream_id); + nghttp2_session_del(session); nghttp2_bufs_free(&bufs); }