Validate HTTP semantics by default

Previously we did not check HTTP semantics and it is left out for
application.  Although checking is relatively easy, but they are
scattered and error prone.  We have implemented these checks in our
applications and also feel they are tedious.  To make application
development a bit easier, this commit adds basic HTTP semantics
validation to library code.  We do following checks:

server:

* HEADERS is either request header or trailer header.  Other type of
header is disallowed.

client:

* HEADERS is either zero or more non-final response header or final
  response header or trailer header.  Other type of header is
  disallowed.

For both:

* Check mandatory pseudo header fields.
* Make sure that content-length matches the amount of DATA we
  received.

If validation fails, RST_STREAM of type PROTOCOL_ERROR is issued.
This commit is contained in:
Tatsuhiro Tsujikawa 2015-02-20 00:01:15 +09:00
parent 9845729709
commit b157d4ebb2
15 changed files with 1868 additions and 134 deletions

85
genlibtokenlookup.py Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env python
HEADERS = [
':authority',
':method',
':path',
':scheme',
':status',
"content-length",
"host",
"te",
'connection',
'keep-alive',
'proxy-connection',
'transfer-encoding',
'upgrade'
]
def to_enum_hd(k):
res = 'NGHTTP2_TOKEN_'
for c in k.upper():
if c == ':' or c == '-':
res += '_'
continue
res += c
return res
def build_header(headers):
res = {}
for k in headers:
size = len(k)
if size not in res:
res[size] = {}
ent = res[size]
c = k[-1]
if c not in ent:
ent[c] = []
ent[c].append(k)
return res
def gen_enum():
print '''\
typedef enum {'''
for k in sorted(HEADERS):
print '''\
{},'''.format(to_enum_hd(k))
print '''\
NGHTTP2_TOKEN_MAXIDX,
} nghttp2_token;'''
def gen_index_header():
print '''\
static int lookup_token(const uint8_t *name, size_t namelen) {
switch (namelen) {'''
b = build_header(HEADERS)
for size in sorted(b.keys()):
ents = b[size]
print '''\
case {}:'''.format(size)
print '''\
switch (name[namelen - 1]) {'''
for c in sorted(ents.keys()):
headers = sorted(ents[c])
print '''\
case '{}':'''.format(c)
for k in headers:
print '''\
if (streq("{}", name, {})) {{
return {};
}}'''.format(k[:-1], size - 1, to_enum_hd(k))
print '''\
break;'''
print '''\
}
break;'''
print '''\
}
return -1;
}'''
if __name__ == '__main__':
gen_enum()
print ''
gen_index_header()

View File

