From 8c2386c221c06e779fa706a717c713591e932fa9 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Tue, 14 Jun 2022 23:30:24 +0900 Subject: [PATCH] Add a server option to fallback to RFC 7540 priorities Add nghttp2_option_set_server_fallback_rfc7540_priorities. If it is set to nonzero, and server submits SETTINGS_NO_RFC7540_PRIORITIES = 1, but it does not receive SETTINGS_NO_RFC7540_PRIORITIES from client, server falls back to RFC 7540 priorities. Only minimal set of features are enabled in this fallback case. --- doc/Makefile.am | 1 + lib/includes/nghttp2/nghttp2.h | 18 ++++ lib/nghttp2_option.c | 6 ++ lib/nghttp2_option.h | 5 + lib/nghttp2_session.c | 35 ++++-- lib/nghttp2_session.h | 5 +- tests/main.c | 2 + tests/nghttp2_session_test.c | 190 +++++++++++++++++++++++++++++++++ tests/nghttp2_session_test.h | 1 + 9 files changed, 254 insertions(+), 9 deletions(-) diff --git a/doc/Makefile.am b/doc/Makefile.am index 6256ab45..ba5b7888 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -69,6 +69,7 @@ APIDOCS= \ nghttp2_option_set_no_http_messaging.rst \ nghttp2_option_set_no_recv_client_magic.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 \ nghttp2_option_set_max_outbound_ack.rst \ nghttp2_option_set_max_settings.rst \ diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index 71f957f5..effed7ff 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -2732,6 +2732,24 @@ NGHTTP2_EXTERN void nghttp2_option_set_max_outbound_ack(nghttp2_option *option, NGHTTP2_EXTERN void nghttp2_option_set_max_settings(nghttp2_option *option, size_t val); +/** + * @function + * + * This option, if set to nonzero, allows server to fallback to + * :rfc:`7540` priorities if SETTINGS_NO_RFC7540_PRIORITIES was not + * received from client, and server submitted + * :enum:`nghttp2_settings_id.NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES` + * = 1 via `nghttp2_submit_settings()`. Most of the advanced + * functionality for RFC 7540 priorities are still disabled. This + * fallback only enables the minimal feature set of RFC 7540 + * priorities to deal with priority signaling from client. + * + * Client session ignores this option. + */ +NGHTTP2_EXTERN void +nghttp2_option_set_server_fallback_rfc7540_priorities(nghttp2_option *option, + int val); + /** * @function * diff --git a/lib/nghttp2_option.c b/lib/nghttp2_option.c index ab98df59..ffd20322 100644 --- a/lib/nghttp2_option.c +++ b/lib/nghttp2_option.c @@ -130,3 +130,9 @@ void nghttp2_option_set_max_settings(nghttp2_option *option, size_t val) { option->opt_set_mask |= NGHTTP2_OPT_MAX_SETTINGS; option->max_settings = val; } + +void nghttp2_option_set_server_fallback_rfc7540_priorities( + nghttp2_option *option, int val) { + option->opt_set_mask |= NGHTTP2_OPT_SERVER_FALLBACK_RFC7540_PRIORITIES; + option->server_fallback_rfc7540_priorities = val; +} diff --git a/lib/nghttp2_option.h b/lib/nghttp2_option.h index 939729fd..0400d56d 100644 --- a/lib/nghttp2_option.h +++ b/lib/nghttp2_option.h @@ -68,6 +68,7 @@ typedef enum { NGHTTP2_OPT_NO_CLOSED_STREAMS = 1 << 10, NGHTTP2_OPT_MAX_OUTBOUND_ACK = 1 << 11, NGHTTP2_OPT_MAX_SETTINGS = 1 << 12, + NGHTTP2_OPT_SERVER_FALLBACK_RFC7540_PRIORITIES = 1 << 13, } nghttp2_option_flag; /** @@ -127,6 +128,10 @@ struct nghttp2_option { * NGHTTP2_OPT_NO_CLOSED_STREAMS */ int no_closed_streams; + /** + * NGHTTP2_OPT_SERVER_FALLBACK_RFC7540_PRIORITIES + */ + int server_fallback_rfc7540_priorities; /** * NGHTTP2_OPT_USER_RECV_EXT_TYPES */ diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 7a43f4d7..dc8eb349 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -144,6 +144,11 @@ static int session_detect_idle_stream(nghttp2_session *session, return 0; } +static int session_no_rfc7540_pri_no_fallback(nghttp2_session *session) { + return session->pending_no_rfc7540_priorities == 1 && + !session->fallback_rfc7540_priorities; +} + static int check_ext_type_set(const uint8_t *ext_types, uint8_t type) { return (ext_types[type / 8] & (1 << (type & 0x7))) > 0; } @@ -554,6 +559,13 @@ static int session_new(nghttp2_session **session_ptr, option->max_settings) { (*session_ptr)->max_settings = option->max_settings; } + + if ((option->opt_set_mask & + NGHTTP2_OPT_SERVER_FALLBACK_RFC7540_PRIORITIES) && + option->server_fallback_rfc7540_priorities) { + (*session_ptr)->opt_flags |= + NGHTTP2_OPTMASK_SERVER_FALLBACK_RFC7540_PRIORITIES; + } } rv = nghttp2_hd_deflate_init2(&(*session_ptr)->hd_deflater, @@ -810,7 +822,8 @@ int nghttp2_session_reprioritize_stream( nghttp2_priority_spec pri_spec_default; const nghttp2_priority_spec *pri_spec = pri_spec_in; - assert(session->pending_no_rfc7540_priorities != 1); + assert((!session->server && session->pending_no_rfc7540_priorities != 1) || + (session->server && !session_no_rfc7540_pri_no_fallback(session))); assert(pri_spec->stream_id != stream->stream_id); if (!nghttp2_stream_in_dep_tree(stream)) { @@ -1296,7 +1309,7 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, return NULL; } - if (session->pending_no_rfc7540_priorities == 1) { + if (session_no_rfc7540_pri_no_fallback(session)) { stream->flags |= NGHTTP2_STREAM_FLAG_NO_RFC7540_PRIORITIES; } } @@ -1309,7 +1322,7 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, stream_alloc = 1; } - if (session->pending_no_rfc7540_priorities == 1 || + if (session_no_rfc7540_pri_no_fallback(session) || session->remote_settings.no_rfc7540_priorities == 1) { /* For client which has not received server SETTINGS_NO_RFC7540_PRIORITIES = 1, send a priority signal @@ -1369,7 +1382,7 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, (int32_t)session->local_settings.initial_window_size, stream_user_data, mem); - if (session->pending_no_rfc7540_priorities == 1) { + if (session_no_rfc7540_pri_no_fallback(session)) { stream->seq = session->stream_seq++; } @@ -4440,7 +4453,7 @@ int nghttp2_session_on_priority_received(nghttp2_session *session, int rv; nghttp2_stream *stream; - assert(session->pending_no_rfc7540_priorities != 1); + assert(!session_no_rfc7540_pri_no_fallback(session)); if (frame->hd.stream_id == 0) { return session_handle_invalid_connection(session, frame, NGHTTP2_ERR_PROTO, @@ -4499,7 +4512,7 @@ static int session_process_priority_frame(nghttp2_session *session) { nghttp2_inbound_frame *iframe = &session->iframe; nghttp2_frame *frame = &iframe->frame; - assert(session->pending_no_rfc7540_priorities != 1); + assert(!session_no_rfc7540_pri_no_fallback(session)); nghttp2_frame_unpack_priority_payload(&frame->priority, iframe->sbuf.pos); @@ -4927,6 +4940,12 @@ int nghttp2_session_on_settings_received(nghttp2_session *session, if (session->remote_settings.no_rfc7540_priorities == UINT32_MAX) { session->remote_settings.no_rfc7540_priorities = 0; + + if (session->server && session->pending_no_rfc7540_priorities && + (session->opt_flags & + NGHTTP2_OPTMASK_SERVER_FALLBACK_RFC7540_PRIORITIES)) { + session->fallback_rfc7540_priorities = 1; + } } if (!noack && !session_is_closing(session)) { @@ -6380,7 +6399,7 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, break; } - if (session->pending_no_rfc7540_priorities != 1 || + if (!session_no_rfc7540_pri_no_fallback(session) || iframe->payloadleft > sizeof(iframe->raw_sbuf)) { busy = 1; iframe->state = NGHTTP2_IB_IGN_PAYLOAD; @@ -6498,7 +6517,7 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, break; case NGHTTP2_PRIORITY: - if (session->pending_no_rfc7540_priorities != 1 && + if (!session_no_rfc7540_pri_no_fallback(session) && session->remote_settings.no_rfc7540_priorities != 1) { rv = session_process_priority_frame(session); if (nghttp2_is_fatal(rv)) { diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index b4afa7bb..164b5fd9 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -52,7 +52,8 @@ typedef enum { NGHTTP2_OPTMASK_NO_RECV_CLIENT_MAGIC = 1 << 1, NGHTTP2_OPTMASK_NO_HTTP_MESSAGING = 1 << 2, NGHTTP2_OPTMASK_NO_AUTO_PING_ACK = 1 << 3, - NGHTTP2_OPTMASK_NO_CLOSED_STREAMS = 1 << 4 + NGHTTP2_OPTMASK_NO_CLOSED_STREAMS = 1 << 4, + NGHTTP2_OPTMASK_SERVER_FALLBACK_RFC7540_PRIORITIES = 1 << 5 } nghttp2_optmask; /* @@ -340,6 +341,8 @@ struct nghttp2_session { /* Unacked local SETTINGS_NO_RFC7540_PRIORITIES value, which is effective before it is acknowledged. */ uint8_t pending_no_rfc7540_priorities; + /* Turn on fallback to RFC 7540 priorities; for server use only. */ + uint8_t fallback_rfc7540_priorities; /* Nonzero if the session is server side. */ uint8_t server; /* Flags indicating GOAWAY is sent and/or received. The flags are diff --git a/tests/main.c b/tests/main.c index 9d62f06b..92ce6679 100644 --- a/tests/main.c +++ b/tests/main.c @@ -339,6 +339,8 @@ int main(void) { test_nghttp2_session_set_stream_user_data) || !CU_add_test(pSuite, "session_no_rfc7540_priorities", test_nghttp2_session_no_rfc7540_priorities) || + !CU_add_test(pSuite, "session_server_fallback_rfc7540_priorities", + test_nghttp2_session_server_fallback_rfc7540_priorities) || !CU_add_test(pSuite, "http_mandatory_headers", test_nghttp2_http_mandatory_headers) || !CU_add_test(pSuite, "http_content_length", diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index 0cac46f7..cf5a775d 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -11637,6 +11637,196 @@ void test_nghttp2_session_no_rfc7540_priorities(void) { nghttp2_session_del(session); } +void test_nghttp2_session_server_fallback_rfc7540_priorities(void) { + nghttp2_session *session; + nghttp2_option *option; + nghttp2_session_callbacks callbacks; + nghttp2_frame frame; + nghttp2_bufs bufs; + nghttp2_buf *buf; + ssize_t rv; + nghttp2_settings_entry iv; + nghttp2_mem *mem; + nghttp2_hd_deflater deflater; + nghttp2_nv *nva; + size_t nvlen; + nghttp2_priority_spec pri_spec; + nghttp2_stream *anchor_stream, *stream; + my_user_data ud; + nghttp2_ext_priority_update priority_update; + static const uint8_t field_value[] = "u=0"; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + callbacks.on_frame_recv_callback = on_frame_recv_callback; + + nghttp2_option_new(&option); + nghttp2_option_set_server_fallback_rfc7540_priorities(option, 1); + + iv.settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; + iv.value = 1; + + /* Server falls back to RFC 7540 priorities. */ + nghttp2_session_server_new2(&session, &callbacks, &ud, option); + + rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, &iv, 1); + + CU_ASSERT(0 == rv); + + rv = nghttp2_session_send(session); + + CU_ASSERT(0 == rv); + + nghttp2_frame_settings_init(&frame.settings, NGHTTP2_FLAG_NONE, NULL, 0); + rv = nghttp2_frame_pack_settings(&bufs, &frame.settings); + + CU_ASSERT(0 == rv); + + nghttp2_frame_settings_free(&frame.settings, mem); + + buf = &bufs.head->buf; + rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv); + CU_ASSERT(1 == session->fallback_rfc7540_priorities); + + nghttp2_hd_deflate_init(&deflater, mem); + + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); + nghttp2_priority_spec_init(&pri_spec, 3, 111, 1); + nghttp2_frame_headers_init(&frame.headers, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_PRIORITY, + 1, NGHTTP2_HCAT_HEADERS, &pri_spec, nva, nvlen); + nghttp2_bufs_reset(&bufs); + rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); + + CU_ASSERT(0 == rv); + + nghttp2_frame_headers_free(&frame.headers, mem); + + buf = &bufs.head->buf; + rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv); + + anchor_stream = nghttp2_session_get_stream_raw(session, 3); + + CU_ASSERT(NGHTTP2_STREAM_IDLE == anchor_stream->state); + CU_ASSERT( + !(anchor_stream->flags & NGHTTP2_STREAM_FLAG_NO_RFC7540_PRIORITIES)); + CU_ASSERT(&session->root == anchor_stream->dep_prev); + + stream = nghttp2_session_get_stream(session, 1); + + CU_ASSERT(NGHTTP2_STREAM_OPENING == stream->state); + CU_ASSERT(!(stream->flags & NGHTTP2_STREAM_FLAG_NO_RFC7540_PRIORITIES)); + CU_ASSERT(anchor_stream == stream->dep_prev); + + /* Make sure that PRIORITY frame updates stream priority. */ + nghttp2_priority_spec_init(&pri_spec, 5, 1, 0); + nghttp2_frame_priority_init(&frame.priority, 1, &pri_spec); + nghttp2_bufs_reset(&bufs); + rv = nghttp2_frame_pack_priority(&bufs, &frame.priority); + + CU_ASSERT(0 == rv); + + nghttp2_frame_priority_free(&frame.priority); + + buf = &bufs.head->buf; + rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv); + + anchor_stream = nghttp2_session_get_stream_raw(session, 5); + + CU_ASSERT(NGHTTP2_STREAM_IDLE == anchor_stream->state); + CU_ASSERT(&session->root == anchor_stream->dep_prev); + CU_ASSERT(anchor_stream == stream->dep_prev); + + /* Make sure that PRIORITY_UPDATE frame is ignored. */ + frame.ext.payload = &priority_update; + nghttp2_frame_priority_update_init(&frame.ext, 1, (uint8_t *)field_value, + sizeof(field_value) - 1); + nghttp2_bufs_reset(&bufs); + nghttp2_frame_pack_priority_update(&bufs, &frame.ext); + + ud.frame_recv_cb_called = 0; + buf = &bufs.head->buf; + rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv); + CU_ASSERT(0 == ud.frame_recv_cb_called); + CU_ASSERT(NGHTTP2_EXTPRI_DEFAULT_URGENCY == stream->extpri); + + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + + /* Server does not fallback to RFC 7540 priorities. */ + nghttp2_session_server_new2(&session, &callbacks, &ud, option); + + rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, &iv, 1); + + CU_ASSERT(0 == rv); + + rv = nghttp2_session_send(session); + + CU_ASSERT(0 == rv); + + iv.settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; + iv.value = 0; + + nghttp2_frame_settings_init(&frame.settings, NGHTTP2_FLAG_NONE, + dup_iv(&iv, 1), 1); + nghttp2_bufs_reset(&bufs); + rv = nghttp2_frame_pack_settings(&bufs, &frame.settings); + + CU_ASSERT(0 == rv); + + nghttp2_frame_settings_free(&frame.settings, mem); + + buf = &bufs.head->buf; + rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv); + CU_ASSERT(0 == session->fallback_rfc7540_priorities); + + nghttp2_hd_deflate_init(&deflater, mem); + + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); + nghttp2_priority_spec_init(&pri_spec, 3, 111, 1); + nghttp2_frame_headers_init(&frame.headers, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_PRIORITY, + 1, NGHTTP2_HCAT_HEADERS, &pri_spec, nva, nvlen); + nghttp2_bufs_reset(&bufs); + rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); + + CU_ASSERT(0 == rv); + + nghttp2_frame_headers_free(&frame.headers, mem); + + buf = &bufs.head->buf; + rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv); + CU_ASSERT(NULL == nghttp2_session_get_stream_raw(session, 3)); + + stream = nghttp2_session_get_stream(session, 1); + + CU_ASSERT(NGHTTP2_STREAM_OPENING == stream->state); + CU_ASSERT(stream->flags & NGHTTP2_STREAM_FLAG_NO_RFC7540_PRIORITIES); + + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + + nghttp2_option_del(option); + nghttp2_bufs_free(&bufs); +} + static void check_nghttp2_http_recv_headers_fail( nghttp2_session *session, nghttp2_hd_deflater *deflater, int32_t stream_id, int stream_state, const nghttp2_nv *nva, size_t nvlen) { diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index add36d9f..a46d0220 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -166,6 +166,7 @@ void test_nghttp2_session_pause_data(void); void test_nghttp2_session_no_closed_streams(void); void test_nghttp2_session_set_stream_user_data(void); void test_nghttp2_session_no_rfc7540_priorities(void); +void test_nghttp2_session_server_fallback_rfc7540_priorities(void); void test_nghttp2_http_mandatory_headers(void); void test_nghttp2_http_content_length(void); void test_nghttp2_http_content_length_mismatch(void);