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 78435578..e85b97cd 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -1721,6 +1721,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 * @@ -2079,6 +2138,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 * @@ -2738,12 +2820,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 f93cd172..0d8734c4 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -3345,6 +3345,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) { @@ -3608,6 +3633,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 3e2d69aa..95117092 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) { @@ -10613,6 +10652,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); @@ -10630,6 +10670,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) { @@ -10650,7 +10692,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); }