@ -355,9 +355,8 @@ func TestH2H1MultipleRequestCL(t *testing.T) {
if err != nil {
t.Fatalf("Error st.http2() = %v", err)
}
want := 400
if got := res.status; got != want {
t.Errorf("status: %v; want %v", got, want)
if got, want := res.errCode, http2.ErrCodeProtocol; got != want {
t.Errorf("res.errCode: %v; want %v", got, want)
}
}
@ -378,9 +377,8 @@ func TestH2H1InvalidRequestCL(t *testing.T) {
if err != nil {
t.Fatalf("Error st.http2() = %v", err)
}
want := 400
if got := res.status; got != want {
t.Errorf("status: %v; want %v", got, want)
if got, want := res.errCode, http2.ErrCodeProtocol; got != want {
t.Errorf("res.errCode: %v; want %v", got, want)
}
}
@ -614,9 +612,8 @@ func TestH2H2MultipleResponseCL(t *testing.T) {
if err != nil {
t.Fatalf("Error st.http2() = %v", err)
}
want := 502
if got := res.status; got != want {
t.Errorf("status: %v; want %v", got, want)
if got, want := res.errCode, http2.ErrCodeInternal; got != want {
t.Errorf("res.errCode: %v; want %v", got, want)
}
}
@ -635,9 +632,8 @@ func TestH2H2InvalidResponseCL(t *testing.T) {
if err != nil {
t.Fatalf("Error st.http2() = %v", err)
}
want := 502
if got := res.status; got != want {
t.Errorf("status: %v; want %v", got, want)
if got, want := res.errCode, http2.ErrCodeInternal; got != want {
t.Errorf("res.errCode: %v; want %v", got, want)
}
}

View File

@ -45,7 +45,8 @@ OBJECTS = nghttp2_pq.c nghttp2_map.c nghttp2_queue.c \
nghttp2_priority_spec.c \
nghttp2_option.c \
nghttp2_callbacks.c \
nghttp2_mem.c
nghttp2_mem.c \
nghttp2_http.c
HFILES = nghttp2_pq.h nghttp2_int.h nghttp2_map.h nghttp2_queue.h \
nghttp2_frame.h \
@ -58,7 +59,8 @@ HFILES = nghttp2_pq.h nghttp2_int.h nghttp2_map.h nghttp2_queue.h \
nghttp2_priority_spec.h \
nghttp2_option.h \
nghttp2_callbacks.h \
nghttp2_mem.h
nghttp2_mem.h \
nghttp2_http.h
libnghttp2_la_SOURCES = $(HFILES) $(OBJECTS)
libnghttp2_la_LDFLAGS = -no-undefined \

511
lib/nghttp2_http.c Normal file
View File

@ -0,0 +1,511 @@
/*
* nghttp2 - HTTP/2 C Library
*
* Copyright (c) 2015 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_http.h"
#include <string.h>
#include <assert.h>
#include <stdio.h>
static int memeq(const void *a, const void *b, size_t n) {
return memcmp(a, b, n) == 0;
}
#define streq(A, B, N) ((sizeof((A)) - 1) == (N) && memeq((A), (B), (N)))
static char downcase(char c) {
return 'A' <= c && c <= 'Z' ? (c - 'A' + 'a') : c;
}
static int memieq(const void *a, const void *b, size_t n) {
size_t i;
const uint8_t *aa = a, *bb = b;
for (i = 0; i < n; ++i) {
if (downcase(aa[i]) != downcase(bb[i])) {
return 0;
}
}
return 1;
}
#define strieq(A, B, N) ((sizeof((A)) - 1) == (N) && memieq((A), (B), (N)))
typedef enum {
NGHTTP2_TOKEN__AUTHORITY,
NGHTTP2_TOKEN__METHOD,
NGHTTP2_TOKEN__PATH,
NGHTTP2_TOKEN__SCHEME,
NGHTTP2_TOKEN__STATUS,
NGHTTP2_TOKEN_CONNECTION,
NGHTTP2_TOKEN_CONTENT_LENGTH,
NGHTTP2_TOKEN_HOST,
NGHTTP2_TOKEN_KEEP_ALIVE,
NGHTTP2_TOKEN_PROXY_CONNECTION,
NGHTTP2_TOKEN_TE,
NGHTTP2_TOKEN_TRANSFER_ENCODING,
NGHTTP2_TOKEN_UPGRADE,
NGHTTP2_TOKEN_MAXIDX,
} nghttp2_token;
// This function was generated by genlibtokenlookup.py. Inspired by
// h2o header lookup. https://github.com/h2o/h2o
static int lookup_token(const uint8_t *name, size_t namelen) {
switch (namelen) {
case 2:
switch (name[namelen - 1]) {
case 'e':
if (streq("t", name, 1)) {
return NGHTTP2_TOKEN_TE;
}
break;
}
break;
case 4:
switch (name[namelen - 1]) {
case 't':
if (streq("hos", name, 3)) {
return NGHTTP2_TOKEN_HOST;
}
break;
}
break;
case 5:
switch (name[namelen - 1]) {
case 'h':
if (streq(":pat", name, 4)) {
return NGHTTP2_TOKEN__PATH;
}
break;
}
break;
case 7:
switch (name[namelen - 1]) {
case 'd':
if (streq(":metho", name, 6)) {
return NGHTTP2_TOKEN__METHOD;
}
break;
case 'e':
if (streq(":schem", name, 6)) {
return NGHTTP2_TOKEN__SCHEME;
}
if (streq("upgrad", name, 6)) {
return NGHTTP2_TOKEN_UPGRADE;
}
break;
case 's':
if (streq(":statu", name, 6)) {
return NGHTTP2_TOKEN__STATUS;
}
break;
}
break;
case 10:
switch (name[namelen - 1]) {
case 'e':
if (streq("keep-aliv", name, 9)) {
return NGHTTP2_TOKEN_KEEP_ALIVE;
}
break;
case 'n':
if (streq("connectio", name, 9)) {
return NGHTTP2_TOKEN_CONNECTION;
}
break;
case 'y':
if (streq(":authorit", name, 9)) {
return NGHTTP2_TOKEN__AUTHORITY;
}
break;
}
break;
case 14:
switch (name[namelen - 1]) {
case 'h':
if (streq("content-lengt", name, 13)) {
return NGHTTP2_TOKEN_CONTENT_LENGTH;
}
break;
}
break;
case 16:
switch (name[namelen - 1]) {
case 'n':
if (streq("proxy-connectio", name, 15)) {
return NGHTTP2_TOKEN_PROXY_CONNECTION;
}
break;
}
break;
case 17:
switch (name[namelen - 1]) {
case 'g':
if (streq("transfer-encodin", name, 16)) {
return NGHTTP2_TOKEN_TRANSFER_ENCODING;
}
break;
}
break;
}
return -1;
}
static int64_t parse_uint(const uint8_t *s, size_t len) {
int64_t n = 0;
size_t i;
if (len == 0) {
return -1;
}
for (i = 0; i < len; ++i) {
if ('0' <= s[i] && s[i] <= '9') {
if (n > INT64_MAX / 10) {
return -1;
}
n *= 10;
if (n > INT64_MAX - (s[i] - '0')) {
return -1;
}
n += s[i] - '0';
continue;
}
return -1;
}
return n;
}
static int lws(const uint8_t *s, size_t n) {
size_t i;
for (i = 0; i < n; ++i) {
if (s[i] != ' ' && s[i] != '\t') {
return 0;
}
}
return 1;
}
static int check_pseudo_header(nghttp2_stream *stream, const nghttp2_nv *nv,
int flag) {
if (stream->http_flags & flag) {
return 0;
}
if (lws(nv->value, nv->valuelen)) {
return 0;
}
stream->http_flags |= flag;
return 1;
}
static int expect_response_body(nghttp2_stream *stream) {
return (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_HEAD) == 0 &&
stream->status_code / 100 != 1 && stream->status_code != 304 &&
stream->status_code != 204;
}
static int http_request_on_header(nghttp2_stream *stream, nghttp2_nv *nv,
int trailer) {
int token;
if (nv->name[0] == ':') {
if (trailer ||
(stream->http_flags & NGHTTP2_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED)) {
return -1;
}
}
token = lookup_token(nv->name, nv->namelen);
switch (token) {
case NGHTTP2_TOKEN__AUTHORITY:
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_AUTHORITY)) {
return -1;
}
break;
case NGHTTP2_TOKEN__METHOD:
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_METHOD)) {
return -1;
}
if (streq("HEAD", nv->value, nv->valuelen)) {
stream->http_flags |= NGHTTP2_HTTP_FLAG_METH_HEAD;
} else if (streq("CONNECT", nv->value, nv->valuelen)) {
if (stream->stream_id % 2 == 0) {
/* we won't allow CONNECT for push */
return -1;
}
stream->http_flags |= NGHTTP2_HTTP_FLAG_METH_CONNECT;
if (stream->http_flags &
(NGHTTP2_HTTP_FLAG_PATH | NGHTTP2_HTTP_FLAG_SCHEME)) {
return -1;
}
}
break;
case NGHTTP2_TOKEN__PATH:
if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) {
return -1;
}
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_PATH)) {
return -1;
}
break;
case NGHTTP2_TOKEN__SCHEME:
if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) {
return -1;
}
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_SCHEME)) {
return -1;
}
break;
case NGHTTP2_TOKEN_HOST:
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_HOST)) {
return -1;
}
break;
case NGHTTP2_TOKEN_CONTENT_LENGTH: {
if (stream->content_length != -1) {
return -1;
}
stream->content_length = parse_uint(nv->value, nv->valuelen);
if (stream->content_length == -1) {
return -1;
}
break;
}
/* disallowed header fields */
case NGHTTP2_TOKEN_CONNECTION:
case NGHTTP2_TOKEN_KEEP_ALIVE:
case NGHTTP2_TOKEN_PROXY_CONNECTION:
case NGHTTP2_TOKEN_TRANSFER_ENCODING:
case NGHTTP2_TOKEN_UPGRADE:
return -1;
case NGHTTP2_TOKEN_TE:
if (!strieq("trailers", nv->value, nv->valuelen)) {
return -1;
}
break;
default:
if (nv->name[0] == ':') {
return -1;
}
}
if (nv->name[0] != ':') {
stream->http_flags |= NGHTTP2_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED;
}
return 0;
}
static int http_response_on_header(nghttp2_stream *stream, nghttp2_nv *nv,
int trailer) {
int token;
if (nv->name[0] == ':') {
if (trailer ||
(stream->http_flags & NGHTTP2_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED)) {
return -1;
}
}
token = lookup_token(nv->name, nv->namelen);
switch (token) {
case NGHTTP2_TOKEN__STATUS: {
if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_STATUS)) {
return -1;
}
if (nv->valuelen != 3) {
return -1;
}
stream->status_code = parse_uint(nv->value, nv->valuelen);
if (stream->status_code == -1) {
return -1;
}
break;
}
case NGHTTP2_TOKEN_CONTENT_LENGTH: {
if (stream->content_length != -1) {
return -1;
}
stream->content_length = parse_uint(nv->value, nv->valuelen);
if (stream->content_length == -1) {
return -1;
}
break;
}
/* disallowed header fields */
case NGHTTP2_TOKEN_CONNECTION:
case NGHTTP2_TOKEN_KEEP_ALIVE:
case NGHTTP2_TOKEN_PROXY_CONNECTION:
case NGHTTP2_TOKEN_TRANSFER_ENCODING:
case NGHTTP2_TOKEN_UPGRADE:
return -1;
case NGHTTP2_TOKEN_TE:
if (!strieq("trailers", nv->value, nv->valuelen)) {
return -1;
}
break;
default:
if (nv->name[0] == ':') {
return -1;
}
}
if (nv->name[0] != ':') {
stream->http_flags |= NGHTTP2_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED;
}
return 0;
}
int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream,
nghttp2_frame *frame, nghttp2_nv *nv, int trailer) {
if (!nghttp2_check_header_name(nv->name, nv->namelen) ||
!nghttp2_check_header_value(nv->value, nv->valuelen)) {
return -1;
}
if (session->server || frame->hd.type == NGHTTP2_PUSH_PROMISE) {
return http_request_on_header(stream, nv, trailer);
}
return http_response_on_header(stream, nv, trailer);
}
int nghttp2_http_on_request_headers(nghttp2_stream *stream,
nghttp2_frame *frame) {
if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) {
if ((stream->http_flags & NGHTTP2_HTTP_FLAG_AUTHORITY) == 0) {
return -1;
}
stream->content_length = -1;
} else if ((stream->http_flags & NGHTTP2_HTTP_FLAG_REQ_HEADERS) !=
NGHTTP2_HTTP_FLAG_REQ_HEADERS ||
(stream->http_flags &
(NGHTTP2_HTTP_FLAG_AUTHORITY | NGHTTP2_HTTP_FLAG_HOST)) == 0) {
return -1;
}
if (frame->hd.type == NGHTTP2_PUSH_PROMISE) {
/* we are going to reuse data fields for upcoming response. Clear
them now, except for method flags. */
stream->http_flags &= NGHTTP2_HTTP_FLAG_METH_ALL;
stream->content_length = -1;
}
return 0;
}
int nghttp2_http_on_response_headers(nghttp2_stream *stream) {
if ((stream->http_flags & NGHTTP2_HTTP_FLAG_STATUS) == 0) {
return -1;
}
if (stream->status_code / 100 == 1) {
/* non-final response */
stream->http_flags = (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_ALL) |
NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE;
stream->content_length = -1;
stream->status_code = -1;
return 0;
}
stream->http_flags &= ~NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE;
if (!expect_response_body(stream)) {
stream->content_length = 0;
} else if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) {
stream->content_length = -1;
}
return 0;
}
int nghttp2_http_on_trailer_headers(nghttp2_stream *stream _U_,
nghttp2_frame *frame) {
if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) {
return -1;
}
return 0;
}
int nghttp2_http_on_remote_end_stream(nghttp2_stream *stream) {
if (stream->http_flags & NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE) {
return -1;
}
if (stream->content_length != -1 &&
stream->content_length != stream->recv_content_length) {
return -1;
}
return 0;
}
int nghttp2_http_on_data_chunk(nghttp2_stream *stream, size_t n) {
stream->recv_content_length += n;
if ((stream->http_flags & NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE) ||
(stream->content_length != -1 &&
stream->recv_content_length > stream->content_length)) {
return -1;
}
return 0;
}
void nghttp2_http_record_request_method(nghttp2_stream *stream,
nghttp2_frame *frame) {
const nghttp2_nv *nva;
size_t nvlen;
size_t i;
switch (frame->hd.type) {
case NGHTTP2_HEADERS:
nva = frame->headers.nva;
nvlen = frame->headers.nvlen;
break;
case NGHTTP2_PUSH_PROMISE:
nva = frame->push_promise.nva;
nvlen = frame->push_promise.nvlen;
break;
default:
return;
}
/* TODO we should do this in stricter manner. */
for (i = 0; i < nvlen; ++i) {
const nghttp2_nv *nv = &nva[i];
if (lookup_token(nv->name, nv->namelen) == NGHTTP2_TOKEN__METHOD) {
if (streq("CONNECT", nv->value, nv->valuelen)) {
stream->http_flags |= NGHTTP2_HTTP_FLAG_METH_CONNECT;
return;
}
if (streq("HEAD", nv->value, nv->valuelen)) {
stream->http_flags |= NGHTTP2_HTTP_FLAG_METH_HEAD;
return;
}
}
}
}

