diff --git a/doc/Makefile.am b/doc/Makefile.am index 61e182cd..9a032c32 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -69,6 +69,7 @@ APIDOCS= \ nghttp2_option_set_no_closed_streams.rst \ nghttp2_option_set_no_http_messaging.rst \ nghttp2_option_set_no_recv_client_magic.rst \ + nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation.rst \ nghttp2_option_set_peer_max_concurrent_streams.rst \ nghttp2_option_set_server_fallback_rfc7540_priorities.rst \ nghttp2_option_set_user_recv_extension_type.rst \ diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index bb8aa1d3..61a14d99 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -2750,6 +2750,18 @@ NGHTTP2_EXTERN void nghttp2_option_set_server_fallback_rfc7540_priorities(nghttp2_option *option, int val); +/** + * @function + * + * This option, if set to nonzero, turns off RFC 9113 leading and + * trailing white spaces validation against HTTP field value. Some + * important fields, such as HTTP/2 pseudo header fields, are + * validated more strictly and this option does not apply to them. + */ +NGHTTP2_EXTERN void +nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation( + nghttp2_option *option, int val); + /** * @function * diff --git a/lib/nghttp2_http.c b/lib/nghttp2_http.c index 2522472a..83e5e668 100644 --- a/lib/nghttp2_http.c +++ b/lib/nghttp2_http.c @@ -388,6 +388,10 @@ int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream, case NGHTTP2_TOKEN_HOST: if (session->server || frame->hd.type == NGHTTP2_PUSH_PROMISE) { rv = nghttp2_check_authority(nv->value->base, nv->value->len); + } else if ( + stream->flags & + NGHTTP2_STREAM_FLAG_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION) { + rv = nghttp2_check_header_value(nv->value->base, nv->value->len); } else { rv = nghttp2_check_header_value_rfc9113(nv->value->base, nv->value->len); } @@ -399,13 +403,20 @@ int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream, /* Check the value consists of just white spaces, which was done in check_pseudo_header before nghttp2_check_header_value_rfc9113 has been introduced. */ - if (lws(nv->value->base, nv->value->len)) { + if ((stream->flags & + NGHTTP2_STREAM_FLAG_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION) && + lws(nv->value->base, nv->value->len)) { rv = 0; break; } /* fall through */ default: - rv = nghttp2_check_header_value_rfc9113(nv->value->base, nv->value->len); + if (stream->flags & + NGHTTP2_STREAM_FLAG_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION) { + rv = nghttp2_check_header_value(nv->value->base, nv->value->len); + } else { + rv = nghttp2_check_header_value_rfc9113(nv->value->base, nv->value->len); + } } if (rv == 0) { diff --git a/lib/nghttp2_option.c b/lib/nghttp2_option.c index ffd20322..ee0cd0f0 100644 --- a/lib/nghttp2_option.c +++ b/lib/nghttp2_option.c @@ -136,3 +136,10 @@ void nghttp2_option_set_server_fallback_rfc7540_priorities( option->opt_set_mask |= NGHTTP2_OPT_SERVER_FALLBACK_RFC7540_PRIORITIES; option->server_fallback_rfc7540_priorities = val; } + +void nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation( + nghttp2_option *option, int val) { + option->opt_set_mask |= + NGHTTP2_OPT_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION; + option->no_rfc9113_leading_and_trailing_ws_validation = val; +} diff --git a/lib/nghttp2_option.h b/lib/nghttp2_option.h index 0400d56d..b228a075 100644 --- a/lib/nghttp2_option.h +++ b/lib/nghttp2_option.h @@ -69,6 +69,7 @@ typedef enum { NGHTTP2_OPT_MAX_OUTBOUND_ACK = 1 << 11, NGHTTP2_OPT_MAX_SETTINGS = 1 << 12, NGHTTP2_OPT_SERVER_FALLBACK_RFC7540_PRIORITIES = 1 << 13, + NGHTTP2_OPT_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION = 1 << 14, } nghttp2_option_flag; /** @@ -132,6 +133,10 @@ struct nghttp2_option { * NGHTTP2_OPT_SERVER_FALLBACK_RFC7540_PRIORITIES */ int server_fallback_rfc7540_priorities; + /** + * NGHTTP2_OPT_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION + */ + int no_rfc9113_leading_and_trailing_ws_validation; /** * NGHTTP2_OPT_USER_RECV_EXT_TYPES */ diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 134b0efb..93f3f07c 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -566,6 +566,13 @@ static int session_new(nghttp2_session **session_ptr, (*session_ptr)->opt_flags |= NGHTTP2_OPTMASK_SERVER_FALLBACK_RFC7540_PRIORITIES; } + + if ((option->opt_set_mask & + NGHTTP2_OPT_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION) && + option->no_rfc9113_leading_and_trailing_ws_validation) { + (*session_ptr)->opt_flags |= + NGHTTP2_OPTMASK_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION; + } } rv = nghttp2_hd_deflate_init2(&(*session_ptr)->hd_deflater, @@ -1296,6 +1303,11 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, mem = &session->mem; stream = nghttp2_session_get_stream_raw(session, stream_id); + if (session->opt_flags & + NGHTTP2_OPTMASK_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION) { + flags |= NGHTTP2_STREAM_FLAG_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION; + } + if (stream) { assert(stream->state == NGHTTP2_STREAM_IDLE); assert((stream->flags & NGHTTP2_STREAM_FLAG_NO_RFC7540_PRIORITIES) || diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index 164b5fd9..34d2d585 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -53,7 +53,8 @@ typedef enum { NGHTTP2_OPTMASK_NO_HTTP_MESSAGING = 1 << 2, NGHTTP2_OPTMASK_NO_AUTO_PING_ACK = 1 << 3, NGHTTP2_OPTMASK_NO_CLOSED_STREAMS = 1 << 4, - NGHTTP2_OPTMASK_SERVER_FALLBACK_RFC7540_PRIORITIES = 1 << 5 + NGHTTP2_OPTMASK_SERVER_FALLBACK_RFC7540_PRIORITIES = 1 << 5, + NGHTTP2_OPTMASK_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION = 1 << 6, } nghttp2_optmask; /* diff --git a/lib/nghttp2_stream.h b/lib/nghttp2_stream.h index 0ec77bf0..7a8e4c6c 100644 --- a/lib/nghttp2_stream.h +++ b/lib/nghttp2_stream.h @@ -96,6 +96,9 @@ typedef enum { NGHTTP2_STREAM_FLAG_NO_RFC7540_PRIORITIES = 0x10, /* Ignore client RFC 9218 priority signal. */ NGHTTP2_STREAM_FLAG_IGNORE_CLIENT_PRIORITIES = 0x20, + /* Indicates that RFC 9113 leading and trailing white spaces + validation against a field value is not performed. */ + NGHTTP2_STREAM_FLAG_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION = 0x40, } nghttp2_stream_flag; /* HTTP related flags to enforce HTTP semantics */ diff --git a/tests/main.c b/tests/main.c index bde299a5..6ff97d05 100644 --- a/tests/main.c +++ b/tests/main.c @@ -363,6 +363,9 @@ int main(void) { test_nghttp2_http_push_promise) || !CU_add_test(pSuite, "http_head_method_upgrade_workaround", test_nghttp2_http_head_method_upgrade_workaround) || + !CU_add_test( + pSuite, "http_no_rfc9113_leading_and_trailing_ws_validation", + test_nghttp2_http_no_rfc9113_leading_and_trailing_ws_validation) || !CU_add_test(pSuite, "frame_pack_headers", test_nghttp2_frame_pack_headers) || !CU_add_test(pSuite, "frame_pack_headers_frame_too_large", diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index a1a969ba..08152d47 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -13224,3 +13224,83 @@ void test_nghttp2_http_head_method_upgrade_workaround(void) { nghttp2_session_del(session); nghttp2_bufs_free(&bufs); } + +void test_nghttp2_http_no_rfc9113_leading_and_trailing_ws_validation(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_bufs bufs; + ssize_t rv; + const nghttp2_nv ws_reqnv[] = { + MAKE_NV(":path", "/"), + MAKE_NV(":method", "GET"), + MAKE_NV(":authority", "localhost"), + MAKE_NV(":scheme", "https"), + MAKE_NV("foo", "bar "), + }; + nghttp2_outbound_item *item; + nghttp2_option *option; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + /* By default, the leading and trailing white spaces validation is + enabled as per RFC 9113. */ + nghttp2_session_server_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + rv = pack_headers(&bufs, &deflater, 1, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, + ws_reqnv, ARRLEN(ws_reqnv), mem); + + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + + /* Turn off the validation */ + nghttp2_option_new(&option); + nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation(option, 1); + + nghttp2_session_server_new2(&session, &callbacks, NULL, option); + + nghttp2_hd_deflate_init(&deflater, mem); + + rv = pack_headers(&bufs, &deflater, 1, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, + ws_reqnv, ARRLEN(ws_reqnv), mem); + + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NULL == item); + + nghttp2_bufs_reset(&bufs); + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + nghttp2_option_del(option); + + nghttp2_bufs_free(&bufs); +} diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index e2710e34..e776813f 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -178,5 +178,6 @@ void test_nghttp2_http_ignore_content_length(void); void test_nghttp2_http_record_request_method(void); void test_nghttp2_http_push_promise(void); void test_nghttp2_http_head_method_upgrade_workaround(void); +void test_nghttp2_http_no_rfc9113_leading_and_trailing_ws_validation(void); #endif /* NGHTTP2_SESSION_TEST_H */