Commit message: Earlier check for settings flood
CVE: CVE-2020-11080
CWE: CWE-707
Vulnerability description: In nghttp2 before version 1.41.0, the overly large HTTP/2 SETTINGS frame payload causes denial of service. The proof of concept attack involves a malicious client constructing a SETTINGS frame with a length of 14,400 bytes (2400 individual settings entries) over and over again. The attack causes the CPU to spike at 100%. nghttp2 v1.41.0 fixes this vulnerability. There is a workaround to this vulnerability. Implement nghttp2_on_frame_recv_callback callback, and if received frame is SETTINGS frame and the number of settings entries are large (e.g., > 32), then drop the connection.

Project: nghttp2

static int state_reserved_remote(nghttp2_session *session,
                                 nghttp2_stream *stream) {
  return stream->state == NGHTTP2_STREAM_RESERVED &&
         !nghttp2_session_is_my_stream_id(session, stream->stream_id);
}

Project: nghttp2

static int session_call_error_callback(nghttp2_session *session,
                                       int lib_error_code, const char *fmt,
                                       ...) {
  size_t bufsize;
  va_list ap;
  char *buf;
  int rv;
  nghttp2_mem *mem;

  if (!session->callbacks.error_callback &&
      !session->callbacks.error_callback2) {
    return 0;
  }

  mem = &session->mem;

  va_start(ap, fmt);
  rv = vsnprintf(NULL, 0, fmt, ap);
  va_end(ap);

  if (rv < 0) {
    return NGHTTP2_ERR_NOMEM;
  }

  bufsize = (size_t)(rv + 1);

  buf = nghttp2_mem_malloc(mem, bufsize);
  if (buf == NULL) {
    return NGHTTP2_ERR_NOMEM;
  }

  va_start(ap, fmt);
  rv = vsnprintf(buf, bufsize, fmt, ap);
  va_end(ap);

  if (rv < 0) {
    nghttp2_mem_free(mem, buf);
    /* vsnprintf may return error because of various things we can
       imagine, but typically we don't want to drop session just for
       debug callback. */
    DEBUGF("error_callback: vsnprintf failed. The template was %s\n", fmt);
    return 0;
  }

  if (session->callbacks.error_callback2) {
    rv = session->callbacks.error_callback2(session, lib_error_code, buf,
                                            (size_t)rv, session->user_data);
  } else {
    rv = session->callbacks.error_callback(session, buf, (size_t)rv,
                                           session->user_data);
  }

  nghttp2_mem_free(mem, buf);

  if (rv != 0) {
    return NGHTTP2_ERR_CALLBACK_FAILURE;
  }

  return 0;
}

Project: nghttp2

static int session_close_stream_on_goaway(nghttp2_session *session,
                                          int32_t last_stream_id,
                                          int incoming) {
  int rv;
  nghttp2_stream *stream, *next_stream;
  nghttp2_close_stream_on_goaway_arg arg = {session, NULL, last_stream_id,
                                            incoming};

  rv = nghttp2_map_each(&session->streams, find_stream_on_goaway_func, &arg);
  assert(rv == 0);

  stream = arg.head;
  while (stream) {
    next_stream = stream->closed_next;
    stream->closed_next = NULL;
    rv = nghttp2_session_close_stream(session, stream->stream_id,
                                      NGHTTP2_REFUSED_STREAM);

    /* stream may be deleted here */

    stream = next_stream;

    if (nghttp2_is_fatal(rv)) {
      /* Clean up closed_next member just in case */
      while (stream) {
        next_stream = stream->closed_next;
        stream->closed_next = NULL;
        stream = next_stream;
      }
      return rv;
    }
  }

  return 0;
}

Project: nghttp2

