2957 lines
101 KiB
C
2957 lines
101 KiB
C
/*
|
|
* nghttp2 - HTTP/2.0 C Library
|
|
*
|
|
* Copyright (c) 2012 Tatsuhiro Tsujikawa
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
#include "nghttp2_session.h"
|
|
|
|
#include <string.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <assert.h>
|
|
|
|
#include "nghttp2_helper.h"
|
|
#include "nghttp2_net.h"
|
|
|
|
/*
|
|
* Returns non-zero if the number of outgoing opened streams is larger
|
|
* than or equal to
|
|
* remote_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS].
|
|
*/
|
|
static int nghttp2_session_is_outgoing_concurrent_streams_max
|
|
(nghttp2_session *session)
|
|
{
|
|
return session->remote_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS]
|
|
<= session->num_outgoing_streams;
|
|
}
|
|
|
|
/*
|
|
* Returns non-zero if the number of incoming opened streams is larger
|
|
* than or equal to
|
|
* local_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS].
|
|
*/
|
|
static int nghttp2_session_is_incoming_concurrent_streams_max
|
|
(nghttp2_session *session)
|
|
{
|
|
return session->local_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS]
|
|
<= session->num_incoming_streams;
|
|
}
|
|
|
|
/*
|
|
* Returns non-zero if |error| is non-fatal error.
|
|
*/
|
|
static int nghttp2_is_non_fatal(int error)
|
|
{
|
|
return error < 0 && error > NGHTTP2_ERR_FATAL;
|
|
}
|
|
|
|
/*
|
|
* Returns non-zero if |error| is fatal error.
|
|
*/
|
|
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)
|
|
{
|
|
session->goaway_flags |= NGHTTP2_GOAWAY_FAIL_ON_SEND;
|
|
return nghttp2_submit_goaway(session, error_code, NULL, 0);
|
|
}
|
|
|
|
int nghttp2_session_is_my_stream_id(nghttp2_session *session,
|
|
int32_t stream_id)
|
|
{
|
|
int r;
|
|
if(stream_id == 0) {
|
|
return 0;
|
|
}
|
|
r = stream_id % 2;
|
|
return (session->server && r == 0) || (!session->server && r == 1);
|
|
}
|
|
|
|
nghttp2_stream* nghttp2_session_get_stream(nghttp2_session *session,
|
|
int32_t stream_id)
|
|
{
|
|
return (nghttp2_stream*)nghttp2_map_find(&session->streams, stream_id);
|
|
}
|
|
|
|
static int nghttp2_outbound_item_compar(const void *lhsx, const void *rhsx)
|
|
{
|
|
const nghttp2_outbound_item *lhs, *rhs;
|
|
lhs = (const nghttp2_outbound_item*)lhsx;
|
|
rhs = (const nghttp2_outbound_item*)rhsx;
|
|
if(lhs->pri == rhs->pri) {
|
|
return (lhs->seq < rhs->seq) ? -1 : ((lhs->seq > rhs->seq) ? 1 : 0);
|
|
} else {
|
|
return lhs->pri - rhs->pri;
|
|
}
|
|
}
|
|
|
|
static void nghttp2_inbound_frame_reset(nghttp2_inbound_frame *iframe)
|
|
{
|
|
iframe->state = NGHTTP2_RECV_HEAD;
|
|
iframe->payloadlen = iframe->buflen = iframe->off = 0;
|
|
iframe->headbufoff = 0;
|
|
iframe->error_code = 0;
|
|
}
|
|
|
|
static int nghttp2_session_new(nghttp2_session **session_ptr,
|
|
const nghttp2_session_callbacks *callbacks,
|
|
void *user_data,
|
|
nghttp2_hd_side side)
|
|
{
|
|
int r;
|
|
*session_ptr = malloc(sizeof(nghttp2_session));
|
|
if(*session_ptr == NULL) {
|
|
r = NGHTTP2_ERR_NOMEM;
|
|
goto fail_session;
|
|
}
|
|
memset(*session_ptr, 0, sizeof(nghttp2_session));
|
|
|
|
/* next_stream_id and last_recv_stream_id are initialized in either
|
|
nghttp2_session_client_new or nghttp2_session_server_new */
|
|
|
|
(*session_ptr)->next_seq = 0;
|
|
|
|
(*session_ptr)->remote_flow_control = 1;
|
|
(*session_ptr)->local_flow_control = 1;
|
|
(*session_ptr)->window_size = NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE;
|
|
(*session_ptr)->recv_window_size = 0;
|
|
|
|
(*session_ptr)->goaway_flags = NGHTTP2_GOAWAY_NONE;
|
|
(*session_ptr)->last_stream_id = 0;
|
|
|
|
r = nghttp2_hd_deflate_init(&(*session_ptr)->hd_deflater, side);
|
|
if(r != 0) {
|
|
goto fail_hd_deflater;
|
|
}
|
|
r = nghttp2_hd_inflate_init(&(*session_ptr)->hd_inflater, side);
|
|
if(r != 0) {
|
|
goto fail_hd_inflater;
|
|
}
|
|
nghttp2_map_init(&(*session_ptr)->streams);
|
|
r = nghttp2_pq_init(&(*session_ptr)->ob_pq, nghttp2_outbound_item_compar);
|
|
if(r != 0) {
|
|
goto fail_ob_pq;
|
|
}
|
|
r = nghttp2_pq_init(&(*session_ptr)->ob_ss_pq, nghttp2_outbound_item_compar);
|
|
if(r != 0) {
|
|
goto fail_ob_ss_pq;
|
|
}
|
|
|
|
(*session_ptr)->aob.framebuf = malloc
|
|
(NGHTTP2_INITIAL_OUTBOUND_FRAMEBUF_LENGTH);
|
|
if((*session_ptr)->aob.framebuf == NULL) {
|
|
r = NGHTTP2_ERR_NOMEM;
|
|
goto fail_aob_framebuf;
|
|
}
|
|
(*session_ptr)->aob.framebufmax = NGHTTP2_INITIAL_OUTBOUND_FRAMEBUF_LENGTH;
|
|
|
|
(*session_ptr)->nvbuf = malloc(NGHTTP2_INITIAL_NV_BUFFER_LENGTH);
|
|
if((*session_ptr)->nvbuf == NULL) {
|
|
r = NGHTTP2_ERR_NOMEM;
|
|
goto fail_nvbuf;
|
|
}
|
|
(*session_ptr)->nvbuflen = NGHTTP2_INITIAL_NV_BUFFER_LENGTH;
|
|
|
|
memset((*session_ptr)->remote_settings, 0,
|
|
sizeof((*session_ptr)->remote_settings));
|
|
(*session_ptr)->remote_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS] =
|
|
NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS;
|
|
(*session_ptr)->remote_settings[NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE] =
|
|
NGHTTP2_INITIAL_WINDOW_SIZE;
|
|
|
|
memset((*session_ptr)->local_settings, 0,
|
|
sizeof((*session_ptr)->local_settings));
|
|
(*session_ptr)->local_settings[NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS] =
|
|
NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS;
|
|
(*session_ptr)->local_settings[NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE] =
|
|
NGHTTP2_INITIAL_WINDOW_SIZE;
|
|
|
|
(*session_ptr)->callbacks = *callbacks;
|
|
(*session_ptr)->user_data = user_data;
|
|
|
|
(*session_ptr)->iframe.buf = malloc(NGHTTP2_INITIAL_INBOUND_FRAMEBUF_LENGTH);
|
|
if((*session_ptr)->iframe.buf == NULL) {
|
|
r = NGHTTP2_ERR_NOMEM;
|
|
goto fail_iframe_buf;
|
|
}
|
|
(*session_ptr)->iframe.bufmax = NGHTTP2_INITIAL_INBOUND_FRAMEBUF_LENGTH;
|
|
|
|
nghttp2_inbound_frame_reset(&(*session_ptr)->iframe);
|
|
|
|
return 0;
|
|
|
|
fail_iframe_buf:
|
|
free((*session_ptr)->nvbuf);
|
|
fail_nvbuf:
|
|
free((*session_ptr)->aob.framebuf);
|
|
fail_aob_framebuf:
|
|
nghttp2_pq_free(&(*session_ptr)->ob_ss_pq);
|
|
fail_ob_ss_pq:
|
|
nghttp2_pq_free(&(*session_ptr)->ob_pq);
|
|
fail_ob_pq:
|
|
/* No need to free (*session_ptr)->streams) here. */
|
|
nghttp2_hd_inflate_free(&(*session_ptr)->hd_inflater);
|
|
fail_hd_inflater:
|
|
nghttp2_hd_deflate_free(&(*session_ptr)->hd_deflater);
|
|
fail_hd_deflater:
|
|
free(*session_ptr);
|
|
fail_session:
|
|
return r;
|
|
}
|
|
|
|
int nghttp2_session_client_new(nghttp2_session **session_ptr,
|
|
const nghttp2_session_callbacks *callbacks,
|
|
void *user_data)
|
|
{
|
|
int r;
|
|
/* For client side session, header compression is disabled. */
|
|
r = nghttp2_session_new(session_ptr, callbacks, user_data,
|
|
NGHTTP2_HD_SIDE_CLIENT);
|
|
if(r == 0) {
|
|
/* IDs for use in client */
|
|
(*session_ptr)->next_stream_id = 1;
|
|
(*session_ptr)->last_recv_stream_id = 0;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
int nghttp2_session_server_new(nghttp2_session **session_ptr,
|
|
const nghttp2_session_callbacks *callbacks,
|
|
void *user_data)
|
|
{
|
|
int r;
|
|
/* Enable header compression on server side. */
|
|
r = nghttp2_session_new(session_ptr, callbacks, user_data,
|
|
NGHTTP2_HD_SIDE_SERVER);
|
|
if(r == 0) {
|
|
(*session_ptr)->server = 1;
|
|
/* IDs for use in client */
|
|
(*session_ptr)->next_stream_id = 2;
|
|
(*session_ptr)->last_recv_stream_id = 0;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static int nghttp2_free_streams(nghttp2_map_entry *entry, void *ptr)
|
|
{
|
|
nghttp2_stream_free((nghttp2_stream*)entry);
|
|
free(entry);
|
|
return 0;
|
|
}
|
|
|
|
static void nghttp2_session_ob_pq_free(nghttp2_pq *pq)
|
|
{
|
|
while(!nghttp2_pq_empty(pq)) {
|
|
nghttp2_outbound_item *item = (nghttp2_outbound_item*)nghttp2_pq_top(pq);
|
|
nghttp2_outbound_item_free(item);
|
|
free(item);
|
|
nghttp2_pq_pop(pq);
|
|
}
|
|
nghttp2_pq_free(pq);
|
|
}
|
|
|
|
static void nghttp2_active_outbound_item_reset
|
|
(nghttp2_active_outbound_item *aob)
|
|
{
|
|
nghttp2_outbound_item_free(aob->item);
|
|
free(aob->item);
|
|
aob->item = NULL;
|
|
aob->framebuflen = aob->framebufoff = 0;
|
|
}
|
|
|
|
void nghttp2_session_del(nghttp2_session *session)
|
|
{
|
|
if(session == NULL) {
|
|
return;
|
|
}
|
|
nghttp2_map_each_free(&session->streams, nghttp2_free_streams, NULL);
|
|
nghttp2_session_ob_pq_free(&session->ob_pq);
|
|
nghttp2_session_ob_pq_free(&session->ob_ss_pq);
|
|
nghttp2_hd_deflate_free(&session->hd_deflater);
|
|
nghttp2_hd_inflate_free(&session->hd_inflater);
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
free(session->aob.framebuf);
|
|
free(session->nvbuf);
|
|
free(session->iframe.buf);
|
|
free(session);
|
|
}
|
|
|
|
int nghttp2_session_add_frame(nghttp2_session *session,
|
|
nghttp2_frame_category frame_cat,
|
|
void *abs_frame,
|
|
void *aux_data)
|
|
{
|
|
int r = 0;
|
|
nghttp2_outbound_item *item;
|
|
item = malloc(sizeof(nghttp2_outbound_item));
|
|
if(item == NULL) {
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
item->frame_cat = frame_cat;
|
|
item->frame = abs_frame;
|
|
item->aux_data = aux_data;
|
|
item->seq = session->next_seq++;
|
|
/* Set priority to the default value at the moment. */
|
|
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) {
|
|
/* Initial HEADERS, which will open stream */
|
|
item->pri = frame->headers.pri;
|
|
} else {
|
|
/* Otherwise, the frame must have stream ID. We use its
|
|
priority value. */
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream) {
|
|
item->pri = stream->pri;
|
|
}
|
|
}
|
|
break;
|
|
case NGHTTP2_PRIORITY: {
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream) {
|
|
item->pri = stream->pri;
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_RST_STREAM: {
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream) {
|
|
stream->state = NGHTTP2_STREAM_CLOSING;
|
|
item->pri = stream->pri;
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_SETTINGS:
|
|
/* 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;
|
|
break;
|
|
case NGHTTP2_GOAWAY:
|
|
/* Should GOAWAY have higher priority? */
|
|
break;
|
|
case NGHTTP2_WINDOW_UPDATE:
|
|
if(frame->hd.stream_id == 0) {
|
|
/* Connection level window update should have higher priority */
|
|
item->pri = -1;
|
|
} else {
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream) {
|
|
item->pri = stream->pri;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
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);
|
|
}
|
|
} else if(frame_cat == NGHTTP2_CAT_DATA) {
|
|
nghttp2_data *data_frame = (nghttp2_data*)abs_frame;
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, data_frame->hd.stream_id);
|
|
if(stream) {
|
|
item->pri = stream->pri;
|
|
}
|
|
r = nghttp2_pq_push(&session->ob_pq, item);
|
|
} else {
|
|
/* Unreachable */
|
|
assert(0);
|
|
}
|
|
if(r != 0) {
|
|
free(item);
|
|
return r;
|
|
}
|
|
item->inipri = item->pri;
|
|
item->pridecay = 1;
|
|
return 0;
|
|
}
|
|
|
|
int nghttp2_session_add_rst_stream(nghttp2_session *session,
|
|
int32_t stream_id,
|
|
nghttp2_error_code error_code)
|
|
{
|
|
int r;
|
|
nghttp2_frame *frame;
|
|
frame = malloc(sizeof(nghttp2_frame));
|
|
if(frame == NULL) {
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
nghttp2_frame_rst_stream_init(&frame->rst_stream, stream_id, error_code);
|
|
r = nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL);
|
|
if(r != 0) {
|
|
nghttp2_frame_rst_stream_free(&frame->rst_stream);
|
|
free(frame);
|
|
return r;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
nghttp2_stream* nghttp2_session_open_stream(nghttp2_session *session,
|
|
int32_t stream_id,
|
|
uint8_t flags, int32_t pri,
|
|
nghttp2_stream_state initial_state,
|
|
void *stream_user_data)
|
|
{
|
|
int r;
|
|
nghttp2_stream *stream = malloc(sizeof(nghttp2_stream));
|
|
if(stream == NULL) {
|
|
return NULL;
|
|
}
|
|
nghttp2_stream_init(stream, stream_id, flags, pri, initial_state,
|
|
!session->remote_settings
|
|
[NGHTTP2_SETTINGS_FLOW_CONTROL_OPTIONS],
|
|
!session->local_settings
|
|
[NGHTTP2_SETTINGS_FLOW_CONTROL_OPTIONS],
|
|
session->remote_settings
|
|
[NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE],
|
|
stream_user_data);
|
|
r = nghttp2_map_insert(&session->streams, &stream->map_entry);
|
|
if(r != 0) {
|
|
free(stream);
|
|
return NULL;
|
|
}
|
|
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 {
|
|
if(nghttp2_session_is_my_stream_id(session, stream_id)) {
|
|
++session->num_outgoing_streams;
|
|
} else {
|
|
++session->num_incoming_streams;
|
|
}
|
|
}
|
|
return stream;
|
|
}
|
|
|
|
int nghttp2_session_close_stream(nghttp2_session *session, int32_t stream_id,
|
|
nghttp2_error_code error_code)
|
|
{
|
|
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(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);
|
|
free(stream);
|
|
return 0;
|
|
} else {
|
|
return NGHTTP2_ERR_INVALID_ARGUMENT;
|
|
}
|
|
}
|
|
|
|
int nghttp2_session_close_stream_if_shut_rdwr(nghttp2_session *session,
|
|
nghttp2_stream *stream)
|
|
{
|
|
if((stream->shut_flags & NGHTTP2_SHUT_RDWR) == NGHTTP2_SHUT_RDWR) {
|
|
return nghttp2_session_close_stream(session, stream->stream_id,
|
|
NGHTTP2_NO_ERROR);
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Check that we can send a frame to the |stream|. This function
|
|
* returns 0 if we can send a frame to the |frame|, or one of the
|
|
* following negative error codes:
|
|
*
|
|
* NGHTTP2_ERR_STREAM_CLOSED
|
|
* The stream is already closed.
|
|
* NGHTTP2_ERR_STREAM_SHUT_WR
|
|
* The stream is half-closed for transmission.
|
|
*/
|
|
static int nghttp2_predicate_stream_for_send(nghttp2_stream *stream)
|
|
{
|
|
if(stream == NULL) {
|
|
return NGHTTP2_ERR_STREAM_CLOSED;
|
|
} else if(stream->shut_flags & NGHTTP2_SHUT_WR) {
|
|
return NGHTTP2_ERR_STREAM_SHUT_WR;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This function checks HEADERS frame |frame|, which opens stream, 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.
|
|
*/
|
|
static int nghttp2_session_predicate_request_headers_send
|
|
(nghttp2_session *session, nghttp2_headers *frame)
|
|
{
|
|
if(session->goaway_flags) {
|
|
/* When GOAWAY is sent or received, peer must not send new request
|
|
HEADERS. */
|
|
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 HEADERS, which is the first frame from the
|
|
* server, with the 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_STREAM_CLOSED
|
|
* The stream is already closed or does not exist.
|
|
* 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)
|
|
* NGHTTP2_ERR_INVALID_STREAM_ID
|
|
* The stream ID is invalid.
|
|
* NGHTTP2_ERR_STREAM_CLOSING
|
|
* RST_STREAM was queued for this stream.
|
|
* NGHTTP2_ERR_INVALID_STREAM_STATE
|
|
* The state of the stream is not valid.
|
|
*/
|
|
static int nghttp2_session_predicate_response_headers_send
|
|
(nghttp2_session *session, int32_t stream_id)
|
|
{
|
|
nghttp2_stream *stream = nghttp2_session_get_stream(session, stream_id);
|
|
int r;
|
|
r = nghttp2_predicate_stream_for_send(stream);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
if(nghttp2_session_is_my_stream_id(session, stream_id)) {
|
|
return NGHTTP2_ERR_INVALID_STREAM_ID;
|
|
} else {
|
|
if(stream->state == NGHTTP2_STREAM_OPENING) {
|
|
return 0;
|
|
} else if(stream->state == NGHTTP2_STREAM_CLOSING) {
|
|
return NGHTTP2_ERR_STREAM_CLOSING;
|
|
} else {
|
|
return NGHTTP2_ERR_INVALID_STREAM_STATE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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_response_headers_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.
|
|
*
|
|
* 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_SHUT_WR
|
|
* The transmission is not allowed for this stream (e.g., a frame
|
|
* with END_STREAM flag set has already sent)
|
|
* NGHTTP2_ERR_STREAM_CLOSING
|
|
* RST_STREAM was queued for this stream.
|
|
* NGHTTP2_ERR_INVALID_STREAM_STATE
|
|
* The state of the stream is not valid.
|
|
*/
|
|
static int nghttp2_session_predicate_stream_frame_send
|
|
(nghttp2_session* session, int32_t stream_id)
|
|
{
|
|
nghttp2_stream *stream = nghttp2_session_get_stream(session, stream_id);
|
|
int r;
|
|
r = nghttp2_predicate_stream_for_send(stream);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
if(nghttp2_session_is_my_stream_id(session, stream_id)) {
|
|
if(stream->state != NGHTTP2_STREAM_CLOSING) {
|
|
return 0;
|
|
} else {
|
|
return NGHTTP2_ERR_STREAM_CLOSING;
|
|
}
|
|
} else {
|
|
if(stream->state == NGHTTP2_STREAM_OPENED) {
|
|
return 0;
|
|
} else if(stream->state == NGHTTP2_STREAM_CLOSING) {
|
|
return NGHTTP2_ERR_STREAM_CLOSING;
|
|
} else {
|
|
return NGHTTP2_ERR_INVALID_STREAM_STATE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This function checks HEADERS, which is neither stream-opening nor
|
|
* first response header, with the stream ID |stream_id| can be sent
|
|
* at this time.
|
|
*/
|
|
static int nghttp2_session_predicate_headers_send(nghttp2_session *session,
|
|
int32_t stream_id)
|
|
{
|
|
return nghttp2_session_predicate_stream_frame_send(session, stream_id);
|
|
}
|
|
|
|
/*
|
|
* This function checks PRIORITY frame with stream ID |stream_id| can
|
|
* be sent at this time.
|
|
*/
|
|
static int nghttp2_session_predicate_priority_send
|
|
(nghttp2_session *session, int32_t stream_id)
|
|
{
|
|
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 END_STREAM flag of the previous
|
|
* frame does not affect the transmission of the WINDOW_UPDATE frame.
|
|
*
|
|
* 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_window_update_send
|
|
(nghttp2_session *session, int32_t stream_id)
|
|
{
|
|
nghttp2_stream *stream;
|
|
if(stream_id == 0) {
|
|
/* Connection-level window update */
|
|
return 0;
|
|
}
|
|
stream = nghttp2_session_get_stream(session, stream_id);
|
|
if(stream == NULL) {
|
|
return NGHTTP2_ERR_STREAM_CLOSED;
|
|
}
|
|
if(stream->state != NGHTTP2_STREAM_CLOSING &&
|
|
stream->state != NGHTTP2_STREAM_RESERVED) {
|
|
return 0;
|
|
} else {
|
|
return NGHTTP2_ERR_STREAM_CLOSING;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Returns the maximum length of next data read. If the
|
|
* connection-level and/or stream-wise flow control are enabled, the
|
|
* return value takes into account those current window sizes.
|
|
*/
|
|
static size_t nghttp2_session_next_data_read(nghttp2_session *session,
|
|
nghttp2_stream *stream)
|
|
{
|
|
/* TODO implement connection-level flow control here */
|
|
if(session->remote_flow_control == 0 && stream->remote_flow_control == 0) {
|
|
return NGHTTP2_DATA_PAYLOAD_LENGTH;
|
|
} else {
|
|
int32_t session_window_size =
|
|
session->remote_flow_control ? session->window_size : INT32_MAX;
|
|
int32_t stream_window_size =
|
|
stream->remote_flow_control ? stream->window_size : INT32_MAX;
|
|
int32_t window_size = nghttp2_min(session_window_size,
|
|
stream_window_size);
|
|
if(window_size > 0) {
|
|
return window_size < NGHTTP2_DATA_PAYLOAD_LENGTH ?
|
|
window_size : NGHTTP2_DATA_PAYLOAD_LENGTH;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This function checks DATA with the 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_STREAM_CLOSED
|
|
* The stream is already closed or does not exist.
|
|
* 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)
|
|
* NGHTTP2_ERR_DEFERRED_DATA_EXIST
|
|
* Another DATA frame has already been deferred.
|
|
* NGHTTP2_ERR_STREAM_CLOSING
|
|
* RST_STREAM was queued for this stream.
|
|
* NGHTTP2_ERR_INVALID_STREAM_STATE
|
|
* The state of the stream is not valid.
|
|
*/
|
|
static int nghttp2_session_predicate_data_send(nghttp2_session *session,
|
|
int32_t stream_id)
|
|
{
|
|
nghttp2_stream *stream = nghttp2_session_get_stream(session, stream_id);
|
|
int r;
|
|
r = nghttp2_predicate_stream_for_send(stream);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
if(stream->deferred_data != NULL) {
|
|
/* stream->deferred_data != NULL means previously queued DATA
|
|
frame has not been sent. We don't allow new DATA frame is sent
|
|
in this case. */
|
|
return NGHTTP2_ERR_DEFERRED_DATA_EXIST;
|
|
}
|
|
if(nghttp2_session_is_my_stream_id(session, stream_id)) {
|
|
/* If stream->state is NGHTTP2_STREAM_CLOSING, RST_STREAM was
|
|
queued but not yet sent. In this case, we won't send DATA
|
|
frames. This is because in the current architecture, DATA and
|
|
RST_STREAM in the same stream have same priority and DATA is
|
|
small seq number. So RST_STREAM will not be sent until all DATA
|
|
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 &&
|
|
stream->state != NGHTTP2_STREAM_RESERVED) {
|
|
return 0;
|
|
} else {
|
|
return NGHTTP2_ERR_STREAM_CLOSING;
|
|
}
|
|
} else {
|
|
if(stream->state == NGHTTP2_STREAM_OPENED) {
|
|
return 0;
|
|
} else if(stream->state == NGHTTP2_STREAM_CLOSING) {
|
|
return NGHTTP2_ERR_STREAM_CLOSING;
|
|
} else {
|
|
return NGHTTP2_ERR_INVALID_STREAM_STATE;
|
|
}
|
|
}
|
|
}
|
|
|
|
static ssize_t nghttp2_session_prep_frame(nghttp2_session *session,
|
|
nghttp2_outbound_item *item)
|
|
{
|
|
ssize_t framebuflen = 0;
|
|
if(item->frame_cat == NGHTTP2_CAT_CTRL) {
|
|
nghttp2_frame *frame;
|
|
frame = nghttp2_outbound_item_get_ctrl_frame(item);
|
|
switch(frame->hd.type) {
|
|
case NGHTTP2_HEADERS: {
|
|
if(frame->hd.stream_id == -1) {
|
|
/* initial HEADERS, which opens stream */
|
|
int r;
|
|
frame->headers.cat = NGHTTP2_HCAT_REQUEST;
|
|
r = nghttp2_session_predicate_request_headers_send(session,
|
|
&frame->headers);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
frame->hd.stream_id = session->next_stream_id;
|
|
session->next_stream_id += 2;
|
|
} else if(nghttp2_session_predicate_push_response_headers_send
|
|
(session, frame->hd.stream_id) == 0) {
|
|
frame->headers.cat = NGHTTP2_HCAT_PUSH_RESPONSE;
|
|
} else if(nghttp2_session_predicate_response_headers_send
|
|
(session, frame->hd.stream_id) == 0) {
|
|
frame->headers.cat = NGHTTP2_HCAT_RESPONSE;
|
|
} else {
|
|
int r;
|
|
frame->headers.cat = NGHTTP2_HCAT_HEADERS;
|
|
r = nghttp2_session_predicate_headers_send(session,
|
|
frame->hd.stream_id);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
}
|
|
framebuflen = nghttp2_frame_pack_headers(&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
&frame->headers,
|
|
&session->hd_deflater);
|
|
nghttp2_hd_end_headers(&session->hd_deflater);
|
|
if(framebuflen < 0) {
|
|
return framebuflen;
|
|
}
|
|
switch(frame->headers.cat) {
|
|
case NGHTTP2_HCAT_REQUEST: {
|
|
nghttp2_headers_aux_data *aux_data;
|
|
aux_data = (nghttp2_headers_aux_data*)item->aux_data;
|
|
if(nghttp2_session_open_stream
|
|
(session, frame->hd.stream_id,
|
|
frame->hd.flags,
|
|
frame->headers.pri,
|
|
NGHTTP2_STREAM_INITIAL,
|
|
aux_data ? aux_data->stream_user_data : NULL) == NULL) {
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_HCAT_PUSH_RESPONSE: {
|
|
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;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_PRIORITY: {
|
|
int r;
|
|
r = nghttp2_session_predicate_priority_send
|
|
(session, frame->hd.stream_id);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
framebuflen = nghttp2_frame_pack_priority(&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
&frame->priority);
|
|
if(framebuflen < 0) {
|
|
return framebuflen;
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_RST_STREAM:
|
|
framebuflen = nghttp2_frame_pack_rst_stream(&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
&frame->rst_stream);
|
|
if(framebuflen < 0) {
|
|
return framebuflen;
|
|
}
|
|
break;
|
|
case NGHTTP2_SETTINGS:
|
|
framebuflen = nghttp2_frame_pack_settings(&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
&frame->settings);
|
|
if(framebuflen < 0) {
|
|
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,
|
|
&frame->ping);
|
|
if(framebuflen < 0) {
|
|
return framebuflen;
|
|
}
|
|
break;
|
|
case NGHTTP2_WINDOW_UPDATE: {
|
|
int r;
|
|
r = nghttp2_session_predicate_window_update_send
|
|
(session, frame->hd.stream_id);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
framebuflen = nghttp2_frame_pack_window_update(&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
&frame->window_update);
|
|
if(framebuflen < 0) {
|
|
return framebuflen;
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_GOAWAY:
|
|
if(session->goaway_flags & NGHTTP2_GOAWAY_SEND) {
|
|
/* TODO The spec does not mandate that both endpoints have to
|
|
exchange GOAWAY. This implementation allows receiver of
|
|
first GOAWAY can sent its own GOAWAY to tell the remote
|
|
peer that last-stream-id. */
|
|
return NGHTTP2_ERR_GOAWAY_ALREADY_SENT;
|
|
}
|
|
framebuflen = nghttp2_frame_pack_goaway(&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
&frame->goaway);
|
|
if(framebuflen < 0) {
|
|
return framebuflen;
|
|
}
|
|
break;
|
|
default:
|
|
framebuflen = NGHTTP2_ERR_INVALID_ARGUMENT;
|
|
}
|
|
} else if(item->frame_cat == NGHTTP2_CAT_DATA) {
|
|
size_t next_readmax;
|
|
nghttp2_stream *stream;
|
|
nghttp2_data *data_frame;
|
|
int r;
|
|
data_frame = nghttp2_outbound_item_get_data_frame(item);
|
|
r = nghttp2_session_predicate_data_send(session, data_frame->hd.stream_id);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
stream = nghttp2_session_get_stream(session, data_frame->hd.stream_id);
|
|
/* Assuming stream is not NULL */
|
|
assert(stream);
|
|
next_readmax = nghttp2_session_next_data_read(session, stream);
|
|
if(next_readmax == 0) {
|
|
nghttp2_stream_defer_data(stream, item, NGHTTP2_DEFERRED_FLOW_CONTROL);
|
|
return NGHTTP2_ERR_DEFERRED;
|
|
}
|
|
framebuflen = nghttp2_session_pack_data(session,
|
|
&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
next_readmax,
|
|
data_frame);
|
|
if(framebuflen == NGHTTP2_ERR_DEFERRED) {
|
|
nghttp2_stream_defer_data(stream, item, NGHTTP2_DEFERRED_NONE);
|
|
return NGHTTP2_ERR_DEFERRED;
|
|
} else if(framebuflen == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
|
|
r = nghttp2_session_add_rst_stream(session, data_frame->hd.stream_id,
|
|
NGHTTP2_INTERNAL_ERROR);
|
|
if(r == 0) {
|
|
return framebuflen;
|
|
} else {
|
|
return r;
|
|
}
|
|
} else if(framebuflen < 0) {
|
|
return framebuflen;
|
|
}
|
|
} else {
|
|
/* Unreachable */
|
|
assert(0);
|
|
}
|
|
return framebuflen;
|
|
}
|
|
|
|
nghttp2_outbound_item* nghttp2_session_get_ob_pq_top
|
|
(nghttp2_session *session)
|
|
{
|
|
return (nghttp2_outbound_item*)nghttp2_pq_top(&session->ob_pq);
|
|
}
|
|
|
|
nghttp2_outbound_item* nghttp2_session_get_next_ob_item
|
|
(nghttp2_session *session)
|
|
{
|
|
if(nghttp2_pq_empty(&session->ob_pq)) {
|
|
if(nghttp2_pq_empty(&session->ob_ss_pq)) {
|
|
return NULL;
|
|
} else {
|
|
/* Return item only when concurrent connection limit is not
|
|
reached */
|
|
if(nghttp2_session_is_outgoing_concurrent_streams_max(session)) {
|
|
return NULL;
|
|
} else {
|
|
return nghttp2_pq_top(&session->ob_ss_pq);
|
|
}
|
|
}
|
|
} else {
|
|
if(nghttp2_pq_empty(&session->ob_ss_pq)) {
|
|
return nghttp2_pq_top(&session->ob_pq);
|
|
} else {
|
|
nghttp2_outbound_item *item, *headers_item;
|
|
item = nghttp2_pq_top(&session->ob_pq);
|
|
headers_item = nghttp2_pq_top(&session->ob_ss_pq);
|
|
if(nghttp2_session_is_outgoing_concurrent_streams_max(session) ||
|
|
item->pri < headers_item->pri ||
|
|
(item->pri == headers_item->pri &&
|
|
item->seq < headers_item->seq)) {
|
|
return item;
|
|
} else {
|
|
return headers_item;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
nghttp2_outbound_item* nghttp2_session_pop_next_ob_item
|
|
(nghttp2_session *session)
|
|
{
|
|
if(nghttp2_pq_empty(&session->ob_pq)) {
|
|
if(nghttp2_pq_empty(&session->ob_ss_pq)) {
|
|
return NULL;
|
|
} else {
|
|
/* Pop item only when concurrent connection limit is not
|
|
reached */
|
|
if(nghttp2_session_is_outgoing_concurrent_streams_max(session)) {
|
|
return NULL;
|
|
} else {
|
|
nghttp2_outbound_item *item;
|
|
item = nghttp2_pq_top(&session->ob_ss_pq);
|
|
nghttp2_pq_pop(&session->ob_ss_pq);
|
|
return item;
|
|
}
|
|
}
|
|
} else {
|
|
if(nghttp2_pq_empty(&session->ob_ss_pq)) {
|
|
nghttp2_outbound_item *item;
|
|
item = nghttp2_pq_top(&session->ob_pq);
|
|
nghttp2_pq_pop(&session->ob_pq);
|
|
return item;
|
|
} else {
|
|
nghttp2_outbound_item *item, *headers_item;
|
|
item = nghttp2_pq_top(&session->ob_pq);
|
|
headers_item = nghttp2_pq_top(&session->ob_ss_pq);
|
|
if(nghttp2_session_is_outgoing_concurrent_streams_max(session) ||
|
|
item->pri < headers_item->pri ||
|
|
(item->pri == headers_item->pri &&
|
|
item->seq < headers_item->seq)) {
|
|
nghttp2_pq_pop(&session->ob_pq);
|
|
return item;
|
|
} else {
|
|
nghttp2_pq_pop(&session->ob_ss_pq);
|
|
return headers_item;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Adjust priority of item so that the higher priority long DATA
|
|
* frames don't starve lower priority streams.
|
|
*/
|
|
static void nghttp2_outbound_item_adjust_pri(nghttp2_session *session,
|
|
nghttp2_outbound_item *item)
|
|
{
|
|
if(item->pri == NGHTTP2_PRI_LOWEST) {
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream
|
|
(session, nghttp2_outbound_item_get_data_frame(item)->hd.stream_id);
|
|
assert(stream);
|
|
item->pri = item->inipri = stream->pri;
|
|
item->pridecay = 1;
|
|
} else if(item->pri + item->pridecay > NGHTTP2_PRI_LOWEST) {
|
|
item->pri = NGHTTP2_PRI_LOWEST;
|
|
} else {
|
|
item->pri = item->inipri + item->pridecay;
|
|
item->pridecay *= 2;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Called after a frame is sent.
|
|
*
|
|
* This function returns 0 if it succeeds, or one of the following
|
|
* negative error codes:
|
|
*
|
|
* NGHTTP2_ERR_NOMEM
|
|
* Out of memory.
|
|
* NGHTTP2_ERR_CALLBACK_FAILURE
|
|
* The callback function failed.
|
|
*/
|
|
static int nghttp2_session_after_frame_sent(nghttp2_session *session)
|
|
{
|
|
nghttp2_outbound_item *item = session->aob.item;
|
|
if(item->frame_cat == NGHTTP2_CAT_CTRL) {
|
|
nghttp2_frame *frame;
|
|
frame = nghttp2_outbound_item_get_ctrl_frame(session->aob.item);
|
|
if(session->callbacks.on_frame_send_callback) {
|
|
session->callbacks.on_frame_send_callback(session, frame,
|
|
session->user_data);
|
|
}
|
|
switch(frame->hd.type) {
|
|
case NGHTTP2_HEADERS: {
|
|
nghttp2_stream *stream =
|
|
nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
nghttp2_headers_aux_data *aux_data;
|
|
if(stream) {
|
|
switch(frame->headers.cat) {
|
|
case NGHTTP2_HCAT_REQUEST: {
|
|
stream->state = NGHTTP2_STREAM_OPENING;
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR);
|
|
}
|
|
nghttp2_session_close_stream_if_shut_rdwr(session, stream);
|
|
/* We assume aux_data is a pointer to nghttp2_headers_aux_data */
|
|
aux_data = (nghttp2_headers_aux_data*)item->aux_data;
|
|
if(aux_data && aux_data->data_prd) {
|
|
int r;
|
|
/* nghttp2_submit_data() makes a copy of aux_data->data_prd */
|
|
r = nghttp2_submit_data(session, NGHTTP2_FLAG_END_STREAM,
|
|
frame->hd.stream_id, aux_data->data_prd);
|
|
if(r != 0) {
|
|
/* FATAL error */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
/* TODO If r is not FATAL, we should send RST_STREAM. */
|
|
return r;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_HCAT_RESPONSE:
|
|
case NGHTTP2_HCAT_PUSH_RESPONSE:
|
|
stream->state = NGHTTP2_STREAM_OPENED;
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR);
|
|
}
|
|
nghttp2_session_close_stream_if_shut_rdwr(session, stream);
|
|
/* We assume aux_data is a pointer to nghttp2_headers_aux_data */
|
|
aux_data = (nghttp2_headers_aux_data*)item->aux_data;
|
|
if(aux_data && aux_data->data_prd) {
|
|
int r;
|
|
r = nghttp2_submit_data(session, NGHTTP2_FLAG_END_STREAM,
|
|
frame->hd.stream_id, aux_data->data_prd);
|
|
if(r != 0) {
|
|
/* FATAL error */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
/* TODO If r is not FATAL, we should send RST_STREAM. */
|
|
return r;
|
|
}
|
|
}
|
|
break;
|
|
case NGHTTP2_HCAT_HEADERS:
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR);
|
|
}
|
|
nghttp2_session_close_stream_if_shut_rdwr(session, stream);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case NGHTTP2_PRIORITY:
|
|
// TODO Update priority of the stream if the stream is initiated
|
|
// by the local endpoint. The spec is not detailed about this.
|
|
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) {
|
|
stream->pri = frame->priority.pri;
|
|
}
|
|
}
|
|
break;
|
|
case NGHTTP2_RST_STREAM:
|
|
nghttp2_session_close_stream(session, frame->hd.stream_id,
|
|
frame->rst_stream.error_code);
|
|
break;
|
|
case NGHTTP2_SETTINGS:
|
|
/* nothing to do */
|
|
break;
|
|
case NGHTTP2_PUSH_PROMISE:
|
|
/* nothing to do */
|
|
break;
|
|
case NGHTTP2_PING:
|
|
/* nothing to do */
|
|
break;
|
|
case NGHTTP2_GOAWAY:
|
|
session->goaway_flags |= NGHTTP2_GOAWAY_SEND;
|
|
break;
|
|
case NGHTTP2_WINDOW_UPDATE:
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_FLOW_CONTROL) {
|
|
if(frame->hd.stream_id == 0) {
|
|
session->local_flow_control = 0;
|
|
} else {
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream) {
|
|
stream->local_flow_control = 0;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
} else if(item->frame_cat == NGHTTP2_CAT_DATA) {
|
|
int r;
|
|
nghttp2_data *data_frame;
|
|
data_frame = nghttp2_outbound_item_get_data_frame(session->aob.item);
|
|
if(session->callbacks.on_data_send_callback) {
|
|
session->callbacks.on_data_send_callback
|
|
(session,
|
|
session->aob.framebuflen - NGHTTP2_FRAME_HEAD_LENGTH,
|
|
data_frame->eof ? data_frame->hd.flags :
|
|
(data_frame->hd.flags & (~NGHTTP2_FLAG_END_STREAM)),
|
|
data_frame->hd.stream_id,
|
|
session->user_data);
|
|
}
|
|
if(data_frame->eof && (data_frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) {
|
|
nghttp2_stream *stream =
|
|
nghttp2_session_get_stream(session, data_frame->hd.stream_id);
|
|
if(stream) {
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR);
|
|
nghttp2_session_close_stream_if_shut_rdwr(session, stream);
|
|
}
|
|
}
|
|
/* If session is closed or RST_STREAM was queued, we won't send
|
|
further data. */
|
|
if(data_frame->eof ||
|
|
nghttp2_session_predicate_data_send(session,
|
|
data_frame->hd.stream_id) != 0) {
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
} else {
|
|
nghttp2_outbound_item* next_item;
|
|
next_item = nghttp2_session_get_next_ob_item(session);
|
|
nghttp2_outbound_item_adjust_pri(session, session->aob.item);
|
|
/* If priority of this stream is higher or equal to other stream
|
|
waiting at the top of the queue, we continue to send this
|
|
data. */
|
|
if(next_item == NULL || session->aob.item->pri <= next_item->pri) {
|
|
size_t next_readmax;
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, data_frame->hd.stream_id);
|
|
/* Assuming stream is not NULL */
|
|
assert(stream);
|
|
next_readmax = nghttp2_session_next_data_read(session, stream);
|
|
if(next_readmax == 0) {
|
|
nghttp2_stream_defer_data(stream, session->aob.item,
|
|
NGHTTP2_DEFERRED_FLOW_CONTROL);
|
|
session->aob.item = NULL;
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
return 0;
|
|
}
|
|
r = nghttp2_session_pack_data(session,
|
|
&session->aob.framebuf,
|
|
&session->aob.framebufmax,
|
|
next_readmax,
|
|
data_frame);
|
|
if(r == NGHTTP2_ERR_DEFERRED) {
|
|
nghttp2_stream_defer_data(stream, session->aob.item,
|
|
NGHTTP2_DEFERRED_NONE);
|
|
session->aob.item = NULL;
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
} else if(r == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
|
|
/* Stop DATA frame chain and issue RST_STREAM to close the
|
|
stream. We don't return
|
|
NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE intentionally. */
|
|
r = nghttp2_session_add_rst_stream(session, data_frame->hd.stream_id,
|
|
NGHTTP2_INTERNAL_ERROR);
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
} else if(r < 0) {
|
|
/* In this context, r is either NGHTTP2_ERR_NOMEM or
|
|
NGHTTP2_ERR_CALLBACK_FAILURE */
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
return r;
|
|
} else {
|
|
session->aob.framebuflen = r;
|
|
session->aob.framebufoff = 0;
|
|
}
|
|
} else {
|
|
r = nghttp2_pq_push(&session->ob_pq, session->aob.item);
|
|
if(r == 0) {
|
|
session->aob.item = NULL;
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
} else {
|
|
/* FATAL error */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
nghttp2_active_outbound_item_reset(&session->aob);
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/* Unreachable */
|
|
assert(0);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int nghttp2_session_send(nghttp2_session *session)
|
|
{
|
|
int r;
|
|
while(1) {
|
|
const uint8_t *data;
|
|
size_t datalen;
|
|
ssize_t sentlen;
|
|
if(session->aob.item == NULL) {
|
|
nghttp2_outbound_item *item;
|
|
ssize_t framebuflen;
|
|
item = nghttp2_session_pop_next_ob_item(session);
|
|
if(item == NULL) {
|
|
break;
|
|
}
|
|
framebuflen = nghttp2_session_prep_frame(session, item);
|
|
if(framebuflen == NGHTTP2_ERR_DEFERRED) {
|
|
continue;
|
|
} else if(framebuflen < 0) {
|
|
/* TODO If the error comes from compressor, the connection
|
|
must be closed. */
|
|
if(item->frame_cat == NGHTTP2_CAT_CTRL &&
|
|
session->callbacks.on_frame_not_send_callback &&
|
|
nghttp2_is_non_fatal(framebuflen)) {
|
|
/* The library is responsible for the transmission of
|
|
WINDOW_UPDATE frame, so we don't call error callback for
|
|
it. */
|
|
nghttp2_frame *frame = nghttp2_outbound_item_get_ctrl_frame(item);
|
|
if(frame->hd.type != NGHTTP2_WINDOW_UPDATE) {
|
|
session->callbacks.on_frame_not_send_callback
|
|
(session, frame, framebuflen, session->user_data);
|
|
}
|
|
}
|
|
nghttp2_outbound_item_free(item);
|
|
free(item);
|
|
|
|
if(framebuflen == NGHTTP2_ERR_HEADER_COMP) {
|
|
// If header compression error occurred, should terminiate
|
|
// connection.
|
|
framebuflen = nghttp2_session_fail_session(session,
|
|
NGHTTP2_INTERNAL_ERROR);
|
|
}
|
|
if(nghttp2_is_fatal(framebuflen)) {
|
|
return framebuflen;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
session->aob.item = item;
|
|
session->aob.framebuflen = framebuflen;
|
|
/* Call before_send callback */
|
|
if(item->frame_cat == NGHTTP2_CAT_CTRL &&
|
|
session->callbacks.before_frame_send_callback) {
|
|
session->callbacks.before_frame_send_callback
|
|
(session,
|
|
nghttp2_outbound_item_get_ctrl_frame(item),
|
|
session->user_data);
|
|
}
|
|
}
|
|
data = session->aob.framebuf + session->aob.framebufoff;
|
|
datalen = session->aob.framebuflen - session->aob.framebufoff;
|
|
sentlen = session->callbacks.send_callback(session, data, datalen, 0,
|
|
session->user_data);
|
|
if(sentlen < 0) {
|
|
if(sentlen == NGHTTP2_ERR_WOULDBLOCK) {
|
|
return 0;
|
|
} else {
|
|
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
|
}
|
|
} else {
|
|
session->aob.framebufoff += sentlen;
|
|
if(session->aob.item->frame_cat == NGHTTP2_CAT_DATA) {
|
|
nghttp2_data *frame;
|
|
nghttp2_stream *stream;
|
|
uint16_t len = nghttp2_get_uint16(&session->aob.framebuf[0]);
|
|
frame = nghttp2_outbound_item_get_data_frame(session->aob.item);
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream && stream->remote_flow_control) {
|
|
stream->window_size -= len;
|
|
}
|
|
if(session->remote_flow_control) {
|
|
session->window_size -= len;
|
|
}
|
|
}
|
|
if(session->aob.framebufoff == session->aob.framebuflen) {
|
|
/* Frame has completely sent */
|
|
r = nghttp2_session_after_frame_sent(session);
|
|
if(r < 0) {
|
|
/* FATAL */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t nghttp2_recv(nghttp2_session *session, uint8_t *buf, size_t len)
|
|
{
|
|
ssize_t r;
|
|
r = session->callbacks.recv_callback
|
|
(session, buf, len, 0, session->user_data);
|
|
if(r > 0) {
|
|
if((size_t)r > len) {
|
|
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
|
}
|
|
} else if(r < 0) {
|
|
if(r != NGHTTP2_ERR_WOULDBLOCK && r != NGHTTP2_ERR_EOF) {
|
|
r = NGHTTP2_ERR_CALLBACK_FAILURE;
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static void nghttp2_session_call_on_request_recv
|
|
(nghttp2_session *session, int32_t stream_id)
|
|
{
|
|
if(session->callbacks.on_request_recv_callback) {
|
|
session->callbacks.on_request_recv_callback(session, stream_id,
|
|
session->user_data);
|
|
}
|
|
}
|
|
|
|
static void nghttp2_session_call_on_frame_received
|
|
(nghttp2_session *session, nghttp2_frame *frame)
|
|
{
|
|
if(session->callbacks.on_frame_recv_callback) {
|
|
session->callbacks.on_frame_recv_callback(session, frame,
|
|
session->user_data);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Checks whether received stream_id is valid.
|
|
* This function returns 1 if it succeeds, or 0.
|
|
*/
|
|
static int nghttp2_session_is_new_peer_stream_id(nghttp2_session *session,
|
|
int32_t stream_id)
|
|
{
|
|
if(stream_id == 0) {
|
|
return 0;
|
|
}
|
|
if(session->server) {
|
|
return stream_id % 2 == 1 && session->last_recv_stream_id < stream_id;
|
|
} else {
|
|
return stream_id % 2 == 0 && session->last_recv_stream_id < stream_id;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Validates received HEADERS frame |frame| with NGHTTP2_HCAT_REQUEST
|
|
* or NGHTTP2_HCAT_PUSH_RESPONSE category, which both opens new
|
|
* stream.
|
|
*
|
|
* This function returns 0 if it succeeds, or non-zero
|
|
* nghttp2_error_code.
|
|
*/
|
|
static int nghttp2_session_validate_request_headers(nghttp2_session *session,
|
|
nghttp2_headers *frame)
|
|
{
|
|
if(nghttp2_session_is_incoming_concurrent_streams_max(session)) {
|
|
/* The spec does not clearly say what to do when max concurrent
|
|
streams number is reached. The mod_spdy sends
|
|
NGHTTP2_REFUSED_STREAM and we think it is reasonable. So we
|
|
follow it. */
|
|
return NGHTTP2_REFUSED_STREAM;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int nghttp2_session_handle_parse_error(nghttp2_session *session,
|
|
nghttp2_frame_type type,
|
|
int lib_error_code,
|
|
nghttp2_error_code error_code)
|
|
{
|
|
if(session->callbacks.on_frame_recv_parse_error_callback) {
|
|
session->callbacks.on_frame_recv_parse_error_callback
|
|
(session,
|
|
type,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen,
|
|
lib_error_code,
|
|
session->user_data);
|
|
}
|
|
return nghttp2_session_fail_session(session, error_code);
|
|
}
|
|
|
|
static int nghttp2_session_handle_invalid_stream
|
|
(nghttp2_session *session,
|
|
nghttp2_frame *frame,
|
|
nghttp2_error_code error_code)
|
|
{
|
|
int r;
|
|
r = nghttp2_session_add_rst_stream(session, frame->hd.stream_id, error_code);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
if(session->callbacks.on_invalid_frame_recv_callback) {
|
|
session->callbacks.on_invalid_frame_recv_callback
|
|
(session, frame, error_code, session->user_data);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Handles invalid frame which causes connection error.
|
|
*/
|
|
static int nghttp2_session_handle_invalid_connection
|
|
(nghttp2_session *session,
|
|
nghttp2_frame *frame,
|
|
nghttp2_error_code error_code)
|
|
{
|
|
if(session->callbacks.on_invalid_frame_recv_callback) {
|
|
session->callbacks.on_invalid_frame_recv_callback
|
|
(session, frame, error_code, session->user_data);
|
|
}
|
|
return nghttp2_session_fail_session(session, error_code);
|
|
}
|
|
|
|
int nghttp2_session_on_request_headers_received(nghttp2_session *session,
|
|
nghttp2_frame *frame)
|
|
{
|
|
int r = 0;
|
|
nghttp2_error_code error_code = NGHTTP2_NO_ERROR;
|
|
if(frame->hd.stream_id == 0) {
|
|
return nghttp2_session_handle_invalid_connection(session, frame,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
if(session->goaway_flags) {
|
|
/* We don't accept new stream after GOAWAY is sent or received. */
|
|
return 0;
|
|
}
|
|
if(!nghttp2_session_is_new_peer_stream_id(session, frame->hd.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 */
|
|
return nghttp2_session_handle_invalid_connection
|
|
(session, frame, NGHTTP2_PROTOCOL_ERROR);
|
|
} else {
|
|
session->last_recv_stream_id = frame->hd.stream_id;
|
|
error_code = nghttp2_session_validate_request_headers
|
|
(session, &frame->headers);
|
|
}
|
|
if(error_code == 0) {
|
|
uint8_t flags = frame->hd.flags;
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_open_stream(session,
|
|
frame->hd.stream_id,
|
|
frame->hd.flags,
|
|
frame->headers.pri,
|
|
NGHTTP2_STREAM_OPENING,
|
|
NULL);
|
|
if(!stream) {
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
if(flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD);
|
|
}
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
if(flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_session_call_on_request_recv(session, frame->hd.stream_id);
|
|
}
|
|
} else {
|
|
r = nghttp2_session_handle_invalid_stream(session, frame, error_code);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
int nghttp2_session_on_response_headers_received(nghttp2_session *session,
|
|
nghttp2_frame *frame,
|
|
nghttp2_stream *stream)
|
|
{
|
|
/* 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) {
|
|
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:
|
|
|
|
If an endpoint receives additional frames for a stream that is
|
|
in this state it MUST respond with a stream error (Section
|
|
5.4.2) of type STREAM_CLOSED.
|
|
*/
|
|
return nghttp2_session_handle_invalid_stream(session, frame,
|
|
NGHTTP2_STREAM_CLOSED);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int nghttp2_session_on_push_response_headers_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);
|
|
}
|
|
if(session->goaway_flags) {
|
|
/* We don't accept new stream after GOAWAY is sent or received. */
|
|
return 0;
|
|
}
|
|
rv = nghttp2_session_validate_request_headers(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,
|
|
nghttp2_frame *frame,
|
|
nghttp2_stream *stream)
|
|
{
|
|
int r = 0;
|
|
int valid = 0;
|
|
nghttp2_error_code error_code = NGHTTP2_PROTOCOL_ERROR;
|
|
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)) {
|
|
if(stream->state == NGHTTP2_STREAM_OPENED) {
|
|
valid = 1;
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD);
|
|
nghttp2_session_close_stream_if_shut_rdwr(session, stream);
|
|
}
|
|
} else if(stream->state == NGHTTP2_STREAM_CLOSING) {
|
|
/* This is race condition. NGHTTP2_STREAM_CLOSING indicates
|
|
that we queued RST_STREAM but it has not been sent. It will
|
|
eventually sent, so we just ignore this frame. */
|
|
valid = 1;
|
|
}
|
|
} else {
|
|
/* If this is remote peer initiated stream, it is OK unless it
|
|
has sent END_STREAM frame already. But if stream is in
|
|
NGHTTP2_STREAM_CLOSING, we discard the frame. This is a race
|
|
condition. */
|
|
valid = 1;
|
|
if(stream->state != NGHTTP2_STREAM_CLOSING) {
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_session_call_on_request_recv(session, frame->hd.stream_id);
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD);
|
|
nghttp2_session_close_stream_if_shut_rdwr(session, stream);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/* half closed (remote): from the spec:
|
|
|
|
If an endpoint receives additional frames for a stream that is
|
|
in this state it MUST respond with a stream error (Section
|
|
5.4.2) of type STREAM_CLOSED.
|
|
*/
|
|
error_code = NGHTTP2_STREAM_CLOSED;
|
|
}
|
|
if(!valid) {
|
|
r = nghttp2_session_handle_invalid_stream(session, frame, error_code);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
int nghttp2_session_on_priority_received(nghttp2_session *session,
|
|
nghttp2_frame *frame)
|
|
{
|
|
nghttp2_stream *stream;
|
|
if(frame->hd.stream_id == 0) {
|
|
return nghttp2_session_handle_invalid_connection(session, frame,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream) {
|
|
if((stream->shut_flags & NGHTTP2_SHUT_RD) == 0) {
|
|
// Update the priority of the stream if the stream is initiated
|
|
// by the remote endpoint.
|
|
if(!nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) {
|
|
stream->pri = frame->priority.pri;
|
|
}
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
} else {
|
|
return nghttp2_session_handle_invalid_stream(session, frame,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int nghttp2_session_on_rst_stream_received(nghttp2_session *session,
|
|
nghttp2_frame *frame)
|
|
{
|
|
if(frame->hd.stream_id == 0) {
|
|
return nghttp2_session_handle_invalid_connection(session, frame,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
nghttp2_session_close_stream(session, frame->hd.stream_id,
|
|
frame->rst_stream.error_code);
|
|
return 0;
|
|
}
|
|
|
|
static int nghttp2_update_initial_window_size_func(nghttp2_map_entry *entry,
|
|
void *ptr)
|
|
{
|
|
nghttp2_update_window_size_arg *arg;
|
|
nghttp2_stream *stream;
|
|
arg = (nghttp2_update_window_size_arg*)ptr;
|
|
stream = (nghttp2_stream*)entry;
|
|
nghttp2_stream_update_initial_window_size(stream,
|
|
arg->new_window_size,
|
|
arg->old_window_size);
|
|
/* If window size gets positive, push deferred DATA frame to
|
|
outbound queue. */
|
|
if(stream->deferred_data &&
|
|
(stream->deferred_flags & NGHTTP2_DEFERRED_FLOW_CONTROL) &&
|
|
stream->window_size > 0 &&
|
|
(arg->session->remote_flow_control == 0 ||
|
|
arg->session->window_size > 0)) {
|
|
int rv;
|
|
rv = nghttp2_pq_push(&arg->session->ob_pq, stream->deferred_data);
|
|
if(rv == 0) {
|
|
nghttp2_stream_detach_deferred_data(stream);
|
|
} else {
|
|
/* FATAL */
|
|
assert(rv < NGHTTP2_ERR_FATAL);
|
|
return rv;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Updates the initial window size of all active streams.
|
|
* If error occurs, all streams may not be updated.
|
|
*
|
|
* This function returns 0 if it succeeds, or one of the following
|
|
* negative error codes:
|
|
*
|
|
* NGHTTP2_ERR_NOMEM
|
|
* Out of memory.
|
|
*/
|
|
static int nghttp2_session_update_initial_window_size
|
|
(nghttp2_session *session,
|
|
int32_t new_initial_window_size)
|
|
{
|
|
nghttp2_update_window_size_arg arg;
|
|
arg.session = session;
|
|
arg.new_window_size = new_initial_window_size;
|
|
arg.old_window_size =
|
|
session->remote_settings[NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE];
|
|
return nghttp2_map_each(&session->streams,
|
|
nghttp2_update_initial_window_size_func,
|
|
&arg);
|
|
}
|
|
|
|
static int nghttp2_disable_flow_control_func(nghttp2_map_entry *entry,
|
|
void *ptr)
|
|
{
|
|
nghttp2_session *session;
|
|
nghttp2_stream *stream;
|
|
session = (nghttp2_session*)ptr;
|
|
stream = (nghttp2_stream*)entry;
|
|
stream->remote_flow_control = 0;
|
|
/* If DATA frame is deferred due to flow control, push it back to
|
|
outbound queue. */
|
|
if(stream->deferred_data &&
|
|
(stream->deferred_flags & NGHTTP2_DEFERRED_FLOW_CONTROL)) {
|
|
int rv;
|
|
rv = nghttp2_pq_push(&session->ob_pq, stream->deferred_data);
|
|
if(rv == 0) {
|
|
nghttp2_stream_detach_deferred_data(stream);
|
|
} else {
|
|
/* FATAL */
|
|
assert(rv < NGHTTP2_ERR_FATAL);
|
|
return rv;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Disable connection-level flow control and stream-level flow control
|
|
* of existing streams.
|
|
*/
|
|
static int nghttp2_session_disable_flow_control(nghttp2_session *session)
|
|
{
|
|
session->remote_flow_control = 0;
|
|
return nghttp2_map_each(&session->streams,
|
|
nghttp2_disable_flow_control_func, session);
|
|
}
|
|
|
|
void nghttp2_session_update_local_settings(nghttp2_session *session,
|
|
nghttp2_settings_entry *iv,
|
|
size_t niv)
|
|
{
|
|
size_t i;
|
|
for(i = 0; i < niv; ++i) {
|
|
assert(iv[i].settings_id > 0 && iv[i].settings_id <= NGHTTP2_SETTINGS_MAX);
|
|
session->local_settings[iv[i].settings_id] = iv[i].value;
|
|
}
|
|
}
|
|
|
|
int nghttp2_session_on_settings_received(nghttp2_session *session,
|
|
nghttp2_frame *frame)
|
|
{
|
|
int rv;
|
|
size_t i;
|
|
int check[NGHTTP2_SETTINGS_MAX+1];
|
|
if(frame->hd.stream_id != 0) {
|
|
return nghttp2_session_handle_invalid_connection(session, frame,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
/* Check ID/value pairs and persist them if necessary. */
|
|
memset(check, 0, sizeof(check));
|
|
for(i = 0; i < frame->settings.niv; ++i) {
|
|
nghttp2_settings_entry *entry = &frame->settings.iv[i];
|
|
/* The spec says if the multiple values for the same ID were
|
|
found, use the first one and ignore the rest. */
|
|
if(entry->settings_id > NGHTTP2_SETTINGS_MAX || entry->settings_id == 0 ||
|
|
check[entry->settings_id] == 1) {
|
|
continue;
|
|
}
|
|
check[entry->settings_id] = 1;
|
|
switch(entry->settings_id) {
|
|
case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE:
|
|
/* Update the initial window size of the all active streams */
|
|
/* Check that initial_window_size < (1u << 31) */
|
|
if(entry->value < (1u << 31)) {
|
|
rv = nghttp2_session_update_initial_window_size(session, entry->value);
|
|
if(rv != 0) {
|
|
return rv;
|
|
}
|
|
} else {
|
|
return nghttp2_session_handle_invalid_connection
|
|
(session, frame, NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
break;
|
|
case NGHTTP2_SETTINGS_FLOW_CONTROL_OPTIONS:
|
|
if(entry->value == 1) {
|
|
if(session->remote_settings[entry->settings_id] == 0) {
|
|
rv = nghttp2_session_disable_flow_control(session);
|
|
if(rv != 0) {
|
|
return rv;
|
|
}
|
|
}
|
|
} else if(session->remote_settings[entry->settings_id] == 1) {
|
|
/* Re-enabling flow control is subject to connection-level
|
|
error(?) */
|
|
return nghttp2_session_handle_invalid_connection
|
|
(session, frame, NGHTTP2_FLOW_CONTROL_ERROR);
|
|
}
|
|
break;
|
|
}
|
|
session->remote_settings[entry->settings_id] = entry->value;
|
|
}
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
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)
|
|
{
|
|
int r = 0;
|
|
if(frame->hd.stream_id != 0) {
|
|
return nghttp2_session_handle_invalid_connection(session, frame,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
if((frame->hd.flags & NGHTTP2_FLAG_PONG) == 0) {
|
|
/* Peer sent ping, so ping it back */
|
|
r = nghttp2_session_add_ping(session, NGHTTP2_FLAG_PONG,
|
|
frame->ping.opaque_data);
|
|
}
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
return r;
|
|
}
|
|
|
|
int nghttp2_session_on_goaway_received(nghttp2_session *session,
|
|
nghttp2_frame *frame)
|
|
{
|
|
session->last_stream_id = frame->goaway.last_stream_id;
|
|
session->goaway_flags |= NGHTTP2_GOAWAY_RECV;
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
return 0;
|
|
}
|
|
|
|
static int nghttp2_push_back_deferred_data_func(nghttp2_map_entry *entry,
|
|
void *ptr)
|
|
{
|
|
nghttp2_session *session;
|
|
nghttp2_stream *stream;
|
|
session = (nghttp2_session*)ptr;
|
|
stream = (nghttp2_stream*)entry;
|
|
/* If DATA frame is deferred due to flow control, push it back to
|
|
outbound queue. */
|
|
if(stream->deferred_data &&
|
|
(stream->deferred_flags & NGHTTP2_DEFERRED_FLOW_CONTROL) &&
|
|
(stream->remote_flow_control == 0 || stream->window_size > 0)) {
|
|
int rv;
|
|
rv = nghttp2_pq_push(&session->ob_pq, stream->deferred_data);
|
|
if(rv == 0) {
|
|
nghttp2_stream_detach_deferred_data(stream);
|
|
} else {
|
|
/* FATAL */
|
|
assert(rv < NGHTTP2_ERR_FATAL);
|
|
return rv;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Push back deferred DATA frames to queue if they are deferred due to
|
|
* connection-level flow control.
|
|
*/
|
|
static int nghttp2_session_push_back_deferred_data(nghttp2_session *session)
|
|
{
|
|
return nghttp2_map_each(&session->streams,
|
|
nghttp2_push_back_deferred_data_func, session);
|
|
}
|
|
|
|
int nghttp2_session_on_window_update_received(nghttp2_session *session,
|
|
nghttp2_frame *frame)
|
|
{
|
|
if(frame->hd.stream_id == 0) {
|
|
/* Handle connection-level flow control */
|
|
if(session->remote_flow_control == 0) {
|
|
/* The sepc says receiving WINDOW_UPDATE from peer when flow
|
|
control is disabled is error, but disabling flow control and
|
|
receiving WINDOW_UPDATE are asynchronous, so it is hard to
|
|
determine that the peer is misbehaving or not without
|
|
measuring RTT. For now, we just ignore such frames. */
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
return 0;
|
|
}
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_FLOW_CONTROL) {
|
|
if(session->remote_flow_control) {
|
|
/* Disable connection-level flow control and push back
|
|
deferred DATA frame if any */
|
|
session->remote_flow_control = 0;
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
return nghttp2_session_push_back_deferred_data(session);
|
|
}
|
|
return 0;
|
|
}
|
|
if(INT32_MAX - frame->window_update.window_size_increment <
|
|
session->window_size) {
|
|
return nghttp2_session_handle_invalid_connection
|
|
(session, frame, NGHTTP2_FLOW_CONTROL_ERROR);
|
|
}
|
|
session->window_size += frame->window_update.window_size_increment;
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
/* To queue the DATA deferred by connection-level flow-control, we
|
|
have to check all streams. Bad. */
|
|
if(session->window_size > 0) {
|
|
return nghttp2_session_push_back_deferred_data(session);
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else {
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
|
|
if(stream) {
|
|
if(stream->remote_flow_control == 0) {
|
|
/* Same reason with connection-level flow control */
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
return 0;
|
|
}
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_FLOW_CONTROL) {
|
|
stream->remote_flow_control = 0;
|
|
if(stream->remote_flow_control &&
|
|
stream->deferred_data != NULL &&
|
|
(stream->deferred_flags & NGHTTP2_DEFERRED_FLOW_CONTROL)) {
|
|
int r;
|
|
r = nghttp2_pq_push(&session->ob_pq, stream->deferred_data);
|
|
if(r == 0) {
|
|
nghttp2_stream_detach_deferred_data(stream);
|
|
} else if(r < 0) {
|
|
/* FATAL */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
return r;
|
|
}
|
|
}
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
return 0;
|
|
}
|
|
if(INT32_MAX - frame->window_update.window_size_increment <
|
|
stream->window_size) {
|
|
int r;
|
|
r = nghttp2_session_handle_invalid_stream
|
|
(session, frame, NGHTTP2_FLOW_CONTROL_ERROR);
|
|
return r;
|
|
} else {
|
|
stream->window_size += frame->window_update.window_size_increment;
|
|
if(stream->window_size > 0 &&
|
|
(session->remote_flow_control == 0 ||
|
|
session->window_size > 0) &&
|
|
stream->deferred_data != NULL &&
|
|
(stream->deferred_flags & NGHTTP2_DEFERRED_FLOW_CONTROL)) {
|
|
int r;
|
|
r = nghttp2_pq_push(&session->ob_pq, stream->deferred_data);
|
|
if(r == 0) {
|
|
nghttp2_stream_detach_deferred_data(stream);
|
|
} else if(r < 0) {
|
|
/* FATAL */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
return r;
|
|
}
|
|
}
|
|
nghttp2_session_call_on_frame_received(session, frame);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int nghttp2_get_status_code_from_error_code(int lib_error_code)
|
|
{
|
|
switch(lib_error_code) {
|
|
case(NGHTTP2_ERR_FRAME_TOO_LARGE):
|
|
return NGHTTP2_FRAME_TOO_LARGE;
|
|
default:
|
|
return NGHTTP2_PROTOCOL_ERROR;
|
|
}
|
|
}
|
|
|
|
/* For errors, this function only returns FATAL error. */
|
|
static int nghttp2_session_process_ctrl_frame(nghttp2_session *session)
|
|
{
|
|
int r = 0;
|
|
uint16_t type;
|
|
nghttp2_frame frame;
|
|
type = session->iframe.headbuf[2];
|
|
switch(type) {
|
|
case NGHTTP2_HEADERS:
|
|
if(session->iframe.error_code == 0) {
|
|
r = nghttp2_frame_unpack_headers(&frame.headers,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen,
|
|
&session->hd_inflater);
|
|
} else if(session->iframe.error_code == NGHTTP2_ERR_FRAME_TOO_LARGE) {
|
|
r = nghttp2_frame_unpack_headers_without_nv
|
|
(&frame.headers,
|
|
session->iframe.headbuf, sizeof(session->iframe.headbuf),
|
|
session->iframe.buf, session->iframe.buflen);
|
|
if(r == 0) {
|
|
r = session->iframe.error_code;
|
|
}
|
|
} else {
|
|
r = session->iframe.error_code;
|
|
}
|
|
if(r == 0) {
|
|
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_RESPONSE;
|
|
r = nghttp2_session_on_response_headers_received
|
|
(session, &frame, stream);
|
|
} else {
|
|
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_PUSH_RESPONSE;
|
|
r = nghttp2_session_on_push_response_headers_received
|
|
(session, &frame, stream);
|
|
} else {
|
|
frame.headers.cat = NGHTTP2_HCAT_HEADERS;
|
|
r = nghttp2_session_on_headers_received(session, &frame, stream);
|
|
}
|
|
} else {
|
|
frame.headers.cat = NGHTTP2_HCAT_REQUEST;
|
|
r = nghttp2_session_on_request_headers_received(session, &frame);
|
|
}
|
|
nghttp2_frame_headers_free(&frame.headers);
|
|
nghttp2_hd_end_headers(&session->hd_inflater);
|
|
} else if(r == NGHTTP2_ERR_INVALID_HEADER_BLOCK) {
|
|
r = nghttp2_session_handle_invalid_stream
|
|
(session, &frame, nghttp2_get_status_code_from_error_code(r));
|
|
/* TODO test this. It seems NGHTTP2_ERR_INVALID_HEADER_BLOCK is
|
|
not used in framing anymore. */
|
|
nghttp2_frame_headers_free(&frame.headers);
|
|
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_PRIORITY:
|
|
r = nghttp2_frame_unpack_priority(&frame.priority,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen);
|
|
if(r == 0) {
|
|
r = nghttp2_session_on_priority_received(session, &frame);
|
|
nghttp2_frame_priority_free(&frame.priority);
|
|
} else if(nghttp2_is_non_fatal(r)) {
|
|
r = nghttp2_session_handle_parse_error(session, type, r,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
break;
|
|
case NGHTTP2_RST_STREAM:
|
|
r = nghttp2_frame_unpack_rst_stream(&frame.rst_stream,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen);
|
|
if(r == 0) {
|
|
r = nghttp2_session_on_rst_stream_received(session, &frame);
|
|
nghttp2_frame_rst_stream_free(&frame.rst_stream);
|
|
} else if(nghttp2_is_non_fatal(r)) {
|
|
r = nghttp2_session_handle_parse_error(session, type, r,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
break;
|
|
case NGHTTP2_SETTINGS:
|
|
r = nghttp2_frame_unpack_settings(&frame.settings,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen);
|
|
if(r == 0) {
|
|
r = nghttp2_session_on_settings_received(session, &frame);
|
|
nghttp2_frame_settings_free(&frame.settings);
|
|
} else if(nghttp2_is_non_fatal(r)) {
|
|
r = nghttp2_session_handle_parse_error(session, type, r,
|
|
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,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen);
|
|
if(r == 0) {
|
|
r = nghttp2_session_on_ping_received(session, &frame);
|
|
nghttp2_frame_ping_free(&frame.ping);
|
|
} else if(nghttp2_is_non_fatal(r)) {
|
|
r = nghttp2_session_handle_parse_error(session, type, r,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
break;
|
|
case NGHTTP2_GOAWAY:
|
|
r = nghttp2_frame_unpack_goaway(&frame.goaway,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen);
|
|
if(r == 0) {
|
|
r = nghttp2_session_on_goaway_received(session, &frame);
|
|
nghttp2_frame_goaway_free(&frame.goaway);
|
|
} else if(nghttp2_is_non_fatal(r)) {
|
|
r = nghttp2_session_handle_parse_error(session, type, r,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
break;
|
|
case NGHTTP2_WINDOW_UPDATE:
|
|
r = nghttp2_frame_unpack_window_update(&frame.window_update,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen);
|
|
if(r == 0) {
|
|
r = nghttp2_session_on_window_update_received(session, &frame);
|
|
nghttp2_frame_window_update_free(&frame.window_update);
|
|
} else if(nghttp2_is_non_fatal(r)) {
|
|
r = nghttp2_session_handle_parse_error(session, type, r,
|
|
NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
break;
|
|
default:
|
|
/* Unknown frame */
|
|
if(session->callbacks.on_unknown_frame_recv_callback) {
|
|
session->callbacks.on_unknown_frame_recv_callback
|
|
(session,
|
|
session->iframe.headbuf,
|
|
sizeof(session->iframe.headbuf),
|
|
session->iframe.buf,
|
|
session->iframe.buflen,
|
|
session->user_data);
|
|
}
|
|
}
|
|
if(nghttp2_is_fatal(r)) {
|
|
return r;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int nghttp2_session_on_data_received(nghttp2_session *session,
|
|
uint16_t length, uint8_t flags,
|
|
int32_t stream_id)
|
|
{
|
|
int r = 0;
|
|
nghttp2_error_code error_code = 0;
|
|
nghttp2_stream *stream;
|
|
if(stream_id == 0) {
|
|
/* The spec says that if a DATA frame is received whose stream ID
|
|
is 0, the recipient MUST respond with a connection error of
|
|
type PROTOCOL_ERROR. */
|
|
return nghttp2_session_fail_session(session, NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
stream = nghttp2_session_get_stream(session, stream_id);
|
|
if(stream) {
|
|
if((stream->shut_flags & NGHTTP2_SHUT_RD) == 0) {
|
|
int valid = 0;
|
|
if(nghttp2_session_is_my_stream_id(session, stream_id)) {
|
|
if(stream->state == NGHTTP2_STREAM_OPENED) {
|
|
valid = 1;
|
|
if(session->callbacks.on_data_recv_callback) {
|
|
session->callbacks.on_data_recv_callback
|
|
(session, length, flags, stream_id, session->user_data);
|
|
}
|
|
} else if(stream->state != NGHTTP2_STREAM_CLOSING) {
|
|
error_code = NGHTTP2_PROTOCOL_ERROR;
|
|
}
|
|
} else if(stream->state != NGHTTP2_STREAM_CLOSING) {
|
|
/* It is OK if this is remote peer initiated stream and we did
|
|
not receive END_STREAM unless stream is in
|
|
NGHTTP2_STREAM_CLOSING state. This is a race condition. */
|
|
valid = 1;
|
|
if(session->callbacks.on_data_recv_callback) {
|
|
session->callbacks.on_data_recv_callback
|
|
(session, length, flags, stream_id, session->user_data);
|
|
}
|
|
if(flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_session_call_on_request_recv(session, stream_id);
|
|
}
|
|
}
|
|
if(valid) {
|
|
if(flags & NGHTTP2_FLAG_END_STREAM) {
|
|
nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD);
|
|
nghttp2_session_close_stream_if_shut_rdwr(session, stream);
|
|
}
|
|
}
|
|
} else {
|
|
/* half closed (remote): from the spec:
|
|
|
|
If an endpoint receives additional frames for a stream that is
|
|
in this state it MUST respond with a stream error (Section
|
|
5.4.2) of type STREAM_CLOSED.
|
|
*/
|
|
error_code = NGHTTP2_STREAM_CLOSED;
|
|
}
|
|
} else {
|
|
error_code = NGHTTP2_PROTOCOL_ERROR;
|
|
}
|
|
if(error_code != 0) {
|
|
r = nghttp2_session_add_rst_stream(session, stream_id, error_code);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
/* For errors, this function only returns FATAL error. */
|
|
static int nghttp2_session_process_data_frame(nghttp2_session *session)
|
|
{
|
|
int r;
|
|
nghttp2_frame_hd hd;
|
|
nghttp2_frame_unpack_frame_hd(&hd, session->iframe.headbuf);
|
|
r = nghttp2_session_on_data_received(session, hd.length, hd.flags,
|
|
hd.stream_id);
|
|
if(nghttp2_is_fatal(r)) {
|
|
return r;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int32_t adjust_recv_window_size(int32_t recv_window_size, int32_t delta)
|
|
{
|
|
/* If NGHTTP2_OPT_NO_AUTO_WINDOW_UPDATE is set and the application
|
|
does not send WINDOW_UPDATE and the remote endpoint keeps
|
|
sending data, stream->recv_window_size will eventually
|
|
overflow. */
|
|
if(recv_window_size > INT32_MAX - delta) {
|
|
recv_window_size = INT32_MAX;
|
|
} else {
|
|
recv_window_size += delta;
|
|
}
|
|
return recv_window_size;
|
|
}
|
|
|
|
/*
|
|
* Accumulates received bytes |delta_size| and decides whether to send
|
|
* WINDOW_UPDATE. If NGHTTP2_OPT_NO_AUTO_WINDOW_UPDATE is set,
|
|
* WINDOW_UPDATE will not be sent.
|
|
*
|
|
* This function returns 0 if it succeeds, or one of the following
|
|
* negative error codes:
|
|
*
|
|
* NGHTTP2_ERR_NOMEM
|
|
* Out of memory.
|
|
*/
|
|
static int nghttp2_session_update_recv_window_size(nghttp2_session *session,
|
|
nghttp2_stream *stream,
|
|
int32_t delta_size)
|
|
{
|
|
if(stream && stream->local_flow_control) {
|
|
stream->recv_window_size = adjust_recv_window_size
|
|
(stream->recv_window_size, delta_size);
|
|
if(!(session->opt_flags & NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE)) {
|
|
/* This is just a heuristics. */
|
|
/* We have to use local_settings here because it is the constraint
|
|
the remote endpoint should honor. */
|
|
if((size_t)stream->recv_window_size*2 >=
|
|
session->local_settings[NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE]) {
|
|
int r;
|
|
r = nghttp2_session_add_window_update(session,
|
|
NGHTTP2_FLAG_NONE,
|
|
stream->stream_id,
|
|
stream->recv_window_size);
|
|
if(r == 0) {
|
|
stream->recv_window_size = 0;
|
|
} else {
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(session->local_flow_control) {
|
|
session->recv_window_size = adjust_recv_window_size
|
|
(session->recv_window_size, delta_size);
|
|
if(!(session->opt_flags & NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE)) {
|
|
/* Same heuristics above */
|
|
if((size_t)session->recv_window_size*2 >=
|
|
NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE) {
|
|
int r;
|
|
/* Use stream ID 0 to update connection-level flow control
|
|
window */
|
|
r = nghttp2_session_add_window_update(session,
|
|
NGHTTP2_FLAG_NONE,
|
|
0,
|
|
session->recv_window_size);
|
|
if(r == 0) {
|
|
session->recv_window_size = 0;
|
|
} else {
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Returns nonzero if the reception of DATA for stream |stream_id| is
|
|
* allowed.
|
|
*/
|
|
static int nghttp2_session_check_data_recv_allowed(nghttp2_session *session,
|
|
int32_t stream_id)
|
|
{
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, stream_id);
|
|
if(stream) {
|
|
if((stream->shut_flags & NGHTTP2_SHUT_RD) == 0) {
|
|
if(nghttp2_session_is_my_stream_id(session, stream_id)) {
|
|
if(stream->state == NGHTTP2_STREAM_OPENED) {
|
|
return 1;
|
|
}
|
|
} else if(stream->state != NGHTTP2_STREAM_CLOSING) {
|
|
/* It is OK if this is remote peer initiated stream and we did
|
|
not receive END_STREAM unless stream is in
|
|
NGHTTP2_STREAM_CLOSING state. This is a race condition. */
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
ssize_t nghttp2_session_mem_recv(nghttp2_session *session,
|
|
const uint8_t *in, size_t inlen)
|
|
{
|
|
const uint8_t *inmark, *inlimit;
|
|
inmark = in;
|
|
inlimit = in+inlen;
|
|
while(1) {
|
|
ssize_t r;
|
|
if(session->iframe.state == NGHTTP2_RECV_HEAD) {
|
|
size_t remheadbytes;
|
|
size_t readlen;
|
|
size_t bufavail = inlimit-inmark;
|
|
if(bufavail == 0) {
|
|
break;
|
|
}
|
|
remheadbytes = NGHTTP2_FRAME_HEAD_LENGTH - session->iframe.headbufoff;
|
|
readlen = nghttp2_min(remheadbytes, bufavail);
|
|
memcpy(session->iframe.headbuf+session->iframe.headbufoff,
|
|
inmark, readlen);
|
|
inmark += readlen;
|
|
session->iframe.headbufoff += readlen;
|
|
if(session->iframe.headbufoff == NGHTTP2_FRAME_HEAD_LENGTH) {
|
|
session->iframe.state = NGHTTP2_RECV_PAYLOAD;
|
|
session->iframe.payloadlen =
|
|
nghttp2_get_uint16(&session->iframe.headbuf[0]);
|
|
if(!nghttp2_frame_is_data_frame(session->iframe.headbuf)) {
|
|
/* control frame */
|
|
ssize_t buflen = session->iframe.payloadlen;
|
|
session->iframe.buflen = buflen;
|
|
r = nghttp2_reserve_buffer(&session->iframe.buf,
|
|
&session->iframe.bufmax,
|
|
buflen);
|
|
if(r != 0) {
|
|
/* FATAL */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
return r;
|
|
}
|
|
} else {
|
|
/* Check stream is open. If it is not open or closing,
|
|
ignore payload. */
|
|
int32_t stream_id;
|
|
stream_id = nghttp2_get_uint32(&session->iframe.headbuf[4]) &
|
|
NGHTTP2_STREAM_ID_MASK;
|
|
if(!nghttp2_session_check_data_recv_allowed(session, stream_id)) {
|
|
session->iframe.state = NGHTTP2_RECV_PAYLOAD_IGN;
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if(session->iframe.state == NGHTTP2_RECV_PAYLOAD ||
|
|
session->iframe.state == NGHTTP2_RECV_PAYLOAD_IGN) {
|
|
size_t rempayloadlen;
|
|
size_t bufavail, readlen;
|
|
int32_t data_stream_id = 0;
|
|
uint8_t data_flags = NGHTTP2_FLAG_NONE;
|
|
|
|
rempayloadlen = session->iframe.payloadlen - session->iframe.off;
|
|
bufavail = inlimit - inmark;
|
|
if(rempayloadlen > 0 && bufavail == 0) {
|
|
break;
|
|
}
|
|
readlen = nghttp2_min(bufavail, rempayloadlen);
|
|
if(!nghttp2_frame_is_data_frame(session->iframe.headbuf)) {
|
|
if(session->iframe.state != NGHTTP2_RECV_PAYLOAD_IGN) {
|
|
memcpy(session->iframe.buf+session->iframe.off, inmark, readlen);
|
|
}
|
|
} else {
|
|
/* For data frame, We don't buffer data. Instead, just pass
|
|
received data to callback function. */
|
|
data_stream_id = nghttp2_get_uint32(&session->iframe.headbuf[4]) &
|
|
NGHTTP2_STREAM_ID_MASK;
|
|
data_flags = session->iframe.headbuf[3];
|
|
if(session->iframe.state != NGHTTP2_RECV_PAYLOAD_IGN) {
|
|
if(session->callbacks.on_data_chunk_recv_callback) {
|
|
session->callbacks.on_data_chunk_recv_callback(session,
|
|
data_flags,
|
|
data_stream_id,
|
|
inmark,
|
|
readlen,
|
|
session->user_data);
|
|
}
|
|
}
|
|
}
|
|
session->iframe.off += readlen;
|
|
inmark += readlen;
|
|
|
|
if(session->iframe.state != NGHTTP2_RECV_PAYLOAD_IGN &&
|
|
nghttp2_frame_is_data_frame(session->iframe.headbuf) &&
|
|
readlen > 0 &&
|
|
(session->iframe.payloadlen != session->iframe.off ||
|
|
(data_flags & NGHTTP2_FLAG_END_STREAM) == 0)) {
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, data_stream_id);
|
|
if(session->local_flow_control || stream->local_flow_control) {
|
|
r = nghttp2_session_update_recv_window_size(session,
|
|
stream,
|
|
readlen);
|
|
if(r < 0) {
|
|
/* FATAL */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
if(session->iframe.payloadlen == session->iframe.off) {
|
|
if(!nghttp2_frame_is_data_frame(session->iframe.headbuf)) {
|
|
r = nghttp2_session_process_ctrl_frame(session);
|
|
} else {
|
|
r = nghttp2_session_process_data_frame(session);
|
|
}
|
|
if(r < 0) {
|
|
/* FATAL */
|
|
assert(r < NGHTTP2_ERR_FATAL);
|
|
return r;
|
|
}
|
|
nghttp2_inbound_frame_reset(&session->iframe);
|
|
}
|
|
}
|
|
}
|
|
return inmark-in;
|
|
}
|
|
|
|
int nghttp2_session_recv(nghttp2_session *session)
|
|
{
|
|
uint8_t buf[NGHTTP2_INBOUND_BUFFER_LENGTH];
|
|
while(1) {
|
|
ssize_t readlen;
|
|
readlen = nghttp2_recv(session, buf, sizeof(buf));
|
|
if(readlen > 0) {
|
|
ssize_t proclen = nghttp2_session_mem_recv(session, buf, readlen);
|
|
if(proclen < 0) {
|
|
return proclen;
|
|
}
|
|
assert(proclen == readlen);
|
|
} else if(readlen == 0 || readlen == NGHTTP2_ERR_WOULDBLOCK) {
|
|
return 0;
|
|
} else if(readlen == NGHTTP2_ERR_EOF) {
|
|
return readlen;
|
|
} else if(readlen < 0) {
|
|
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
|
}
|
|
}
|
|
}
|
|
|
|
int nghttp2_session_want_read(nghttp2_session *session)
|
|
{
|
|
/* If these flags are set, we don't want to read. The application
|
|
should drop the connection. */
|
|
if((session->goaway_flags & NGHTTP2_GOAWAY_FAIL_ON_SEND) &&
|
|
(session->goaway_flags & NGHTTP2_GOAWAY_SEND)) {
|
|
return 0;
|
|
}
|
|
/* Unless GOAWAY is sent or received, we always want to read
|
|
incoming frames. After GOAWAY is sent or received, we are only
|
|
interested in active streams. */
|
|
return !session->goaway_flags || nghttp2_map_size(&session->streams) > 0;
|
|
}
|
|
|
|
int nghttp2_session_want_write(nghttp2_session *session)
|
|
{
|
|
/* If these flags are set, we don't want to write any data. The
|
|
application should drop the connection. */
|
|
if((session->goaway_flags & NGHTTP2_GOAWAY_FAIL_ON_SEND) &&
|
|
(session->goaway_flags & NGHTTP2_GOAWAY_SEND)) {
|
|
return 0;
|
|
}
|
|
/*
|
|
* Unless GOAWAY is sent or received, we want to write frames if
|
|
* there is pending ones. If pending frame is request/push response
|
|
* HEADERS and concurrent stream limit is reached, we don't want to
|
|
* write them. After GOAWAY is sent or received, we want to write
|
|
* frames if there is pending ones AND there are active frames.
|
|
*/
|
|
return (session->aob.item != NULL || !nghttp2_pq_empty(&session->ob_pq) ||
|
|
(!nghttp2_pq_empty(&session->ob_ss_pq) &&
|
|
!nghttp2_session_is_outgoing_concurrent_streams_max(session))) &&
|
|
(!session->goaway_flags || nghttp2_map_size(&session->streams) > 0);
|
|
}
|
|
|
|
int nghttp2_session_add_ping(nghttp2_session *session, uint8_t flags,
|
|
uint8_t *opaque_data)
|
|
{
|
|
int r;
|
|
nghttp2_frame *frame;
|
|
frame = malloc(sizeof(nghttp2_frame));
|
|
if(frame == NULL) {
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
nghttp2_frame_ping_init(&frame->ping, flags, opaque_data);
|
|
r = nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL);
|
|
if(r != 0) {
|
|
nghttp2_frame_ping_free(&frame->ping);
|
|
free(frame);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
int nghttp2_session_add_goaway(nghttp2_session *session,
|
|
int32_t last_stream_id,
|
|
nghttp2_error_code error_code,
|
|
uint8_t *opaque_data, size_t opaque_data_len)
|
|
{
|
|
int r;
|
|
nghttp2_frame *frame;
|
|
uint8_t *opaque_data_copy = NULL;
|
|
if(opaque_data_len) {
|
|
if(opaque_data_len > UINT16_MAX - 8) {
|
|
return NGHTTP2_ERR_INVALID_ARGUMENT;
|
|
}
|
|
opaque_data_copy = malloc(opaque_data_len);
|
|
if(opaque_data_copy == NULL) {
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
memcpy(opaque_data_copy, opaque_data, opaque_data_len);
|
|
}
|
|
frame = malloc(sizeof(nghttp2_frame));
|
|
if(frame == NULL) {
|
|
free(opaque_data_copy);
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
nghttp2_frame_goaway_init(&frame->goaway, last_stream_id, error_code,
|
|
opaque_data_copy, opaque_data_len);
|
|
r = nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL);
|
|
if(r != 0) {
|
|
nghttp2_frame_goaway_free(&frame->goaway);
|
|
free(frame);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
int nghttp2_session_add_window_update(nghttp2_session *session, uint8_t flags,
|
|
int32_t stream_id,
|
|
int32_t window_size_increment)
|
|
{
|
|
int r;
|
|
nghttp2_frame *frame;
|
|
frame = malloc(sizeof(nghttp2_frame));
|
|
if(frame == NULL) {
|
|
return NGHTTP2_ERR_NOMEM;
|
|
}
|
|
nghttp2_frame_window_update_init(&frame->window_update, flags,
|
|
stream_id, window_size_increment);
|
|
r = nghttp2_session_add_frame(session, NGHTTP2_CAT_CTRL, frame, NULL);
|
|
if(r != 0) {
|
|
nghttp2_frame_window_update_free(&frame->window_update);
|
|
free(frame);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
ssize_t nghttp2_session_pack_data(nghttp2_session *session,
|
|
uint8_t **buf_ptr, size_t *buflen_ptr,
|
|
size_t datamax,
|
|
nghttp2_data *frame)
|
|
{
|
|
ssize_t framelen = datamax+8, r;
|
|
int eof_flags;
|
|
uint8_t flags;
|
|
r = nghttp2_reserve_buffer(buf_ptr, buflen_ptr, framelen);
|
|
if(r != 0) {
|
|
return r;
|
|
}
|
|
eof_flags = 0;
|
|
r = frame->data_prd.read_callback
|
|
(session, frame->hd.stream_id, (*buf_ptr)+8, datamax,
|
|
&eof_flags, &frame->data_prd.source, session->user_data);
|
|
if(r == NGHTTP2_ERR_DEFERRED || r == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
|
|
return r;
|
|
} else if(r < 0 || datamax < (size_t)r) {
|
|
/* This is the error code when callback is failed. */
|
|
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
|
}
|
|
memset(*buf_ptr, 0, NGHTTP2_FRAME_HEAD_LENGTH);
|
|
nghttp2_put_uint16be(&(*buf_ptr)[0], r);
|
|
flags = 0;
|
|
if(eof_flags) {
|
|
frame->eof = 1;
|
|
if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
|
|
flags |= NGHTTP2_FLAG_END_STREAM;
|
|
}
|
|
}
|
|
(*buf_ptr)[3] = flags;
|
|
nghttp2_put_uint32be(&(*buf_ptr)[4], frame->hd.stream_id);
|
|
return r+8;
|
|
}
|
|
|
|
void* nghttp2_session_get_stream_user_data(nghttp2_session *session,
|
|
int32_t stream_id)
|
|
{
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, stream_id);
|
|
if(stream) {
|
|
return stream->stream_user_data;
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
int nghttp2_session_resume_data(nghttp2_session *session, int32_t stream_id)
|
|
{
|
|
int r;
|
|
nghttp2_stream *stream;
|
|
stream = nghttp2_session_get_stream(session, stream_id);
|
|
if(stream == NULL || stream->deferred_data == NULL ||
|
|
(stream->deferred_flags & NGHTTP2_DEFERRED_FLOW_CONTROL)) {
|
|
return NGHTTP2_ERR_INVALID_ARGUMENT;
|
|
}
|
|
r = nghttp2_pq_push(&session->ob_pq, stream->deferred_data);
|
|
if(r == 0) {
|
|
nghttp2_stream_detach_deferred_data(stream);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
size_t nghttp2_session_get_outbound_queue_size(nghttp2_session *session)
|
|
{
|
|
return nghttp2_pq_size(&session->ob_pq)+nghttp2_pq_size(&session->ob_ss_pq);
|
|
}
|
|
|
|
int nghttp2_session_set_option(nghttp2_session *session,
|
|
int optname, void *optval, size_t optlen)
|
|
{
|
|
switch(optname) {
|
|
case NGHTTP2_OPT_NO_AUTO_WINDOW_UPDATE:
|
|
if(optlen == sizeof(int)) {
|
|
int intval = *(int*)optval;
|
|
if(intval) {
|
|
session->opt_flags |= NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE;
|
|
} else {
|
|
session->opt_flags &= ~NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE;
|
|
}
|
|
} else {
|
|
return NGHTTP2_ERR_INVALID_ARGUMENT;
|
|
}
|
|
break;
|
|
default:
|
|
return NGHTTP2_ERR_INVALID_ARGUMENT;
|
|
}
|
|
return 0;
|
|
}
|