Add API for HTTP Upgrade

This commit is contained in:
Tatsuhiro Tsujikawa 2013-08-03 18:05:14 +09:00
parent 5594f0ef0b
commit 737ac01d91
10 changed files with 375 additions and 24 deletions

View File

@ -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
*

View File

@ -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;
}

View File

@ -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 */

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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) ||

View File

@ -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));
}

View File

@ -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 */

View File

@ -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);
}

View File

@ -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 */