88
lib/nghttp2_http.h Normal file
View File

@ -0,0 +1,88 @@
/*
* nghttp2 - HTTP/2 C Library
*
* Copyright (c) 2015 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.
*/
#ifndef NGHTTP2_HTTP_H
#define NGHTTP2_HTTP_H
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */
#include <nghttp2/nghttp2.h>
#include "nghttp2_session.h"
#include "nghttp2_stream.h"
/*
* This function is called when HTTP header field |nv| in |frame| is
* received for |stream|. This function will validate |nv| against
* the current state of stream. This function returns 0 if it
* succeeds, or -1.
*/
int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream,
nghttp2_frame *frame, nghttp2_nv *nv, int trailer);
/*
* This function is called when request header is received. This
* function performs validation and returns 0 if it succeeds, or -1.
*/
int nghttp2_http_on_request_headers(nghttp2_stream *stream,
nghttp2_frame *frame);
/*
* This function is called when response header is received. This
* function performs validation and returns 0 if it succeeds, or -1.
*/
int nghttp2_http_on_response_headers(nghttp2_stream *stream);
/*
* This function is called trailer header (for both request and
* response) is received. This function performs validation and
* returns 0 if it succeeds, or -1.
*/
int nghttp2_http_on_trailer_headers(nghttp2_stream *stream,
nghttp2_frame *frame);
/*
* This function is called when END_STREAM flag is seen in incoming
* frame. This function performs validation and returns 0 if it
* succeeds, or -1.
*/
int nghttp2_http_on_remote_end_stream(nghttp2_stream *stream);
/*
* This function is called when chunk of data is received. This
* function performs validation and returns 0 if it succeeds, or -1.
*/
int nghttp2_http_on_data_chunk(nghttp2_stream *stream, size_t n);
/*
* This function inspects header field in |frame| and records its
* method in stream->http_flags. If frame->hd.type is neither
* NGHTTP2_HEADERS nor NGHTTP2_PUSH_PROMISE, this function does
* nothing.
*/
void nghttp2_http_record_request_method(nghttp2_stream *stream,
nghttp2_frame *frame);
#endif /* NGHTTP2_HTTP_H */