int nghttp2_session_add_goaway(nghttp2_session *session, int32_t last_stream_id,
                               uint32_t error_code, const uint8_t *opaque_data,
                               size_t opaque_data_len, uint8_t aux_flags) {
  int rv;
  nghttp2_outbound_item *item;
  nghttp2_frame *frame;
  uint8_t *opaque_data_copy = NULL;
  nghttp2_goaway_aux_data *aux_data;
  nghttp2_mem *mem;

  mem = &session->mem;

  if (nghttp2_session_is_my_stream_id(session, last_stream_id)) {
    return NGHTTP2_ERR_INVALID_ARGUMENT;
  }

  if (opaque_data_len) {
    if (opaque_data_len + 8 > NGHTTP2_MAX_PAYLOADLEN) {
      return NGHTTP2_ERR_INVALID_ARGUMENT;
    }
    opaque_data_copy = nghttp2_mem_malloc(mem, opaque_data_len);
    if (opaque_data_copy == NULL) {
      return NGHTTP2_ERR_NOMEM;
    }
    memcpy(opaque_data_copy, opaque_data, opaque_data_len);
  }

  item = nghttp2_mem_malloc(mem, sizeof(nghttp2_outbound_item));
  if (item == NULL) {
    nghttp2_mem_free(mem, opaque_data_copy);
    return NGHTTP2_ERR_NOMEM;
  }

  nghttp2_outbound_item_init(item);

  frame = &item->frame;

  /* last_stream_id must not be increased from the value previously
     sent */
  last_stream_id = nghttp2_min(last_stream_id, session->local_last_stream_id);

  nghttp2_frame_goaway_init(&frame->goaway, last_stream_id, error_code,
                            opaque_data_copy, opaque_data_len);

  aux_data = &item->aux_data.goaway;
  aux_data->flags = aux_flags;

  rv = nghttp2_session_add_item(session, item);
  if (rv != 0) {
    nghttp2_frame_goaway_free(&frame->goaway, mem);
    nghttp2_mem_free(mem, item);
    return rv;
  }
  return 0;
}

Project: nghttp2

int nghttp2_session_consume_connection(nghttp2_session *session, size_t size) {
  int rv;

  if (!(session->opt_flags & NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE)) {
    return NGHTTP2_ERR_INVALID_STATE;
  }

  rv = session_update_connection_consumed_size(session, size);

  if (nghttp2_is_fatal(rv)) {
    return rv;
  }

  return 0;
}

Project: nghttp2

static int session_predicate_for_stream_send(nghttp2_session *session,
                                             nghttp2_stream *stream) {
  if (stream == NULL) {
    return NGHTTP2_ERR_STREAM_CLOSED;
  }
  if (session_is_closing(session)) {
    return NGHTTP2_ERR_SESSION_CLOSING;
  }
  if (stream->shut_flags & NGHTTP2_SHUT_WR) {
    return NGHTTP2_ERR_STREAM_SHUT_WR;
  }
  return 0;
}

Project: nghttp2

int nghttp2_session_on_priority_received(nghttp2_session *session,
                                         nghttp2_frame *frame) {
  int rv;
  nghttp2_stream *stream;

  if (frame->hd.stream_id == 0) {
    return session_handle_invalid_connection(session, frame, NGHTTP2_ERR_PROTO,
                                             "PRIORITY: stream_id == 0");
  }

  if (frame->priority.pri_spec.stream_id == frame->hd.stream_id) {
    return nghttp2_session_terminate_session_with_reason(
        session, NGHTTP2_PROTOCOL_ERROR, "depend on itself");
  }

  if (!session->server) {
    /* Re-prioritization works only in server */
    return session_call_on_frame_received(session, frame);
  }

  stream = nghttp2_session_get_stream_raw(session, frame->hd.stream_id);

  if (!stream) {
    /* PRIORITY against idle stream can create anchor node in
       dependency tree. */
    if (!session_detect_idle_stream(session, frame->hd.stream_id)) {
      return 0;
    }

    stream = nghttp2_session_open_stream(
        session, frame->hd.stream_id, NGHTTP2_STREAM_FLAG_NONE,
        &frame->priority.pri_spec, NGHTTP2_STREAM_IDLE, NULL);

    if (stream == NULL) {
      return NGHTTP2_ERR_NOMEM;
    }

    rv = nghttp2_session_adjust_idle_stream(session);
    if (nghttp2_is_fatal(rv)) {
      return rv;
    }
  } else {
    rv = nghttp2_session_reprioritize_stream(session, stream,
                                             &frame->priority.pri_spec);

    if (nghttp2_is_fatal(rv)) {
      return rv;
    }

    rv = nghttp2_session_adjust_idle_stream(session);
    if (nghttp2_is_fatal(rv)) {
      return rv;
    }
  }

  return session_call_on_frame_received(session, frame);
}

