Skip to content

Commit 7f65818

Browse files
authored
H2 cookie header field (#219)
Compressing the cookie header field (RFC 7540 8.1.2.5)
1 parent 86477a5 commit 7f65818

File tree

5 files changed

+131
-13
lines changed

5 files changed

+131
-13
lines changed

include/aws/http/private/http_impl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ enum aws_http_header_name {
5151
AWS_HTTP_HEADER_CONTENT_LENGTH,
5252
AWS_HTTP_HEADER_EXPECT,
5353
AWS_HTTP_HEADER_TRANSFER_ENCODING,
54+
AWS_HTTP_HEADER_COOKIE,
5455

5556
AWS_HTTP_HEADER_COUNT, /* Number of enums */
5657
};

source/h2_decoder.c

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ static const size_t s_scratch_space_size = 9;
3636
/* Stream ids & dependencies should only write the bottom 31 bits */
3737
static const uint32_t s_31_bit_mask = UINT32_MAX >> 1;
3838

39+
/* initial size for cookie buffer, buffer will grow if needed */
40+
static const size_t s_decoder_cookie_buffer_initial_size = 512;
41+
3942
#define DECODER_LOGF(level, decoder, text, ...) \
4043
AWS_LOGF_##level(AWS_LS_HTTP_DECODER, "id=%p " text, (decoder)->logging_id, __VA_ARGS__)
4144
#define DECODER_LOG(level, decoder, text) DECODER_LOGF(level, decoder, "%s", text)
@@ -245,6 +248,12 @@ struct aws_h2_decoder {
245248
* A malformed header-block is not a connection error, it's a Stream Error (RFC-7540 5.4.2).
246249
* We continue decoding and report that it's malformed in on_headers_end(). */
247250
bool malformed;
251+
252+
/* Buffer up cookie header fields to concatenate separate ones */
253+
struct aws_byte_buf cookies;
254+
/* If separate cookie fields have different compression types, the concatenated cookie uses the strictest type.
255+
*/
256+
enum aws_http_header_compression cookie_header_compression_type;
248257
} header_block_in_progress;
249258

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

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

296305
decoder->hpack = aws_hpack_context_new(params->alloc, AWS_LS_HTTP_DECODER, decoder);
297306
if (!decoder->hpack) {
298-
goto failed_new_hpack;
307+
goto error;
299308
}
300309

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

312321
if (aws_array_list_init_dynamic(
313322
&decoder->settings_buffer_list, decoder->alloc, 0, sizeof(struct aws_h2_frame_setting))) {
314-
goto array_list_failed;
323+
goto error;
324+
}
325+
326+
if (aws_byte_buf_init(
327+
&decoder->header_block_in_progress.cookies, decoder->alloc, s_decoder_cookie_buffer_initial_size)) {
328+
goto error;
315329
}
316330

317331
return decoder;
318332

319-
failed_new_hpack:
320-
array_list_failed:
333+
error:
334+
if (decoder) {
335+
aws_hpack_context_destroy(decoder->hpack);
336+
aws_array_list_clean_up(&decoder->settings_buffer_list);
337+
aws_byte_buf_clean_up(&decoder->header_block_in_progress.cookies);
338+
}
321339
aws_mem_release(params->alloc, allocation);
322-
failed_alloc:
323340
return NULL;
324341
}
325342

326343
static void s_reset_header_block_in_progress(struct aws_h2_decoder *decoder) {
327344
for (size_t i = 0; i < PSEUDOHEADER_COUNT; ++i) {
328345
aws_string_destroy(decoder->header_block_in_progress.pseudoheader_values[i]);
329346
}
347+
struct aws_byte_buf cookie_backup = decoder->header_block_in_progress.cookies;
330348
AWS_ZERO_STRUCT(decoder->header_block_in_progress);
349+
decoder->header_block_in_progress.cookies = cookie_backup;
350+
aws_byte_buf_reset(&decoder->header_block_in_progress.cookies, false);
331351
}
332352