View File

@ -33,6 +33,7 @@
#include "nghttp2_net.h"
#include "nghttp2_priority_spec.h"
#include "nghttp2_option.h"
#include "nghttp2_http.h"
/*
* Returns non-zero if the number of outgoing opened streams is larger
@ -76,6 +77,27 @@ static int is_non_fatal(int lib_error) {
int nghttp2_is_fatal(int lib_error) { return lib_error < NGHTTP2_ERR_FATAL; }
static int session_enforce_http_semantics(nghttp2_session *session) {
return (session->opt_flags & NGHTTP2_OPTMASK_NO_HTTP_SEMANTICS) == 0;
}
/*
* Returns nonzero if |frame| is trailer headers.
*/
static int session_trailer_headers(nghttp2_session *session,
nghttp2_stream *stream,
nghttp2_frame *frame) {
if (!stream || frame->hd.type != NGHTTP2_HEADERS) {
return 0;
}
if (session->server) {
return frame->headers.cat == NGHTTP2_HCAT_HEADERS;
}
return frame->headers.cat == NGHTTP2_HCAT_HEADERS &&
(stream->http_flags & NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE) == 0;
}
/* Returns nonzero if the |stream| is in reserved(remote) state */
static int state_reserved_remote(nghttp2_session *session,
nghttp2_stream *stream) {
@ -1738,6 +1760,10 @@ static int session_prep_frame(nghttp2_session *session,
if (rv != 0) {
return rv;
}
if (session_enforce_http_semantics(session)) {
nghttp2_http_record_request_method(stream, frame);
}
} else {
nghttp2_stream *stream;
@ -3082,8 +3108,19 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame,
int inflate_flags;
nghttp2_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(fprintf(stderr, "recv: decoding header block %zu bytes\n", inlen));
for (;;) {
@ -3095,8 +3132,6 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame,
}
if (proclen < 0) {
if (session->iframe.state == NGHTTP2_IB_READ_HEADER_BLOCK) {
stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
if (stream && stream->state != NGHTTP2_STREAM_CLOSING) {
/* Adding RST_STREAM here is very important. It prevents
from invoking subsequent callbacks for the same stream
@ -3124,11 +3159,29 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame,
DEBUGF(fprintf(stderr, "recv: proclen=%zd\n", proclen));
if (call_header_cb && (inflate_flags & NGHTTP2_HD_INFLATE_EMIT)) {
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 (subject_stream && session_enforce_http_semantics(session)) {
rv = nghttp2_http_on_header(session, subject_stream, frame, &nv,
trailer);
if (rv != 0) {
DEBUGF(fprintf(
stderr, "recv: HTTP error: type=%d, id=%d, header %.*s: %.*s\n",
frame->hd.type, subject_stream->stream_id, (int)nv.namelen,
nv.name, (int)nv.valuelen, nv.value));
rv = nghttp2_session_add_rst_stream(
session, subject_stream->stream_id, NGHTTP2_PROTOCOL_ERROR);
if (nghttp2_is_fatal(rv)) {
return rv;
}
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
}
}
if (call_header_cb) {
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) {
@ -3224,7 +3277,8 @@ int nghttp2_session_end_headers_received(nghttp2_session *session,
}
static int session_after_header_block_received(nghttp2_session *session) {
int rv;
int rv = 0;
int call_cb = 1;
nghttp2_frame *frame = &session->iframe.frame;
nghttp2_stream *stream;
@ -3235,9 +3289,64 @@ static int session_after_header_block_received(nghttp2_session *session) {
return 0;
}
rv = session_call_on_frame_received(session, frame);
if (nghttp2_is_fatal(rv)) {
return rv;
if (session_enforce_http_semantics(session)) {
if (frame->hd.type == NGHTTP2_PUSH_PROMISE) {
nghttp2_stream *subject_stream;
subject_stream = nghttp2_session_get_stream(
session, frame->push_promise.promised_stream_id);
if (subject_stream) {
rv = nghttp2_http_on_request_headers(subject_stream, frame);
}
} else {
assert(frame->hd.type == NGHTTP2_HEADERS);
switch (frame->headers.cat) {
case NGHTTP2_HCAT_REQUEST:
rv = nghttp2_http_on_request_headers(stream, frame);
break;
case NGHTTP2_HCAT_RESPONSE:
case NGHTTP2_HCAT_PUSH_RESPONSE:
rv = nghttp2_http_on_response_headers(stream);
break;
case NGHTTP2_HCAT_HEADERS:
if (stream->http_flags & NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE) {
assert(!session->server);
rv = nghttp2_http_on_response_headers(stream);
} else {
rv = nghttp2_http_on_trailer_headers(stream, frame);
}
break;
default:
assert(0);
}
if (rv == 0 && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) {
rv = nghttp2_http_on_remote_end_stream(stream);
}
}
if (rv != 0) {
int32_t stream_id;
if (frame->hd.type == NGHTTP2_PUSH_PROMISE) {
stream_id = frame->push_promise.promised_stream_id;
} else {
stream_id = frame->hd.stream_id;
}
call_cb = 0;
rv = nghttp2_session_add_rst_stream(session, stream_id,
NGHTTP2_PROTOCOL_ERROR);
if (nghttp2_is_fatal(rv)) {
return rv;
}
}
}
if (call_cb) {
rv = session_call_on_frame_received(session, frame);
if (nghttp2_is_fatal(rv)) {
return rv;
}
}
if (frame->hd.type != NGHTTP2_HEADERS) {
@ -4234,6 +4343,7 @@ static int session_process_window_update_frame(nghttp2_session *session) {
int nghttp2_session_on_data_received(nghttp2_session *session,
nghttp2_frame *frame) {
int rv = 0;
int call_cb = 1;
nghttp2_stream *stream;
/* We don't call on_frame_recv_callback if stream has been closed
@ -4246,9 +4356,23 @@ int nghttp2_session_on_data_received(nghttp2_session *session,
return 0;
}
rv = session_call_on_frame_received(session, frame);
if (nghttp2_is_fatal(rv)) {
return rv;
if (session_enforce_http_semantics(session) &&
(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) {
if (nghttp2_http_on_remote_end_stream(stream) != 0) {
call_cb = 0;
rv = nghttp2_session_add_rst_stream(session, stream->stream_id,
NGHTTP2_PROTOCOL_ERROR);
if (nghttp2_is_fatal(rv)) {
return rv;
}
}
}
if (call_cb) {
rv = session_call_on_frame_received(session, frame);
if (nghttp2_is_fatal(rv)) {
return rv;
}
}
if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
@ -5582,17 +5706,30 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in,
DEBUGF(fprintf(stderr, "recv: data_readlen=%zd\n", data_readlen));
if (stream && data_readlen > 0 &&
session->callbacks.on_data_chunk_recv_callback) {
rv = session->callbacks.on_data_chunk_recv_callback(
session, iframe->frame.hd.flags, iframe->frame.hd.stream_id,
in - readlen, data_readlen, session->user_data);
if (rv == NGHTTP2_ERR_PAUSE) {
return in - first;
if (stream && data_readlen > 0) {
if (session_enforce_http_semantics(session)) {
if (nghttp2_http_on_data_chunk(stream, data_readlen) != 0) {
rv = nghttp2_session_add_rst_stream(
session, iframe->frame.hd.stream_id, NGHTTP2_PROTOCOL_ERROR);
if (nghttp2_is_fatal(rv)) {
return rv;
}
busy = 1;
iframe->state = NGHTTP2_IB_IGN_DATA;
break;
}
}
if (session->callbacks.on_data_chunk_recv_callback) {
rv = session->callbacks.on_data_chunk_recv_callback(
session, iframe->frame.hd.flags, iframe->frame.hd.stream_id,
in - readlen, data_readlen, session->user_data);
if (rv == NGHTTP2_ERR_PAUSE) {
return in - first;
}
if (nghttp2_is_fatal(rv)) {
return NGHTTP2_ERR_CALLBACK_FAILURE;
if (nghttp2_is_fatal(rv)) {
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
}
}
}

View File

@ -47,6 +47,7 @@
typedef enum {
NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE = 1 << 0,
NGHTTP2_OPTMASK_RECV_CLIENT_PREFACE = 1 << 1,
NGHTTP2_OPTMASK_NO_HTTP_SEMANTICS = 1 << 2,
} nghttp2_optmask;
typedef enum {

View File

@ -68,6 +68,11 @@ void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id,
stream->roots = roots;
stream->root_prev = NULL;
stream->root_next = NULL;
stream->http_flags = NGHTTP2_HTTP_FLAG_NONE;
stream->content_length = -1;
stream->recv_content_length = 0;
stream->status_code = -1;
}
void nghttp2_stream_free(nghttp2_stream *stream _U_) {

View File

@ -99,6 +99,33 @@ typedef enum {
} nghttp2_stream_flag;
/* HTTP related flags to enforce HTTP semantics */
typedef enum {
NGHTTP2_HTTP_FLAG_NONE = 0,
/* header field seen so far */
NGHTTP2_HTTP_FLAG_AUTHORITY = 1,
NGHTTP2_HTTP_FLAG_PATH = 1 << 1,
NGHTTP2_HTTP_FLAG_METHOD = 1 << 2,
NGHTTP2_HTTP_FLAG_SCHEME = 1 << 3,
/* host is not pseudo header, but we require either host or
:authority */
NGHTTP2_HTTP_FLAG_HOST = 1 << 4,
NGHTTP2_HTTP_FLAG_STATUS = 1 << 5,
/* required header fields for HTTP request except for CONNECT
method. */
NGHTTP2_HTTP_FLAG_REQ_HEADERS = NGHTTP2_HTTP_FLAG_METHOD |
NGHTTP2_HTTP_FLAG_PATH |
NGHTTP2_HTTP_FLAG_SCHEME,
NGHTTP2_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED = 1 << 6,
/* HTTP method flags */
NGHTTP2_HTTP_FLAG_METH_CONNECT = 1 << 7,
NGHTTP2_HTTP_FLAG_METH_HEAD = 1 << 8,
NGHTTP2_HTTP_FLAG_METH_ALL =
NGHTTP2_HTTP_FLAG_METH_CONNECT | NGHTTP2_HTTP_FLAG_METH_HEAD,
/* set if final response is expected */
NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE = 1 << 9,
} nghttp2_http_flag;
typedef enum {
NGHTTP2_STREAM_DPRI_NONE = 0,
NGHTTP2_STREAM_DPRI_NO_ITEM = 0x01,
@ -184,6 +211,14 @@ struct nghttp2_stream {
uint8_t flags;
/* Bitwise OR of zero or more nghttp2_shut_flag values */
uint8_t shut_flags;
/* Content-Length of request/response body. -1 if unknown. */
int64_t content_length;
/* Received body so far */
int64_t recv_content_length;
/* status code from remote server */
int16_t status_code;
/* Bitwise OR of zero or more nghttp2_http_flag values */
uint16_t http_flags;
};
void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id,

View File

@ -664,6 +664,11 @@ int on_stream_close_callback(nghttp2_session *session, int32_t stream_id,
Downstream::MSG_BAD_HEADER) {
downstream->set_response_state(Downstream::MSG_RESET);
}
if (downstream->get_response_state() == Downstream::MSG_RESET &&
downstream->get_response_rst_stream_error_code() ==
NGHTTP2_NO_ERROR) {
downstream->set_response_rst_stream_error_code(error_code);
}
call_downstream_readcb(http2session, downstream);
// dconn may be deleted
}

View File

@ -259,6 +259,22 @@ int main(int argc _U_, char *argv[] _U_) {
test_nghttp2_session_cancel_reserved_remote) ||
!CU_add_test(pSuite, "session_reset_pending_headers",
test_nghttp2_session_reset_pending_headers) ||
!CU_add_test(pSuite, "http_mandatory_headers",
test_nghttp2_http_mandatory_headers) ||
!CU_add_test(pSuite, "http_content_length",
test_nghttp2_http_content_length) ||
!CU_add_test(pSuite, "http_content_length_mismatch",
test_nghttp2_http_content_length_mismatch) ||
!CU_add_test(pSuite, "http_non_final_response",
test_nghttp2_http_non_final_response) ||
!CU_add_test(pSuite, "http_trailer_headers",
test_nghttp2_http_trailer_headers) ||
!CU_add_test(pSuite, "http_ignore_content_length",
test_nghttp2_http_ignore_content_length) ||
!CU_add_test(pSuite, "http_record_request_method",
test_nghttp2_http_record_request_method) ||
!CU_add_test(pSuite, "http_push_promise",
test_nghttp2_http_push_promise) ||
!CU_add_test(pSuite, "frame_pack_headers",
test_nghttp2_frame_pack_headers) ||
!CU_add_test(pSuite, "frame_pack_headers_frame_too_large",

File diff suppressed because it is too large Load Diff

View File

@ -122,5 +122,13 @@ void test_nghttp2_session_delete_data_item(void);
void test_nghttp2_session_open_idle_stream(void);
void test_nghttp2_session_cancel_reserved_remote(void);
void test_nghttp2_session_reset_pending_headers(void);
void test_nghttp2_http_mandatory_headers(void);
void test_nghttp2_http_content_length(void);
void test_nghttp2_http_content_length_mismatch(void);
void test_nghttp2_http_non_final_response(void);
void test_nghttp2_http_trailer_headers(void);
void test_nghttp2_http_ignore_content_length(void);
void test_nghttp2_http_record_request_method(void);
void test_nghttp2_http_push_promise(void);
#endif /* NGHTTP2_SESSION_TEST_H */

View File

@ -200,6 +200,42 @@ ssize_t inflate_hd(nghttp2_hd_inflater *inflater, nva_out *out,
return processed;
}
int pack_headers(nghttp2_bufs *bufs, nghttp2_hd_deflater *deflater,
int32_t stream_id, int flags, const nghttp2_nv *nva,
size_t nvlen, nghttp2_mem *mem) {
nghttp2_nv *dnva;
nghttp2_frame frame;
int rv;
nghttp2_nv_array_copy(&dnva, nva, nvlen, mem);
nghttp2_frame_headers_init(&frame.headers, flags, stream_id,
NGHTTP2_HCAT_HEADERS, NULL, dnva, nvlen);
rv = nghttp2_frame_pack_headers(bufs, &frame.headers, deflater);
nghttp2_frame_headers_free(&frame.headers, mem);
return rv;
}
int pack_push_promise(nghttp2_bufs *bufs, nghttp2_hd_deflater *deflater,
int32_t stream_id, int flags, int32_t promised_stream_id,
const nghttp2_nv *nva, size_t nvlen, nghttp2_mem *mem) {
nghttp2_nv *dnva;
nghttp2_frame frame;
int rv;
nghttp2_nv_array_copy(&dnva, nva, nvlen, mem);
nghttp2_frame_push_promise_init(&frame.push_promise, flags, stream_id,
promised_stream_id, dnva, nvlen);
rv = nghttp2_frame_pack_push_promise(bufs, &frame.push_promise, deflater);
nghttp2_frame_push_promise_free(&frame.push_promise, mem);
return rv;
}
int frame_pack_bufs_init(nghttp2_bufs *bufs) {
/* 1 for Pad Length */
return nghttp2_bufs_init2(bufs, 4096, 16, NGHTTP2_FRAME_HDLEN + 1,

View File

@ -43,7 +43,7 @@
#define assert_nv_equal(A, B, len) \
do { \
size_t alloclen = sizeof(nghttp2_nv) * len; \
nghttp2_nv *sa = A, *sb = B; \
const nghttp2_nv *sa = A, *sb = B; \
nghttp2_nv *a = malloc(alloclen); \
nghttp2_nv *b = malloc(alloclen); \
ssize_t i_; \
@ -81,6 +81,14 @@ void add_out(nva_out *out, nghttp2_nv *nv);
ssize_t inflate_hd(nghttp2_hd_inflater *inflater, nva_out *out,
nghttp2_bufs *bufs, size_t offset);
int pack_headers(nghttp2_bufs *bufs, nghttp2_hd_deflater *deflater,
int32_t stream_id, int flags, const nghttp2_nv *nva,
size_t nvlen, nghttp2_mem *mem);
int pack_push_promise(nghttp2_bufs *bufs, nghttp2_hd_deflater *deflater,
int32_t stream_id, int flags, int32_t promised_stream_id,
const nghttp2_nv *nva, size_t nvlen, nghttp2_mem *mem);
int frame_pack_bufs_init(nghttp2_bufs *bufs);
void bufs_large_init(nghttp2_bufs *bufs, size_t chunk_size);