From 84cbebf4f7052e8c297c54d9002d90ddb7095faa Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Thu, 25 Jul 2013 01:49:05 +0900 Subject: [PATCH] Implement server push --- lib/includes/nghttp2/nghttp2.h | 45 +++- lib/nghttp2_frame.c | 82 +++++++ lib/nghttp2_frame.h | 83 +++++++ lib/nghttp2_outbound_item.c | 3 + lib/nghttp2_session.c | 366 ++++++++++++++++++++++++++---- lib/nghttp2_session.h | 17 ++ lib/nghttp2_stream.c | 5 + lib/nghttp2_stream.h | 11 +- lib/nghttp2_submit.c | 33 +++ tests/main.c | 16 ++ tests/nghttp2_frame_test.c | 38 +++- tests/nghttp2_frame_test.h | 1 + tests/nghttp2_session_test.c | 402 +++++++++++++++++++++++++++++++-- tests/nghttp2_session_test.h | 7 + tests/nghttp2_test_helper.c | 8 + 15 files changed, 1049 insertions(+), 68 deletions(-) diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index ae22d6e3..e6125e4d 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -575,6 +575,14 @@ typedef struct { * The promised stream ID */ int32_t promised_stream_id; + /** + * The name/value pairs. + */ + nghttp2_nv *nva; + /** + * The number of name/value pairs in |nva|. + */ + size_t nvlen; } nghttp2_push_promise; /** @@ -1405,11 +1413,10 @@ int nghttp2_submit_response(nghttp2_session *session, /** * @function * - * Submits SYN_STREAM frame. The |flags| is bitwise OR of the + * Submits HEADERS frame. The |flags| is bitwise OR of the * following values: * * * :enum:`NGHTTP2_FLAG_END_STREAM` - * * :enum:`NGHTTP2_FLAG_END_HEADERS` * * :enum:`NGHTTP2_FLAG_PRIORITY` * * If |flags| includes :enum:`NGHTTP2_FLAG_END_STREAM`, this frame has @@ -1431,8 +1438,10 @@ int nghttp2_submit_response(nghttp2_session *session, * This function creates copies of all name/value pairs in |nv|. It * also lower-cases all names in |nv|. * - * The |stream_user_data| is a pointer to an arbitrary - * data which is associated to the stream this frame will open. + * The |stream_user_data| is a pointer to an arbitrary data which is + * associated to the stream this frame will open. Therefore it is only + * used if this frame opens streams, in other words, it changes stream + * state from idle or reserved to open. * * This function is low-level in a sense that the application code can * specify flags and the Associated-To-Stream-ID directly. For usual @@ -1527,6 +1536,34 @@ int nghttp2_submit_rst_stream(nghttp2_session *session, int32_t stream_id, int nghttp2_submit_settings(nghttp2_session *session, const nghttp2_settings_entry *iv, size_t niv); + +/** + * @function + * + * Submits PUSH_PROMISE frame. The |flags| is currently ignored. + * + * The |stream_id| must be client initiated stream ID. + * + * The |nv| contains the name/value pairs. For i >= 0, ``nv[2*i]`` + * contains a pointer to the name string and ``nv[2*i+1]`` contains a + * pointer to the value string. The one beyond last value must be + * ``NULL``. That is, if the |nv| contains N name/value pairs, + * ``nv[2*N]`` must be ``NULL``. + * + * This function creates copies of all name/value pairs in |nv|. It + * also lower-cases all names in |nv|. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * The |nv| includes empty name or NULL value. + * :enum:`NGHTTP2_ERR_NOMEM` + * Out of memory. + */ +int nghttp2_submit_push_promise(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const char **nv); + /** * @function * diff --git a/lib/nghttp2_frame.c b/lib/nghttp2_frame.c index 01bd5e77..306063c3 100644 --- a/lib/nghttp2_frame.c +++ b/lib/nghttp2_frame.c @@ -252,6 +252,22 @@ void nghttp2_frame_settings_free(nghttp2_settings *frame) free(frame->iv); } +void nghttp2_frame_push_promise_init(nghttp2_push_promise *frame, + uint8_t flags, int32_t stream_id, + int32_t promised_stream_id, + nghttp2_nv *nva, size_t nvlen) +{ + memset(frame, 0, sizeof(nghttp2_push_promise)); + nghttp2_frame_set_hd(&frame->hd, 0, NGHTTP2_PUSH_PROMISE, flags, stream_id); + frame->promised_stream_id = promised_stream_id; + frame->nva = nva; + frame->nvlen = nvlen; +} + +void nghttp2_frame_push_promise_free(nghttp2_push_promise *frame) +{ + nghttp2_nv_array_del(frame->nva); +} void nghttp2_frame_ping_init(nghttp2_ping *frame, uint8_t flags, const uint8_t *opaque_data) @@ -490,6 +506,72 @@ int nghttp2_frame_unpack_settings(nghttp2_settings *frame, return 0; } +ssize_t nghttp2_frame_pack_push_promise(uint8_t **buf_ptr, + size_t *buflen_ptr, + nghttp2_push_promise *frame, + nghttp2_hd_context *deflater) +{ + ssize_t framelen; + size_t nv_offset = NGHTTP2_FRAME_HEAD_LENGTH + 4; + ssize_t rv; + rv = nghttp2_hd_deflate_hd(deflater, buf_ptr, buflen_ptr, nv_offset, + frame->nva, frame->nvlen); + if(rv < 0) { + return rv; + } + framelen = rv + nv_offset; + frame->hd.length = framelen - NGHTTP2_FRAME_HEAD_LENGTH; + /* If frame->nvlen == 0, *buflen_ptr may be smaller than + nv_offset */ + rv = nghttp2_reserve_buffer(buf_ptr, buflen_ptr, nv_offset); + if(rv < 0) { + return rv; + } + memset(*buf_ptr, 0, nv_offset); + /* pack ctrl header after length is determined */ + nghttp2_frame_pack_frame_hd(*buf_ptr, &frame->hd); + nghttp2_put_uint32be(&(*buf_ptr)[8], frame->promised_stream_id); + return framelen; +} + +int nghttp2_frame_unpack_push_promise(nghttp2_push_promise *frame, + const uint8_t *head, size_t headlen, + const uint8_t *payload, + size_t payloadlen, + nghttp2_hd_context *inflater) +{ + ssize_t r; + r = nghttp2_frame_unpack_push_promise_without_nv(frame, head, headlen, + payload, payloadlen); + if(r < 0) { + return r; + } + r = nghttp2_hd_inflate_hd(inflater, &frame->nva, + (uint8_t*)payload + 4, payloadlen - 4); + if(r < 0) { + return r; + } + frame->nvlen = r; + return 0; +} + +int nghttp2_frame_unpack_push_promise_without_nv(nghttp2_push_promise *frame, + const uint8_t *head, + size_t headlen, + const uint8_t *payload, + size_t payloadlen) +{ + nghttp2_frame_unpack_frame_hd(&frame->hd, head); + if(payloadlen < 4) { + return NGHTTP2_ERR_INVALID_FRAME; + } + frame->promised_stream_id = nghttp2_get_uint32(payload) & + NGHTTP2_STREAM_ID_MASK; + frame->nva = NULL; + frame->nvlen = 0; + return 0; +} + ssize_t nghttp2_frame_pack_ping(uint8_t **buf_ptr, size_t *buflen_ptr, nghttp2_ping *frame) { diff --git a/lib/nghttp2_frame.h b/lib/nghttp2_frame.h index a5c03dbc..12e13047 100644 --- a/lib/nghttp2_frame.h +++ b/lib/nghttp2_frame.h @@ -238,6 +238,78 @@ int nghttp2_frame_unpack_settings(nghttp2_settings *frame, const uint8_t *head, size_t headlen, 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. + * This function expands |*buf_ptr| as necessary to store frame. When + * expansion occurred, memory previously pointed by |*buf_ptr| may be + * freed. |*buf_ptr| and |*buflen_ptr| are updated accordingly. + * + * frame->hd.length is assigned after length is determined during + * packing process. + * + * This function returns the size of packed frame if it succeeds, or + * returns one of the following negative error codes: + * + * NGHTTP2_ERR_HEADER_COMP + * The deflate operation failed. + * NGHTTP2_ERR_FRAME_TOO_LARGE + * The length of the frame is too large. + * NGHTTP2_ERR_NOMEM + * Out of memory. + */ +ssize_t nghttp2_frame_pack_push_promise(uint8_t **buf_ptr, + size_t *buflen_ptr, + nghttp2_push_promise *frame, + nghttp2_hd_context *deflater); + +/* + * Unpacks PUSH_PROMISE frame byte sequence into |frame|. The control + * frame header is given in |head| with |headlen| length. In the spec, + * headlen is 8 bytes. |payload| is the data after frame header and + * just before name/value header block. + * + * The |inflater| inflates name/value header block. + * + * This function also validates the name/value pairs. If unpacking + * succeeds but validation fails, it is indicated by returning + * NGHTTP2_ERR_INVALID_HEADER_BLOCK. + * + * This function returns 0 if it succeeds or one of the following + * negative error codes: + * + * NGHTTP2_ERR_HEADER_COMP + * The inflate operation failed. + * NGHTTP2_ERR_INVALID_HEADER_BLOCK + * Unpacking succeeds but the header block is invalid. + * NGHTTP2_ERR_INVALID_FRAME + * The input data are invalid. + * NGHTTP2_ERR_NOMEM + * Out of memory. + */ +int nghttp2_frame_unpack_push_promise(nghttp2_push_promise *frame, + const uint8_t *head, size_t headlen, + const uint8_t *payload, + size_t payloadlen, + nghttp2_hd_context *inflater); + +/* + * Unpacks PUSH_PROMISE frame byte sequence into |frame|. This function + * only unapcks bytes that come before name/value header block. + * + * This function returns 0 if it succeeds or one of the following + * negative error codes: + * + * NGHTTP2_ERR_INVALID_FRAME + * The input data are invalid. + */ +int nghttp2_frame_unpack_push_promise_without_nv(nghttp2_push_promise *frame, + const uint8_t *head, + size_t headlen, + const uint8_t *payload, + size_t payloadlen); + /* * Packs PING frame |frame| in wire format and store it in * |*buf_ptr|. The capacity of |*buf_ptr| is |*buflen_ptr| @@ -367,6 +439,17 @@ void nghttp2_frame_rst_stream_init(nghttp2_rst_stream *frame, void nghttp2_frame_rst_stream_free(nghttp2_rst_stream *frame); +/* + * Initializes PUSH_PROMISE frame |frame| with given values. |frame| + * takes ownership of |nva|, so caller must not free it. + */ +void nghttp2_frame_push_promise_init(nghttp2_push_promise *frame, + uint8_t flags, int32_t stream_id, + int32_t promised_stream_id, + nghttp2_nv *nva, size_t nvlen); + +void nghttp2_frame_push_promise_free(nghttp2_push_promise *frame); + /* * Initializes SETTINGS frame |frame| with given values. |frame| takes * ownership of |iv|, so caller must not free it. The |flags| are diff --git a/lib/nghttp2_outbound_item.c b/lib/nghttp2_outbound_item.c index 65b67550..84d2b659 100644 --- a/lib/nghttp2_outbound_item.c +++ b/lib/nghttp2_outbound_item.c @@ -50,6 +50,9 @@ void nghttp2_outbound_item_free(nghttp2_outbound_item *item) case NGHTTP2_SETTINGS: nghttp2_frame_settings_free(&frame->settings); break; + case NGHTTP2_PUSH_PROMISE: + nghttp2_frame_push_promise_free(&frame->push_promise); + break; case NGHTTP2_PING: nghttp2_frame_ping_free(&frame->ping); break; diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index be3eeffe..7ad532af 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -72,6 +72,14 @@ static int nghttp2_is_fatal(int error) return error < NGHTTP2_ERR_FATAL; } +/* Returns the pushed stream's priority based on the associated stream + |stream|. */ +static int32_t nghttp2_pushed_stream_pri(nghttp2_stream *stream) +{ + return stream->pri == NGHTTP2_PRI_LOWEST ? + (int32_t)NGHTTP2_PRI_LOWEST : stream->pri + 1; +} + int nghttp2_session_fail_session(nghttp2_session *session, nghttp2_error_code error_code) { @@ -318,6 +326,7 @@ int nghttp2_session_add_frame(nghttp2_session *session, item->pri = NGHTTP2_PRI_DEFAULT; if(frame_cat == NGHTTP2_CAT_CTRL) { nghttp2_frame *frame = (nghttp2_frame*)abs_frame; + nghttp2_stream *stream = NULL; switch(frame->hd.type) { case NGHTTP2_HEADERS: if(frame->hd.stream_id == -1) { @@ -326,7 +335,6 @@ int nghttp2_session_add_frame(nghttp2_session *session, } else { /* Otherwise, the frame must have stream ID. We use its priority value. */ - nghttp2_stream *stream; stream = nghttp2_session_get_stream(session, frame->hd.stream_id); if(stream) { item->pri = stream->pri; @@ -334,7 +342,6 @@ int nghttp2_session_add_frame(nghttp2_session *session, } break; case NGHTTP2_PRIORITY: { - nghttp2_stream *stream; stream = nghttp2_session_get_stream(session, frame->hd.stream_id); if(stream) { item->pri = stream->pri; @@ -342,7 +349,6 @@ int nghttp2_session_add_frame(nghttp2_session *session, break; } case NGHTTP2_RST_STREAM: { - nghttp2_stream *stream; stream = nghttp2_session_get_stream(session, frame->hd.stream_id); if(stream) { stream->state = NGHTTP2_STREAM_CLOSING; @@ -354,6 +360,14 @@ int nghttp2_session_add_frame(nghttp2_session *session, /* Should NGHTTP2_SETTINGS have higher priority? */ item->pri = -1; break; + case NGHTTP2_PUSH_PROMISE: { + /* Use priority of associated stream */ + stream = nghttp2_session_get_stream(session, frame->hd.stream_id); + if(stream) { + item->pri = stream->pri; + } + break; + } case NGHTTP2_PING: /* Ping has highest priority. */ item->pri = NGHTTP2_OB_PRI_PING; @@ -366,7 +380,6 @@ int nghttp2_session_add_frame(nghttp2_session *session, /* Connection level window update should have higher priority */ item->pri = -1; } else { - nghttp2_stream *stream; stream = nghttp2_session_get_stream(session, frame->hd.stream_id); if(stream) { item->pri = stream->pri; @@ -374,7 +387,13 @@ int nghttp2_session_add_frame(nghttp2_session *session, } break; } - if(frame->hd.type == NGHTTP2_HEADERS && frame->hd.stream_id == -1) { + if(frame->hd.type == NGHTTP2_HEADERS && + (frame->hd.stream_id == -1 || + (stream && stream->state == NGHTTP2_STREAM_RESERVED && + session->server))) { + /* TODO If 2 HEADERS are submitted for reserved stream, then + both of them are queued into ob_ss_pq, which is not + desirable. */ r = nghttp2_pq_push(&session->ob_ss_pq, item); } else { r = nghttp2_pq_push(&session->ob_pq, item); @@ -442,12 +461,24 @@ nghttp2_stream* nghttp2_session_open_stream(nghttp2_session *session, r = nghttp2_map_insert(&session->streams, &stream->map_entry); if(r != 0) { free(stream); - stream = NULL; + return NULL; } - if(nghttp2_session_is_my_stream_id(session, stream_id)) { - ++session->num_outgoing_streams; + if(initial_state == NGHTTP2_STREAM_RESERVED) { + if(nghttp2_session_is_my_stream_id(session, stream_id)) { + /* half closed (remote) */ + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + } else { + /* half closed (local) */ + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR); + } + /* Reserved stream does not count in the concurrent streams + limit. That is one of the DOS vector. */ } else { - ++session->num_incoming_streams; + if(nghttp2_session_is_my_stream_id(session, stream_id)) { + ++session->num_outgoing_streams; + } else { + ++session->num_incoming_streams; + } } return stream; } @@ -458,15 +489,20 @@ int nghttp2_session_close_stream(nghttp2_session *session, int32_t stream_id, nghttp2_stream *stream = nghttp2_session_get_stream(session, stream_id); if(stream) { if(stream->state != NGHTTP2_STREAM_INITIAL && + stream->state != NGHTTP2_STREAM_RESERVED && + /* TODO Should on_stream_close_callback be called against + NGHTTP2_STREAM_RESERVED? It is actually not opened yet. */ session->callbacks.on_stream_close_callback) { session->callbacks.on_stream_close_callback(session, stream_id, error_code, session->user_data); } - if(nghttp2_session_is_my_stream_id(session, stream_id)) { - --session->num_outgoing_streams; - } else { - --session->num_incoming_streams; + if(stream->state != NGHTTP2_STREAM_RESERVED) { + if(nghttp2_session_is_my_stream_id(session, stream_id)) { + --session->num_outgoing_streams; + } else { + --session->num_incoming_streams; + } } nghttp2_map_remove(&session->streams, stream_id); nghttp2_stream_free(stream); @@ -580,6 +616,43 @@ static int nghttp2_session_predicate_syn_reply_send(nghttp2_session *session, } } +/* + * This function checks HEADERS for reserved stream can be sent. The + * stream |stream_id| must be reserved state and the |session| is + * server side. + * + * This function returns 0 if it succeeds, or one of the following + * error codes: + * + * NGHTTP2_ERR_STREAM_CLOSED + * The stream is already closed. + * NGHTTP2_ERR_STREAM_SHUT_WR + * The stream is half-closed for transmission. + * NGHTTP2_ERR_PROTO + * The session is client-side and/or stream is not reserved state + * NGHTTP2_ERR_STREAM_CLOSED + * RST_STREAM was queued for this stream. + */ +static int nghttp2_session_predicate_push_reply_send(nghttp2_session *session, + int32_t stream_id) +{ + nghttp2_stream *stream = nghttp2_session_get_stream(session, stream_id); + int r; + /* TODO Should disallow HEADERS if GOAWAY has already been issued? */ + r = nghttp2_predicate_stream_for_send(stream); + if(r != 0) { + return r; + } + if(!session->server || stream->state != NGHTTP2_STREAM_RESERVED) { + /* Only server can send HEADERS for reserved streams */ + return NGHTTP2_ERR_PROTO; + } + if(stream->state == NGHTTP2_STREAM_CLOSING) { + return NGHTTP2_ERR_STREAM_CLOSING; + } + return 0; +} + /* * This function checks frames belongs to the stream |stream_id| can * be sent. @@ -645,6 +718,60 @@ static int nghttp2_session_predicate_priority_send return nghttp2_session_predicate_stream_frame_send(session, stream_id); } +/* + * This function checks PUSH_PROMISE frame |frame| with stream ID + * |stream_id| can be sent at this time. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_START_STREAM_NOT_ALLOWED + * New stream cannot be created because GOAWAY is already sent or + * received. + * NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE + * Stream ID has reached the maximum value. Therefore no stream ID + * is available. + * NGHTTP2_ERR_PROTO + * The client side attempts to send PUSH_PROMISE, or the server + * sends PUSH_PROMISE for the stream not initiated by the client. + * NGHTTP2_ERR_STREAM_CLOSED + * The stream is already closed or does not exist. + * NGHTTP2_ERR_STREAM_CLOSING + * RST_STREAM was queued for this stream. + * NGHTTP2_ERR_STREAM_SHUT_WR + * The transmission is not allowed for this stream (e.g., a frame + * with END_STREAM flag set has already sent) + */ +static int nghttp2_session_predicate_push_promise_send +(nghttp2_session *session, int32_t stream_id) +{ + int rv; + nghttp2_stream *stream; + if(!session->server || nghttp2_session_is_my_stream_id(session, stream_id)) { + /* Only server is allowed to push. And associated stream must be + created from client side */ + return NGHTTP2_ERR_PROTO; + } + stream = nghttp2_session_get_stream(session, stream_id); + rv = nghttp2_predicate_stream_for_send(stream); + if(rv != 0) { + return rv; + } + if(stream->state == NGHTTP2_STREAM_CLOSING) { + return NGHTTP2_ERR_STREAM_CLOSING; + } + if(session->goaway_flags) { + /* When GOAWAY is sent or received, peer must not promise new + stream ID */ + return NGHTTP2_ERR_START_STREAM_NOT_ALLOWED; + } + /* All 32bit signed stream IDs are spent. */ + if(session->next_stream_id > INT32_MAX) { + return NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE; + } + return 0; +} + /* * This function checks WINDOW_UPDATE with the stream ID |stream_id| * can be sent at this time. Note that FIN flag of the previous frame @@ -670,7 +797,8 @@ static int nghttp2_session_predicate_window_update_send if(stream == NULL) { return NGHTTP2_ERR_STREAM_CLOSED; } - if(stream->state != NGHTTP2_STREAM_CLOSING) { + if(stream->state != NGHTTP2_STREAM_CLOSING && + stream->state != NGHTTP2_STREAM_RESERVED) { return 0; } else { return NGHTTP2_ERR_STREAM_CLOSING; @@ -748,7 +876,8 @@ static int nghttp2_session_predicate_data_send(nghttp2_session *session, frames are sent. This is not desirable situation; we want to close stream as soon as possible. To achieve this, we remove DATA frame before RST_STREAM. */ - if(stream->state != NGHTTP2_STREAM_CLOSING) { + if(stream->state != NGHTTP2_STREAM_CLOSING && + stream->state != NGHTTP2_STREAM_RESERVED) { return 0; } else { return NGHTTP2_ERR_STREAM_CLOSING; @@ -772,7 +901,8 @@ static ssize_t nghttp2_session_prep_frame(nghttp2_session *session, nghttp2_frame *frame; frame = nghttp2_outbound_item_get_ctrl_frame(item); switch(frame->hd.type) { - case NGHTTP2_HEADERS: + case NGHTTP2_HEADERS: { + int push_reply = 0; if(frame->hd.stream_id == -1) { /* initial HEADERS, which opens stream */ int r; @@ -784,6 +914,11 @@ static ssize_t nghttp2_session_prep_frame(nghttp2_session *session, } frame->hd.stream_id = session->next_stream_id; session->next_stream_id += 2; + } else if(nghttp2_session_predicate_push_reply_send + (session, frame->hd.stream_id) == 0) { + /* HEADERS against promised stream */ + push_reply = 1; + frame->headers.cat = NGHTTP2_HCAT_REPLY; } else if(nghttp2_session_predicate_syn_reply_send (session, frame->hd.stream_id) == 0) { /* first response HEADERS */ @@ -816,8 +951,17 @@ static ssize_t nghttp2_session_prep_frame(nghttp2_session *session, aux_data ? aux_data->stream_user_data : NULL) == NULL) { return NGHTTP2_ERR_NOMEM; } + } else if(push_reply) { + nghttp2_headers_aux_data *aux_data; + aux_data = (nghttp2_headers_aux_data*)item->aux_data; + if(aux_data) { + nghttp2_stream *stream; + stream = nghttp2_session_get_stream(session, frame->hd.stream_id); + stream->stream_user_data = aux_data->stream_user_data; + } } break; + } case NGHTTP2_PRIORITY: { int r; r = nghttp2_session_predicate_priority_send @@ -849,6 +993,36 @@ static ssize_t nghttp2_session_prep_frame(nghttp2_session *session, return framebuflen; } break; + case NGHTTP2_PUSH_PROMISE: { + int r; + nghttp2_stream *stream; + r = nghttp2_session_predicate_push_promise_send(session, + frame->hd.stream_id); + if(r != 0) { + return r; + } + frame->push_promise.promised_stream_id = session->next_stream_id; + session->next_stream_id += 2; + framebuflen = nghttp2_frame_pack_push_promise(&session->aob.framebuf, + &session->aob.framebufmax, + &frame->push_promise, + &session->hd_deflater); + nghttp2_hd_end_headers(&session->hd_deflater); + if(framebuflen < 0) { + return framebuflen; + } + stream = nghttp2_session_get_stream(session, frame->hd.stream_id); + assert(stream); + if(nghttp2_session_open_stream + (session, frame->push_promise.promised_stream_id, + frame->hd.flags, + nghttp2_pushed_stream_pri(stream), + NGHTTP2_STREAM_RESERVED, + NULL) == NULL) { + return NGHTTP2_ERR_NOMEM; + } + break; + } case NGHTTP2_PING: framebuflen = nghttp2_frame_pack_ping(&session->aob.framebuf, &session->aob.framebufmax, @@ -1137,6 +1311,9 @@ static int nghttp2_session_after_frame_sent(nghttp2_session *session) case NGHTTP2_SETTINGS: /* nothing to do */ break; + case NGHTTP2_PUSH_PROMISE: + /* nothing to do */ + break; case NGHTTP2_PING: /* nothing to do */ break; @@ -1525,28 +1702,22 @@ int nghttp2_session_on_syn_reply_received(nghttp2_session *session, nghttp2_frame *frame, nghttp2_stream *stream) { - int r = 0; - int valid = 0; - nghttp2_error_code error_code = NGHTTP2_PROTOCOL_ERROR; + /* This function is only called if stream->state == + NGHTTP2_STREAM_OPENING and stream_id is local side initiated. */ + assert(stream->state == NGHTTP2_STREAM_OPENING && + nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)); if(frame->hd.stream_id == 0) { return nghttp2_session_handle_invalid_connection(session, frame, NGHTTP2_PROTOCOL_ERROR); } if((stream->shut_flags & NGHTTP2_SHUT_RD) == 0) { - if(nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) { - /* This function is only called if stream->state == - NGHTTP2_STREAM_OPENING. If server push is implemented, it may - be called on reserved state. */ - assert(stream->state == NGHTTP2_STREAM_OPENING); - valid = 1; - stream->state = NGHTTP2_STREAM_OPENED; - nghttp2_session_call_on_frame_received(session, frame); - if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { - /* This is the last frame of this stream, so disallow - further receptions. */ - nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); - nghttp2_session_close_stream_if_shut_rdwr(session, stream); - } + stream->state = NGHTTP2_STREAM_OPENED; + nghttp2_session_call_on_frame_received(session, frame); + if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + /* This is the last frame of this stream, so disallow + further receptions. */ + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + nghttp2_session_close_stream_if_shut_rdwr(session, stream); } } else { /* half closed (remote): from the spec: @@ -1555,12 +1726,34 @@ int nghttp2_session_on_syn_reply_received(nghttp2_session *session, in this state it MUST respond with a stream error (Section 5.4.2) of type STREAM_CLOSED. */ - error_code = NGHTTP2_STREAM_CLOSED; + return nghttp2_session_handle_invalid_stream(session, frame, + NGHTTP2_STREAM_CLOSED); } - if(!valid) { - r = nghttp2_session_handle_invalid_stream(session, frame, error_code); + return 0; +} + +int nghttp2_session_on_push_reply_received(nghttp2_session *session, + nghttp2_frame *frame, + nghttp2_stream *stream) +{ + int rv = 0; + assert(!session->server && stream->state == NGHTTP2_STREAM_RESERVED); + if(frame->hd.stream_id == 0) { + return nghttp2_session_handle_invalid_connection(session, frame, + NGHTTP2_PROTOCOL_ERROR); } - return r; + if(session->goaway_flags) { + /* We don't accept new stream after GOAWAY is sent or received. */ + return 0; + } + rv = nghttp2_session_validate_syn_stream(session, &frame->headers); + if(rv != 0) { + return nghttp2_session_handle_invalid_stream(session, frame, rv); + } + nghttp2_stream_promise_fulfilled(stream); + ++session->num_incoming_streams; + nghttp2_session_call_on_frame_received(session, frame); + return 0; } int nghttp2_session_on_headers_received(nghttp2_session *session, @@ -1815,6 +2008,69 @@ int nghttp2_session_on_settings_received(nghttp2_session *session, return 0; } +int nghttp2_session_on_push_promise_received(nghttp2_session *session, + nghttp2_frame *frame) +{ + nghttp2_stream *stream; + if(session->server || frame->hd.stream_id == 0) { + return nghttp2_session_handle_invalid_connection(session, frame, + NGHTTP2_PROTOCOL_ERROR); + } + if(session->goaway_flags) { + /* We just dicard PUSH_PROMISE after GOAWAY is sent or + received. */ + return 0; + } + if(!nghttp2_session_is_new_peer_stream_id + (session, frame->push_promise.promised_stream_id)) { + /* The spec says if an endpoint receives a HEADERS with invalid + stream ID, it MUST issue connection error with error code + PROTOCOL_ERROR. It applies to PUSH_PROMISE too. */ + return nghttp2_session_handle_invalid_connection + (session, frame, NGHTTP2_PROTOCOL_ERROR); + } + session->last_recv_stream_id = frame->push_promise.promised_stream_id; + stream = nghttp2_session_get_stream(session, frame->hd.stream_id); + if(stream) { + if((stream->shut_flags & NGHTTP2_SHUT_RD) == 0) { + if(stream->state == NGHTTP2_STREAM_CLOSING) { + return nghttp2_session_add_rst_stream + (session, + frame->push_promise.promised_stream_id, + NGHTTP2_REFUSED_STREAM); + } else { + nghttp2_stream *promised_stream; + promised_stream = nghttp2_session_open_stream + (session, + frame->push_promise.promised_stream_id, + frame->hd.flags, + nghttp2_pushed_stream_pri(stream), + NGHTTP2_STREAM_RESERVED, + NULL); + if(!promised_stream) { + return NGHTTP2_ERR_NOMEM; + } + nghttp2_session_call_on_frame_received(session, frame); + } + } else { + if(session->callbacks.on_invalid_frame_recv_callback) { + session->callbacks.on_invalid_frame_recv_callback + (session, frame, NGHTTP2_PROTOCOL_ERROR, session->user_data); + } + return nghttp2_session_add_rst_stream + (session, + frame->push_promise.promised_stream_id, + NGHTTP2_PROTOCOL_ERROR); + } + } else { + return nghttp2_session_add_rst_stream + (session, + frame->push_promise.promised_stream_id, + NGHTTP2_REFUSED_STREAM); + } + return 0; +} + int nghttp2_session_on_ping_received(nghttp2_session *session, nghttp2_frame *frame) { @@ -2009,10 +2265,10 @@ static int nghttp2_session_process_ctrl_frame(nghttp2_session *session) r = session->iframe.error_code; } if(r == 0) { - if(nghttp2_session_is_my_stream_id(session, frame.hd.stream_id)) { - nghttp2_stream *stream; - stream = nghttp2_session_get_stream(session, frame.hd.stream_id); - if(stream) { + nghttp2_stream *stream; + stream = nghttp2_session_get_stream(session, frame.hd.stream_id); + if(stream) { + if(nghttp2_session_is_my_stream_id(session, frame.hd.stream_id)) { if(stream->state == NGHTTP2_STREAM_OPENING) { frame.headers.cat = NGHTTP2_HCAT_REPLY; r = nghttp2_session_on_syn_reply_received(session, &frame, stream); @@ -2020,9 +2276,13 @@ static int nghttp2_session_process_ctrl_frame(nghttp2_session *session) frame.headers.cat = NGHTTP2_HCAT_HEADERS; r = nghttp2_session_on_headers_received(session, &frame, stream); } + } else if(!session->server && + stream->state == NGHTTP2_STREAM_RESERVED) { + frame.headers.cat = NGHTTP2_HCAT_REPLY; + r = nghttp2_session_on_push_reply_received(session, &frame, stream); } else { - r = nghttp2_session_handle_invalid_stream - (session, &frame, NGHTTP2_PROTOCOL_ERROR); + frame.headers.cat = NGHTTP2_HCAT_HEADERS; + r = nghttp2_session_on_headers_received(session, &frame, stream); } } else { frame.headers.cat = NGHTTP2_HCAT_START_STREAM; @@ -2084,6 +2344,26 @@ static int nghttp2_session_process_ctrl_frame(nghttp2_session *session) NGHTTP2_PROTOCOL_ERROR); } break; + case NGHTTP2_PUSH_PROMISE: + if(session->iframe.error_code == 0) { + r = nghttp2_frame_unpack_push_promise(&frame.push_promise, + session->iframe.headbuf, + sizeof(session->iframe.headbuf), + session->iframe.buf, + session->iframe.buflen, + &session->hd_inflater); + } else { + r = session->iframe.error_code; + } + if(r == 0) { + r = nghttp2_session_on_push_promise_received(session, &frame); + nghttp2_frame_push_promise_free(&frame.push_promise); + nghttp2_hd_end_headers(&session->hd_inflater); + } else if(nghttp2_is_non_fatal(r)) { + r = nghttp2_session_handle_parse_error(session, type, r, + NGHTTP2_PROTOCOL_ERROR); + } + break; case NGHTTP2_PING: r = nghttp2_frame_unpack_ping(&frame.ping, session->iframe.headbuf, diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index 80c2d019..61c48118 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -345,6 +345,10 @@ int nghttp2_session_on_syn_reply_received(nghttp2_session *session, nghttp2_frame *frame, nghttp2_stream *stream); +int nghttp2_session_on_push_reply_received(nghttp2_session *session, + nghttp2_frame *frame, + nghttp2_stream *stream); + /* * Called when HEADERS is received, assuming |frame| is properly * initialized. This function does first validate received frame and @@ -392,6 +396,19 @@ int nghttp2_session_on_rst_stream_received(nghttp2_session *session, int nghttp2_session_on_settings_received(nghttp2_session *session, nghttp2_frame *frame); +/* + * Called when PUSH_PROMISE is received, assuming |frame| is properly + * initialized. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_NOMEM + * Out of memory. + */ +int nghttp2_session_on_push_promise_received(nghttp2_session *session, + nghttp2_frame *frame); + /* * Called when PING is received, assuming |frame| is properly * initialized. diff --git a/lib/nghttp2_stream.c b/lib/nghttp2_stream.c index 354c3b5a..3b7bc474 100644 --- a/lib/nghttp2_stream.c +++ b/lib/nghttp2_stream.c @@ -82,3 +82,8 @@ void nghttp2_stream_update_initial_window_size(nghttp2_stream *stream, stream->window_size = new_initial_window_size-(old_initial_window_size-stream->window_size); } + +void nghttp2_stream_promise_fulfilled(nghttp2_stream *stream) +{ + stream->state = NGHTTP2_STREAM_OPENED; +} diff --git a/lib/nghttp2_stream.h b/lib/nghttp2_stream.h index a98c4a8d..e38557be 100644 --- a/lib/nghttp2_stream.h +++ b/lib/nghttp2_stream.h @@ -56,7 +56,9 @@ typedef enum { NGHTTP2_STREAM_OPENED, /* RST_STREAM is received, but somehow we need to keep stream in memory. */ - NGHTTP2_STREAM_CLOSING + NGHTTP2_STREAM_CLOSING, + /* PUSH_PROMISE is received or sent */ + NGHTTP2_STREAM_RESERVED } nghttp2_stream_state; typedef enum { @@ -155,4 +157,11 @@ void nghttp2_stream_update_initial_window_size(nghttp2_stream *stream, int32_t new_initial_window_size, int32_t old_initial_window_size); +/* + * Call this function if promised stream |stream| is replied with + * HEADERS. This function makes the state of the |stream| to + * NGHTTP2_STREAM_OPENED. + */ +void nghttp2_stream_promise_fulfilled(nghttp2_stream *stream); + #endif /* NGHTTP2_STREAM */ diff --git a/lib/nghttp2_submit.c b/lib/nghttp2_submit.c index d52a089d..a6270c4e 100644 --- a/lib/nghttp2_submit.c +++ b/lib/nghttp2_submit.c @@ -186,6 +186,39 @@ int nghttp2_submit_settings(nghttp2_session *session, return r; } +int nghttp2_submit_push_promise(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const char **nv) +{ + nghttp2_frame *frame; + nghttp2_nv *nva; + ssize_t nvlen; + uint8_t flags_copy; + int r; + + if(!nghttp2_frame_nv_check_null(nv)) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + frame = malloc(sizeof(nghttp2_frame)); + if(frame == NULL) { + return NGHTTP2_ERR_NOMEM; + } + nvlen = nghttp2_nv_array_from_cstr(&nva, nv); + if(nvlen < 0) { + free(frame); + return nvlen; + } + /* TODO Implement header continuation */ + flags_copy = NGHTTP2_FLAG_END_PUSH_PROMISE; + nghttp2_frame_push_promise_init(&frame->push_promise, flags_copy, + stream_id, -1, nva, nvlen); + r = nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL); + if(r != 0) { + nghttp2_frame_push_promise_free(&frame->push_promise); + free(frame); + } + return 0; +} + int nghttp2_submit_window_update(nghttp2_session *session, uint8_t flags, int32_t stream_id, int32_t window_size_increment) diff --git a/tests/main.c b/tests/main.c index b72236ad..9d352b18 100644 --- a/tests/main.c +++ b/tests/main.c @@ -90,12 +90,16 @@ int main(int argc, char* argv[]) test_nghttp2_session_on_syn_reply_received) || !CU_add_test(pSuite, "session_on_headers_received", test_nghttp2_session_on_headers_received) || + !CU_add_test(pSuite, "session_on_push_reply_received", + test_nghttp2_session_on_push_reply_received) || !CU_add_test(pSuite, "session_on_priority_received", test_nghttp2_session_on_priority_received) || !CU_add_test(pSuite, "session_on_rst_stream_received", test_nghttp2_session_on_rst_stream_received) || !CU_add_test(pSuite, "session_on_settings_received", test_nghttp2_session_on_settings_received) || + !CU_add_test(pSuite, "session_on_push_promise_received", + test_nghttp2_session_on_push_promise_received) || !CU_add_test(pSuite, "session_on_ping_received", test_nghttp2_session_on_ping_received) || !CU_add_test(pSuite, "session_on_goaway_received", @@ -110,10 +114,14 @@ int main(int argc, char* argv[]) test_nghttp2_session_send_headers_reply) || !CU_add_test(pSuite, "session_send_headers_header_comp_error", test_nghttp2_session_send_headers_header_comp_error) || + !CU_add_test(pSuite, "session_send_headers_push_reply", + test_nghttp2_session_send_headers_push_reply) || !CU_add_test(pSuite, "session_send_priority", test_nghttp2_session_send_priority) || !CU_add_test(pSuite, "session_send_rst_stream", test_nghttp2_session_send_rst_stream) || + !CU_add_test(pSuite, "session_send_push_promise", + 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, "submit_response", test_nghttp2_submit_response) || @@ -127,14 +135,20 @@ int main(int argc, char* argv[]) test_nghttp2_submit_headers_start_stream) || !CU_add_test(pSuite, "submit_headers_reply", test_nghttp2_submit_headers_reply) || + !CU_add_test(pSuite, "submit_headers_push_reply", + test_nghttp2_submit_headers_push_reply) || !CU_add_test(pSuite, "submit_headers", test_nghttp2_submit_headers) || !CU_add_test(pSuite, "submit_priority", test_nghttp2_submit_priority) || !CU_add_test(pSuite, "session_submit_settings", test_nghttp2_submit_settings) || + !CU_add_test(pSuite, "session_submit_push_promise", + test_nghttp2_submit_push_promise) || !CU_add_test(pSuite, "submit_window_update", test_nghttp2_submit_window_update) || !CU_add_test(pSuite, "submit_invalid_nv", test_nghttp2_submit_invalid_nv) || + !CU_add_test(pSuite, "session_open_stream", + test_nghttp2_session_open_stream) || !CU_add_test(pSuite, "session_get_next_ob_item", test_nghttp2_session_get_next_ob_item) || !CU_add_test(pSuite, "session_pop_next_ob_item", @@ -182,6 +196,8 @@ int main(int argc, char* argv[]) test_nghttp2_frame_pack_rst_stream) || !CU_add_test(pSuite, "frame_pack_settings", test_nghttp2_frame_pack_settings) || + !CU_add_test(pSuite, "frame_pack_push_promise", + test_nghttp2_frame_pack_push_promise) || !CU_add_test(pSuite, "frame_pack_ping", test_nghttp2_frame_pack_ping) || !CU_add_test(pSuite, "frame_pack_goaway", test_nghttp2_frame_pack_goaway) || diff --git a/tests/nghttp2_frame_test.c b/tests/nghttp2_frame_test.c index b5463168..271e81d0 100644 --- a/tests/nghttp2_frame_test.c +++ b/tests/nghttp2_frame_test.c @@ -129,7 +129,6 @@ void test_nghttp2_frame_pack_headers() /* We didn't include PRIORITY flag so priority is not packed */ CU_ASSERT(1 << 30 == oframe.pri); CU_ASSERT(7 == oframe.nvlen); - CU_ASSERT(memcmp("method", oframe.nva[0].name, oframe.nva[0].namelen) == 0); CU_ASSERT(nvnameeq("method", &oframe.nva[0])); CU_ASSERT(nvvalueeq("GET", &oframe.nva[0])); @@ -263,6 +262,43 @@ void test_nghttp2_frame_pack_settings() nghttp2_frame_settings_free(&oframe); } +void test_nghttp2_frame_pack_push_promise() +{ + nghttp2_hd_context deflater, inflater; + nghttp2_push_promise frame, oframe; + uint8_t *buf = NULL; + size_t buflen = 0; + ssize_t framelen; + nghttp2_nv *nva; + ssize_t nvlen; + + nghttp2_hd_deflate_init(&deflater, NGHTTP2_HD_SIDE_CLIENT); + nghttp2_hd_inflate_init(&inflater, NGHTTP2_HD_SIDE_SERVER); + + nvlen = nghttp2_nv_array_from_cstr(&nva, headers); + nghttp2_frame_push_promise_init(&frame, NGHTTP2_FLAG_END_PUSH_PROMISE, + 1000000007, (1U << 31) - 1, nva, nvlen); + framelen = nghttp2_frame_pack_push_promise(&buf, &buflen, &frame, &deflater); + + CU_ASSERT(0 == unpack_frame_with_nv_block((nghttp2_frame*)&oframe, + NGHTTP2_PUSH_PROMISE, + &inflater, + buf, framelen)); + check_frame_header(framelen - NGHTTP2_FRAME_HEAD_LENGTH, + NGHTTP2_PUSH_PROMISE, + NGHTTP2_FLAG_END_PUSH_PROMISE, 1000000007, &oframe.hd); + CU_ASSERT((1U << 31) - 1 == oframe.promised_stream_id); + CU_ASSERT(7 == oframe.nvlen); + CU_ASSERT(nvnameeq("method", &oframe.nva[0])); + CU_ASSERT(nvvalueeq("GET", &oframe.nva[0])); + + free(buf); + nghttp2_frame_push_promise_free(&oframe); + nghttp2_frame_push_promise_free(&frame); + nghttp2_hd_inflate_free(&inflater); + nghttp2_hd_deflate_free(&deflater); +} + void test_nghttp2_frame_pack_ping(void) { nghttp2_ping frame, oframe; diff --git a/tests/nghttp2_frame_test.h b/tests/nghttp2_frame_test.h index dd6e2bdb..d1ad9e41 100644 --- a/tests/nghttp2_frame_test.h +++ b/tests/nghttp2_frame_test.h @@ -33,6 +33,7 @@ void test_nghttp2_frame_pack_headers_frame_too_large(void); void test_nghttp2_frame_pack_priority(void); void test_nghttp2_frame_pack_rst_stream(void); void test_nghttp2_frame_pack_settings(void); +void test_nghttp2_frame_pack_push_promise(void); void test_nghttp2_frame_pack_ping(void); void test_nghttp2_frame_pack_goaway(void); void test_nghttp2_frame_pack_window_update(void); diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index e4526de8..bdc18e03 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -441,13 +441,13 @@ void test_nghttp2_session_recv_invalid_frame(void) CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(0 == user_data.frame_send_cb_called); - /* Receive exactly same bytes of SYN_STREAM causes error */ + /* Receive exactly same bytes of HEADERS is treated as subsequent + HEADERS (e.g., trailers */ scripted_data_feed_init(&df, framedata, framelen); CU_ASSERT(0 == nghttp2_session_recv(session)); CU_ASSERT(0 == nghttp2_session_send(session)); - CU_ASSERT(1 == user_data.frame_send_cb_called); - CU_ASSERT(NGHTTP2_GOAWAY == user_data.sent_frame_type); + CU_ASSERT(0 == user_data.frame_send_cb_called); free(framedata); nghttp2_frame_headers_free(&frame.headers); @@ -671,7 +671,6 @@ void test_nghttp2_session_on_syn_reply_received(void) const char *nv[] = { NULL }; nghttp2_frame frame; nghttp2_stream *stream; - nghttp2_outbound_item *item; nghttp2_nv *nva; ssize_t nvlen; @@ -694,21 +693,6 @@ void test_nghttp2_session_on_syn_reply_received(void) CU_ASSERT(1 == user_data.frame_recv_cb_called); CU_ASSERT(NGHTTP2_STREAM_OPENED == stream->state); - /* Check the situation when response HEADERS is received after peer - sends FIN */ - stream = nghttp2_session_open_stream(session, 3, NGHTTP2_FLAG_NONE, - NGHTTP2_PRI_DEFAULT, - NGHTTP2_STREAM_OPENED, NULL); - nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); - frame.hd.stream_id = 3; - - CU_ASSERT(0 == nghttp2_session_on_syn_reply_received(session, &frame, - stream)); - CU_ASSERT(1 == user_data.invalid_frame_recv_cb_called); - item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(NGHTTP2_RST_STREAM == OB_CTRL_TYPE(item)); - CU_ASSERT(NGHTTP2_STREAM_CLOSED == OB_CTRL(item)->rst_stream.error_code); - nghttp2_frame_headers_free(&frame.headers); nghttp2_session_del(session); } @@ -784,6 +768,55 @@ void test_nghttp2_session_on_headers_received(void) nghttp2_session_del(session); } +void test_nghttp2_session_on_push_reply_received(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + my_user_data user_data; + const char *nv[] = { NULL }; + nghttp2_frame frame; + nghttp2_stream *stream; + nghttp2_nv *nva; + ssize_t nvlen; + nghttp2_outbound_item *item; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.on_frame_recv_callback = on_frame_recv_callback; + callbacks.on_invalid_frame_recv_callback = on_invalid_frame_recv_callback; + user_data.frame_recv_cb_called = 0; + user_data.invalid_frame_recv_cb_called = 0; + + nghttp2_session_client_new(&session, &callbacks, &user_data); + stream = nghttp2_session_open_stream(session, 2, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_RESERVED, NULL); + nvlen = nghttp2_nv_array_from_cstr(&nva, nv); + nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 2, + NGHTTP2_PRI_DEFAULT, nva, nvlen); + /* nghttp2_session_on_push_reply_received assumes stream's state is + NGHTTP2_STREAM_RESERVED and session->server is 0. */ + CU_ASSERT(0 == nghttp2_session_on_push_reply_received(session, &frame, + stream)); + CU_ASSERT(1 == user_data.frame_recv_cb_called); + CU_ASSERT(NGHTTP2_STREAM_OPENED == stream->state); + CU_ASSERT(1 == session->num_incoming_streams); + + /* If max concurrent streams limit is exceeded, RST_STREAMed */ + session->local_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS] = 1; + stream = nghttp2_session_open_stream(session, 4, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_RESERVED, NULL); + frame.hd.stream_id = 4; + CU_ASSERT(0 == nghttp2_session_on_push_reply_received(session, &frame, + stream)); + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == OB_CTRL_TYPE(item)); + CU_ASSERT(NGHTTP2_REFUSED_STREAM == OB_CTRL(item)->rst_stream.error_code); + + nghttp2_frame_headers_free(&frame.headers); + nghttp2_session_del(session); +} + void test_nghttp2_session_on_priority_received(void) { nghttp2_session *session; @@ -896,6 +929,121 @@ void test_nghttp2_session_on_settings_received(void) nghttp2_session_del(session); } +void test_nghttp2_session_on_push_promise_received(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + my_user_data user_data; + const char *nv[] = { NULL }; + nghttp2_frame frame; + nghttp2_stream *stream, *promised_stream; + nghttp2_outbound_item *item; + nghttp2_nv *nva; + ssize_t nvlen; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + callbacks.on_frame_recv_callback = on_frame_recv_callback; + callbacks.on_invalid_frame_recv_callback = on_invalid_frame_recv_callback; + user_data.frame_recv_cb_called = 0; + user_data.invalid_frame_recv_cb_called = 0; + + nghttp2_session_client_new(&session, &callbacks, &user_data); + stream = nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_OPENING, NULL); + nvlen = nghttp2_nv_array_from_cstr(&nva, nv); + nghttp2_frame_push_promise_init(&frame.push_promise, + NGHTTP2_FLAG_END_PUSH_PROMISE, 1, 2, + nva, nvlen); + + CU_ASSERT(0 == nghttp2_session_on_push_promise_received(session, &frame)); + + CU_ASSERT(1 == user_data.frame_recv_cb_called); + promised_stream = nghttp2_session_get_stream(session, 2); + CU_ASSERT(NGHTTP2_STREAM_RESERVED == promised_stream->state); + CU_ASSERT(2 == session->last_recv_stream_id); + + /* Attempt to PUSH_PROMISE against half close (remote) */ + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + frame.push_promise.promised_stream_id = 4; + + user_data.frame_recv_cb_called = 0; + user_data.invalid_frame_recv_cb_called = 0; + CU_ASSERT(0 == nghttp2_session_on_push_promise_received(session, &frame)); + + CU_ASSERT(0 == user_data.frame_recv_cb_called); + CU_ASSERT(1 == user_data.invalid_frame_recv_cb_called); + CU_ASSERT(NULL == nghttp2_session_get_stream(session, 4)); + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == OB_CTRL_TYPE(item)); + CU_ASSERT(4 == OB_CTRL(item)->hd.stream_id); + CU_ASSERT(NGHTTP2_PROTOCOL_ERROR == OB_CTRL(item)->rst_stream.error_code); + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(4 == session->last_recv_stream_id); + + /* Attempt to PUSH_PROMISE against stream in closing state */ + stream->shut_flags = NGHTTP2_SHUT_NONE; + stream->state = NGHTTP2_STREAM_CLOSING; + frame.push_promise.promised_stream_id = 6; + + user_data.frame_recv_cb_called = 0; + user_data.invalid_frame_recv_cb_called = 0; + CU_ASSERT(0 == nghttp2_session_on_push_promise_received(session, &frame)); + + CU_ASSERT(0 == user_data.frame_recv_cb_called); + CU_ASSERT(NULL == nghttp2_session_get_stream(session, 6)); + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == OB_CTRL_TYPE(item)); + CU_ASSERT(6 == OB_CTRL(item)->hd.stream_id); + CU_ASSERT(NGHTTP2_REFUSED_STREAM == OB_CTRL(item)->rst_stream.error_code); + CU_ASSERT(0 == nghttp2_session_send(session)); + + /* Attempt to PUSH_PROMISE against non-existent stream */ + frame.hd.stream_id = 3; + frame.push_promise.promised_stream_id = 8; + + user_data.frame_recv_cb_called = 0; + user_data.invalid_frame_recv_cb_called = 0; + CU_ASSERT(0 == nghttp2_session_on_push_promise_received(session, &frame)); + + CU_ASSERT(0 == user_data.frame_recv_cb_called); + CU_ASSERT(NULL == nghttp2_session_get_stream(session, 8)); + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == OB_CTRL_TYPE(item)); + CU_ASSERT(8 == OB_CTRL(item)->hd.stream_id); + CU_ASSERT(NGHTTP2_REFUSED_STREAM == OB_CTRL(item)->rst_stream.error_code); + CU_ASSERT(0 == nghttp2_session_send(session)); + + /* Same ID twice */ + stream->state = NGHTTP2_STREAM_OPENING; + + user_data.frame_recv_cb_called = 0; + user_data.invalid_frame_recv_cb_called = 0; + CU_ASSERT(0 == nghttp2_session_on_push_promise_received(session, &frame)); + + CU_ASSERT(0 == user_data.frame_recv_cb_called); + CU_ASSERT(NULL == nghttp2_session_get_stream(session, 8)); + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_GOAWAY == OB_CTRL_TYPE(item)); + CU_ASSERT(NGHTTP2_PROTOCOL_ERROR == OB_CTRL(item)->goaway.error_code); + CU_ASSERT(0 == nghttp2_session_send(session)); + + /* After GOAWAY, PUSH_PROMISE will be discarded */ + frame.push_promise.promised_stream_id = 10; + + user_data.frame_recv_cb_called = 0; + user_data.invalid_frame_recv_cb_called = 0; + CU_ASSERT(0 == nghttp2_session_on_push_promise_received(session, &frame)); + + CU_ASSERT(0 == user_data.frame_recv_cb_called); + CU_ASSERT(NULL == nghttp2_session_get_stream(session, 10)); + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + nghttp2_frame_push_promise_free(&frame.push_promise); + nghttp2_session_del(session); +} + void test_nghttp2_session_on_ping_received(void) { nghttp2_session *session; @@ -1150,6 +1298,34 @@ void test_nghttp2_session_send_headers_header_comp_error(void) nghttp2_session_del(session); } +void test_nghttp2_session_send_headers_push_reply(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + const char *nv[] = { NULL }; + nghttp2_frame *frame = malloc(sizeof(nghttp2_frame)); + nghttp2_stream *stream; + nghttp2_nv *nva; + ssize_t nvlen; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + CU_ASSERT(0 == nghttp2_session_server_new(&session, &callbacks, NULL)); + nghttp2_session_open_stream(session, 2, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_RESERVED, NULL); + nvlen = nghttp2_nv_array_from_cstr(&nva, nv); + nghttp2_frame_headers_init(&frame->headers, NGHTTP2_FLAG_END_HEADERS, 2, + NGHTTP2_PRI_DEFAULT, nva, nvlen); + nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL); + CU_ASSERT(0 == nghttp2_session_send(session)); + stream = nghttp2_session_get_stream(session, 2); + CU_ASSERT(NGHTTP2_STREAM_OPENED == stream->state); + + nghttp2_session_del(session); +} + void test_nghttp2_session_send_priority(void) { nghttp2_session *session; @@ -1196,6 +1372,53 @@ void test_nghttp2_session_send_rst_stream(void) nghttp2_session_del(session); } +void test_nghttp2_session_send_push_promise(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + const char *nv[] = { NULL }; + nghttp2_frame *frame = malloc(sizeof(nghttp2_frame)); + nghttp2_stream *stream; + nghttp2_nv *nva; + ssize_t nvlen; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_server_new(&session, &callbacks, NULL); + nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, NGHTTP2_STREAM_OPENING, + NULL); + nvlen = nghttp2_nv_array_from_cstr(&nva, nv); + nghttp2_frame_push_promise_init(&frame->push_promise, + NGHTTP2_FLAG_END_PUSH_PROMISE, 1, -1, + nva, nvlen); + nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL); + + CU_ASSERT(0 == nghttp2_session_send(session)); + stream = nghttp2_session_get_stream(session, 2); + CU_ASSERT(NGHTTP2_STREAM_RESERVED == stream->state); + + nghttp2_session_del(session); + + /* PUSH_PROMISE from client is error */ + nghttp2_session_client_new(&session, &callbacks, NULL); + nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, NGHTTP2_STREAM_OPENING, + NULL); + nvlen = nghttp2_nv_array_from_cstr(&nva, nv); + frame = malloc(sizeof(nghttp2_frame)); + nghttp2_frame_push_promise_init(&frame->push_promise, + NGHTTP2_FLAG_END_PUSH_PROMISE, 1, -1, + nva, nvlen); + nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL); + + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(NULL == nghttp2_session_get_stream(session, 3)); + + nghttp2_session_del(session); +} + void test_nghttp2_session_is_my_stream_id(void) { nghttp2_session *session; @@ -1403,6 +1626,56 @@ void test_nghttp2_submit_headers_reply(void) nghttp2_session_del(session); } +void test_nghttp2_submit_headers_push_reply(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + const char *nv[] = { ":version", "HTTP/1.1", NULL }; + my_user_data ud; + nghttp2_stream *stream; + int foo; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + callbacks.on_frame_send_callback = on_frame_send_callback; + + CU_ASSERT(0 == nghttp2_session_server_new(&session, &callbacks, &ud)); + stream = nghttp2_session_open_stream(session, 2, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_RESERVED, NULL); + CU_ASSERT(0 == nghttp2_submit_headers(session, + NGHTTP2_FLAG_NONE, + 2, NGHTTP2_PRI_DEFAULT, + nv, &foo)); + + ud.frame_send_cb_called = 0; + ud.sent_frame_type = 0; + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(1 == ud.frame_send_cb_called); + CU_ASSERT(NGHTTP2_HEADERS == ud.sent_frame_type); + CU_ASSERT(NGHTTP2_STREAM_OPENED == stream->state); + CU_ASSERT(&foo == stream->stream_user_data); + + nghttp2_session_del(session); + + /* Sending HEADERS from client against stream in reserved state is + error */ + CU_ASSERT(0 == nghttp2_session_client_new(&session, &callbacks, &ud)); + stream = nghttp2_session_open_stream(session, 2, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_RESERVED, NULL); + CU_ASSERT(0 == nghttp2_submit_headers(session, + NGHTTP2_FLAG_NONE, + 2, NGHTTP2_PRI_DEFAULT, + nv, NULL)); + + ud.frame_send_cb_called = 0; + ud.sent_frame_type = 0; + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(0 == ud.frame_send_cb_called); + + nghttp2_session_del(session); +} void test_nghttp2_submit_headers(void) { @@ -1543,6 +1816,37 @@ void test_nghttp2_submit_settings(void) nghttp2_session_del(session); } +void test_nghttp2_submit_push_promise(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + const char *nv[] = { ":version", "HTTP/1.1", NULL }; + my_user_data ud; + nghttp2_stream *stream; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + callbacks.on_frame_send_callback = on_frame_send_callback; + + CU_ASSERT(0 == nghttp2_session_server_new(&session, &callbacks, &ud)); + nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_OPENING, NULL); + CU_ASSERT(0 == nghttp2_submit_push_promise(session, + NGHTTP2_FLAG_NONE, + 1, nv)); + + ud.frame_send_cb_called = 0; + ud.sent_frame_type = 0; + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(1 == ud.frame_send_cb_called); + CU_ASSERT(NGHTTP2_PUSH_PROMISE == ud.sent_frame_type); + stream = nghttp2_session_get_stream(session, 2); + CU_ASSERT(NGHTTP2_STREAM_RESERVED == stream->state); + + nghttp2_session_del(session); +} + void test_nghttp2_submit_window_update(void) { nghttp2_session *session; @@ -1647,6 +1951,54 @@ void test_nghttp2_submit_invalid_nv(void) nghttp2_session_del(session); } +void test_nghttp2_session_open_stream(void) +{ + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_stream *stream; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + nghttp2_session_server_new(&session, &callbacks, NULL); + + stream = nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_NONE, + 1000000007, NGHTTP2_STREAM_OPENED, + NULL); + CU_ASSERT(1 == session->num_incoming_streams); + CU_ASSERT(0 == session->num_outgoing_streams); + CU_ASSERT(NGHTTP2_STREAM_OPENED == stream->state); + CU_ASSERT(1000000007 == stream->pri); + CU_ASSERT(NGHTTP2_SHUT_NONE == stream->shut_flags); + + stream = nghttp2_session_open_stream(session, 2, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_OPENING, + NULL); + CU_ASSERT(1 == session->num_incoming_streams); + CU_ASSERT(1 == session->num_outgoing_streams); + CU_ASSERT(NGHTTP2_SHUT_NONE == stream->shut_flags); + + stream = nghttp2_session_open_stream(session, 4, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_RESERVED, + NULL); + CU_ASSERT(1 == session->num_incoming_streams); + CU_ASSERT(1 == session->num_outgoing_streams); + CU_ASSERT(NGHTTP2_SHUT_RD == stream->shut_flags); + + nghttp2_session_del(session); + + nghttp2_session_client_new(&session, &callbacks, NULL); + stream = nghttp2_session_open_stream(session, 4, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, + NGHTTP2_STREAM_RESERVED, + NULL); + CU_ASSERT(0 == session->num_incoming_streams); + CU_ASSERT(0 == session->num_outgoing_streams); + CU_ASSERT(NGHTTP2_SHUT_WR == stream->shut_flags); + + nghttp2_session_del(session); +} + void test_nghttp2_session_get_next_ob_item(void) { nghttp2_session *session; @@ -1747,6 +2099,18 @@ void test_nghttp2_session_pop_next_ob_item(void) free(item); nghttp2_session_del(session); + + /* Check that push reply HEADERS are queued into ob_ss_pq */ + nghttp2_session_server_new(&session, &callbacks, NULL); + session->remote_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS] = 0; + nghttp2_session_open_stream(session, 2, NGHTTP2_FLAG_NONE, + NGHTTP2_PRI_DEFAULT, NGHTTP2_STREAM_RESERVED, + NULL); + CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_END_STREAM, + 2, NGHTTP2_PRI_DEFAULT, nv, NULL)); + CU_ASSERT(NULL == nghttp2_session_pop_next_ob_item(session)); + CU_ASSERT(1 == nghttp2_pq_size(&session->ob_ss_pq)); + nghttp2_session_del(session); } void test_nghttp2_session_reply_fail(void) diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index a21acca5..97aacc1d 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -34,9 +34,11 @@ void test_nghttp2_session_add_frame(void); void test_nghttp2_session_on_syn_stream_received(void); void test_nghttp2_session_on_syn_reply_received(void); void test_nghttp2_session_on_headers_received(void); +void test_nghttp2_session_on_push_reply_received(void); void test_nghttp2_session_on_priority_received(void); void test_nghttp2_session_on_rst_stream_received(void); void test_nghttp2_session_on_settings_received(void); +void test_nghttp2_session_on_push_promise_received(void); void test_nghttp2_session_on_ping_received(void); void test_nghttp2_session_on_goaway_received(void); void test_nghttp2_session_on_window_update_received(void); @@ -44,8 +46,10 @@ void test_nghttp2_session_on_data_received(void); void test_nghttp2_session_send_headers_start_stream(void); void test_nghttp2_session_send_headers_reply(void); void test_nghttp2_session_send_headers_header_comp_error(void); +void test_nghttp2_session_send_headers_push_reply(void); 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_submit_response(void); void test_nghttp2_submit_response_without_data(void); @@ -53,11 +57,14 @@ void test_nghttp2_submit_request_with_data(void); void test_nghttp2_submit_request_without_data(void); void test_nghttp2_submit_headers_start_stream(void); void test_nghttp2_submit_headers_reply(void); +void test_nghttp2_submit_headers_push_reply(void); void test_nghttp2_submit_headers(void); void test_nghttp2_submit_priority(void); void test_nghttp2_submit_settings(void); +void test_nghttp2_submit_push_promise(void); void test_nghttp2_submit_window_update(void); void test_nghttp2_submit_invalid_nv(void); +void test_nghttp2_session_open_stream(void); void test_nghttp2_session_get_next_ob_item(void); void test_nghttp2_session_pop_next_ob_item(void); void test_nghttp2_session_reply_fail(void); diff --git a/tests/nghttp2_test_helper.c b/tests/nghttp2_test_helper.c index e6dbbd3b..25916813 100644 --- a/tests/nghttp2_test_helper.c +++ b/tests/nghttp2_test_helper.c @@ -44,6 +44,14 @@ ssize_t unpack_frame_with_nv_block(nghttp2_frame *frame, len - NGHTTP2_FRAME_HEAD_LENGTH, inflater); break; + case NGHTTP2_PUSH_PROMISE: + rv = nghttp2_frame_unpack_push_promise + ((nghttp2_push_promise*)frame, + &in[0], NGHTTP2_FRAME_HEAD_LENGTH, + &in[NGHTTP2_FRAME_HEAD_LENGTH], + len - NGHTTP2_FRAME_HEAD_LENGTH, + inflater); + break; default: /* Must not be reachable */ assert(0);