Skip to content
1 change: 1 addition & 0 deletions include/aws/http/private/http_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ enum aws_http_header_name {
AWS_HTTP_HEADER_CONTENT_LENGTH,
AWS_HTTP_HEADER_EXPECT,
AWS_HTTP_HEADER_TRANSFER_ENCODING,
AWS_HTTP_HEADER_COOKIE,

AWS_HTTP_HEADER_COUNT, /* Number of enums */
};
Expand Down
96 changes: 83 additions & 13 deletions source/h2_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ static const size_t s_scratch_space_size = 9;
/* Stream ids & dependencies should only write the bottom 31 bits */
static const uint32_t s_31_bit_mask = UINT32_MAX >> 1;

/* initial size for cookie buffer, buffer will grow if needed */
static const size_t s_decoder_cookie_buffer_initial_size = 512;

#define DECODER_LOGF(level, decoder, text, ...) \
AWS_LOGF_##level(AWS_LS_HTTP_DECODER, "id=%p " text, (decoder)->logging_id, __VA_ARGS__)
#define DECODER_LOG(level, decoder, text) DECODER_LOGF(level, decoder, "%s", text)
Expand Down Expand Up @@ -245,6 +248,12 @@ struct aws_h2_decoder {
* A malformed header-block is not a connection error, it's a Stream Error (RFC-7540 5.4.2).
* We continue decoding and report that it's malformed in on_headers_end(). */
bool malformed;

/* Buffer up cookie header fields to concatenate separate ones */
struct aws_byte_buf cookies;
/* If separate cookie fields have different compression types, the concatenated cookie uses the strictest type.
*/
enum aws_http_header_compression cookie_header_compression_type;
} header_block_in_progress;

/* Settings for decoder, which is based on the settings sent to the peer and ACKed by peer */
Expand Down Expand Up @@ -280,7 +289,7 @@ struct aws_h2_decoder *aws_h2_decoder_new(struct aws_h2_decoder_params *params)
void *allocation = aws_mem_acquire_many(
params->alloc, 2, &decoder, sizeof(struct aws_h2_decoder), &scratch_buf, s_scratch_space_size);
if (!allocation) {
goto failed_alloc;
goto error;
}

AWS_ZERO_STRUCT(*decoder);
Expand All @@ -295,7 +304,7 @@ struct aws_h2_decoder *aws_h2_decoder_new(struct aws_h2_decoder_params *params)

decoder->hpack = aws_hpack_context_new(params->alloc, AWS_LS_HTTP_DECODER, decoder);
if (!decoder->hpack) {
goto failed_new_hpack;
goto error;
}

if (decoder->is_server && !params->skip_connection_preface) {
Expand All @@ -311,23 +320,34 @@ struct aws_h2_decoder *aws_h2_decoder_new(struct aws_h2_decoder_params *params)

if (aws_array_list_init_dynamic(
&decoder->settings_buffer_list, decoder->alloc, 0, sizeof(struct aws_h2_frame_setting))) {
goto array_list_failed;
goto error;
}

if (aws_byte_buf_init(
&decoder->header_block_in_progress.cookies, decoder->alloc, s_decoder_cookie_buffer_initial_size)) {
goto error;
}

return decoder;

failed_new_hpack:
array_list_failed:
error:
if (decoder) {
aws_hpack_context_destroy(decoder->hpack);
aws_array_list_clean_up(&decoder->settings_buffer_list);
aws_byte_buf_clean_up(&decoder->header_block_in_progress.cookies);
}
aws_mem_release(params->alloc, allocation);
failed_alloc:
return NULL;
}

static void s_reset_header_block_in_progress(struct aws_h2_decoder *decoder) {
for (size_t i = 0; i < PSEUDOHEADER_COUNT; ++i) {
aws_string_destroy(decoder->header_block_in_progress.pseudoheader_values[i]);
}
struct aws_byte_buf cookie_backup = decoder->header_block_in_progress.cookies;
AWS_ZERO_STRUCT(decoder->header_block_in_progress);
decoder->header_block_in_progress.cookies = cookie_backup;
aws_byte_buf_reset(&decoder->header_block_in_progress.cookies, false);
}

void aws_h2_decoder_destroy(struct aws_h2_decoder *decoder) {
Expand All @@ -337,6 +357,7 @@ void aws_h2_decoder_destroy(struct aws_h2_decoder *decoder) {
aws_array_list_clean_up(&decoder->settings_buffer_list);
aws_hpack_context_destroy(decoder->hpack);
s_reset_header_block_in_progress(decoder);
aws_byte_buf_clean_up(&decoder->header_block_in_progress.cookies);
aws_mem_release(decoder->alloc, decoder);
}

Expand Down Expand Up @@ -1263,11 +1284,35 @@ static int s_process_header_field(struct aws_h2_decoder *decoder, const struct a

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

/* Deliver header-field via callback */
if (current_block->is_push_promise) {
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_push_promise_i, header_field, name_enum);
} else {
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_headers_i, header_field, name_enum, current_block->block_type);
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
* buffer */
if (header_field->compression > current_block->cookie_header_compression_type) {
current_block->cookie_header_compression_type = header_field->compression;
}

if (current_block->cookies.len) {
/* add a delimiter */
struct aws_byte_cursor delimiter = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("; ");
if (aws_byte_buf_append_dynamic(&current_block->cookies, &delimiter)) {
return AWS_OP_ERR;
}
}
if (aws_byte_buf_append_dynamic(&current_block->cookies, &header_field->value)) {
return AWS_OP_ERR;
}
break;

default:
/* Deliver header-field via callback */
if (current_block->is_push_promise) {
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_push_promise_i, header_field, name_enum);
} else {
DECODER_CALL_VTABLE_STREAM_ARGS(
decoder, on_headers_i, header_field, name_enum, current_block->block_type);
}
break;
}
}