Project: nghttp2

int nghttp2_session_get_stream_local_close(nghttp2_session *session,
                                           int32_t stream_id) {
  nghttp2_stream *stream;

  stream = nghttp2_session_get_stream(session, stream_id);

  if (!stream) {
    return -1;
  }

  return (stream->shut_flags & NGHTTP2_SHUT_WR) != 0;
}

Project: nghttp2

static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame,
                                size_t *readlen_ptr, uint8_t *in, size_t inlen,
                                int final, int call_header_cb) {
  ssize_t proclen;
  int rv;
  int inflate_flags;
  nghttp2_hd_nv nv;
  nghttp2_stream *stream;
  nghttp2_stream *subject_stream;
  int trailer = 0;

  *readlen_ptr = 0;
  stream = nghttp2_session_get_stream(session, frame->hd.stream_id);

  if (frame->hd.type == NGHTTP2_PUSH_PROMISE) {
    subject_stream = nghttp2_session_get_stream(
        session, frame->push_promise.promised_stream_id);
  } else {
    subject_stream = stream;
    trailer = session_trailer_headers(session, stream, frame);
  }

  DEBUGF("recv: decoding header block %zu bytes\n", inlen);
  for (;;) {
    inflate_flags = 0;
    proclen = nghttp2_hd_inflate_hd_nv(&session->hd_inflater, &nv,
                                       &inflate_flags, in, inlen, final);
    if (nghttp2_is_fatal((int)proclen)) {
      return (int)proclen;
    }
    if (proclen < 0) {
      if (session->iframe.state == NGHTTP2_IB_READ_HEADER_BLOCK) {
        if (subject_stream && subject_stream->state != NGHTTP2_STREAM_CLOSING) {
          /* Adding RST_STREAM here is very important. It prevents
             from invoking subsequent callbacks for the same stream
             ID. */
          rv = nghttp2_session_add_rst_stream(
              session, subject_stream->stream_id, NGHTTP2_COMPRESSION_ERROR);

          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
        }
      }
      rv =
          nghttp2_session_terminate_session(session, NGHTTP2_COMPRESSION_ERROR);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      return NGHTTP2_ERR_HEADER_COMP;
    }
    in += proclen;
    inlen -= (size_t)proclen;
    *readlen_ptr += (size_t)proclen;

    DEBUGF("recv: proclen=%zd\n", proclen);

    if (call_header_cb && (inflate_flags & NGHTTP2_HD_INFLATE_EMIT)) {
      rv = 0;
      if (subject_stream) {
        if (session_enforce_http_messaging(session)) {
          rv = nghttp2_http_on_header(session, subject_stream, frame, &nv,
                                      trailer);

          if (rv == NGHTTP2_ERR_IGN_HTTP_HEADER) {
            /* Don't overwrite rv here */
            int rv2;

            rv2 = session_call_on_invalid_header(session, frame, &nv);
            if (rv2 == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
              rv = NGHTTP2_ERR_HTTP_HEADER;
            } else {
              if (rv2 != 0) {
                return rv2;
              }

              /* header is ignored */
              DEBUGF("recv: HTTP ignored: type=%u, id=%d, header %.*s: %.*s\n",
                     frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
                     nv.name->base, (int)nv.value->len, nv.value->base);

              rv2 = session_call_error_callback(
                  session, NGHTTP2_ERR_HTTP_HEADER,
                  "Ignoring received invalid HTTP header field: frame type: "
                  "%u, stream: %d, name: [%.*s], value: [%.*s]",
                  frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
                  nv.name->base, (int)nv.value->len, nv.value->base);

              if (nghttp2_is_fatal(rv2)) {
                return rv2;
              }
            }
          }

          if (rv == NGHTTP2_ERR_HTTP_HEADER) {
            DEBUGF("recv: HTTP error: type=%u, id=%d, header %.*s: %.*s\n",
                   frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
                   nv.name->base, (int)nv.value->len, nv.value->base);

            rv = session_call_error_callback(
                session, NGHTTP2_ERR_HTTP_HEADER,
                "Invalid HTTP header field was received: frame type: "
                "%u, stream: %d, name: [%.*s], value: [%.*s]",
                frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
                nv.name->base, (int)nv.value->len, nv.value->base);

            if (nghttp2_is_fatal(rv)) {
              return rv;
            }

            rv = session_handle_invalid_stream2(session,
                                                subject_stream->stream_id,
                                                frame, NGHTTP2_ERR_HTTP_HEADER);
            if (nghttp2_is_fatal(rv)) {
              return rv;
            }
            return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
          }
        }
        if (rv == 0) {
          rv = session_call_on_header(session, frame, &nv);
          /* This handles NGHTTP2_ERR_PAUSE and
             NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE as well */
          if (rv != 0) {
            return rv;
          }
        }
      }
    }
    if (inflate_flags & NGHTTP2_HD_INFLATE_FINAL) {
      nghttp2_hd_inflate_end_headers(&session->hd_inflater);
      break;
    }
    if ((inflate_flags & NGHTTP2_HD_INFLATE_EMIT) == 0 && inlen == 0) {
      break;
    }
  }
  return 0;
}