333353
void aws_h2_decoder_destroy(struct aws_h2_decoder *decoder) {
@@ -337,6 +357,7 @@ void aws_h2_decoder_destroy(struct aws_h2_decoder *decoder) {
337357
aws_array_list_clean_up(&decoder->settings_buffer_list);
338358
aws_hpack_context_destroy(decoder->hpack);
339359
s_reset_header_block_in_progress(decoder);
360+
aws_byte_buf_clean_up(&decoder->header_block_in_progress.cookies);
340361
aws_mem_release(decoder->alloc, decoder);
341362
}
342363

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

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

1266-
/* Deliver header-field via callback */
1267-
if (current_block->is_push_promise) {
1268-
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_push_promise_i, header_field, name_enum);
1269-
} else {
1270-
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_headers_i, header_field, name_enum, current_block->block_type);
1287+
switch (name_enum) {
1288+
case AWS_HTTP_HEADER_COOKIE:
1289+
/* for a header cookie, we will not fire callback until we concatenate them all, let's store it at the
1290+
* buffer */
1291+
if (header_field->compression > current_block->cookie_header_compression_type) {
1292+
current_block->cookie_header_compression_type = header_field->compression;
1293+
}
1294+
1295+
if (current_block->cookies.len) {
1296+
/* add a delimiter */
1297+
struct aws_byte_cursor delimiter = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("; ");
1298+
if (aws_byte_buf_append_dynamic(&current_block->cookies, &delimiter)) {
1299+
return AWS_OP_ERR;
1300+
}
1301+
}
1302+
if (aws_byte_buf_append_dynamic(&current_block->cookies, &header_field->value)) {
1303+
return AWS_OP_ERR;
1304+
}
1305+
break;
1306+
1307+
default:
1308+
/* Deliver header-field via callback */
1309+
if (current_block->is_push_promise) {
1310+
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_push_promise_i, header_field, name_enum);
1311+
} else {
1312+
DECODER_CALL_VTABLE_STREAM_ARGS(
1313+
decoder, on_headers_i, header_field, name_enum, current_block->block_type);
1314+
}
1315+
break;
12711316
}
12721317
}
12731318

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

