Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/aws/http/request_response.h
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,9 @@ int aws_http_message_erase_header(struct aws_http_message *message, size_t index
* header
* - When HTTP/1 message sent on HTTP/2 connection, `aws_http2_message_new_from_http1` will be applied under the hood.
* - When HTTP/2 message sent on HTTP/1 connection, no change will be made.
* - `request` is never modified. Protocol conversion builds a new message; otherwise only a refcount is taken.
* - For HTTP/2, a missing `:method` or `:path` fails this call with AWS_ERROR_HTTP_INVALID_METHOD / _INVALID_PATH.
* The remaining pseudo-header rules (RFC-9113 8.3) are enforced by the peer.
*/
AWS_HTTP_API
struct aws_http_stream *aws_http_connection_make_request(
Expand Down
25 changes: 21 additions & 4 deletions source/h2_connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -2186,9 +2186,6 @@ static struct aws_http_stream *s_connection_make_request(

struct aws_h2_connection *connection = AWS_CONTAINER_OF(client_connection, struct aws_h2_connection, base);

/* #TODO: http/2-ify the request (ex: add ":method" header). Should we mutate a copy or the original? Validate?
* Or just pass pointer to headers struct and let encoder transform it while encoding? */

struct aws_h2_stream *stream = aws_h2_stream_new_request(client_connection, options);
if (!stream) {
CONNECTION_LOGF(
Expand Down Expand Up @@ -2216,8 +2213,28 @@ static struct aws_http_stream *s_connection_make_request(
aws_error_name(aws_last_error()));
goto error;
}
struct aws_byte_cursor method;
AWS_ZERO_STRUCT(method);
struct aws_byte_cursor path;
AWS_ZERO_STRUCT(path);

AWS_H2_STREAM_LOG(DEBUG, stream, "Created HTTP/2 request stream"); /* #TODO: print method & path */
if (aws_http_message_get_request_method(stream->thread_data.outgoing_message, &method)) {
aws_raise_error(AWS_ERROR_HTTP_INVALID_METHOD);
CONNECTION_LOG(ERROR, connection, "Cannot create request stream, the `:method` header is missing.");
goto error;
}
if (aws_http_message_get_request_path(stream->thread_data.outgoing_message, &path)) {
aws_raise_error(AWS_ERROR_HTTP_INVALID_PATH);
CONNECTION_LOG(ERROR, connection, "Cannot create request stream, the `:path` header is missing.");
goto error;
}

AWS_H2_STREAM_LOGF(
DEBUG,
stream,
"Created HTTP/2 request stream, method: " PRInSTR ". path: " PRInSTR "",
AWS_BYTE_CURSOR_PRI(method),
AWS_BYTE_CURSOR_PRI(path));
return &stream->base;

error:
Expand Down
94 changes: 86 additions & 8 deletions source/h2_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,60 @@ static struct aws_h2err s_flush_pseudoheaders(struct aws_h2_decoder *decoder) {
if (has_request_pseudoheaders) {
/* Request header-block. */
current_block->block_type = AWS_HTTP_HEADER_BLOCK_MAIN;
const struct aws_string *method_string = current_block->pseudoheader_values[PSEUDOHEADER_METHOD];
const struct aws_string *scheme_string = current_block->pseudoheader_values[PSEUDOHEADER_SCHEME];
const struct aws_string *authority_string = current_block->pseudoheader_values[PSEUDOHEADER_AUTHORITY];
const struct aws_string *path_string = current_block->pseudoheader_values[PSEUDOHEADER_PATH];

if (!method_string) {
DECODER_LOG(ERROR, decoder, "Request is missing :method.");
goto malformed;
}

if (!aws_strutil_is_http_token(aws_byte_cursor_from_string(method_string))) {
DECODER_LOG(ERROR, decoder, "Request method is invalid.");
DECODER_LOGF(
DEBUG,
decoder,
"Bad method is: '" PRInSTR "'",
AWS_BYTE_CURSOR_PRI(aws_byte_cursor_from_string(method_string)));
goto malformed;
}

if (aws_string_eq_byte_cursor(method_string, &aws_http_method_connect)) {
/* RFC-9113 8.5 The ":scheme" and ":path" pseudo-header fields MUST be omitted for CONNECT method */
if (scheme_string) {
DECODER_LOG(ERROR, decoder, "CONNECT request must not contain ':scheme' header");
goto malformed;
}
if (path_string) {
DECODER_LOG(ERROR, decoder, "CONNECT request must not contain ':path' header");
goto malformed;
}
if (!authority_string) {
DECODER_LOG(ERROR, decoder, "CONNECT request is missing :authority.");
goto malformed;
}
} else {
/* RFC-9113 8.3.1 All HTTP/2 requests MUST include exactly one valid value for the ":method", ":scheme",
* and ":path" pseudo-header fields, unless they are CONNECT requests */
if (!scheme_string) {
DECODER_LOGF(
ERROR,
decoder,
"" PRInSTR " request is missing required ':scheme' header",
AWS_BYTE_CURSOR_PRI(aws_byte_cursor_from_string(method_string)));
goto malformed;
}
if (!path_string) {
DECODER_LOGF(
ERROR,
decoder,
"" PRInSTR " request is missing required ':path' header",
AWS_BYTE_CURSOR_PRI(aws_byte_cursor_from_string(method_string)));
goto malformed;
}
}

} else if (has_response_pseudoheaders) {
/* Response header block. */
Expand Down Expand Up @@ -1214,9 +1268,6 @@ static struct aws_h2err s_flush_pseudoheaders(struct aws_h2_decoder *decoder) {
current_block->block_type = AWS_HTTP_HEADER_BLOCK_TRAILING;
}

/* #TODO RFC-7540 8.1.2.3 & 8.3 Validate request has correct pseudoheaders. Note different rules for CONNECT */
/* #TODO validate pseudoheader values. each one has its own special rules */

/* Finally, deliver header-fields via callback */
for (size_t i = 0; i < PSEUDOHEADER_COUNT; ++i) {
const struct aws_string *value_string = current_block->pseudoheader_values[i];
Expand Down Expand Up @@ -1275,6 +1326,26 @@ static struct aws_h2err s_process_header_field(
goto malformed;
}

/* RFC9113 8.2.1 A field value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, 0x20 or
* 0x09). */
if (!aws_strutil_is_http_field_value(header_field->value)) {
/**
* RFC9113 8.2.1 HTTP/2 implementations SHOULD validate field names and values, respectively, and treat
* messages that contain prohibited characters as malformed.
*
* Note: Field values that are not valid according to the definition of the corresponding field do not cause a
* request to be malformed.
*/
DECODER_LOG(ERROR, decoder, "Invalid header field, bad value");
DECODER_LOGF(
DEBUG,
decoder,
"Bad header field is: \"" PRInSTR ": " PRInSTR "\"",
AWS_BYTE_CURSOR_PRI(header_field->name),
AWS_BYTE_CURSOR_PRI(header_field->value));
goto malformed;
}

enum aws_http_header_name name_enum = aws_http_lowercase_str_to_header_name(name);

bool is_pseudoheader = name.ptr[0] == ':';
Expand Down Expand Up @@ -1348,8 +1419,6 @@ static struct aws_h2err s_process_header_field(
}
}

/* #TODO Validate characters used in header_field->value */

switch (name_enum) {
case AWS_HTTP_HEADER_COOKIE:
/* for a header cookie, we will not fire callback until we concatenate them all, let's store it at the
Expand Down Expand Up @@ -1382,7 +1451,18 @@ static struct aws_h2err s_process_header_field(
AWS_BYTE_CURSOR_PRI(name));
goto malformed;
} break;

case AWS_HTTP_HEADER_TE: {
/* the TE header field, which MAY be present in an HTTP/2 request; when it is, it MUST NOT contain any
* value other than "trailers" (RFC9113 8.2.2) */
if (!aws_byte_cursor_eq_c_str(&header_field->value, "trailers")) {
DECODER_LOGF(
ERROR,
decoder,
"TE header has value:'" PRInSTR "', not allowed in HTTP/2",
AWS_BYTE_CURSOR_PRI(header_field->value));
goto malformed;
}
} break;
case AWS_HTTP_HEADER_CONTENT_LENGTH:
if (current_block->body_headers_forbidden) {
/* The content-length are forbidden */
Expand Down Expand Up @@ -1552,8 +1632,6 @@ static struct aws_h2err s_state_fn_header_block_entry(struct aws_h2_decoder *dec
* If dynamic table size changed via SETTINGS frame, next header-block must start with DYNAMIC_TABLE_RESIZE entry.
* Is it illegal to receive a resize entry at other times? */

/* #TODO The TE header field ... MUST NOT contain any value other than "trailers" */

if (result.type == AWS_HPACK_DECODE_T_HEADER_FIELD) {
const struct aws_http_header *header_field = &result.data.header_field;

Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ add_h2_decoder_test_set(h2_decoder_malformed_headers_illegal_name)
add_h2_decoder_test_set(h2_decoder_malformed_headers_response_to_server)
add_h2_decoder_test_set(h2_decoder_malformed_headers_request_to_client)
add_h2_decoder_test_set(h2_decoder_malformed_headers_mixed_pseudoheaders)
add_h2_decoder_test_set(h2_decoder_malformed_headers_missing_expected_pseudo_header)
add_h2_decoder_test_set(h2_decoder_malformed_headers_TE_header_unexpected_value)
add_h2_decoder_test_set(h2_decoder_malformed_headers_late_pseudoheaders)
add_h2_decoder_test_set(h2_decoder_malformed_headers_trailer_must_end_stream)
add_h2_decoder_test_set(h2_decoder_malformed_header_continues_hpack_parsing)
Expand Down
100 changes: 96 additions & 4 deletions tests/test_h2_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -643,12 +643,15 @@ H2_DECODER_ON_SERVER_TEST(h2_decoder_headers_cookies) {
/* clang-format off */
uint8_t input[] = {
/* HEADERS FRAME*/
0x00, 0x00, 0x06, /* Length (24) */
0x00, 0x00, 20, /* Length (24) */
AWS_H2_FRAME_T_HEADERS, /* Type (8) */
AWS_H2_FRAME_F_END_STREAM, /* Flags (8) */
0x76, 0x54, 0x32, 0x10, /* Reserved (1) | Stream Identifier (31) */
/* HEADERS */
0x82, /* ":method: GET" - indexed */
0x86, /* ":scheme: http" - indexed */
0x41, 10, 'a', 'm', 'a', 'z', 'o', 'n', '.', 'c', 'o', 'm', /* ":authority: amazon.com" - indexed name */
0x84, /* ":path: /" - indexed */
0x60, 0x03, 'a', '=', 'b', /* "cache: a=b" - indexed name, uncompressed value */

/* CONTINUATION FRAME*/
Expand All @@ -671,10 +674,13 @@ H2_DECODER_ON_SERVER_TEST(h2_decoder_headers_cookies) {
ASSERT_SUCCESS(h2_decoded_frame_check_finished(frame, AWS_H2_FRAME_T_HEADERS, 0x76543210 /*stream_id*/));
ASSERT_FALSE(frame->headers_malformed);
/* two sepaprate cookie headers are concatenated and moved as the last header*/
ASSERT_UINT_EQUALS(3, aws_http_headers_count(frame->headers));
ASSERT_UINT_EQUALS(6, aws_http_headers_count(frame->headers));
ASSERT_SUCCESS(s_check_header(frame, 0, ":method", "GET", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_SUCCESS(s_check_header(frame, 1, "user-agent", "test", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_SUCCESS(s_check_header(frame, 2, "cookie", "a=b; c=d; e=f", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_SUCCESS(s_check_header(frame, 1, ":scheme", "http", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_SUCCESS(s_check_header(frame, 2, ":authority", "amazon.com", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_SUCCESS(s_check_header(frame, 3, ":path", "/", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_SUCCESS(s_check_header(frame, 4, "user-agent", "test", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_SUCCESS(s_check_header(frame, 5, "cookie", "a=b; c=d; e=f", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
ASSERT_INT_EQUALS(AWS_HTTP_HEADER_BLOCK_MAIN, frame->header_block_type);
ASSERT_TRUE(frame->end_stream);

Expand Down Expand Up @@ -965,6 +971,92 @@ H2_DECODER_ON_CLIENT_TEST(h2_decoder_malformed_headers_mixed_pseudoheaders) {
return AWS_OP_SUCCESS;
}

/* Message is malformed if the required pseudo headers are missing.
* A malformed message is a Stream Error, not a Connection Error, so the decoder should continue */
H2_DECODER_ON_SERVER_TEST(h2_decoder_malformed_headers_missing_expected_pseudo_header) {
(void)allocator;
struct fixture *fixture = ctx;

/* clang-format off */
uint8_t input[] = {
0x00, 0x00, 0x02, /* Length (24) */
AWS_H2_FRAME_T_HEADERS, /* Type (8) */
AWS_H2_FRAME_F_END_HEADERS | AWS_H2_FRAME_F_END_STREAM, /* Flags (8) */
0x00, 0x00, 0x00, 0x01, /* Reserved (1) | Stream Identifier (31) */
/* HEADERS */
0x82, /* ":method: GET" - indexed */
0x84, /* ":path: /" - indexed */
};
/* clang-format on */

/* Decode */
ASSERT_H2ERR_SUCCESS(s_decode_all(fixture, aws_byte_cursor_from_array(input, sizeof(input))));

/* Validate */
struct h2_decoded_frame *frame = h2_decode_tester_latest_frame(&fixture->decode);
ASSERT_SUCCESS(h2_decoded_frame_check_finished(frame, AWS_H2_FRAME_T_HEADERS, 1 /*stream_id*/));
ASSERT_TRUE(frame->headers_malformed);

ASSERT_TRUE(frame->end_stream);
return AWS_OP_SUCCESS;
}

/* Message is malformed if TE header has value other than "trailers".
* A malformed message is a Stream Error, not a Connection Error, so the decoder should continue */
H2_DECODER_ON_SERVER_TEST(h2_decoder_malformed_headers_TE_header_unexpected_value) {
(void)allocator;
struct fixture *fixture = ctx;

/* clang-format off */
uint8_t valid_input[] = {
0x00, 0x00, 16, /* Length (24) */
AWS_H2_FRAME_T_HEADERS, /* Type (8) */
AWS_H2_FRAME_F_END_HEADERS | AWS_H2_FRAME_F_END_STREAM, /* Flags (8) */
0x00, 0x00, 0x00, 0x01, /* Reserved (1) | Stream Identifier (31) */
/* HEADERS */
0x82, /* ":method: GET" - indexed */
0x86, /* ":scheme: http" - indexed */
0x84, /* ":path: /" - indexed */
0x40, 0x02, 't', 'e', 0x08, 't', 'r', 'a', 'i', 'l', 'e', 'r', 's', /* "te: trailers" - TE headers has valid value */
};
/* clang-format on */

/* Decode */
ASSERT_H2ERR_SUCCESS(s_decode_all(fixture, aws_byte_cursor_from_array(valid_input, sizeof(valid_input))));

/* Validate */
struct h2_decoded_frame *frame = h2_decode_tester_latest_frame(&fixture->decode);
ASSERT_SUCCESS(h2_decoded_frame_check_finished(frame, AWS_H2_FRAME_T_HEADERS, 1 /*stream_id*/));
ASSERT_FALSE(frame->headers_malformed);

ASSERT_TRUE(frame->end_stream);

/* clang-format off */
uint8_t invalid_input[] = {
0x00, 0x00, 16, /* Length (24) */
AWS_H2_FRAME_T_HEADERS, /* Type (8) */
AWS_H2_FRAME_F_END_HEADERS | AWS_H2_FRAME_F_END_STREAM, /* Flags (8) */
0x00, 0x00, 0x00, 0x01, /* Reserved (1) | Stream Identifier (31) */
/* HEADERS */
0x82, /* ":method: GET" - indexed */
0x86, /* ":scheme: http" - indexed */
0x84, /* ":path: /" - indexed */
0x40, 0x02, 't', 'e', 0x08, 't', 'r', 'a', 'i', 'l', 'e', 'r', 'r', /* "te: trailerr" - TE headers has invalid value */
};
/* clang-format on */

/* Decode */
ASSERT_H2ERR_SUCCESS(s_decode_all(fixture, aws_byte_cursor_from_array(invalid_input, sizeof(invalid_input))));

/* Validate */
frame = h2_decode_tester_latest_frame(&fixture->decode);
ASSERT_SUCCESS(h2_decoded_frame_check_finished(frame, AWS_H2_FRAME_T_HEADERS, 1 /*stream_id*/));
ASSERT_TRUE(frame->headers_malformed);

ASSERT_TRUE(frame->end_stream);
return AWS_OP_SUCCESS;
}

/* Message is malformed if pseudo-headers come after regular headers.
* A malformed message is a Stream Error, not a Connection Error, so the decoder should continue */
H2_DECODER_ON_CLIENT_TEST(h2_decoder_malformed_headers_late_pseudoheaders) {
Expand Down
Loading