Project: nghttp2

static int session_process_headers_frame(nghttp2_session *session) {
  int rv;
  nghttp2_inbound_frame *iframe = &session->iframe;
  nghttp2_frame *frame = &iframe->frame;
  nghttp2_stream *stream;

  rv = nghttp2_frame_unpack_headers_payload(&frame->headers, iframe->sbuf.pos);

  if (rv != 0) {
    return nghttp2_session_terminate_session_with_reason(
        session, NGHTTP2_PROTOCOL_ERROR, "HEADERS: could not unpack");
  }
  stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
  if (!stream) {
    frame->headers.cat = NGHTTP2_HCAT_REQUEST;
    return nghttp2_session_on_request_headers_received(session, frame);
  }

  if (stream->state == NGHTTP2_STREAM_RESERVED) {
    frame->headers.cat = NGHTTP2_HCAT_PUSH_RESPONSE;
    return nghttp2_session_on_push_response_headers_received(session, frame,
                                                             stream);
  }

  if (stream->state == NGHTTP2_STREAM_OPENING &&
      nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) {
    frame->headers.cat = NGHTTP2_HCAT_RESPONSE;
    return nghttp2_session_on_response_headers_received(session, frame, stream);
  }

  frame->headers.cat = NGHTTP2_HCAT_HEADERS;
  return nghttp2_session_on_headers_received(session, frame, stream);
}

Project: nghttp2

session_predicate_push_response_headers_send(nghttp2_session *session,
                                             nghttp2_stream *stream) {
  int rv;
  /* TODO Should disallow HEADERS if GOAWAY has already been issued? */
  rv = session_predicate_for_stream_send(session, stream);
  if (rv != 0) {
    return rv;
  }
  assert(stream);
  if (!session->server) {
    return NGHTTP2_ERR_PROTO;
  }
  if (stream->state != NGHTTP2_STREAM_RESERVED) {
    return NGHTTP2_ERR_PROTO;
  }
  if (session->goaway_flags & NGHTTP2_GOAWAY_RECV) {
    return NGHTTP2_ERR_START_STREAM_NOT_ALLOWED;
  }
  return 0;
}

Project: nghttp2

