diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index ae7724e9..02a6cdab 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -1323,6 +1323,72 @@ size_t nghttp2_session_get_outbound_queue_size(nghttp2_session *session); int nghttp2_session_fail_session(nghttp2_session *session, nghttp2_error_code error_code); +/** + * @function + * + * Performs post-process of HTTP Upgrade request. This function can be + * called from both client and server, but the behavior is very + * different in each other. + * + * If called from client side, the |settings_payload| must be the + * value sent in HTTP2-Settings header field and must be decoded by + * base64url decoder. The |settings_payloadlen| is the length of + * |settings_payload|. The |settings_payload| is unpacked and its + * setting values will be submitted using + * nghttp2_submit_settings(). This means that the client application + * code does not need to submit SETTINGS by itself. The stream with + * stream ID=1 is opened and the |stream_user_data| is used for its + * stream_user_data. The opened stream becomes half-closed (local) + * state. + * + * If called from server side, the |settings_payload| must be the + * value received in HTTP2-Settings header field and must be decoded + * by base64url decoder. The |settings_payloadlen| is the length of + * |settings_payload|. It is treated as if the SETTINGS frame with + * that payload is received. Thus, callback functions for the + * reception of SETTINGS frame will be invoked. The stream with stream + * ID=1 is opened. The |stream_user_data| is ignored. The opened + * stream becomes half-closed (remote). + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * :enum:`NGHTTP2_ERR_NOMEM` + * Out of memory. + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * The |settings_payload| is badly formed. + * :enum:`NGHTTP2_ERR_PROTO` + * The stream ID 1 is already used or closed; or is not available; + * or the |settings_payload| does not include both + * NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS and + * NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE. + */ +int nghttp2_session_upgrade(nghttp2_session *session, + const uint8_t *settings_payload, + size_t settings_payloadlen, + void *stream_user_data); + +/** + * @function + * + * Serializes the SETTINGS values |iv| in the |buf|. The number of + * entry pointed by |iv| array is given by the |niv|. This function + * may reorder the pointers in |iv|. The |buf| must have enough region + * to hold serialized data. The required space for the |niv| entries + * are 8*|niv| bytes. This function is used mainly for creating + * SETTINGS payload to be sent with HTTP2-Settings header field in + * HTTP Upgrade request. The data written in |buf| is not still + * base64url encoded and the application is responsible to do that. + * + * This function returns the number of bytes written in |buf|, or one + * of the following negative error codes: + * + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * The |iv| contains duplicate settings ID or invalid value. + */ +ssize_t nghttp2_pack_settings_payload(uint8_t *buf, + nghttp2_settings_entry *iv, size_t niv); + /** * @function * diff --git a/lib/nghttp2_frame.c b/lib/nghttp2_frame.c index f98ad59d..7f476d93 100644 --- a/lib/nghttp2_frame.c +++ b/lib/nghttp2_frame.c @@ -467,7 +467,6 @@ ssize_t nghttp2_frame_pack_settings(uint8_t **buf_ptr, size_t *buflen_ptr, nghttp2_settings *frame) { ssize_t framelen = NGHTTP2_FRAME_HEAD_LENGTH + frame->hd.length; - size_t i; int r; r = nghttp2_reserve_buffer(buf_ptr, buflen_ptr, framelen); if(r != 0) { @@ -475,33 +474,55 @@ ssize_t nghttp2_frame_pack_settings(uint8_t **buf_ptr, size_t *buflen_ptr, } memset(*buf_ptr, 0, framelen); nghttp2_frame_pack_frame_hd(*buf_ptr, &frame->hd); - for(i = 0; i < frame->niv; ++i) { - int off = i*8; - nghttp2_put_uint32be(&(*buf_ptr)[8+off], frame->iv[i].settings_id); - nghttp2_put_uint32be(&(*buf_ptr)[12+off], frame->iv[i].value); - } + nghttp2_frame_pack_settings_payload(*buf_ptr + 8, frame->iv, frame->niv); return framelen; } +size_t nghttp2_frame_pack_settings_payload(uint8_t *buf, + nghttp2_settings_entry *iv, + size_t niv) +{ + size_t i; + for(i = 0; i < niv; ++i, buf += 8) { + nghttp2_put_uint32be(buf, iv[i].settings_id); + nghttp2_put_uint32be(buf + 4, iv[i].value); + } + return 8 * niv; +} + int nghttp2_frame_unpack_settings(nghttp2_settings *frame, const uint8_t *head, size_t headlen, const uint8_t *payload, size_t payloadlen) { - size_t i; + int rv; if(payloadlen % 8) { return NGHTTP2_ERR_INVALID_FRAME; } nghttp2_frame_unpack_frame_hd(&frame->hd, head); - frame->niv = payloadlen / 8; - frame->iv = malloc(frame->niv*sizeof(nghttp2_settings_entry)); - if(frame->iv == NULL) { + rv = nghttp2_frame_unpack_settings_payload(&frame->iv, &frame->niv, + payload, payloadlen); + if(rv != 0) { + return rv; + } + return 0; +} + +int nghttp2_frame_unpack_settings_payload(nghttp2_settings_entry **iv_ptr, + size_t *niv_ptr, + const uint8_t *payload, + size_t payloadlen) +{ + size_t i; + *niv_ptr = payloadlen / 8; + *iv_ptr = malloc((*niv_ptr)*sizeof(nghttp2_settings_entry)); + if(*iv_ptr == NULL) { return NGHTTP2_ERR_NOMEM; } - for(i = 0; i < frame->niv; ++i) { + for(i = 0; i < *niv_ptr; ++i) { size_t off = i*8; - frame->iv[i].settings_id = nghttp2_get_uint32(&payload[off]) & + (*iv_ptr)[i].settings_id = nghttp2_get_uint32(&payload[off]) & NGHTTP2_SETTINGS_ID_MASK; - frame->iv[i].value = nghttp2_get_uint32(&payload[4+off]); + (*iv_ptr)[i].value = nghttp2_get_uint32(&payload[4+off]); } return 0; } @@ -809,3 +830,20 @@ ssize_t nghttp2_nv_array_from_cstr(nghttp2_nv **nva_ptr, const char **nv) nghttp2_nv_array_sort(*nva_ptr, nvlen); return nvlen; } + +int nghttp2_settings_check_duplicate(const nghttp2_settings_entry *iv, + size_t niv) +{ + int check[NGHTTP2_SETTINGS_MAX+1]; + size_t i; + memset(check, 0, sizeof(check)); + for(i = 0; i < niv; ++i) { + if(iv[i].settings_id > NGHTTP2_SETTINGS_MAX || iv[i].settings_id == 0 || + check[iv[i].settings_id] == 1) { + return 0; + } else { + check[iv[i].settings_id] = 1; + } + } + return 1; +} diff --git a/lib/nghttp2_frame.h b/lib/nghttp2_frame.h index 12e13047..76beb29a 100644 --- a/lib/nghttp2_frame.h +++ b/lib/nghttp2_frame.h @@ -223,6 +223,16 @@ int nghttp2_frame_unpack_rst_stream(nghttp2_rst_stream *frame, ssize_t nghttp2_frame_pack_settings(uint8_t **buf_ptr, size_t *buflen_ptr, nghttp2_settings *frame); +/* + * Packs the |iv|, which includes |niv| entries, in the |buf|, + * assuming the |buf| has at least 8 * |niv| bytes. + * + * Returns the number of bytes written into the |buf|. + */ +size_t nghttp2_frame_pack_settings_payload(uint8_t *buf, + nghttp2_settings_entry *iv, + size_t niv); + /* * Unpacks SETTINGS wire format into |frame|. * @@ -239,6 +249,23 @@ int nghttp2_frame_unpack_settings(nghttp2_settings *frame, const uint8_t *payload, size_t payloadlen); +/* + * Unpacks SETTINGS payload into |*iv_ptr|. The number of entries are + * assigned to the |*niv_ptr|. This function allocates enough memory + * to store the result in |*iv_ptr|. The caller is responsible to free + * |*iv_ptr| after its use. + * + * This function returns 0 if it succeeds or one of the following + * negative error codes: + * + * NGHTTP2_ERR_NOMEM + * Out of memory. + */ +int nghttp2_frame_unpack_settings_payload(nghttp2_settings_entry **iv_ptr, + size_t *niv_ptr, + const uint8_t *payload, + size_t payloadlen); + /* * Packs PUSH_PROMISE frame |frame| in wire format and store it in * |*buf_ptr|. The capacity of |*buf_ptr| is |*buflen_ptr| bytes. @@ -600,4 +627,13 @@ void nghttp2_nv_array_del(nghttp2_nv *nva); int nghttp2_nv_array_check_null(nghttp2_nv *nva, size_t nvlen); +/* + * Checks that the |iv|, which includes |niv| entries, does not have + * duplicate IDs. + * + * This function returns nonzero if it succeeds, or 0. + */ +int nghttp2_settings_check_duplicate(const nghttp2_settings_entry *iv, + size_t niv); + #endif /* NGHTTP2_FRAME_H */ diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 15981044..55d3e30f 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -357,7 +357,7 @@ int nghttp2_session_add_frame(nghttp2_session *session, break; } case NGHTTP2_SETTINGS: - /* Should NGHTTP2_SETTINGS have higher priority? */ + /* Should NGHTTP2_SETTINGS have higher priority? Yes */ item->pri = -1; break; case NGHTTP2_PUSH_PROMISE: { @@ -2986,3 +2986,69 @@ int nghttp2_session_set_option(nghttp2_session *session, } return 0; } + +int nghttp2_session_upgrade(nghttp2_session *session, + const uint8_t *settings_payload, + size_t settings_payloadlen, + void *stream_user_data) +{ + nghttp2_stream *stream; + nghttp2_frame frame; + nghttp2_settings_entry *iv; + size_t niv; + int rv; + int max_conn_val_seen = 0; + int ini_win_size_seen = 0; + size_t i; + + if((!session->server && session->next_stream_id != 1) || + (session->server && session->last_recv_stream_id >= 1)) { + return NGHTTP2_ERR_PROTO; + } + if(settings_payloadlen % 8) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + rv = nghttp2_frame_unpack_settings_payload(&iv, &niv, settings_payload, + settings_payloadlen); + if(rv != 0) { + return rv; + } + for(i = 0; i < niv; ++i) { + if(iv[i].settings_id == NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS) { + max_conn_val_seen = 1; + } else if(iv[i].settings_id == NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE) { + ini_win_size_seen = 1; + } + } + if(!max_conn_val_seen || !ini_win_size_seen) { + free(iv); + return NGHTTP2_ERR_PROTO; + } + if(session->server) { + memset(&frame.hd, 0, sizeof(frame.hd)); + frame.settings.iv = iv; + frame.settings.niv = niv; + rv = nghttp2_session_on_settings_received(session, &frame); + } else { + rv = nghttp2_submit_settings(session, iv, niv); + } + free(iv); + if(rv != 0) { + return rv; + } + stream = nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_END_STREAM, + 0, NGHTTP2_STREAM_OPENING, + session->server ? + NULL : stream_user_data); + if(stream == NULL) { + return NGHTTP2_ERR_NOMEM; + } + if(session->server) { + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + session->last_recv_stream_id = 1; + } else { + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR); + session->next_stream_id += 2; + } + return 0; +} diff --git a/lib/nghttp2_submit.c b/lib/nghttp2_submit.c index a6270c4e..edd22d8a 100644 --- a/lib/nghttp2_submit.c +++ b/lib/nghttp2_submit.c @@ -153,17 +153,9 @@ int nghttp2_submit_settings(nghttp2_session *session, { nghttp2_frame *frame; nghttp2_settings_entry *iv_copy; - int check[NGHTTP2_SETTINGS_MAX+1]; - size_t i; int r; - memset(check, 0, sizeof(check)); - for(i = 0; i < niv; ++i) { - if(iv[i].settings_id > NGHTTP2_SETTINGS_MAX || iv[i].settings_id == 0 || - check[iv[i].settings_id] == 1) { - return NGHTTP2_ERR_INVALID_ARGUMENT; - } else { - check[iv[i].settings_id] = 1; - } + if(!nghttp2_settings_check_duplicate(iv, niv)) { + return NGHTTP2_ERR_INVALID_ARGUMENT; } frame = malloc(sizeof(nghttp2_frame)); if(frame == NULL) { @@ -297,3 +289,13 @@ int nghttp2_submit_data(nghttp2_session *session, uint8_t flags, } return r; } + +ssize_t nghttp2_pack_settings_payload(uint8_t *buf, + nghttp2_settings_entry *iv, size_t niv) +{ + if(!nghttp2_settings_check_duplicate(iv, niv)) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + nghttp2_frame_iv_sort(iv, niv); + return nghttp2_frame_pack_settings_payload(buf, iv, niv); +} diff --git a/tests/main.c b/tests/main.c index 16c56fb8..bd6eb55a 100644 --- a/tests/main.c +++ b/tests/main.c @@ -124,6 +124,7 @@ int main(int argc, char* argv[]) test_nghttp2_session_send_push_promise) || !CU_add_test(pSuite, "session_is_my_stream_id", test_nghttp2_session_is_my_stream_id) || + !CU_add_test(pSuite, "session_upgrade", test_nghttp2_session_upgrade) || !CU_add_test(pSuite, "submit_response", test_nghttp2_submit_response) || !CU_add_test(pSuite, "submit_response_without_data", test_nghttp2_submit_response_without_data) || @@ -183,6 +184,8 @@ int main(int argc, char* argv[]) test_nghttp2_session_set_option) || !CU_add_test(pSuite, "session_data_backoff_by_high_pri_frame", test_nghttp2_session_data_backoff_by_high_pri_frame) || + !CU_add_test(pSuite, "pack_settings_payload", + test_nghttp2_pack_settings_payload) || !CU_add_test(pSuite, "frame_nv_sort", test_nghttp2_frame_nv_sort) || !CU_add_test(pSuite, "frame_nv_downcase", test_nghttp2_frame_nv_downcase) || @@ -207,6 +210,8 @@ int main(int argc, char* argv[]) test_nghttp2_frame_pack_window_update) || !CU_add_test(pSuite, "nv_array_from_cstr", test_nghttp2_nv_array_from_cstr) || + !CU_add_test(pSuite, "settings_check_duplicate", + test_nghttp2_settings_check_duplicate) || !CU_add_test(pSuite, "hd_deflate", test_nghttp2_hd_deflate) || !CU_add_test(pSuite, "hd_inflate_indname_inc", test_nghttp2_hd_inflate_indname_inc) || diff --git a/tests/nghttp2_frame_test.c b/tests/nghttp2_frame_test.c index b0f5db4c..6839f0b3 100644 --- a/tests/nghttp2_frame_test.c +++ b/tests/nghttp2_frame_test.c @@ -417,3 +417,27 @@ void test_nghttp2_nv_array_from_cstr(void) free(bigval); } + +void test_nghttp2_settings_check_duplicate(void) +{ + nghttp2_settings_entry set[3]; + + set[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + set[0].value = 1; + set[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + set[1].value = 1023; + + CU_ASSERT(nghttp2_settings_check_duplicate(set, 2)); + + set[1] = set[0]; + CU_ASSERT(0 == nghttp2_settings_check_duplicate(set, 2)); + + /* Out-of-bound data is error */ + set[0].settings_id = NGHTTP2_SETTINGS_MAX + 1; + CU_ASSERT(0 == nghttp2_settings_check_duplicate(set, 1)); + + /* settings_id == 0 is error */ + set[0].settings_id = 0; + CU_ASSERT(0 == nghttp2_settings_check_duplicate(set, 1)); +} + diff --git a/tests/nghttp2_frame_test.h b/tests/nghttp2_frame_test.h index d1ad9e41..8649a2d3 100644 --- a/tests/nghttp2_frame_test.h +++ b/tests/nghttp2_frame_test.h @@ -38,5 +38,6 @@ void test_nghttp2_frame_pack_ping(void); void test_nghttp2_frame_pack_goaway(void); void test_nghttp2_frame_pack_window_update(void); void test_nghttp2_nv_array_from_cstr(void); +void test_nghttp2_settings_check_duplicate(void); #endif /* NGHTTP2_FRAME_TEST_H */ diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index 170190af..cd0e43f6 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -1451,6 +1451,91 @@ void test_nghttp2_session_is_my_stream_id(void) nghttp2_session_del(session); } +void test_nghttp2_session_upgrade(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + uint8_t settings_payload[128]; + size_t settings_payloadlen; + nghttp2_settings_entry iv[16]; + nghttp2_stream *stream; + nghttp2_outbound_item *item; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + iv[0].value = 1; + iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + iv[1].value = 4095; + settings_payloadlen = nghttp2_pack_settings_payload(settings_payload, iv, 2); + + /* Check client side */ + nghttp2_session_client_new(&session, &callbacks, NULL); + CU_ASSERT(0 == nghttp2_session_upgrade(session, settings_payload, + settings_payloadlen, &callbacks)); + stream = nghttp2_session_get_stream(session, 1); + CU_ASSERT(stream != NULL); + CU_ASSERT(&callbacks == stream->stream_user_data); + CU_ASSERT(NGHTTP2_SHUT_WR == stream->shut_flags); + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_SETTINGS == OB_CTRL_TYPE(item)); + CU_ASSERT(2 == OB_CTRL(item)->settings.niv); + CU_ASSERT(NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS == + OB_CTRL(item)->settings.iv[0].settings_id); + CU_ASSERT(1 == OB_CTRL(item)->settings.iv[0].value); + CU_ASSERT(NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE == + OB_CTRL(item)->settings.iv[1].settings_id); + CU_ASSERT(4095 == OB_CTRL(item)->settings.iv[1].value); + + /* Call nghttp2_session_upgrade() again is error */ + CU_ASSERT(NGHTTP2_ERR_PROTO == nghttp2_session_upgrade(session, + settings_payload, + settings_payloadlen, + &callbacks)); + nghttp2_session_del(session); + + /* Check server side */ + nghttp2_session_server_new(&session, &callbacks, NULL); + CU_ASSERT(0 == nghttp2_session_upgrade(session, settings_payload, + settings_payloadlen, &callbacks)); + stream = nghttp2_session_get_stream(session, 1); + CU_ASSERT(stream != NULL); + CU_ASSERT(NULL == stream->stream_user_data); + CU_ASSERT(NGHTTP2_SHUT_RD == stream->shut_flags); + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + CU_ASSERT(1 == + session->remote_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS]); + CU_ASSERT(4095 == + session->remote_settings[NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE]); + /* Call nghttp2_session_upgrade() again is error */ + CU_ASSERT(NGHTTP2_ERR_PROTO == nghttp2_session_upgrade(session, + settings_payload, + settings_payloadlen, + &callbacks)); + nghttp2_session_del(session); + + /* Check required settings */ + iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + iv[0].value = 1; + settings_payloadlen = nghttp2_pack_settings_payload(settings_payload, iv, 1); + + nghttp2_session_client_new(&session, &callbacks, NULL); + CU_ASSERT(NGHTTP2_ERR_PROTO == + nghttp2_session_upgrade(session, settings_payload, + settings_payloadlen, NULL)); + nghttp2_session_del(session); + + iv[0].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + iv[0].value = 4095; + settings_payloadlen = nghttp2_pack_settings_payload(settings_payload, iv, 1); + + nghttp2_session_client_new(&session, &callbacks, NULL); + CU_ASSERT(NGHTTP2_ERR_PROTO == + nghttp2_session_upgrade(session, settings_payload, + settings_payloadlen, NULL)); + nghttp2_session_del(session); +} + void test_nghttp2_submit_response(void) { nghttp2_session *session; @@ -2857,3 +2942,29 @@ void test_nghttp2_session_data_backoff_by_high_pri_frame(void) nghttp2_session_del(session); } + +void test_nghttp2_pack_settings_payload(void) +{ + nghttp2_settings_entry iv[2]; + uint8_t buf[64]; + size_t len; + nghttp2_settings_entry *resiv; + size_t resniv; + + iv[0].settings_id = NGHTTP2_SETTINGS_FLOW_CONTROL_OPTIONS; + iv[0].value = 1; + iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + iv[1].value = 4095; + + len = nghttp2_pack_settings_payload(buf, iv, 2); + CU_ASSERT(16 == len); + CU_ASSERT(0 == nghttp2_frame_unpack_settings_payload(&resiv, &resniv, + buf, len)); + CU_ASSERT(2 == resniv); + CU_ASSERT(NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE == resiv[0].settings_id); + CU_ASSERT(4095 == resiv[0].value); + CU_ASSERT(NGHTTP2_SETTINGS_FLOW_CONTROL_OPTIONS == resiv[1].settings_id); + CU_ASSERT(1 == resiv[1].value); + + free(resiv); +} diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index deb9dccb..d40dd980 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -51,6 +51,7 @@ void test_nghttp2_session_send_priority(void); void test_nghttp2_session_send_rst_stream(void); void test_nghttp2_session_send_push_promise(void); void test_nghttp2_session_is_my_stream_id(void); +void test_nghttp2_session_upgrade(void); void test_nghttp2_submit_response(void); void test_nghttp2_submit_response_without_data(void); void test_nghttp2_submit_request_with_data(void); @@ -82,5 +83,6 @@ void test_nghttp2_session_on_ctrl_not_send(void); void test_nghttp2_session_get_outbound_queue_size(void); void test_nghttp2_session_set_option(void); void test_nghttp2_session_data_backoff_by_high_pri_frame(void); +void test_nghttp2_pack_settings_payload(void); #endif /* NGHTTP2_SESSION_TEST_H */