diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index 038030aa..1a92a520 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -443,7 +443,11 @@ typedef enum { /** * The ALTSVC frame. */ - NGHTTP2_ALTSVC = 0x0a + NGHTTP2_ALTSVC = 0x0a, + /** + * The BLOCKED frame. + */ + NGHTTP2_BLOCKED = 0x0b } nghttp2_frame_type; /** @@ -982,6 +986,18 @@ typedef struct { uint16_t port; } nghttp2_altsvc; +/** + * @struct + * + * The BLOCKED frame. It has following members: + */ +typedef struct { + /** + * The frame header. + */ + nghttp2_frame_hd hd; +} nghttp2_blocked; + /** * @union * @@ -1034,6 +1050,10 @@ typedef union { * The ALTSVC frame. */ nghttp2_altsvc altsvc; + /** + * The BLOCKED frame. + */ + nghttp2_blocked blocked; } nghttp2_frame; /** diff --git a/lib/nghttp2_frame.c b/lib/nghttp2_frame.c index 0b8f7fb8..209328ad 100644 --- a/lib/nghttp2_frame.c +++ b/lib/nghttp2_frame.c @@ -216,6 +216,15 @@ void nghttp2_frame_altsvc_free(nghttp2_altsvc *frame) free(frame->protocol_id); } +void nghttp2_frame_blocked_init(nghttp2_blocked *frame, int32_t stream_id) +{ + nghttp2_frame_set_hd(&frame->hd, 0, NGHTTP2_BLOCKED, NGHTTP2_FLAG_NONE, + stream_id); +} + +void nghttp2_frame_blocked_free(nghttp2_blocked *frame) +{} + void nghttp2_frame_data_init(nghttp2_data *frame, nghttp2_private_data *pdata) { frame->hd = pdata->hd; @@ -797,6 +806,20 @@ int nghttp2_frame_unpack_altsvc_payload(nghttp2_altsvc *frame, return 0; } +int nghttp2_frame_pack_blocked(nghttp2_bufs *bufs, nghttp2_blocked *frame) +{ + nghttp2_buf *buf; + + assert(bufs->head == bufs->cur); + + buf = &bufs->head->buf; + buf->pos -= NGHTTP2_FRAME_HDLEN; + + nghttp2_frame_pack_frame_hd(buf->pos, &frame->hd); + + return 0; +} + nghttp2_settings_entry* nghttp2_frame_iv_copy(const nghttp2_settings_entry *iv, size_t niv) { diff --git a/lib/nghttp2_frame.h b/lib/nghttp2_frame.h index 97939d6b..6c93173d 100644 --- a/lib/nghttp2_frame.h +++ b/lib/nghttp2_frame.h @@ -444,6 +444,16 @@ int nghttp2_frame_unpack_altsvc_payload(nghttp2_altsvc *frame, uint8_t *var_gift_payload, size_t var_gift_payloadlen); +/* + * Packs BLOCKED frame |frame| in wire format and store it in |bufs|. + * + * The caller must make sure that nghttp2_bufs_reset(bufs) is called + * before calling this function. + * + * This function always returns 0. + */ +int nghttp2_frame_pack_blocked(nghttp2_bufs *bufs, nghttp2_blocked *frame); + /* * Initializes HEADERS frame |frame| with given values. |frame| takes * ownership of |nva|, so caller must not free it. If |stream_id| is @@ -534,6 +544,10 @@ void nghttp2_frame_altsvc_init(nghttp2_altsvc *frame, int32_t stream_id, void nghttp2_frame_altsvc_free(nghttp2_altsvc *frame); +void nghttp2_frame_blocked_init(nghttp2_blocked *frame, int32_t stream_id); + +void nghttp2_frame_blocked_free(nghttp2_blocked *frame); + void nghttp2_frame_data_init(nghttp2_data *frame, nghttp2_private_data *pdata); /* diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 8e820a8b..13d1c7ec 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -196,6 +196,9 @@ static void nghttp2_inbound_frame_reset(nghttp2_session *session) case NGHTTP2_ALTSVC: nghttp2_frame_altsvc_free(&iframe->frame.altsvc); break; + case NGHTTP2_BLOCKED: + nghttp2_frame_blocked_free(&iframe->frame.blocked); + break; } memset(&iframe->frame, 0, sizeof(nghttp2_frame)); @@ -628,6 +631,8 @@ int nghttp2_session_add_frame(nghttp2_session *session, break; case NGHTTP2_ALTSVC: break; + case NGHTTP2_BLOCKED: + break; } if(frame->hd.type == NGHTTP2_HEADERS && @@ -1292,6 +1297,41 @@ static int nghttp2_session_predicate_altsvc_send return 0; } +/* + * This function checks BLOCKED with the stream ID |stream_id| can be + * sent at this time. If |stream_id| is 0, BLOCKED frame is always + * allowed to send. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_STREAM_CLOSED + * The stream is already closed or does not exist. + * NGHTTP2_ERR_STREAM_CLOSING + * RST_STREAM was queued for this stream. + */ +static int nghttp2_session_predicate_blocked_send +(nghttp2_session *session, int32_t stream_id) +{ + nghttp2_stream *stream; + + if(stream_id == 0) { + return 0; + } + + stream = nghttp2_session_get_stream(session, stream_id); + + if(stream == NULL) { + return NGHTTP2_ERR_STREAM_CLOSED; + } + + if(stream->state == NGHTTP2_STREAM_CLOSING) { + return NGHTTP2_ERR_STREAM_CLOSING; + } + + return 0; +} + /* * This function checks SETTINGS can be sent at this time. * @@ -1436,6 +1476,69 @@ static int session_headers_add_pad(nghttp2_session *session, return 0; } +/* + * Adds BLOCKED frame to outbound queue. The |stream_id| could be 0, + * which means DATA is blocked by connection level flow control. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_NOMEM + * Out of memory + */ +static int session_add_blocked(nghttp2_session *session, int32_t stream_id) +{ + int rv; + nghttp2_frame *frame; + + frame = malloc(sizeof(nghttp2_frame)); + + if(frame == NULL) { + return NGHTTP2_ERR_NOMEM; + } + + nghttp2_frame_blocked_init(&frame->blocked, stream_id); + + rv = nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL); + + if(rv != 0) { + nghttp2_frame_blocked_free(&frame->blocked); + free(frame); + + return rv; + } + return 0; +} + +/* + * Adds BLOCKED frame(s) to outbound queue if they are allowed to + * send. We check BLOCKED frame can be sent for connection and stream + * individually. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_NOMEM + * Out of memory + */ +static int session_consider_blocked(nghttp2_session *session, + nghttp2_stream *stream) +{ + if(session->blocked_sent == 0 && session->remote_window_size <= 0) { + session->blocked_sent = 1; + + return session_add_blocked(session, 0); + } + + if(stream->blocked_sent == 0 && stream->remote_window_size <= 0) { + stream->blocked_sent = 1; + + return session_add_blocked(session, stream->stream_id); + } + + return 0; +} + /* * This function serializes frame for transmission. * @@ -1646,6 +1749,21 @@ static int nghttp2_session_prep_frame(nghttp2_session *session, return framerv; } + break; + case NGHTTP2_BLOCKED: + rv = nghttp2_session_predicate_blocked_send(session, + frame->hd.stream_id); + if(rv != 0) { + return rv; + } + + framerv = nghttp2_frame_pack_blocked(&session->aob.framebufs, + &frame->blocked); + + if(framerv < 0) { + return framerv; + } + break; default: return NGHTTP2_ERR_INVALID_ARGUMENT; @@ -1682,6 +1800,12 @@ static int nghttp2_session_prep_frame(nghttp2_session *session, next_readmax = nghttp2_session_next_data_read(session, stream); if(next_readmax == 0) { + rv = session_consider_blocked(session, stream); + + if(nghttp2_is_fatal(rv)) { + return rv; + } + rv = nghttp2_stream_defer_data(stream, NGHTTP2_STREAM_FLAG_DEFERRED_FLOW_CONTROL, &session->ob_pq); @@ -2020,6 +2144,9 @@ static int nghttp2_session_after_frame_sent(nghttp2_session *session) case NGHTTP2_ALTSVC: /* nothing to do */ break; + case NGHTTP2_BLOCKED: + /* nothing to do */ + break; } nghttp2_active_outbound_item_reset(&session->aob); return 0; @@ -2092,6 +2219,12 @@ static int nghttp2_session_after_frame_sent(nghttp2_session *session) next_readmax = nghttp2_session_next_data_read(session, stream); if(next_readmax == 0) { + rv = session_consider_blocked(session, stream); + + if(nghttp2_is_fatal(rv)) { + return rv; + } + rv = nghttp2_stream_defer_data (stream, NGHTTP2_STREAM_FLAG_DEFERRED_FLOW_CONTROL, &session->ob_pq); @@ -3072,6 +3205,11 @@ static int nghttp2_update_remote_initial_window_size_func return nghttp2_session_terminate_session(arg->session, NGHTTP2_FLOW_CONTROL_ERROR); } + + if(stream->remote_window_size > 0) { + stream->blocked_sent = 0; + } + /* If window size gets positive, push deferred DATA frame to outbound queue. */ if(nghttp2_stream_check_deferred_by_flow_control(stream) && @@ -3561,6 +3699,20 @@ static int session_process_altsvc_frame(nghttp2_session *session) return nghttp2_session_on_altsvc_received(session, frame); } +int nghttp2_session_on_blocked_received(nghttp2_session *session, + nghttp2_frame *frame) +{ + return nghttp2_session_call_on_frame_received(session, frame); +} + +static int session_process_blocked_frame(nghttp2_session *session) +{ + nghttp2_inbound_frame *iframe = &session->iframe; + nghttp2_frame *frame = &iframe->frame; + + return nghttp2_session_on_blocked_received(session, frame); +} + static int nghttp2_push_back_deferred_data_func(nghttp2_map_entry *entry, void *ptr) { @@ -3609,6 +3761,8 @@ static int session_on_connection_window_update_received /* To queue the DATA deferred by connection-level flow-control, we have to check all streams. Bad. */ if(session->remote_window_size > 0) { + session->blocked_sent = 0; + rv = nghttp2_session_push_back_deferred_data(session); if(rv != 0) { /* FATAL */ @@ -3642,6 +3796,11 @@ static int session_on_stream_window_update_received NGHTTP2_FLOW_CONTROL_ERROR); } stream->remote_window_size += frame->window_update.window_size_increment; + + if(stream->remote_window_size > 0) { + stream->blocked_sent = 0; + } + if(stream->remote_window_size > 0 && session->remote_window_size > 0 && nghttp2_stream_check_deferred_by_flow_control(stream)) { @@ -4364,6 +4523,27 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, iframe->state = NGHTTP2_IB_READ_NBYTE; inbound_frame_set_mark(iframe, 8); + break; + case NGHTTP2_BLOCKED: + DEBUGF(fprintf(stderr, "recv: BLOCKED\n")); + + iframe->frame.hd.flags = NGHTTP2_FLAG_NONE; + + if(iframe->payloadleft != 0) { + busy = 1; + + iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR; + + break; + } + + rv = session_process_blocked_frame(session); + if(nghttp2_is_fatal(rv)) { + return rv; + } + + nghttp2_inbound_frame_reset(session); + break; default: DEBUGF(fprintf(stderr, "recv: unknown frame\n")); diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index bcdb0467..16eecb22 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -199,6 +199,9 @@ struct nghttp2_session { /* Flags indicating GOAWAY is sent and/or recieved. The flags are composed by bitwise OR-ing nghttp2_goaway_flag. */ uint8_t goaway_flags; + /* nonzero if blocked was sent and remote_window_size is still 0 or + negative */ + uint8_t blocked_sent; }; /* Struct used when updating initial window size of each active @@ -573,6 +576,19 @@ int nghttp2_session_on_window_update_received(nghttp2_session *session, int nghttp2_session_on_altsvc_received(nghttp2_session *session, nghttp2_frame *frame); +/* + * Called when BLOCKED is received, assuming |frame| is properly + * initialized. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGHTTP2_ERR_CALLBACK_FAILURE + * The callback function failed. + */ +int nghttp2_session_on_blocked_received(nghttp2_session *session, + nghttp2_frame *frame); + /* * Called when DATA is received, assuming |frame| is properly * initialized. diff --git a/lib/nghttp2_stream.c b/lib/nghttp2_stream.c index 0e771710..ac67bee1 100644 --- a/lib/nghttp2_stream.c +++ b/lib/nghttp2_stream.c @@ -49,6 +49,7 @@ void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id, stream->local_window_size = local_initial_window_size; stream->recv_window_size = 0; stream->recv_reduction = 0; + stream->blocked_sent = 0; stream->dep_prev = NULL; stream->dep_next = NULL; diff --git a/lib/nghttp2_stream.h b/lib/nghttp2_stream.h index e47d7498..ceb207b7 100644 --- a/lib/nghttp2_stream.h +++ b/lib/nghttp2_stream.h @@ -173,6 +173,9 @@ struct nghttp2_stream { uint8_t flags; /* Bitwise OR of zero or more nghttp2_shut_flag values */ uint8_t shut_flags; + /* nonzero if blocked was sent and remote_window_size is still 0 or + negative */ + uint8_t blocked_sent; }; void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id, diff --git a/src/app_helper.cc b/src/app_helper.cc index dc78792e..7ab1bd89 100644 --- a/src/app_helper.cc +++ b/src/app_helper.cc @@ -127,6 +127,8 @@ const char* strframetype(uint8_t type) return "WINDOW_UPDATE"; case NGHTTP2_ALTSVC: return "ALTSVC"; + case NGHTTP2_BLOCKED: + return "BLOCKED"; default: return "UNKNOWN"; }