static int find_stream_on_goaway_func(nghttp2_map_entry *entry, void *ptr) {
  nghttp2_close_stream_on_goaway_arg *arg;
  nghttp2_stream *stream;

  arg = (nghttp2_close_stream_on_goaway_arg *)ptr;
  stream = (nghttp2_stream *)entry;

  if (nghttp2_session_is_my_stream_id(arg->session, stream->stream_id)) {
    if (arg->incoming) {
      return 0;
    }
  } else if (!arg->incoming) {
    return 0;
  }

  if (stream->state != NGHTTP2_STREAM_IDLE &&
      (stream->flags & NGHTTP2_STREAM_FLAG_CLOSED) == 0 &&
      stream->stream_id > arg->last_stream_id) {
    /* We are collecting streams to close because we cannot call
       nghttp2_session_close_stream() inside nghttp2_map_each().
       Reuse closed_next member.. bad choice? */
    assert(stream->closed_next == NULL);
    assert(stream->closed_prev == NULL);

    if (arg->head) {
      stream->closed_next = arg->head;
      arg->head = stream;
    } else {
      arg->head = stream;
    }
  }

  return 0;
}

Project: nghttp2

static int session_is_closing(nghttp2_session *session) {
  return (session->goaway_flags & NGHTTP2_GOAWAY_TERM_ON_SEND) != 0 ||
         (nghttp2_session_want_read(session) == 0 &&
          nghttp2_session_want_write(session) == 0);
}

Project: nghttp2

int nghttp2_session_set_next_stream_id(nghttp2_session *session,
                                       int32_t next_stream_id) {
  if (next_stream_id <= 0 ||
      session->next_stream_id > (uint32_t)next_stream_id) {
    return NGHTTP2_ERR_INVALID_ARGUMENT;
  }

  if (session->server) {
    if (next_stream_id % 2) {
      return NGHTTP2_ERR_INVALID_ARGUMENT;
    }
  } else if (next_stream_id % 2 == 0) {
    return NGHTTP2_ERR_INVALID_ARGUMENT;
  }

  session->next_stream_id = (uint32_t)next_stream_id;
  return 0;
}

Project: nghttp2

static void ob_q_free(nghttp2_outbound_queue *q, nghttp2_mem *mem) {
  nghttp2_outbound_item *item, *next;
  for (item = q->head; item;) {
    next = item->qnext;
    nghttp2_outbound_item_free(item, mem);
    nghttp2_mem_free(mem, item);
    item = next;
  }
}

Project: nghttp2

static int session_end_stream_headers_received(nghttp2_session *session,
                                               nghttp2_frame *frame,
                                               nghttp2_stream *stream) {
  int rv;
  if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) {
    return 0;
  }

  nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD);
  rv = nghttp2_session_close_stream_if_shut_rdwr(session, stream);
  if (nghttp2_is_fatal(rv)) {
    return rv;
  }

  return 0;
}

Project: nghttp2

size_t nghttp2_session_get_outbound_queue_size(nghttp2_session *session) {
  return nghttp2_outbound_queue_size(&session->ob_urgent) +
         nghttp2_outbound_queue_size(&session->ob_reg) +
         nghttp2_outbound_queue_size(&session->ob_syn);
  /* TODO account for item attached to stream */
}

Project: nghttp2

static size_t session_get_num_active_streams(nghttp2_session *session) {
  return nghttp2_map_size(&session->streams) - session->num_closed_streams -
         session->num_idle_streams;
}

Project: nghttp2

int32_t nghttp2_session_get_last_proc_stream_id(nghttp2_session *session) {
  return session->last_proc_stream_id;
}

Project: nghttp2

static int is_non_fatal(int lib_error_code) {
  return lib_error_code < 0 && lib_error_code > NGHTTP2_ERR_FATAL;
}

Project: nghttp2

