From b157d4ebb23a9c056b83fdb81393495bb13a54d4 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Fri, 20 Feb 2015 00:01:15 +0900 Subject: [PATCH] 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. --- genlibtokenlookup.py | 85 ++ integration-tests/nghttpx_http2_test.go | 20 +- lib/Makefile.am | 6 +- lib/nghttp2_http.c | 511 ++++++++++++ lib/nghttp2_http.h | 88 +++ lib/nghttp2_session.c | 183 ++++- lib/nghttp2_session.h | 1 + lib/nghttp2_stream.c | 5 + lib/nghttp2_stream.h | 35 + src/shrpx_http2_session.cc | 5 + tests/main.c | 16 + tests/nghttp2_session_test.c | 993 +++++++++++++++++++++--- tests/nghttp2_session_test.h | 8 + tests/nghttp2_test_helper.c | 36 + tests/nghttp2_test_helper.h | 10 +- 15 files changed, 1868 insertions(+), 134 deletions(-) create mode 100755 genlibtokenlookup.py create mode 100644 lib/nghttp2_http.c create mode 100644 lib/nghttp2_http.h diff --git a/genlibtokenlookup.py b/genlibtokenlookup.py new file mode 100755 index 00000000..c396ec08 --- /dev/null +++ b/genlibtokenlookup.py @@ -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() diff --git a/integration-tests/nghttpx_http2_test.go b/integration-tests/nghttpx_http2_test.go index 29e5cc82..4e967d9e 100644 --- a/integration-tests/nghttpx_http2_test.go +++ b/integration-tests/nghttpx_http2_test.go @@ -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) } } diff --git a/lib/Makefile.am b/lib/Makefile.am index 35a05954..6d2393ef 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -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 \ diff --git a/lib/nghttp2_http.c b/lib/nghttp2_http.c new file mode 100644 index 00000000..069b75fc --- /dev/null +++ b/lib/nghttp2_http.c @@ -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 +#include +#include + +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; + } + } + } +} diff --git a/lib/nghttp2_http.h b/lib/nghttp2_http.h new file mode 100644 index 00000000..3f2aa3e3 --- /dev/null +++ b/lib/nghttp2_http.h @@ -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 +#endif /* HAVE_CONFIG_H */ + +#include +#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 */ diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index f390621d..8f0be930 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -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; + } } } } diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index c9422a5d..721a7a0a 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -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 { diff --git a/lib/nghttp2_stream.c b/lib/nghttp2_stream.c index 505a32c6..cdb67f2f 100644 --- a/lib/nghttp2_stream.c +++ b/lib/nghttp2_stream.c @@ -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_) { diff --git a/lib/nghttp2_stream.h b/lib/nghttp2_stream.h index eede2dc3..6fd6046d 100644 --- a/lib/nghttp2_stream.h +++ b/lib/nghttp2_stream.h @@ -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, diff --git a/src/shrpx_http2_session.cc b/src/shrpx_http2_session.cc index 0de9e785..f0af34b2 100644 --- a/src/shrpx_http2_session.cc +++ b/src/shrpx_http2_session.cc @@ -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 } diff --git a/tests/main.c b/tests/main.c index ec7f7eea..9d448fa2 100644 --- a/tests/main.c +++ b/tests/main.c @@ -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", diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index be6016d6..f9f9eec5 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -79,6 +79,15 @@ typedef struct { int begin_frame_cb_called; } my_user_data; +static const nghttp2_nv reqnv[] = { + MAKE_NV(":method", "GET"), MAKE_NV(":path", "/"), + MAKE_NV(":scheme", "https"), MAKE_NV(":authority", "localhost"), +}; + +static const nghttp2_nv resnv[] = { + MAKE_NV(":status", "200"), +}; + static void scripted_data_feed_init2(scripted_data_feed *df, nghttp2_bufs *bufs) { nghttp2_buf_chain *ci; @@ -369,7 +378,6 @@ void test_nghttp2_session_recv(void) { nghttp2_session_callbacks callbacks; scripted_data_feed df; my_user_data user_data; - const nghttp2_nv nv[] = {MAKE_NV("url", "/")}; nghttp2_bufs bufs; ssize_t framelen; nghttp2_frame frame; @@ -395,8 +403,8 @@ void test_nghttp2_session_recv(void) { nghttp2_session_server_new(&session, &callbacks, &user_data); nghttp2_hd_deflate_init(&deflater, mem); - nvlen = ARRLEN(nv); - nghttp2_nv_array_copy(&nva, nv, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 1, NGHTTP2_HCAT_HEADERS, NULL, nva, nvlen); rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); @@ -425,25 +433,6 @@ void test_nghttp2_session_recv(void) { nghttp2_bufs_reset(&bufs); - /* Received HEADERS without header block, which is valid */ - nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 5, - NGHTTP2_HCAT_HEADERS, NULL, NULL, 0); - rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); - - CU_ASSERT(0 == rv); - - nghttp2_frame_headers_free(&frame.headers, mem); - - scripted_data_feed_init2(&df, &bufs); - user_data.frame_recv_cb_called = 0; - user_data.begin_frame_cb_called = 0; - - CU_ASSERT(0 == nghttp2_session_recv(session)); - CU_ASSERT(1 == user_data.frame_recv_cb_called); - CU_ASSERT(1 == user_data.begin_frame_cb_called); - - nghttp2_bufs_reset(&bufs); - /* Receive PRIORITY */ nghttp2_frame_priority_init(&frame.priority, 5, &pri_spec_default); @@ -515,6 +504,8 @@ void test_nghttp2_session_recv_invalid_stream_id(void) { nghttp2_hd_deflater deflater; int rv; nghttp2_mem *mem; + nghttp2_nv *nva; + size_t nvlen; mem = nghttp2_mem_default(); frame_pack_bufs_init(&bufs); @@ -528,8 +519,10 @@ void test_nghttp2_session_recv_invalid_stream_id(void) { nghttp2_session_server_new(&session, &callbacks, &user_data); nghttp2_hd_deflate_init(&deflater, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 2, - NGHTTP2_HCAT_HEADERS, NULL, NULL, 0); + NGHTTP2_HCAT_HEADERS, NULL, nva, nvlen); rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); CU_ASSERT(0 == rv); @@ -551,7 +544,6 @@ void test_nghttp2_session_recv_invalid_frame(void) { nghttp2_session_callbacks callbacks; scripted_data_feed df; my_user_data user_data; - const nghttp2_nv nv[] = {MAKE_NV("url", "/")}; nghttp2_bufs bufs; nghttp2_frame frame; nghttp2_nv *nva; @@ -572,8 +564,8 @@ void test_nghttp2_session_recv_invalid_frame(void) { user_data.frame_send_cb_called = 0; nghttp2_session_server_new(&session, &callbacks, &user_data); nghttp2_hd_deflate_init(&deflater, mem); - nvlen = ARRLEN(nv); - nghttp2_nv_array_copy(&nva, nv, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 1, NGHTTP2_HCAT_HEADERS, NULL, nva, nvlen); rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); @@ -587,13 +579,14 @@ void test_nghttp2_session_recv_invalid_frame(void) { CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(0 == user_data.frame_send_cb_called); - /* Receive exactly same bytes of HEADERS is treated as subsequent - HEADERS (e.g., trailers */ + /* Receive exactly same bytes of HEADERS is treated as error, because it has + * pseudo headers and without END_STREAM flag set */ scripted_data_feed_init2(&df, &bufs); CU_ASSERT(0 == nghttp2_session_recv(session)); CU_ASSERT(0 == nghttp2_session_send(session)); - CU_ASSERT(0 == user_data.frame_send_cb_called); + CU_ASSERT(1 == user_data.frame_send_cb_called); + CU_ASSERT(NGHTTP2_RST_STREAM == user_data.sent_frame_type); nghttp2_bufs_free(&bufs); nghttp2_frame_headers_free(&frame.headers, mem); @@ -743,7 +736,6 @@ void test_nghttp2_session_recv_data(void) { void test_nghttp2_session_recv_continuation(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv1[] = {MAKE_NV("method", "GET"), MAKE_NV("path", "/")}; nghttp2_nv *nva; size_t nvlen; nghttp2_frame frame; @@ -771,8 +763,8 @@ void test_nghttp2_session_recv_continuation(void) { nghttp2_hd_deflate_init(&deflater, mem); /* Make 1 HEADERS and insert CONTINUATION header */ - nvlen = ARRLEN(nv1); - nghttp2_nv_array_copy(&nva, nv1, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_NONE, 1, NGHTTP2_HCAT_HEADERS, NULL, nva, nvlen); rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); @@ -822,7 +814,7 @@ void test_nghttp2_session_recv_continuation(void) { rv = nghttp2_session_mem_recv(session, data, datalen); CU_ASSERT((ssize_t)datalen == rv); - CU_ASSERT(2 == ud.header_cb_called); + CU_ASSERT(4 == ud.header_cb_called); CU_ASSERT(3 == ud.begin_frame_cb_called); nghttp2_hd_deflate_free(&deflater); @@ -834,8 +826,8 @@ void test_nghttp2_session_recv_continuation(void) { nghttp2_hd_deflate_init(&deflater, mem); /* HEADERS without END_HEADERS flag */ - nvlen = ARRLEN(nv1); - nghttp2_nv_array_copy(&nva, nv1, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_NONE, 1, NGHTTP2_HCAT_HEADERS, NULL, nva, nvlen); nghttp2_bufs_reset(&bufs); @@ -883,7 +875,6 @@ void test_nghttp2_session_recv_continuation(void) { void test_nghttp2_session_recv_headers_with_priority(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv1[] = {MAKE_NV("method", "GET"), MAKE_NV("path", "/")}; nghttp2_nv *nva; size_t nvlen; nghttp2_frame frame; @@ -910,8 +901,8 @@ void test_nghttp2_session_recv_headers_with_priority(void) { open_stream(session, 1); /* With NGHTTP2_FLAG_PRIORITY without exclusive flag set */ - nvlen = ARRLEN(nv1); - nghttp2_nv_array_copy(&nva, nv1, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_priority_spec_init(&pri_spec, 1, 99, 0); @@ -945,8 +936,8 @@ void test_nghttp2_session_recv_headers_with_priority(void) { /* With NGHTTP2_FLAG_PRIORITY, but cut last 1 byte to make it invalid. */ - nvlen = ARRLEN(nv1); - nghttp2_nv_array_copy(&nva, nv1, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_priority_spec_init(&pri_spec, 0, 99, 0); @@ -957,12 +948,12 @@ void test_nghttp2_session_recv_headers_with_priority(void) { rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); CU_ASSERT(0 == rv); - CU_ASSERT(NGHTTP2_FRAME_HDLEN + 5 + 2 == nghttp2_bufs_len(&bufs)); + CU_ASSERT(nghttp2_bufs_len(&bufs) > NGHTTP2_FRAME_HDLEN + 5); nghttp2_frame_headers_free(&frame.headers, mem); buf = &bufs.head->buf; - /* Make payload shorter than required length to store priroty + /* Make payload shorter than required length to store priority group */ nghttp2_put_uint32be(buf->pos, (4 << 8) + buf->pos[3]); @@ -992,8 +983,8 @@ void test_nghttp2_session_recv_headers_with_priority(void) { nghttp2_hd_deflate_init(&deflater, mem); - nvlen = ARRLEN(nv1); - nghttp2_nv_array_copy(&nva, nv1, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_priority_spec_init(&pri_spec, 1, 0, 0); @@ -1037,7 +1028,6 @@ void test_nghttp2_session_recv_headers_with_priority(void) { void test_nghttp2_session_recv_premature_headers(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv1[] = {MAKE_NV("method", "GET"), MAKE_NV("path", "/")}; nghttp2_nv *nva; size_t nvlen; nghttp2_frame frame; @@ -1058,8 +1048,8 @@ void test_nghttp2_session_recv_premature_headers(void) { nghttp2_hd_deflate_init(&deflater, mem); - nvlen = ARRLEN(nv1); - nghttp2_nv_array_copy(&nva, nv1, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 1, NGHTTP2_HCAT_HEADERS, NULL, nva, nvlen); rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); @@ -1395,6 +1385,8 @@ void test_nghttp2_session_continue(void) { callbacks.on_begin_headers_callback = on_begin_headers_callback; nghttp2_session_server_new(&session, &callbacks, &user_data); + /* disable strict HTTP layering checks */ + session->opt_flags |= NGHTTP2_OPTMASK_NO_HTTP_SEMANTICS; nghttp2_hd_deflate_init(&deflater, mem); @@ -1553,8 +1545,6 @@ void test_nghttp2_session_add_frame(void) { nghttp2_session_callbacks callbacks; accumulator acc; my_user_data user_data; - const nghttp2_nv nv[] = {MAKE_NV("method", "GET"), MAKE_NV("scheme", "https"), - MAKE_NV("url", "/"), MAKE_NV("version", "HTTP/1.1")}; nghttp2_outbound_item *item; nghttp2_frame *frame; nghttp2_nv *nva; @@ -1576,8 +1566,8 @@ void test_nghttp2_session_add_frame(void) { frame = &item->frame; - nvlen = ARRLEN(nv); - nghttp2_nv_array_copy(&nva, nv, nvlen, mem); + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); nghttp2_frame_headers_init( &frame->headers, NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_PRIORITY, @@ -3293,7 +3283,6 @@ void test_nghttp2_submit_data_twice(void) { void test_nghttp2_submit_request_with_data(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - nghttp2_nv nva[] = {MAKE_NV(":version", "HTTP/1.1")}; nghttp2_data_provider data_prd; my_user_data ud; nghttp2_outbound_item *item; @@ -3304,10 +3293,11 @@ void test_nghttp2_submit_request_with_data(void) { data_prd.read_callback = fixed_length_data_source_read_callback; ud.data_source_length = 64 * 1024 - 1; CU_ASSERT(0 == nghttp2_session_client_new(&session, &callbacks, &ud)); - CU_ASSERT(1 == nghttp2_submit_request(session, NULL, nva, ARRLEN(nva), + CU_ASSERT(1 == nghttp2_submit_request(session, NULL, reqnv, ARRLEN(reqnv), &data_prd, NULL)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(nvnameeq(":version", &item->frame.headers.nva[0])); + CU_ASSERT(ARRLEN(reqnv) == item->frame.headers.nvlen); + assert_nv_equal(reqnv, item->frame.headers.nva, item->frame.headers.nvlen); CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(0 == ud.data_source_length); @@ -3318,7 +3308,6 @@ void test_nghttp2_submit_request_without_data(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; accumulator acc; - nghttp2_nv nva[] = {MAKE_NV(":version", "HTTP/1.1")}; nghttp2_data_provider data_prd = {{-1}, NULL}; nghttp2_outbound_item *item; my_user_data ud; @@ -3339,10 +3328,11 @@ void test_nghttp2_submit_request_without_data(void) { CU_ASSERT(0 == nghttp2_session_client_new(&session, &callbacks, &ud)); nghttp2_hd_inflate_init(&inflater, mem); - CU_ASSERT(1 == nghttp2_submit_request(session, NULL, nva, ARRLEN(nva), + CU_ASSERT(1 == nghttp2_submit_request(session, NULL, reqnv, ARRLEN(reqnv), &data_prd, NULL)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(nvnameeq(":version", &item->frame.headers.nva[0])); + CU_ASSERT(ARRLEN(reqnv) == item->frame.headers.nvlen); + assert_nv_equal(reqnv, item->frame.headers.nva, item->frame.headers.nvlen); CU_ASSERT(item->frame.hd.flags & NGHTTP2_FLAG_END_STREAM); CU_ASSERT(0 == nghttp2_session_send(session)); @@ -3351,7 +3341,8 @@ void test_nghttp2_submit_request_without_data(void) { nghttp2_bufs_add(&bufs, acc.buf, acc.length); inflate_hd(&inflater, &out, &bufs, NGHTTP2_FRAME_HDLEN); - CU_ASSERT(nvnameeq(":version", &out.nva[0])); + CU_ASSERT(ARRLEN(reqnv) == out.nvlen); + assert_nv_equal(reqnv, out.nva, out.nvlen); nghttp2_frame_headers_free(&frame.headers, mem); nva_out_reset(&out); @@ -3363,7 +3354,6 @@ void test_nghttp2_submit_request_without_data(void) { void test_nghttp2_submit_response_with_data(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - nghttp2_nv nva[] = {MAKE_NV(":version", "HTTP/1.1")}; nghttp2_data_provider data_prd; my_user_data ud; nghttp2_outbound_item *item; @@ -3376,10 +3366,11 @@ void test_nghttp2_submit_response_with_data(void) { CU_ASSERT(0 == nghttp2_session_server_new(&session, &callbacks, &ud)); nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_END_STREAM, &pri_spec_default, NGHTTP2_STREAM_OPENING, NULL); - CU_ASSERT(0 == - nghttp2_submit_response(session, 1, nva, ARRLEN(nva), &data_prd)); + CU_ASSERT(0 == nghttp2_submit_response(session, 1, resnv, ARRLEN(resnv), + &data_prd)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(nvnameeq(":version", &item->frame.headers.nva[0])); + CU_ASSERT(ARRLEN(resnv) == item->frame.headers.nvlen); + assert_nv_equal(resnv, item->frame.headers.nva, item->frame.headers.nvlen); CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(0 == ud.data_source_length); @@ -3390,7 +3381,6 @@ void test_nghttp2_submit_response_without_data(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; accumulator acc; - nghttp2_nv nva[] = {MAKE_NV(":version", "HTTP/1.1")}; nghttp2_data_provider data_prd = {{-1}, NULL}; nghttp2_outbound_item *item; my_user_data ud; @@ -3413,10 +3403,11 @@ void test_nghttp2_submit_response_without_data(void) { nghttp2_hd_inflate_init(&inflater, mem); nghttp2_session_open_stream(session, 1, NGHTTP2_FLAG_END_STREAM, &pri_spec_default, NGHTTP2_STREAM_OPENING, NULL); - CU_ASSERT(0 == - nghttp2_submit_response(session, 1, nva, ARRLEN(nva), &data_prd)); + CU_ASSERT(0 == nghttp2_submit_response(session, 1, resnv, ARRLEN(resnv), + &data_prd)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(nvnameeq(":version", &item->frame.headers.nva[0])); + CU_ASSERT(ARRLEN(resnv) == item->frame.headers.nvlen); + assert_nv_equal(resnv, item->frame.headers.nva, item->frame.headers.nvlen); CU_ASSERT(item->frame.hd.flags & NGHTTP2_FLAG_END_STREAM); CU_ASSERT(0 == nghttp2_session_send(session)); @@ -3425,7 +3416,8 @@ void test_nghttp2_submit_response_without_data(void) { nghttp2_bufs_add(&bufs, acc.buf, acc.length); inflate_hd(&inflater, &out, &bufs, NGHTTP2_FRAME_HDLEN); - CU_ASSERT(nvnameeq(":version", &out.nva[0])); + CU_ASSERT(ARRLEN(resnv) == out.nvlen); + assert_nv_equal(resnv, out.nva, out.nvlen); nva_out_reset(&out); nghttp2_bufs_free(&bufs); @@ -3437,15 +3429,15 @@ void test_nghttp2_submit_response_without_data(void) { void test_nghttp2_submit_headers_start_stream(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv[] = {MAKE_NV(":version", "HTTP/1.1")}; nghttp2_outbound_item *item; memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); CU_ASSERT(0 == nghttp2_session_client_new(&session, &callbacks, NULL)); CU_ASSERT(1 == nghttp2_submit_headers(session, NGHTTP2_FLAG_END_STREAM, -1, - NULL, nv, ARRLEN(nv), NULL)); + NULL, reqnv, ARRLEN(reqnv), NULL)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(nvnameeq(":version", &item->frame.headers.nva[0])); + CU_ASSERT(ARRLEN(reqnv) == item->frame.headers.nvlen); + assert_nv_equal(reqnv, item->frame.headers.nva, item->frame.headers.nvlen); CU_ASSERT((NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM) == item->frame.hd.flags); CU_ASSERT(0 == (item->frame.hd.flags & NGHTTP2_FLAG_PRIORITY)); @@ -3456,7 +3448,6 @@ void test_nghttp2_submit_headers_start_stream(void) { void test_nghttp2_submit_headers_reply(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv[] = {MAKE_NV(":version", "HTTP/1.1")}; my_user_data ud; nghttp2_outbound_item *item; nghttp2_stream *stream; @@ -3467,9 +3458,10 @@ void test_nghttp2_submit_headers_reply(void) { CU_ASSERT(0 == nghttp2_session_server_new(&session, &callbacks, &ud)); CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_END_STREAM, 1, - NULL, nv, ARRLEN(nv), NULL)); + NULL, resnv, ARRLEN(resnv), NULL)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(nvnameeq(":version", &item->frame.headers.nva[0])); + CU_ASSERT(ARRLEN(resnv) == item->frame.headers.nvlen); + assert_nv_equal(resnv, item->frame.headers.nva, item->frame.headers.nvlen); CU_ASSERT((NGHTTP2_FLAG_END_STREAM | NGHTTP2_FLAG_END_HEADERS) == item->frame.hd.flags); @@ -3485,7 +3477,7 @@ void test_nghttp2_submit_headers_reply(void) { NGHTTP2_STREAM_OPENING, NULL); CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_END_STREAM, 1, - NULL, nv, ARRLEN(nv), NULL)); + NULL, resnv, ARRLEN(resnv), NULL)); CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(1 == ud.frame_send_cb_called); CU_ASSERT(NGHTTP2_HEADERS == ud.sent_frame_type); @@ -3497,7 +3489,6 @@ void test_nghttp2_submit_headers_reply(void) { void test_nghttp2_submit_headers_push_reply(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv[] = {MAKE_NV(":version", "HTTP/1.1")}; my_user_data ud; nghttp2_stream *stream; int foo; @@ -3510,8 +3501,8 @@ void test_nghttp2_submit_headers_push_reply(void) { stream = nghttp2_session_open_stream(session, 2, NGHTTP2_STREAM_FLAG_NONE, &pri_spec_default, NGHTTP2_STREAM_RESERVED, NULL); - CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_NONE, 2, NULL, nv, - ARRLEN(nv), &foo)); + CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_NONE, 2, NULL, + resnv, ARRLEN(resnv), &foo)); ud.frame_send_cb_called = 0; ud.sent_frame_type = 0; @@ -3529,8 +3520,8 @@ void test_nghttp2_submit_headers_push_reply(void) { stream = nghttp2_session_open_stream(session, 2, NGHTTP2_STREAM_FLAG_NONE, &pri_spec_default, NGHTTP2_STREAM_RESERVED, NULL); - CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_NONE, 2, NULL, nv, - ARRLEN(nv), NULL)); + CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_NONE, 2, NULL, + reqnv, ARRLEN(reqnv), NULL)); ud.frame_send_cb_called = 0; ud.sent_frame_type = 0; @@ -3543,7 +3534,6 @@ void test_nghttp2_submit_headers_push_reply(void) { void test_nghttp2_submit_headers(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv[] = {MAKE_NV(":version", "HTTP/1.1")}; my_user_data ud; nghttp2_outbound_item *item; nghttp2_stream *stream; @@ -3568,9 +3558,10 @@ void test_nghttp2_submit_headers(void) { nghttp2_hd_inflate_init(&inflater, mem); CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_END_STREAM, 1, - NULL, nv, ARRLEN(nv), NULL)); + NULL, reqnv, ARRLEN(reqnv), NULL)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(nvnameeq(":version", &item->frame.headers.nva[0])); + CU_ASSERT(ARRLEN(reqnv) == item->frame.headers.nvlen); + assert_nv_equal(reqnv, item->frame.headers.nva, item->frame.headers.nvlen); CU_ASSERT((NGHTTP2_FLAG_END_STREAM | NGHTTP2_FLAG_END_HEADERS) == item->frame.hd.flags); @@ -3586,7 +3577,7 @@ void test_nghttp2_submit_headers(void) { NGHTTP2_STREAM_OPENING, NULL); CU_ASSERT(0 == nghttp2_submit_headers(session, NGHTTP2_FLAG_END_STREAM, 1, - NULL, nv, ARRLEN(nv), NULL)); + NULL, reqnv, ARRLEN(reqnv), NULL)); CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(1 == ud.frame_send_cb_called); CU_ASSERT(NGHTTP2_HEADERS == ud.sent_frame_type); @@ -3597,7 +3588,8 @@ void test_nghttp2_submit_headers(void) { nghttp2_bufs_add(&bufs, acc.buf, acc.length); inflate_hd(&inflater, &out, &bufs, NGHTTP2_FRAME_HDLEN); - CU_ASSERT(nvnameeq(":version", &out.nva[0])); + CU_ASSERT(ARRLEN(reqnv) == out.nvlen); + assert_nv_equal(reqnv, out.nva, out.nvlen); nva_out_reset(&out); nghttp2_bufs_free(&bufs); @@ -3835,7 +3827,6 @@ void test_nghttp2_submit_settings_update_local_window_size(void) { void test_nghttp2_submit_push_promise(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - const nghttp2_nv nv[] = {MAKE_NV(":version", "HTTP/1.1")}; my_user_data ud; nghttp2_stream *stream; @@ -3847,8 +3838,8 @@ void test_nghttp2_submit_push_promise(void) { CU_ASSERT(0 == nghttp2_session_server_new(&session, &callbacks, &ud)); nghttp2_session_open_stream(session, 1, NGHTTP2_STREAM_FLAG_NONE, &pri_spec_default, NGHTTP2_STREAM_OPENING, NULL); - CU_ASSERT(2 == nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 1, nv, - ARRLEN(nv), &ud)); + CU_ASSERT(2 == nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 1, + reqnv, ARRLEN(reqnv), &ud)); ud.frame_send_cb_called = 0; ud.sent_frame_type = 0; @@ -3860,8 +3851,8 @@ void test_nghttp2_submit_push_promise(void) { CU_ASSERT(&ud == nghttp2_session_get_stream_user_data(session, 2)); /* submit PUSH_PROMISE while associated stream is not opened */ - CU_ASSERT(4 == nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 3, nv, - ARRLEN(nv), &ud)); + CU_ASSERT(4 == nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 3, + reqnv, ARRLEN(reqnv), &ud)); ud.frame_not_send_cb_called = 0; @@ -5174,7 +5165,6 @@ void test_nghttp2_session_pack_headers_with_padding(void) { accumulator acc; my_user_data ud; nghttp2_session_callbacks callbacks; - nghttp2_nv nv = MAKE_NV(":path", "/"); memset(&callbacks, 0, sizeof(callbacks)); callbacks.send_callback = accumulator_send_callback; @@ -5190,7 +5180,8 @@ void test_nghttp2_session_pack_headers_with_padding(void) { ud.padlen = 163; - CU_ASSERT(1 == nghttp2_submit_request(session, NULL, &nv, 1, NULL, NULL)); + CU_ASSERT(1 == nghttp2_submit_request(session, NULL, reqnv, ARRLEN(reqnv), + NULL, NULL)); CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(acc.length < NGHTTP2_MAX_PAYLOADLEN); @@ -6517,10 +6508,10 @@ void test_nghttp2_session_on_header_temporal_failure(void) { nghttp2_hd_deflate_init(&deflater, mem); - nghttp2_nv_array_copy(&nva, nv, 1, mem); + nghttp2_nv_array_copy(&nva, reqnv, ARRLEN(reqnv), mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_STREAM, 1, - NGHTTP2_HCAT_REQUEST, NULL, nva, 1); + NGHTTP2_HCAT_REQUEST, NULL, nva, ARRLEN(reqnv)); nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); nghttp2_frame_headers_free(&frame.headers, mem); @@ -6688,7 +6679,6 @@ void test_nghttp2_session_cancel_reserved_remote(void) { nghttp2_session_callbacks callbacks; nghttp2_stream *stream; nghttp2_frame frame; - const nghttp2_nv nv[] = {MAKE_NV(":status", "200")}; nghttp2_nv *nva; ssize_t nvlen; nghttp2_hd_deflater deflater; @@ -6718,8 +6708,8 @@ void test_nghttp2_session_cancel_reserved_remote(void) { CU_ASSERT(0 == nghttp2_session_send(session)); - nvlen = ARRLEN(nv); - nghttp2_nv_array_copy(&nva, nv, nvlen, mem); + nvlen = ARRLEN(resnv); + nghttp2_nv_array_copy(&nva, resnv, nvlen, mem); nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 2, NGHTTP2_HCAT_PUSH_RESPONSE, NULL, nva, nvlen); @@ -6822,3 +6812,814 @@ void test_nghttp2_session_reset_pending_headers(void) { nghttp2_session_del(session); } + +static void check_nghttp2_http_recv_headers_fail( + nghttp2_session *session, nghttp2_hd_deflater *deflater, int32_t stream_id, + int stream_state, const nghttp2_nv *nva, size_t nvlen) { + nghttp2_mem *mem; + ssize_t rv; + nghttp2_outbound_item *item; + nghttp2_bufs bufs; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + if (stream_state != -1) { + nghttp2_session_open_stream(session, stream_id, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, stream_state, NULL); + } + + rv = pack_headers(&bufs, deflater, stream_id, NGHTTP2_FLAG_END_HEADERS, nva, + nvlen, mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_free(&bufs); +} + +void test_nghttp2_http_mandatory_headers(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + /* test case for response */ + const nghttp2_nv nostatus_resnv[] = {MAKE_NV("server", "foo")}; + const nghttp2_nv dupstatus_resnv[] = {MAKE_NV(":status", "200"), + MAKE_NV(":status", "200")}; + const nghttp2_nv badpseudo_resnv[] = {MAKE_NV(":status", "200"), + MAKE_NV(":scheme", "https")}; + const nghttp2_nv latepseudo_resnv[] = {MAKE_NV("server", "foo"), + MAKE_NV(":status", "200")}; + const nghttp2_nv badstatus_resnv[] = {MAKE_NV(":status", "2000")}; + const nghttp2_nv badcl_resnv[] = {MAKE_NV(":status", "200"), + MAKE_NV("content-length", "-1")}; + const nghttp2_nv dupcl_resnv[] = {MAKE_NV(":status", "200"), + MAKE_NV("content-length", "0"), + MAKE_NV("content-length", "0")}; + const nghttp2_nv badhd_resnv[] = {MAKE_NV(":status", "200"), + MAKE_NV("connection", "close")}; + + /* test case for request */ + const nghttp2_nv nopath_reqnv[] = {MAKE_NV(":scheme", "https"), + MAKE_NV(":method", "GET"), + MAKE_NV(":authority", "localhost")}; + const nghttp2_nv earlyconnect_reqnv[] = { + MAKE_NV(":method", "CONNECT"), MAKE_NV(":scheme", "https"), + MAKE_NV(":path", "/"), MAKE_NV(":authority", "localhost")}; + const nghttp2_nv lateconnect_reqnv[] = { + MAKE_NV(":scheme", "https"), MAKE_NV(":path", "/"), + MAKE_NV(":method", "CONNECT"), MAKE_NV(":authority", "localhost")}; + const nghttp2_nv duppath_reqnv[] = { + MAKE_NV(":scheme", "https"), MAKE_NV(":method", "GET"), + MAKE_NV(":authority", "localhost"), MAKE_NV(":path", "/"), + MAKE_NV(":path", "/")}; + const nghttp2_nv badcl_reqnv[] = { + MAKE_NV(":scheme", "https"), MAKE_NV(":method", "POST"), + MAKE_NV(":authority", "localhost"), MAKE_NV(":path", "/"), + MAKE_NV("content-length", "-1")}; + const nghttp2_nv dupcl_reqnv[] = { + MAKE_NV(":scheme", "https"), MAKE_NV(":method", "POST"), + MAKE_NV(":authority", "localhost"), MAKE_NV(":path", "/"), + MAKE_NV("content-length", "0"), MAKE_NV("content-length", "0")}; + const nghttp2_nv badhd_reqnv[] = { + MAKE_NV(":scheme", "https"), MAKE_NV(":method", "GET"), + MAKE_NV(":authority", "localhost"), MAKE_NV(":path", "/"), + MAKE_NV("connection", "close")}; + + mem = nghttp2_mem_default(); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_client_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + /* response header lacks :status */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 1, + NGHTTP2_STREAM_OPENING, nostatus_resnv, + ARRLEN(nostatus_resnv)); + + /* response header has 2 :status */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 3, + NGHTTP2_STREAM_OPENING, dupstatus_resnv, + ARRLEN(dupstatus_resnv)); + + /* response header has bad pseudo header :scheme */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 5, + NGHTTP2_STREAM_OPENING, badpseudo_resnv, + ARRLEN(badpseudo_resnv)); + + /* response header has :status after regular header field */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 7, + NGHTTP2_STREAM_OPENING, latepseudo_resnv, + ARRLEN(latepseudo_resnv)); + + /* response header has bad status code */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 9, + NGHTTP2_STREAM_OPENING, badstatus_resnv, + ARRLEN(badstatus_resnv)); + + /* response header has bad content-length */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 11, + NGHTTP2_STREAM_OPENING, badcl_resnv, + ARRLEN(badcl_resnv)); + + /* response header has multiple content-length */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 13, + NGHTTP2_STREAM_OPENING, dupcl_resnv, + ARRLEN(dupcl_resnv)); + + /* response header has disallowed header field */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 15, + NGHTTP2_STREAM_OPENING, badhd_resnv, + ARRLEN(badhd_resnv)); + + nghttp2_hd_deflate_free(&deflater); + + nghttp2_session_del(session); + + /* check server side */ + nghttp2_session_server_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + /* request header has no :path */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 1, -1, nopath_reqnv, + ARRLEN(nopath_reqnv)); + + /* request header has CONNECT method, but followed by :path */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 3, -1, + earlyconnect_reqnv, + ARRLEN(earlyconnect_reqnv)); + + /* request header has CONNECT method following :path */ + check_nghttp2_http_recv_headers_fail( + session, &deflater, 5, -1, lateconnect_reqnv, ARRLEN(lateconnect_reqnv)); + + /* request header has multiple :path */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 7, -1, duppath_reqnv, + ARRLEN(duppath_reqnv)); + + /* request header has bad content-length */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 9, -1, badcl_reqnv, + ARRLEN(badcl_reqnv)); + + /* request header has multiple content-length */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 11, -1, dupcl_reqnv, + ARRLEN(dupcl_reqnv)); + + /* request header has disallowed header field */ + check_nghttp2_http_recv_headers_fail(session, &deflater, 13, -1, badhd_reqnv, + ARRLEN(badhd_reqnv)); + + nghttp2_hd_deflate_free(&deflater); + + nghttp2_session_del(session); +} + +void test_nghttp2_http_content_length(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_bufs bufs; + ssize_t rv; + nghttp2_stream *stream; + const nghttp2_nv cl_resnv[] = {MAKE_NV(":status", "200"), + MAKE_NV("te", "trailers"), + MAKE_NV("content-length", "9000000000")}; + const nghttp2_nv cl_reqnv[] = { + MAKE_NV(":path", "/"), MAKE_NV(":method", "PUT"), + MAKE_NV(":scheme", "https"), MAKE_NV("te", "trailers"), + MAKE_NV("host", "localhost"), MAKE_NV("content-length", "9000000000")}; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_client_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + stream = nghttp2_session_open_stream(session, 1, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, + NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_headers(&bufs, &deflater, 1, NGHTTP2_FLAG_END_HEADERS, cl_resnv, + ARRLEN(cl_resnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + CU_ASSERT(9000000000LL == stream->content_length); + CU_ASSERT(200 == stream->status_code); + + nghttp2_hd_deflate_free(&deflater); + + nghttp2_session_del(session); + + nghttp2_bufs_reset(&bufs); + + /* check server side */ + nghttp2_session_server_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + rv = pack_headers(&bufs, &deflater, 1, NGHTTP2_FLAG_END_HEADERS, cl_reqnv, + ARRLEN(cl_reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + stream = nghttp2_session_get_stream(session, 1); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + CU_ASSERT(9000000000LL == stream->content_length); + + nghttp2_hd_deflate_free(&deflater); + + nghttp2_session_del(session); + + nghttp2_bufs_free(&bufs); +} + +void test_nghttp2_http_content_length_mismatch(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_bufs bufs; + ssize_t rv; + const nghttp2_nv cl_reqnv[] = { + MAKE_NV(":path", "/"), MAKE_NV(":method", "PUT"), + MAKE_NV(":authority", "localhost"), MAKE_NV(":scheme", "https"), + MAKE_NV("content-length", "20")}; + nghttp2_outbound_item *item; + nghttp2_frame_hd hd; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_server_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + /* header says content-length: 20, but HEADERS has END_STREAM flag set */ + rv = pack_headers(&bufs, &deflater, 1, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, + cl_reqnv, ARRLEN(cl_reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + /* header says content-length: 20, but DATA has 0 byte */ + rv = pack_headers(&bufs, &deflater, 3, NGHTTP2_FLAG_END_HEADERS, cl_reqnv, + ARRLEN(cl_reqnv), mem); + CU_ASSERT(0 == rv); + + nghttp2_frame_hd_init(&hd, 0, NGHTTP2_DATA, NGHTTP2_FLAG_END_STREAM, 3); + nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd); + bufs.head->buf.last += NGHTTP2_FRAME_HDLEN; + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + /* header says content-length: 20, but DATA has 21 bytes */ + rv = pack_headers(&bufs, &deflater, 5, NGHTTP2_FLAG_END_HEADERS, cl_reqnv, + ARRLEN(cl_reqnv), mem); + CU_ASSERT(0 == rv); + + nghttp2_frame_hd_init(&hd, 21, NGHTTP2_DATA, NGHTTP2_FLAG_END_STREAM, 5); + nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd); + bufs.head->buf.last += NGHTTP2_FRAME_HDLEN + 21; + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + nghttp2_hd_deflate_free(&deflater); + + nghttp2_session_del(session); + + nghttp2_bufs_free(&bufs); +} + +void test_nghttp2_http_non_final_response(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_bufs bufs; + ssize_t rv; + const nghttp2_nv nonfinal_resnv[] = { + MAKE_NV(":status", "100"), + }; + nghttp2_outbound_item *item; + nghttp2_frame_hd hd; + nghttp2_stream *stream; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_client_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + /* non-final HEADERS with END_STREAM is illegal */ + stream = nghttp2_session_open_stream(session, 1, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, + NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_headers(&bufs, &deflater, 1, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, + nonfinal_resnv, ARRLEN(nonfinal_resnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + /* non-final HEADERS followed by non-empty DATA is illegal */ + stream = nghttp2_session_open_stream(session, 3, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, + NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_headers(&bufs, &deflater, 3, NGHTTP2_FLAG_END_HEADERS, + nonfinal_resnv, ARRLEN(nonfinal_resnv), mem); + CU_ASSERT(0 == rv); + + nghttp2_frame_hd_init(&hd, 10, NGHTTP2_DATA, NGHTTP2_FLAG_END_STREAM, 3); + nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd); + bufs.head->buf.last += NGHTTP2_FRAME_HDLEN + 10; + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + /* non-final HEADERS followed by empty DATA (without END_STREAM) is + ok */ + stream = nghttp2_session_open_stream(session, 5, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, + NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_headers(&bufs, &deflater, 5, NGHTTP2_FLAG_END_HEADERS, + nonfinal_resnv, ARRLEN(nonfinal_resnv), mem); + CU_ASSERT(0 == rv); + + nghttp2_frame_hd_init(&hd, 0, NGHTTP2_DATA, NGHTTP2_FLAG_NONE, 5); + nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd); + bufs.head->buf.last += NGHTTP2_FRAME_HDLEN; + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + nghttp2_bufs_reset(&bufs); + + /* non-final HEADERS followed by empty DATA (with END_STREAM) is + illegal */ + stream = nghttp2_session_open_stream(session, 7, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, + NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_headers(&bufs, &deflater, 7, NGHTTP2_FLAG_END_HEADERS, + nonfinal_resnv, ARRLEN(nonfinal_resnv), mem); + CU_ASSERT(0 == rv); + + nghttp2_frame_hd_init(&hd, 0, NGHTTP2_DATA, NGHTTP2_FLAG_END_STREAM, 7); + nghttp2_frame_pack_frame_hd(bufs.head->buf.last, &hd); + bufs.head->buf.last += NGHTTP2_FRAME_HDLEN; + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + /* non-final HEADERS followed by final HEADERS is OK */ + stream = nghttp2_session_open_stream(session, 9, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, + NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_headers(&bufs, &deflater, 9, NGHTTP2_FLAG_END_HEADERS, + nonfinal_resnv, ARRLEN(nonfinal_resnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + nghttp2_bufs_reset(&bufs); + + rv = pack_headers(&bufs, &deflater, 9, NGHTTP2_FLAG_END_HEADERS, resnv, + ARRLEN(resnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + nghttp2_bufs_reset(&bufs); + + nghttp2_hd_deflate_free(&deflater); + + nghttp2_session_del(session); + + nghttp2_bufs_free(&bufs); +} + +void test_nghttp2_http_trailer_headers(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_bufs bufs; + ssize_t rv; + const nghttp2_nv trailer_reqnv[] = { + MAKE_NV("foo", "bar"), + }; + nghttp2_outbound_item *item; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_server_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + /* good trailer header */ + rv = pack_headers(&bufs, &deflater, 1, NGHTTP2_FLAG_END_HEADERS, reqnv, + ARRLEN(reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + nghttp2_bufs_reset(&bufs); + + rv = pack_headers(&bufs, &deflater, 1, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, + trailer_reqnv, ARRLEN(trailer_reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + nghttp2_bufs_reset(&bufs); + + /* trailer header without END_STREAM is illegal */ + rv = pack_headers(&bufs, &deflater, 3, NGHTTP2_FLAG_END_HEADERS, reqnv, + ARRLEN(reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + nghttp2_bufs_reset(&bufs); + + rv = pack_headers(&bufs, &deflater, 3, NGHTTP2_FLAG_END_HEADERS, + trailer_reqnv, ARRLEN(trailer_reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + /* trailer header including pseudo header field is illegal */ + rv = pack_headers(&bufs, &deflater, 5, NGHTTP2_FLAG_END_HEADERS, reqnv, + ARRLEN(reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + nghttp2_bufs_reset(&bufs); + + rv = pack_headers(&bufs, &deflater, 5, NGHTTP2_FLAG_END_HEADERS, reqnv, + ARRLEN(reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + nghttp2_bufs_reset(&bufs); + + nghttp2_hd_deflate_free(&deflater); + + nghttp2_session_del(session); + + nghttp2_bufs_free(&bufs); +} + +void test_nghttp2_http_ignore_content_length(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_bufs bufs; + ssize_t rv; + const nghttp2_nv cl_resnv[] = {MAKE_NV(":status", "304"), + MAKE_NV("content-length", "20")}; + const nghttp2_nv conn_reqnv[] = {MAKE_NV(":authority", "localhost"), + MAKE_NV(":method", "CONNECT"), + MAKE_NV("content-length", "999999")}; + nghttp2_stream *stream; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_client_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + /* If status 304, content-length must be ignored */ + nghttp2_session_open_stream(session, 1, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_headers(&bufs, &deflater, 1, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, + cl_resnv, ARRLEN(cl_resnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + nghttp2_bufs_reset(&bufs); + + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + + /* If request method is CONNECT, content-length must be ignored */ + nghttp2_session_server_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + rv = pack_headers(&bufs, &deflater, 1, NGHTTP2_FLAG_END_HEADERS, conn_reqnv, + ARRLEN(conn_reqnv), mem); + + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + stream = nghttp2_session_get_stream(session, 1); + + CU_ASSERT(-1 == stream->content_length); + CU_ASSERT((stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) > 0); + + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + nghttp2_bufs_free(&bufs); +} + +void test_nghttp2_http_record_request_method(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + const nghttp2_nv conn_reqnv[] = {MAKE_NV(":method", "CONNECT"), + MAKE_NV(":authority", "localhost")}; + const nghttp2_nv conn_resnv[] = {MAKE_NV(":status", "200"), + MAKE_NV("content-length", "9999")}; + nghttp2_stream *stream; + ssize_t rv; + nghttp2_bufs bufs; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + nghttp2_session_client_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + CU_ASSERT(1 == nghttp2_submit_request(session, NULL, conn_reqnv, + ARRLEN(conn_reqnv), NULL, NULL)); + + CU_ASSERT(0 == nghttp2_session_send(session)); + + stream = nghttp2_session_get_stream(session, 1); + + CU_ASSERT(NGHTTP2_HTTP_FLAG_METH_CONNECT == stream->http_flags); + + rv = pack_headers(&bufs, &deflater, 1, NGHTTP2_FLAG_END_HEADERS, conn_resnv, + ARRLEN(conn_resnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT((NGHTTP2_HTTP_FLAG_METH_CONNECT & stream->http_flags) > 0); + CU_ASSERT(-1 == stream->content_length); + + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + nghttp2_bufs_free(&bufs); +} + +void test_nghttp2_http_push_promise(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_bufs bufs; + ssize_t rv; + nghttp2_stream *stream; + const nghttp2_nv bad_reqnv[] = {MAKE_NV(":method", "GET")}; + nghttp2_outbound_item *item; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + + /* good PUSH_PROMISE case */ + nghttp2_session_client_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + nghttp2_session_open_stream(session, 1, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, NGHTTP2_STREAM_OPENING, NULL); + + rv = pack_push_promise(&bufs, &deflater, 1, NGHTTP2_FLAG_END_HEADERS, 2, + reqnv, ARRLEN(reqnv), mem); + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + stream = nghttp2_session_get_stream(session, 2); + CU_ASSERT(NULL != stream); + + nghttp2_bufs_reset(&bufs); + + rv = pack_headers(&bufs, &deflater, 2, NGHTTP2_FLAG_END_HEADERS, resnv, + ARRLEN(resnv), mem); + + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + CU_ASSERT(NULL == nghttp2_session_get_next_ob_item(session)); + + CU_ASSERT(200 == stream->status_code); + + nghttp2_bufs_reset(&bufs); + + /* PUSH_PROMISE lacks mandatory header */ + rv = pack_push_promise(&bufs, &deflater, 1, NGHTTP2_FLAG_END_HEADERS, 4, + bad_reqnv, ARRLEN(bad_reqnv), mem); + + CU_ASSERT(0 == rv); + + rv = nghttp2_session_mem_recv(session, bufs.head->buf.pos, + nghttp2_buf_len(&bufs.head->buf)); + + CU_ASSERT(nghttp2_buf_len(&bufs.head->buf) == rv); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); + CU_ASSERT(4 == item->frame.hd.stream_id); + + nghttp2_bufs_reset(&bufs); + + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + nghttp2_bufs_free(&bufs); +} diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index 3e64c8e7..b0a85e83 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -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 */ diff --git a/tests/nghttp2_test_helper.c b/tests/nghttp2_test_helper.c index ba149015..01872325 100644 --- a/tests/nghttp2_test_helper.c +++ b/tests/nghttp2_test_helper.c @@ -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, diff --git a/tests/nghttp2_test_helper.h b/tests/nghttp2_test_helper.h index 36d44765..1796f937 100644 --- a/tests/nghttp2_test_helper.h +++ b/tests/nghttp2_test_helper.h @@ -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);