Skip to content

Commit 07cac78

Browse files
authored
Chunked trailer (#335)
Add chunked trailer support to HTTP/1.
1 parent 7af5e6f commit 07cac78

File tree

11 files changed

+717
-9
lines changed

11 files changed

+717
-9
lines changed

include/aws/http/private/h1_encoder.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ struct aws_h1_chunk {
1919
struct aws_byte_buf chunk_line;
2020
};
2121

22+
struct aws_h1_trailer {
23+
struct aws_allocator *allocator;
24+
struct aws_byte_buf trailer_data;
25+
};
26+
2227
/**
2328
* Message to be submitted to encoder.
2429
* Contains data necessary for encoder to write an outgoing request or response.
@@ -36,6 +41,9 @@ struct aws_h1_encoder_message {
3641
* A chunk with data_size=0 means "final chunk" */
3742
struct aws_linked_list *pending_chunk_list;
3843

44+
/* Pointer to chunked_trailer, used for chunked_trailer. */
45+
struct aws_h1_trailer *trailer;
46+
3947
/* If non-zero, length of unchunked body to send */
4048
uint64_t content_length;
4149
bool has_connection_close_header;
@@ -71,6 +79,11 @@ struct aws_h1_encoder {
7179
};
7280

7381
struct aws_h1_chunk *aws_h1_chunk_new(struct aws_allocator *allocator, const struct aws_http1_chunk_options *options);
82+
struct aws_h1_trailer *aws_h1_trailer_new(
83+
struct aws_allocator *allocator,
84+
const struct aws_http_headers *trailing_headers);
85+
86+
void aws_h1_trailer_destroy(struct aws_h1_trailer *trailer);
7487

7588
/* Just destroy the chunk (don't fire callback) */
7689
void aws_h1_chunk_destroy(struct aws_h1_chunk *chunk);

include/aws/http/private/h1_stream.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ struct aws_h1_stream {
6262
* Encoder completes/frees/pops front chunk when it's done sending. */
6363
struct aws_linked_list pending_chunk_list;
6464

65+
struct aws_h1_encoder_message message;
66+
6567
/* Size of stream's flow-control window.
6668
* Only body data (not headers, etc) counts against the stream's flow-control window. */
6769
uint64_t stream_window;
@@ -79,6 +81,10 @@ struct aws_h1_stream {
7981
* but haven't yet moved to encoder_message.pending_chunk_list where the encoder will find them. */
8082
struct aws_linked_list pending_chunk_list;
8183

84+
/* trailing headers which have been submitted by user,
85+
* but haven't yet moved to encoder_message where the encoder will find them. */
86+
struct aws_h1_trailer *pending_trailer;
87+
8288
enum aws_h1_stream_api_state api_state;
8389

8490
/* Sum of all aws_http_stream_update_window() calls that haven't yet moved to thread_data.stream_window */
@@ -93,6 +99,12 @@ struct aws_h1_stream {
9399

94100
/* Whether the outgoing message is using chunked encoding */
95101
bool using_chunked_encoding : 1;
102+
103+
/* Whether the final 0 length chunk has already been sent */
104+
bool has_final_chunk : 1;
105+
106+
/* Whether the chunked trailer has already been sent */
107+
bool has_added_trailer : 1;
96108
} synced_data;
97109
};
98110

include/aws/http/private/http_impl.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,28 @@ enum aws_http_header_name {
4242
AWS_HTTP_HEADER_EXPECT,
4343
AWS_HTTP_HEADER_TRANSFER_ENCODING,
4444
AWS_HTTP_HEADER_COOKIE,
45+
AWS_HTTP_HEADER_SET_COOKIE,
4546
AWS_HTTP_HEADER_HOST,
47+
AWS_HTTP_HEADER_CACHE_CONTROL,
48+
AWS_HTTP_HEADER_MAX_FORWARDS,
49+
AWS_HTTP_HEADER_PRAGMA,
50+
AWS_HTTP_HEADER_RANGE,
51+
AWS_HTTP_HEADER_TE,
52+
AWS_HTTP_HEADER_CONTENT_ENCODING,
53+
AWS_HTTP_HEADER_CONTENT_TYPE,
54+
AWS_HTTP_HEADER_CONTENT_RANGE,
55+
AWS_HTTP_HEADER_TRAILER,
56+
AWS_HTTP_HEADER_WWW_AUTHENTICATE,
57+
AWS_HTTP_HEADER_AUTHORIZATION,
58+
AWS_HTTP_HEADER_PROXY_AUTHENTICATE,
59+
AWS_HTTP_HEADER_PROXY_AUTHORIZATION,
60+
AWS_HTTP_HEADER_AGE,
61+
AWS_HTTP_HEADER_EXPIRES,
62+
AWS_HTTP_HEADER_DATE,
63+
AWS_HTTP_HEADER_LOCATION,
64+
AWS_HTTP_HEADER_RETRY_AFTER,
65+
AWS_HTTP_HEADER_VARY,
66+
AWS_HTTP_HEADER_WARNING,
4667

4768
AWS_HTTP_HEADER_COUNT, /* Number of enums */
4869
};

include/aws/http/private/request_response_impl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct aws_http_stream_vtable {
1818
int (*activate)(struct aws_http_stream *stream);
1919

2020
int (*http1_write_chunk)(struct aws_http_stream *http1_stream, const struct aws_http1_chunk_options *options);
21+
int (*http1_add_trailer)(struct aws_http_stream *http1_stream, const struct aws_http_headers *trailing_headers);
2122

2223
int (*http2_reset_stream)(struct aws_http_stream *http2_stream, uint32_t http2_error);
2324
int (*http2_get_received_error_code)(struct aws_http_stream *http2_stream, uint32_t *http2_error);

include/aws/http/request_response.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,27 @@ AWS_HTTP_API int aws_http1_stream_write_chunk(
628628
struct aws_http_stream *http1_stream,
629629
const struct aws_http1_chunk_options *options);
630630

631+
/**
632+
* Add a list of headers to be added as trailing headers sent after the last chunk is sent.
633+
* The stream must have specified "chunked" in a "transfer-encoding" header. The stream should also have
634+
* a "Trailer" header field which indicates the fields present in the trailer.
635+
*
636+
* Certain headers are forbidden in the trailer (e.g., Transfer-Encoding, Content-Length, Host). See RFC-7541
637+
* Section 4.1.2 for more details.
638+
*
639+
* For client streams, activate() must be called before any chunks are submitted.
640+
*
641+
* For server streams, the response must be submitted before the trailer can be added
642+
*
643+
* aws_http1_stream_add_chunked_trailer must be called before the final size 0 chunk, and at the moment can only
644+
* be called once, though this could change if need be.
645+
*
646+
* Returns AWS_OP_SUCCESS if the chunk has been submitted.
647+
*/
648+
AWS_HTTP_API int aws_http1_stream_add_chunked_trailer(
649+
struct aws_http_stream *http1_stream,
650+
const struct aws_http_headers *trailing_headers);
651+
631652
/**
632653
* Get the message's aws_http_headers.
633654
*

source/h1_encoder.c

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,84 @@ static int s_scan_outgoing_headers(
156156
return AWS_OP_SUCCESS;
157157
}
158158

159+
static int s_scan_outgoing_trailer(const struct aws_http_headers *headers, size_t *out_size) {
160+
const size_t num_headers = aws_http_headers_count(headers);
161+
size_t total = 0;
162+
for (size_t i = 0; i < num_headers; i++) {
163+
struct aws_http_header header;
164+
aws_http_headers_get_index(headers, i, &header);
165+
/* Validate header field-name (RFC-7230 3.2): field-name = token */
166+
if (!aws_strutil_is_http_token(header.name)) {
167+
AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=static: Header name is invalid");
168+
return aws_raise_error(AWS_ERROR_HTTP_INVALID_HEADER_NAME);
169+
}
170+
171+
/* Validate header field-value.
172+
* The value itself isn't supposed to have whitespace on either side,
173+
* but we'll trim it off before validation so we don't start needlessly
174+
* failing requests that used to work before we added validation.
175+
* This should be OK because field-value can be sent with any amount
176+
* of whitespace around it, which the other side will just ignore (RFC-7230 3.2):
177+
* header-field = field-name ":" OWS field-value OWS */
178+
struct aws_byte_cursor field_value = aws_strutil_trim_http_whitespace(header.value);
179+
if (!aws_strutil_is_http_field_value(field_value)) {
180+
AWS_LOGF_ERROR(
181+
AWS_LS_HTTP_STREAM,
182+
"id=static: Header '" PRInSTR "' has invalid value",
183+
AWS_BYTE_CURSOR_PRI(header.name));
184+
return aws_raise_error(AWS_ERROR_HTTP_INVALID_HEADER_VALUE);
185+
}
186+
187+
enum aws_http_header_name name_enum = aws_http_str_to_header_name(header.name);
188+
if (name_enum == AWS_HTTP_HEADER_TRANSFER_ENCODING || name_enum == AWS_HTTP_HEADER_CONTENT_LENGTH ||
189+
name_enum == AWS_HTTP_HEADER_HOST || name_enum == AWS_HTTP_HEADER_EXPECT ||
190+
name_enum == AWS_HTTP_HEADER_CACHE_CONTROL || name_enum == AWS_HTTP_HEADER_MAX_FORWARDS ||
191+
name_enum == AWS_HTTP_HEADER_PRAGMA || name_enum == AWS_HTTP_HEADER_RANGE ||
192+
name_enum == AWS_HTTP_HEADER_TE || name_enum == AWS_HTTP_HEADER_CONTENT_ENCODING ||
193+
name_enum == AWS_HTTP_HEADER_CONTENT_TYPE || name_enum == AWS_HTTP_HEADER_CONTENT_RANGE ||
194+
name_enum == AWS_HTTP_HEADER_TRAILER || name_enum == AWS_HTTP_HEADER_WWW_AUTHENTICATE ||
195+
name_enum == AWS_HTTP_HEADER_AUTHORIZATION || name_enum == AWS_HTTP_HEADER_PROXY_AUTHENTICATE ||
196+
name_enum == AWS_HTTP_HEADER_PROXY_AUTHORIZATION || name_enum == AWS_HTTP_HEADER_SET_COOKIE ||
197+
name_enum == AWS_HTTP_HEADER_COOKIE || name_enum == AWS_HTTP_HEADER_AGE ||
198+
name_enum == AWS_HTTP_HEADER_EXPIRES || name_enum == AWS_HTTP_HEADER_DATE ||
199+
name_enum == AWS_HTTP_HEADER_LOCATION || name_enum == AWS_HTTP_HEADER_RETRY_AFTER ||
200+
name_enum == AWS_HTTP_HEADER_VARY || name_enum == AWS_HTTP_HEADER_WARNING) {
201+
AWS_LOGF_ERROR(
202+
AWS_LS_HTTP_STREAM,
203+
"id=static: Trailing Header '" PRInSTR "' has invalid value",
204+
AWS_BYTE_CURSOR_PRI(header.name));
205+
return aws_raise_error(AWS_ERROR_HTTP_INVALID_HEADER_FIELD);
206+
}
207+
208+
int err = 0;
209+
err |= aws_add_size_checked(header.name.len, total, &total);
210+
err |= aws_add_size_checked(header.value.len, total, &total);
211+
err |= aws_add_size_checked(4, total, &total); /* ": " + "\r\n" */
212+
if (err) {
213+
return AWS_OP_ERR;
214+
}
215+
}
216+
if (aws_add_size_checked(2, total, &total)) { /* "\r\n" */
217+
return AWS_OP_ERR;
218+
}
219+
*out_size = total;
220+
return AWS_OP_SUCCESS;
221+
}
222+
159223
static bool s_write_crlf(struct aws_byte_buf *dst) {
160224
AWS_PRECONDITION(aws_byte_buf_is_valid(dst));
161225
struct aws_byte_cursor crlf_cursor = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\r\n");
162226
return aws_byte_buf_write_from_whole_cursor(dst, crlf_cursor);
163227
}
164228

165-
static void s_write_headers(struct aws_byte_buf *dst, const struct aws_http_message *message) {
229+
static void s_write_headers(struct aws_byte_buf *dst, const struct aws_http_headers *headers) {
166230

167-
const size_t num_headers = aws_http_message_get_header_count(message);
231+
const size_t num_headers = aws_http_headers_count(headers);
168232

169233
bool wrote_all = true;
170234
for (size_t i = 0; i < num_headers; ++i) {
171235
struct aws_http_header header;
172-
aws_http_message_get_header(message, &header, i);
236+
aws_http_headers_get_index(headers, i, &header);
173237

174238
/* header-line: "{name}: {value}\r\n" */
175239
wrote_all &= aws_byte_buf_write_from_whole_cursor(dst, header.name);
@@ -264,7 +328,7 @@ int aws_h1_encoder_message_init_from_request(
264328
wrote_all &= aws_byte_buf_write_from_whole_cursor(&message->outgoing_head_buf, version);
265329
wrote_all &= s_write_crlf(&message->outgoing_head_buf);
266330

267-
s_write_headers(&message->outgoing_head_buf, request);
331+
s_write_headers(&message->outgoing_head_buf, aws_http_message_get_const_headers(request));
268332

269333
wrote_all &= s_write_crlf(&message->outgoing_head_buf);
270334
(void)wrote_all;
@@ -352,7 +416,7 @@ int aws_h1_encoder_message_init_from_response(
352416
wrote_all &= aws_byte_buf_write_from_whole_cursor(&message->outgoing_head_buf, status_text);
353417
wrote_all &= s_write_crlf(&message->outgoing_head_buf);
354418

355-
s_write_headers(&message->outgoing_head_buf, response);
419+
s_write_headers(&message->outgoing_head_buf, aws_http_message_get_const_headers(response));
356420

357421
wrote_all &= s_write_crlf(&message->outgoing_head_buf);
358422
(void)wrote_all;
@@ -368,6 +432,7 @@ int aws_h1_encoder_message_init_from_response(
368432

369433
void aws_h1_encoder_message_clean_up(struct aws_h1_encoder_message *message) {
370434
aws_byte_buf_clean_up(&message->outgoing_head_buf);
435+
aws_h1_trailer_destroy(message->trailer);
371436
AWS_ZERO_STRUCT(*message);
372437
}
373438

@@ -443,6 +508,32 @@ static void s_populate_chunk_line_buffer(
443508
AWS_ASSERT(wrote_chunk_line);
444509
}
445510

511+
struct aws_h1_trailer *aws_h1_trailer_new(
512+
struct aws_allocator *allocator,
513+
const struct aws_http_headers *trailing_headers) {
514+
/* Allocate trailer along with storage for the trailer-line */
515+
size_t trailer_size = 0;
516+
if (s_scan_outgoing_trailer(trailing_headers, &trailer_size)) {
517+
return NULL;
518+
}
519+
520+
struct aws_h1_trailer *trailer = aws_mem_calloc(allocator, 1, sizeof(struct aws_h1_trailer));
521+
trailer->allocator = allocator;
522+
523+
aws_byte_buf_init(&trailer->trailer_data, allocator, trailer_size); /* cannot fail */
524+
s_write_headers(&trailer->trailer_data, trailing_headers);
525+
s_write_crlf(&trailer->trailer_data); /* \r\n */
526+
return trailer;
527+
}
528+
529+
void aws_h1_trailer_destroy(struct aws_h1_trailer *trailer) {
530+
if (trailer == NULL) {
531+
return;
532+
}
533+
aws_byte_buf_clean_up(&trailer->trailer_data);
534+
aws_mem_release(trailer->allocator, trailer);
535+
}
536+
446537
struct aws_h1_chunk *aws_h1_chunk_new(struct aws_allocator *allocator, const struct aws_http1_chunk_options *options) {
447538
/* Allocate chunk along with storage for the chunk-line */
448539
struct aws_h1_chunk *chunk;
@@ -748,11 +839,15 @@ static int s_state_fn_chunk_end(struct aws_h1_encoder *encoder, struct aws_byte_
748839

749840
/* Write out trailer after last chunk */
750841
static int s_state_fn_chunk_trailer(struct aws_h1_encoder *encoder, struct aws_byte_buf *dst) {
751-
/* We don't currently have API calls that lets users add trailing headers,
752-
* so just write out the final CRLF */
753-
bool done = s_write_crlf(dst);
842+
bool done;
843+
/* if a chunked trailer was set */
844+
if (encoder->message->trailer) {
845+
done = s_encode_buf(encoder, dst, &encoder->message->trailer->trailer_data);
846+
} else {
847+
done = s_write_crlf(dst);
848+
}
754849
if (!done) {
755-
/* Remain in this state until done writing out CRLF */
850+
/* Remain in this state until we're done writing out trailer */
756851
return AWS_OP_SUCCESS;
757852
}
758853

0 commit comments

Comments
 (0)