Expand All @@ -1282,6 +1327,29 @@ static int s_process_header_field(struct aws_h2_decoder *decoder, const struct a
return AWS_OP_SUCCESS;
}

static int s_flush_cookie_header(struct aws_h2_decoder *decoder) {
struct aws_header_block_in_progress *current_block = &decoder->header_block_in_progress;
if (current_block->malformed) {
return AWS_OP_SUCCESS;
}
if (current_block->cookies.len == 0) {
/* Nothing to flush */
return AWS_OP_SUCCESS;
}
struct aws_http_header concatenated_cookie;
struct aws_byte_cursor header_name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("cookie");
concatenated_cookie.name = header_name;
concatenated_cookie.value = aws_byte_cursor_from_buf(&current_block->cookies);
concatenated_cookie.compression = current_block->cookie_header_compression_type;
if (current_block->is_push_promise) {
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_push_promise_i, &concatenated_cookie, AWS_HTTP_HEADER_COOKIE);
} else {
DECODER_CALL_VTABLE_STREAM_ARGS(
decoder, on_headers_i, &concatenated_cookie, AWS_HTTP_HEADER_COOKIE, current_block->block_type);
}
return AWS_OP_SUCCESS;
}

/* This state checks whether we've consumed the current frame's entire header-block fragment.
* We revisit this state after each entry is decoded.
* This state consumes no data. */
Expand All @@ -1297,6 +1365,10 @@ static int s_state_fn_header_block_loop(struct aws_h2_decoder *decoder, struct a
if (s_flush_pseudoheaders(decoder)) {
return AWS_OP_ERR;
}
/* flush the concatenated cookie header */
if (s_flush_cookie_header(decoder)) {
return AWS_OP_ERR;
}

bool malformed = decoder->header_block_in_progress.malformed;
DECODER_LOGF(TRACE, decoder, "Done decoding header-block, malformed=%d", malformed);
Expand Down Expand Up @@ -1389,8 +1461,6 @@ static int s_state_fn_header_block_entry(struct aws_h2_decoder *decoder, struct
* 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 Cookie headers must be concatenated into single delivery RFC-7540 8.1.2.5 */

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

if (result.type == AWS_HPACK_DECODE_T_HEADER_FIELD) {
Expand Down
1 change: 1 addition & 0 deletions source/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ static void s_headers_init(struct aws_allocator *alloc) {
s_header_enum_to_str[AWS_HTTP_HEADER_AUTHORITY] = aws_byte_cursor_from_c_str(":authority");
s_header_enum_to_str[AWS_HTTP_HEADER_PATH] = aws_byte_cursor_from_c_str(":path");
s_header_enum_to_str[AWS_HTTP_HEADER_STATUS] = aws_byte_cursor_from_c_str(":status");
s_header_enum_to_str[AWS_HTTP_HEADER_COOKIE] = aws_byte_cursor_from_c_str("cookie");
s_header_enum_to_str[AWS_HTTP_HEADER_CONNECTION] = aws_byte_cursor_from_c_str("connection");
s_header_enum_to_str[AWS_HTTP_HEADER_CONTENT_LENGTH] = aws_byte_cursor_from_c_str("content-length");
s_header_enum_to_str[AWS_HTTP_HEADER_EXPECT] = aws_byte_cursor_from_c_str("expect");
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ add_h2_decoder_test_set(h2_decoder_headers_priority)
add_h2_decoder_test_set(h2_decoder_headers_ignores_unknown_flags)
add_h2_decoder_test_set(h2_decoder_headers_response_informational)
add_h2_decoder_test_set(h2_decoder_headers_request)
add_h2_decoder_test_set(h2_decoder_headers_cookies)
add_h2_decoder_test_set(h2_decoder_headers_trailer)
add_h2_decoder_test_set(h2_decoder_headers_empty_trailer)
add_h2_decoder_test_set(h2_decoder_err_headers_requires_stream_id)
Expand Down
45 changes: 45 additions & 0 deletions tests/test_h2_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,51 @@ H2_DECODER_ON_SERVER_TEST(h2_decoder_headers_request) {
return AWS_OP_SUCCESS;
}

H2_DECODER_ON_SERVER_TEST(h2_decoder_headers_cookies) {
(void)allocator;
struct fixture *fixture = ctx;

/* clang-format off */
uint8_t input[] = {
/* HEADERS FRAME*/
0x00, 0x00, 0x06, /* 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 */
0x60, 0x03, 'a', '=', 'b', /* "cache: a=b" - indexed name, uncompressed value */

/* CONTINUATION FRAME*/
0x00, 0x00, 16, /* Length (24) */
AWS_H2_FRAME_T_CONTINUATION,/* Type (8) */
AWS_H2_FRAME_F_END_HEADERS, /* Flags (8) */
0x76, 0x54, 0x32, 0x10, /* Reserved (1) | Stream Identifier (31) */
/* PAYLOAD */
0x7a, 0x04, 't', 'e', 's', 't', /* "user-agent: test" - indexed name, uncompressed value */
0x60, 0x03, 'c', '=', 'd', /* "cache: c=d" - indexed name, uncompressed value */
0x60, 0x03, 'e', '=', 'f', /* "cache: e=f" - indexed name, uncompressed value */
};
/* clang-format on */

/* Decode */
ASSERT_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, 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_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_INT_EQUALS(AWS_HTTP_HEADER_BLOCK_MAIN, frame->header_block_type);
ASSERT_TRUE(frame->end_stream);

return AWS_OP_SUCCESS;
}

/* A trailing header has no psuedo-headers, and always ends the stream */
H2_DECODER_ON_CLIENT_TEST(h2_decoder_headers_trailer) {
(void)allocator;
Expand Down