1330+
static int s_flush_cookie_header(struct aws_h2_decoder *decoder) {
1331+
struct aws_header_block_in_progress *current_block = &decoder->header_block_in_progress;
1332+
if (current_block->malformed) {
1333+
return AWS_OP_SUCCESS;
1334+
}
1335+
if (current_block->cookies.len == 0) {
1336+
/* Nothing to flush */
1337+
return AWS_OP_SUCCESS;
1338+
}
1339+
struct aws_http_header concatenated_cookie;
1340+
struct aws_byte_cursor header_name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("cookie");
1341+
concatenated_cookie.name = header_name;
1342+
concatenated_cookie.value = aws_byte_cursor_from_buf(&current_block->cookies);
1343+
concatenated_cookie.compression = current_block->cookie_header_compression_type;
1344+
if (current_block->is_push_promise) {
1345+
DECODER_CALL_VTABLE_STREAM_ARGS(decoder, on_push_promise_i, &concatenated_cookie, AWS_HTTP_HEADER_COOKIE);
1346+
} else {
1347+
DECODER_CALL_VTABLE_STREAM_ARGS(
1348+
decoder, on_headers_i, &concatenated_cookie, AWS_HTTP_HEADER_COOKIE, current_block->block_type);
1349+
}
1350+
return AWS_OP_SUCCESS;
1351+
}
1352+
12851353
/* This state checks whether we've consumed the current frame's entire header-block fragment.
12861354
* We revisit this state after each entry is decoded.
12871355
* This state consumes no data. */
@@ -1297,6 +1365,10 @@ static int s_state_fn_header_block_loop(struct aws_h2_decoder *decoder, struct a
12971365
if (s_flush_pseudoheaders(decoder)) {
12981366
return AWS_OP_ERR;
12991367
}
1368+
/* flush the concatenated cookie header */
1369+
if (s_flush_cookie_header(decoder)) {
1370+
return AWS_OP_ERR;
1371+
}
13001372

13011373
bool malformed = decoder->header_block_in_progress.malformed;
13021374
DECODER_LOGF(TRACE, decoder, "Done decoding header-block, malformed=%d", malformed);
@@ -1389,8 +1461,6 @@ static int s_state_fn_header_block_entry(struct aws_h2_decoder *decoder, struct
13891461
* If dynamic table size changed via SETTINGS frame, next header-block must start with DYNAMIC_TABLE_RESIZE entry.
13901462
* Is it illegal to receive a resize entry at other times? */
13911463

1392-
/* #TODO Cookie headers must be concatenated into single delivery RFC-7540 8.1.2.5 */
1393-
13941464
/* #TODO The TE header field ... MUST NOT contain any value other than "trailers" */
13951465

13961466
if (result.type == AWS_HPACK_DECODE_T_HEADER_FIELD) {

source/http.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ static void s_headers_init(struct aws_allocator *alloc) {
253253
s_header_enum_to_str[AWS_HTTP_HEADER_AUTHORITY] = aws_byte_cursor_from_c_str(":authority");
254254
s_header_enum_to_str[AWS_HTTP_HEADER_PATH] = aws_byte_cursor_from_c_str(":path");
255255
s_header_enum_to_str[AWS_HTTP_HEADER_STATUS] = aws_byte_cursor_from_c_str(":status");
256+
s_header_enum_to_str[AWS_HTTP_HEADER_COOKIE] = aws_byte_cursor_from_c_str("cookie");
256257
s_header_enum_to_str[AWS_HTTP_HEADER_CONNECTION] = aws_byte_cursor_from_c_str("connection");
257258
s_header_enum_to_str[AWS_HTTP_HEADER_CONTENT_LENGTH] = aws_byte_cursor_from_c_str("content-length");
258259
s_header_enum_to_str[AWS_HTTP_HEADER_EXPECT] = aws_byte_cursor_from_c_str("expect");

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ add_h2_decoder_test_set(h2_decoder_headers_priority)
246246
add_h2_decoder_test_set(h2_decoder_headers_ignores_unknown_flags)
247247
add_h2_decoder_test_set(h2_decoder_headers_response_informational)
248248
add_h2_decoder_test_set(h2_decoder_headers_request)
249+
add_h2_decoder_test_set(h2_decoder_headers_cookies)
249250
add_h2_decoder_test_set(h2_decoder_headers_trailer)
250251
add_h2_decoder_test_set(h2_decoder_headers_empty_trailer)
251252
add_h2_decoder_test_set(h2_decoder_err_headers_requires_stream_id)

tests/test_h2_decoder.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,51 @@ H2_DECODER_ON_SERVER_TEST(h2_decoder_headers_request) {
634634
return AWS_OP_SUCCESS;
635635
}
636636

637+
H2_DECODER_ON_SERVER_TEST(h2_decoder_headers_cookies) {
638+
(void)allocator;
639+
struct fixture *fixture = ctx;
640+
641+
/* clang-format off */
642+
uint8_t input[] = {
643+
/* HEADERS FRAME*/
644+
0x00, 0x00, 0x06, /* Length (24) */
645+
AWS_H2_FRAME_T_HEADERS, /* Type (8) */
646+
AWS_H2_FRAME_F_END_STREAM, /* Flags (8) */
647+
0x76, 0x54, 0x32, 0x10, /* Reserved (1) | Stream Identifier (31) */
648+
/* HEADERS */
649+
0x82, /* ":method: GET" - indexed */
650+
0x60, 0x03, 'a', '=', 'b', /* "cache: a=b" - indexed name, uncompressed value */
651+
652+
/* CONTINUATION FRAME*/
653+
0x00, 0x00, 16, /* Length (24) */
654+
AWS_H2_FRAME_T_CONTINUATION,/* Type (8) */
655+
AWS_H2_FRAME_F_END_HEADERS, /* Flags (8) */
656+
0x76, 0x54, 0x32, 0x10, /* Reserved (1) | Stream Identifier (31) */
657+
/* PAYLOAD */
658+
0x7a, 0x04, 't', 'e', 's', 't', /* "user-agent: test" - indexed name, uncompressed value */
659+
0x60, 0x03, 'c', '=', 'd', /* "cache: c=d" - indexed name, uncompressed value */
660+
0x60, 0x03, 'e', '=', 'f', /* "cache: e=f" - indexed name, uncompressed value */
661+
};
662+
/* clang-format on */
663+
664+
/* Decode */
665+
ASSERT_SUCCESS(s_decode_all(fixture, aws_byte_cursor_from_array(input, sizeof(input))));
666+
667+
/* Validate */
668+
struct h2_decoded_frame *frame = h2_decode_tester_latest_frame(&fixture->decode);
669+
ASSERT_SUCCESS(h2_decoded_frame_check_finished(frame, AWS_H2_FRAME_T_HEADERS, 0x76543210 /*stream_id*/));
670+
ASSERT_FALSE(frame->headers_malformed);
671+
/* two sepaprate cookie headers are concatenated and moved as the last header*/
672+
ASSERT_UINT_EQUALS(3, aws_http_headers_count(frame->headers));
673+
ASSERT_SUCCESS(s_check_header(frame, 0, ":method", "GET", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
674+
ASSERT_SUCCESS(s_check_header(frame, 1, "user-agent", "test", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
675+
ASSERT_SUCCESS(s_check_header(frame, 2, "cookie", "a=b; c=d; e=f", AWS_HTTP_HEADER_COMPRESSION_USE_CACHE));
676+
ASSERT_INT_EQUALS(AWS_HTTP_HEADER_BLOCK_MAIN, frame->header_block_type);
677+
ASSERT_TRUE(frame->end_stream);
678+
679+
return AWS_OP_SUCCESS;
680+
}
681+
637682
/* A trailing header has no psuedo-headers, and always ends the stream */
638683
H2_DECODER_ON_CLIENT_TEST(h2_decoder_headers_trailer) {
639684
(void)allocator;

0 commit comments

Comments
 (0)