static int session_headers_add_pad(nghttp2_session *session,
                                   nghttp2_frame *frame) {
  int rv;
  ssize_t padded_payloadlen;
  nghttp2_active_outbound_item *aob;
  nghttp2_bufs *framebufs;
  size_t padlen;
  size_t max_payloadlen;

  aob = &session->aob;
  framebufs = &aob->framebufs;

  max_payloadlen = nghttp2_min(NGHTTP2_MAX_PAYLOADLEN,
                               frame->hd.length + NGHTTP2_MAX_PADLEN);

  padded_payloadlen =
      session_call_select_padding(session, frame, max_payloadlen);

  if (nghttp2_is_fatal((int)padded_payloadlen)) {
    return (int)padded_payloadlen;
  }

  padlen = (size_t)padded_payloadlen - frame->hd.length;

  DEBUGF("send: padding selected: payloadlen=%zd, padlen=%zu\n",
         padded_payloadlen, padlen);

  rv = nghttp2_frame_add_pad(framebufs, &frame->hd, padlen, 0);

  if (rv != 0) {
    return rv;
  }

  frame->headers.padlen = padlen;

  return 0;
}

Project: nghttp2

static int session_after_frame_sent1(nghttp2_session *session) {
  int rv;
  nghttp2_active_outbound_item *aob = &session->aob;
  nghttp2_outbound_item *item = aob->item;
  nghttp2_bufs *framebufs = &aob->framebufs;
  nghttp2_frame *frame;
  nghttp2_stream *stream;

  frame = &item->frame;

  if (frame->hd.type == NGHTTP2_DATA) {
    nghttp2_data_aux_data *aux_data;

    aux_data = &item->aux_data.data;

    stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
    /* We update flow control window after a frame was completely
       sent. This is possible because we choose payload length not to
       exceed the window */
    session->remote_window_size -= (int32_t)frame->hd.length;
    if (stream) {
      stream->remote_window_size -= (int32_t)frame->hd.length;
    }

    if (stream && aux_data->eof) {
      rv = nghttp2_stream_detach_item(stream);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      /* Call on_frame_send_callback after
         nghttp2_stream_detach_item(), so that application can issue
         nghttp2_submit_data() in the callback. */
      if (session->callbacks.on_frame_send_callback) {
        rv = session_call_on_frame_send(session, frame);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }
      }

      if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
        int stream_closed;

        stream_closed =
            (stream->shut_flags & NGHTTP2_SHUT_RDWR) == NGHTTP2_SHUT_RDWR;

        nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR);

        rv = nghttp2_session_close_stream_if_shut_rdwr(session, stream);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }
        /* stream may be NULL if it was closed */
        if (stream_closed) {
          stream = NULL;
        }
      }
      return 0;
    }

    if (session->callbacks.on_frame_send_callback) {
      rv = session_call_on_frame_send(session, frame);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }
    }

    return 0;
  }

  /* non-DATA frame */

  if (frame->hd.type == NGHTTP2_HEADERS ||
      frame->hd.type == NGHTTP2_PUSH_PROMISE) {
    if (nghttp2_bufs_next_present(framebufs)) {
      DEBUGF("send: CONTINUATION exists, just return\n");
      return 0;
    }
  }
  rv = session_call_on_frame_send(session, frame);
  if (nghttp2_is_fatal(rv)) {
    return rv;
  }
  switch (frame->hd.type) {
  case NGHTTP2_HEADERS: {
    nghttp2_headers_aux_data *aux_data;

    stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
    if (!stream) {
      return 0;
    }

    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);
      }
      rv = nghttp2_session_close_stream_if_shut_rdwr(session, stream);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }
      /* We assume aux_data is a pointer to nghttp2_headers_aux_data */
      aux_data = &item->aux_data.headers;
      if (aux_data->data_prd.read_callback) {
        /* nghttp2_submit_data() makes a copy of aux_data->data_prd */
        rv = nghttp2_submit_data(session, NGHTTP2_FLAG_END_STREAM,
                                 frame->hd.stream_id, &aux_data->data_prd);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }
        /* TODO nghttp2_submit_data() may fail if stream has already
           DATA frame item.  We might have to handle it here. */
      }
      return 0;
    }
    case NGHTTP2_HCAT_PUSH_RESPONSE:
      stream->flags = (uint8_t)(stream->flags & ~NGHTTP2_STREAM_FLAG_PUSH);
      ++session->num_outgoing_streams;
    /* Fall through */
    case NGHTTP2_HCAT_RESPONSE:
      stream->state = NGHTTP2_STREAM_OPENED;
    /* Fall through */
    case NGHTTP2_HCAT_HEADERS:
      if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
        nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_WR);
      }
      rv = nghttp2_session_close_stream_if_shut_rdwr(session, stream);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }
      /* We assume aux_data is a pointer to nghttp2_headers_aux_data */
      aux_data = &item->aux_data.headers;
      if (aux_data->data_prd.read_callback) {
        rv = nghttp2_submit_data(session, NGHTTP2_FLAG_END_STREAM,
                                 frame->hd.stream_id, &aux_data->data_prd);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }
        /* TODO nghttp2_submit_data() may fail if stream has already
           DATA frame item.  We might have to handle it here. */
      }
      return 0;
    default:
      /* Unreachable */
      assert(0);
      return 0;
    }
  }
  case NGHTTP2_PRIORITY:
    if (session->server) {
      return 0;
      ;
    }

    stream = nghttp2_session_get_stream_raw(session, frame->hd.stream_id);

    if (!stream) {
      if (!session_detect_idle_stream(session, frame->hd.stream_id)) {
        return 0;
      }

      stream = nghttp2_session_open_stream(
          session, frame->hd.stream_id, NGHTTP2_FLAG_NONE,
          &frame->priority.pri_spec, NGHTTP2_STREAM_IDLE, NULL);
      if (!stream) {
        return NGHTTP2_ERR_NOMEM;
      }
    } else {
      rv = nghttp2_session_reprioritize_stream(session, stream,
                                               &frame->priority.pri_spec);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }
    }

    rv = nghttp2_session_adjust_idle_stream(session);

    if (nghttp2_is_fatal(rv)) {
      return rv;
    }

    return 0;
  case NGHTTP2_RST_STREAM:
    rv = nghttp2_session_close_stream(session, frame->hd.stream_id,
                                      frame->rst_stream.error_code);
    if (nghttp2_is_fatal(rv)) {
      return rv;
    }
    return 0;
  case NGHTTP2_GOAWAY: {
    nghttp2_goaway_aux_data *aux_data;

    aux_data = &item->aux_data.goaway;

    if ((aux_data->flags & NGHTTP2_GOAWAY_AUX_SHUTDOWN_NOTICE) == 0) {

      if (aux_data->flags & NGHTTP2_GOAWAY_AUX_TERM_ON_SEND) {
        session->goaway_flags |= NGHTTP2_GOAWAY_TERM_SENT;
      }

      session->goaway_flags |= NGHTTP2_GOAWAY_SENT;

      rv = session_close_stream_on_goaway(session, frame->goaway.last_stream_id,
                                          1);

      if (nghttp2_is_fatal(rv)) {
        return rv;
      }
    }

    return 0;
  }
  case NGHTTP2_WINDOW_UPDATE:
    if (frame->hd.stream_id == 0) {
      session->window_update_queued = 0;
      if (session->opt_flags & NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE) {
        rv = session_update_connection_consumed_size(session, 0);
      } else {
        rv = nghttp2_session_update_recv_connection_window_size(session, 0);
      }

      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      return 0;
    }

    stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
    if (!stream) {
      return 0;
    }

    stream->window_update_queued = 0;

    /* We don't have to send WINDOW_UPDATE if END_STREAM from peer
       is seen. */
    if (stream->shut_flags & NGHTTP2_SHUT_RD) {
      return 0;
    }

    if (session->opt_flags & NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE) {
      rv = session_update_stream_consumed_size(session, stream, 0);
    } else {
      rv =
          nghttp2_session_update_recv_stream_window_size(session, stream, 0, 1);
    }

    if (nghttp2_is_fatal(rv)) {
      return rv;
    }

    return 0;
  default:
    return 0;
  }
}