diff --git a/include/aws/http/private/h1_encoder.h b/include/aws/http/private/h1_encoder.h index 5ebe38ce2..11b4965c0 100644 --- a/include/aws/http/private/h1_encoder.h +++ b/include/aws/http/private/h1_encoder.h @@ -19,6 +19,11 @@ struct aws_h1_chunk { struct aws_byte_buf chunk_line; }; +struct aws_h1_trailer { + struct aws_allocator *allocator; + struct aws_byte_buf trailer_data; +}; + /** * Message to be submitted to encoder. * Contains data necessary for encoder to write an outgoing request or response. @@ -36,6 +41,9 @@ struct aws_h1_encoder_message { * A chunk with data_size=0 means "final chunk" */ struct aws_linked_list *pending_chunk_list; + /* Pointer to chunked_trailer, used for chunked_trailer. */ + struct aws_h1_trailer *trailer; + /* If non-zero, length of unchunked body to send */ uint64_t content_length; bool has_connection_close_header; @@ -71,6 +79,11 @@ struct aws_h1_encoder { }; struct aws_h1_chunk *aws_h1_chunk_new(struct aws_allocator *allocator, const struct aws_http1_chunk_options *options); +struct aws_h1_trailer *aws_h1_trailer_new( + struct aws_allocator *allocator, + const struct aws_http_headers *trailing_headers); + +void aws_h1_trailer_destroy(struct aws_h1_trailer *trailer); /* Just destroy the chunk (don't fire callback) */ void aws_h1_chunk_destroy(struct aws_h1_chunk *chunk); diff --git a/include/aws/http/private/h1_stream.h b/include/aws/http/private/h1_stream.h index 8ce59bffc..df1446ec9 100644 --- a/include/aws/http/private/h1_stream.h +++ b/include/aws/http/private/h1_stream.h @@ -62,6 +62,8 @@ struct aws_h1_stream { * Encoder completes/frees/pops front chunk when it's done sending. */ struct aws_linked_list pending_chunk_list; + struct aws_h1_encoder_message message; + /* Size of stream's flow-control window. * Only body data (not headers, etc) counts against the stream's flow-control window. */ uint64_t stream_window; @@ -79,6 +81,10 @@ struct aws_h1_stream { * but haven't yet moved to encoder_message.pending_chunk_list where the encoder will find them. */ struct aws_linked_list pending_chunk_list; + /* trailing headers which have been submitted by user, + * but haven't yet moved to encoder_message where the encoder will find them. */ + struct aws_h1_trailer *pending_trailer; + enum aws_h1_stream_api_state api_state; /* 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 { /* Whether the outgoing message is using chunked encoding */ bool using_chunked_encoding : 1; + + /* Whether the final 0 length chunk has already been sent */ + bool has_final_chunk : 1; + + /* Whether the chunked trailer has already been sent */ + bool has_added_trailer : 1; } synced_data; }; diff --git a/include/aws/http/private/http_impl.h b/include/aws/http/private/http_impl.h index e095fe374..da7424b88 100644 --- a/include/aws/http/private/http_impl.h +++ b/include/aws/http/private/http_impl.h @@ -42,7 +42,28 @@ enum aws_http_header_name { AWS_HTTP_HEADER_EXPECT, AWS_HTTP_HEADER_TRANSFER_ENCODING, AWS_HTTP_HEADER_COOKIE, + AWS_HTTP_HEADER_SET_COOKIE, AWS_HTTP_HEADER_HOST, + AWS_HTTP_HEADER_CACHE_CONTROL, + AWS_HTTP_HEADER_MAX_FORWARDS, + AWS_HTTP_HEADER_PRAGMA, + AWS_HTTP_HEADER_RANGE, + AWS_HTTP_HEADER_TE, + AWS_HTTP_HEADER_CONTENT_ENCODING, + AWS_HTTP_HEADER_CONTENT_TYPE, + AWS_HTTP_HEADER_CONTENT_RANGE, + AWS_HTTP_HEADER_TRAILER, + AWS_HTTP_HEADER_WWW_AUTHENTICATE, + AWS_HTTP_HEADER_AUTHORIZATION, + AWS_HTTP_HEADER_PROXY_AUTHENTICATE, + AWS_HTTP_HEADER_PROXY_AUTHORIZATION, + AWS_HTTP_HEADER_AGE, + AWS_HTTP_HEADER_EXPIRES, + AWS_HTTP_HEADER_DATE, + AWS_HTTP_HEADER_LOCATION, + AWS_HTTP_HEADER_RETRY_AFTER, + AWS_HTTP_HEADER_VARY, + AWS_HTTP_HEADER_WARNING, AWS_HTTP_HEADER_COUNT, /* Number of enums */ }; diff --git a/include/aws/http/private/request_response_impl.h b/include/aws/http/private/request_response_impl.h index f8342697c..d84687fcb 100644 --- a/include/aws/http/private/request_response_impl.h +++ b/include/aws/http/private/request_response_impl.h @@ -18,6 +18,7 @@ struct aws_http_stream_vtable { int (*activate)(struct aws_http_stream *stream); int (*http1_write_chunk)(struct aws_http_stream *http1_stream, const struct aws_http1_chunk_options *options); + int (*http1_add_trailer)(struct aws_http_stream *http1_stream, const struct aws_http_headers *trailing_headers); int (*http2_reset_stream)(struct aws_http_stream *http2_stream, uint32_t http2_error); int (*http2_get_received_error_code)(struct aws_http_stream *http2_stream, uint32_t *http2_error); diff --git a/include/aws/http/request_response.h b/include/aws/http/request_response.h index ecca085fd..f906ff5e5 100644 --- a/include/aws/http/request_response.h +++ b/include/aws/http/request_response.h @@ -628,6 +628,27 @@ AWS_HTTP_API int aws_http1_stream_write_chunk( struct aws_http_stream *http1_stream, const struct aws_http1_chunk_options *options); +/** + * Add a list of headers to be added as trailing headers sent after the last chunk is sent. + * The stream must have specified "chunked" in a "transfer-encoding" header. The stream should also have + * a "Trailer" header field which indicates the fields present in the trailer. + * + * Certain headers are forbidden in the trailer (e.g., Transfer-Encoding, Content-Length, Host). See RFC-7541 + * Section 4.1.2 for more details. + * + * For client streams, activate() must be called before any chunks are submitted. + * + * For server streams, the response must be submitted before the trailer can be added + * + * aws_http1_stream_add_chunked_trailer must be called before the final size 0 chunk, and at the moment can only + * be called once, though this could change if need be. + * + * Returns AWS_OP_SUCCESS if the chunk has been submitted. + */ +AWS_HTTP_API int aws_http1_stream_add_chunked_trailer( + struct aws_http_stream *http1_stream, + const struct aws_http_headers *trailing_headers); + /** * Get the message's aws_http_headers. * diff --git a/source/h1_encoder.c b/source/h1_encoder.c index d35af599b..a66143f53 100644 --- a/source/h1_encoder.c +++ b/source/h1_encoder.c @@ -156,20 +156,84 @@ static int s_scan_outgoing_headers( return AWS_OP_SUCCESS; } +static int s_scan_outgoing_trailer(const struct aws_http_headers *headers, size_t *out_size) { + const size_t num_headers = aws_http_headers_count(headers); + size_t total = 0; + for (size_t i = 0; i < num_headers; i++) { + struct aws_http_header header; + aws_http_headers_get_index(headers, i, &header); + /* Validate header field-name (RFC-7230 3.2): field-name = token */ + if (!aws_strutil_is_http_token(header.name)) { + AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=static: Header name is invalid"); + return aws_raise_error(AWS_ERROR_HTTP_INVALID_HEADER_NAME); + } + + /* Validate header field-value. + * The value itself isn't supposed to have whitespace on either side, + * but we'll trim it off before validation so we don't start needlessly + * failing requests that used to work before we added validation. + * This should be OK because field-value can be sent with any amount + * of whitespace around it, which the other side will just ignore (RFC-7230 3.2): + * header-field = field-name ":" OWS field-value OWS */ + struct aws_byte_cursor field_value = aws_strutil_trim_http_whitespace(header.value); + if (!aws_strutil_is_http_field_value(field_value)) { + AWS_LOGF_ERROR( + AWS_LS_HTTP_STREAM, + "id=static: Header '" PRInSTR "' has invalid value", + AWS_BYTE_CURSOR_PRI(header.name)); + return aws_raise_error(AWS_ERROR_HTTP_INVALID_HEADER_VALUE); + } + + enum aws_http_header_name name_enum = aws_http_str_to_header_name(header.name); + if (name_enum == AWS_HTTP_HEADER_TRANSFER_ENCODING || name_enum == AWS_HTTP_HEADER_CONTENT_LENGTH || + name_enum == AWS_HTTP_HEADER_HOST || name_enum == AWS_HTTP_HEADER_EXPECT || + name_enum == AWS_HTTP_HEADER_CACHE_CONTROL || name_enum == AWS_HTTP_HEADER_MAX_FORWARDS || + name_enum == AWS_HTTP_HEADER_PRAGMA || name_enum == AWS_HTTP_HEADER_RANGE || + name_enum == AWS_HTTP_HEADER_TE || name_enum == AWS_HTTP_HEADER_CONTENT_ENCODING || + name_enum == AWS_HTTP_HEADER_CONTENT_TYPE || name_enum == AWS_HTTP_HEADER_CONTENT_RANGE || + name_enum == AWS_HTTP_HEADER_TRAILER || name_enum == AWS_HTTP_HEADER_WWW_AUTHENTICATE || + name_enum == AWS_HTTP_HEADER_AUTHORIZATION || name_enum == AWS_HTTP_HEADER_PROXY_AUTHENTICATE || + name_enum == AWS_HTTP_HEADER_PROXY_AUTHORIZATION || name_enum == AWS_HTTP_HEADER_SET_COOKIE || + name_enum == AWS_HTTP_HEADER_COOKIE || name_enum == AWS_HTTP_HEADER_AGE || + name_enum == AWS_HTTP_HEADER_EXPIRES || name_enum == AWS_HTTP_HEADER_DATE || + name_enum == AWS_HTTP_HEADER_LOCATION || name_enum == AWS_HTTP_HEADER_RETRY_AFTER || + name_enum == AWS_HTTP_HEADER_VARY || name_enum == AWS_HTTP_HEADER_WARNING) { + AWS_LOGF_ERROR( + AWS_LS_HTTP_STREAM, + "id=static: Trailing Header '" PRInSTR "' has invalid value", + AWS_BYTE_CURSOR_PRI(header.name)); + return aws_raise_error(AWS_ERROR_HTTP_INVALID_HEADER_FIELD); + } + + int err = 0; + err |= aws_add_size_checked(header.name.len, total, &total); + err |= aws_add_size_checked(header.value.len, total, &total); + err |= aws_add_size_checked(4, total, &total); /* ": " + "\r\n" */ + if (err) { + return AWS_OP_ERR; + } + } + if (aws_add_size_checked(2, total, &total)) { /* "\r\n" */ + return AWS_OP_ERR; + } + *out_size = total; + return AWS_OP_SUCCESS; +} + static bool s_write_crlf(struct aws_byte_buf *dst) { AWS_PRECONDITION(aws_byte_buf_is_valid(dst)); struct aws_byte_cursor crlf_cursor = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("\r\n"); return aws_byte_buf_write_from_whole_cursor(dst, crlf_cursor); } -static void s_write_headers(struct aws_byte_buf *dst, const struct aws_http_message *message) { +static void s_write_headers(struct aws_byte_buf *dst, const struct aws_http_headers *headers) { - const size_t num_headers = aws_http_message_get_header_count(message); + const size_t num_headers = aws_http_headers_count(headers); bool wrote_all = true; for (size_t i = 0; i < num_headers; ++i) { struct aws_http_header header; - aws_http_message_get_header(message, &header, i); + aws_http_headers_get_index(headers, i, &header); /* header-line: "{name}: {value}\r\n" */ wrote_all &= aws_byte_buf_write_from_whole_cursor(dst, header.name); @@ -264,7 +328,7 @@ int aws_h1_encoder_message_init_from_request( wrote_all &= aws_byte_buf_write_from_whole_cursor(&message->outgoing_head_buf, version); wrote_all &= s_write_crlf(&message->outgoing_head_buf); - s_write_headers(&message->outgoing_head_buf, request); + s_write_headers(&message->outgoing_head_buf, aws_http_message_get_const_headers(request)); wrote_all &= s_write_crlf(&message->outgoing_head_buf); (void)wrote_all; @@ -352,7 +416,7 @@ int aws_h1_encoder_message_init_from_response( wrote_all &= aws_byte_buf_write_from_whole_cursor(&message->outgoing_head_buf, status_text); wrote_all &= s_write_crlf(&message->outgoing_head_buf); - s_write_headers(&message->outgoing_head_buf, response); + s_write_headers(&message->outgoing_head_buf, aws_http_message_get_const_headers(response)); wrote_all &= s_write_crlf(&message->outgoing_head_buf); (void)wrote_all; @@ -368,6 +432,7 @@ int aws_h1_encoder_message_init_from_response( void aws_h1_encoder_message_clean_up(struct aws_h1_encoder_message *message) { aws_byte_buf_clean_up(&message->outgoing_head_buf); + aws_h1_trailer_destroy(message->trailer); AWS_ZERO_STRUCT(*message); } @@ -443,6 +508,32 @@ static void s_populate_chunk_line_buffer( AWS_ASSERT(wrote_chunk_line); } +struct aws_h1_trailer *aws_h1_trailer_new( + struct aws_allocator *allocator, + const struct aws_http_headers *trailing_headers) { + /* Allocate trailer along with storage for the trailer-line */ + size_t trailer_size = 0; + if (s_scan_outgoing_trailer(trailing_headers, &trailer_size)) { + return NULL; + } + + struct aws_h1_trailer *trailer = aws_mem_calloc(allocator, 1, sizeof(struct aws_h1_trailer)); + trailer->allocator = allocator; + + aws_byte_buf_init(&trailer->trailer_data, allocator, trailer_size); /* cannot fail */ + s_write_headers(&trailer->trailer_data, trailing_headers); + s_write_crlf(&trailer->trailer_data); /* \r\n */ + return trailer; +} + +void aws_h1_trailer_destroy(struct aws_h1_trailer *trailer) { + if (trailer == NULL) { + return; + } + aws_byte_buf_clean_up(&trailer->trailer_data); + aws_mem_release(trailer->allocator, trailer); +} + struct aws_h1_chunk *aws_h1_chunk_new(struct aws_allocator *allocator, const struct aws_http1_chunk_options *options) { /* Allocate chunk along with storage for the chunk-line */ struct aws_h1_chunk *chunk; @@ -748,11 +839,15 @@ static int s_state_fn_chunk_end(struct aws_h1_encoder *encoder, struct aws_byte_ /* Write out trailer after last chunk */ static int s_state_fn_chunk_trailer(struct aws_h1_encoder *encoder, struct aws_byte_buf *dst) { - /* We don't currently have API calls that lets users add trailing headers, - * so just write out the final CRLF */ - bool done = s_write_crlf(dst); + bool done; + /* if a chunked trailer was set */ + if (encoder->message->trailer) { + done = s_encode_buf(encoder, dst, &encoder->message->trailer->trailer_data); + } else { + done = s_write_crlf(dst); + } if (!done) { - /* Remain in this state until done writing out CRLF */ + /* Remain in this state until we're done writing out trailer */ return AWS_OP_SUCCESS; } diff --git a/source/h1_stream.c b/source/h1_stream.c index 1bbc1b0c2..eb0c4bfb6 100644 --- a/source/h1_stream.c +++ b/source/h1_stream.c @@ -61,6 +61,9 @@ static void s_stream_cross_thread_work_task(struct aws_channel_task *task, void bool found_chunks = !aws_linked_list_empty(&stream->synced_data.pending_chunk_list); aws_linked_list_move_all_back(&stream->thread_data.pending_chunk_list, &stream->synced_data.pending_chunk_list); + stream->encoder_message.trailer = stream->synced_data.pending_trailer; + stream->synced_data.pending_trailer = NULL; + bool has_outgoing_response = stream->synced_data.has_outgoing_response; uint64_t pending_window_update = stream->synced_data.pending_window_update; @@ -184,7 +187,17 @@ static int s_stream_write_chunk(struct aws_http_stream *stream_base, const struc goto unlock; } + if (stream->synced_data.has_final_chunk) { + AWS_LOGF_ERROR( + AWS_LS_HTTP_STREAM, "id=%p: Cannot write additional chunk after final chunk.", (void *)stream_base); + error_code = AWS_ERROR_INVALID_STATE; + goto unlock; + } + /* success */ + if (chunk->data_size == 0) { + stream->synced_data.has_final_chunk = true; + } aws_linked_list_push_back(&stream->synced_data.pending_chunk_list, &chunk->node); should_schedule_task = !stream->synced_data.is_cross_thread_work_task_scheduled; stream->synced_data.is_cross_thread_work_task_scheduled = true; @@ -225,11 +238,99 @@ static int s_stream_write_chunk(struct aws_http_stream *stream_base, const struc return AWS_OP_SUCCESS; } +static int s_stream_add_trailer(struct aws_http_stream *stream_base, const struct aws_http_headers *trailing_headers) { + AWS_PRECONDITION(stream_base); + AWS_PRECONDITION(trailing_headers); + struct aws_h1_stream *stream = AWS_CONTAINER_OF(stream_base, struct aws_h1_stream, base); + + struct aws_h1_trailer *trailer = aws_h1_trailer_new(stream_base->alloc, trailing_headers); + if (AWS_UNLIKELY(NULL == trailer)) { + AWS_LOGF_ERROR( + AWS_LS_HTTP_STREAM, + "id=%p: Failed to initialize streamed trailer, error %d (%s).", + (void *)stream_base, + aws_last_error(), + aws_error_name(aws_last_error())); + return AWS_OP_ERR; + } + + int error_code = 0; + bool should_schedule_task = false; + + { /* BEGIN CRITICAL SECTION */ + s_stream_lock_synced_data(stream); + /* Can only add trailers while stream is active. */ + if (stream->synced_data.api_state != AWS_H1_STREAM_API_STATE_ACTIVE) { + error_code = (stream->synced_data.api_state == AWS_H1_STREAM_API_STATE_INIT) + ? AWS_ERROR_HTTP_STREAM_NOT_ACTIVATED + : AWS_ERROR_HTTP_STREAM_HAS_COMPLETED; + goto unlock; + } + + if (!stream->synced_data.using_chunked_encoding) { + AWS_LOGF_ERROR( + AWS_LS_HTTP_STREAM, + "id=%p: Cannot write trailers without 'transfer-encoding: chunked' header.", + (void *)stream_base); + error_code = AWS_ERROR_INVALID_STATE; + goto unlock; + } + + if (stream->synced_data.has_added_trailer) { + AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=%p: Cannot write trailers twice.", (void *)stream_base); + error_code = AWS_ERROR_INVALID_STATE; + goto unlock; + } + + if (stream->synced_data.has_final_chunk) { + AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=%p: Cannot write trailers after final chunk.", (void *)stream_base); + error_code = AWS_ERROR_INVALID_STATE; + goto unlock; + } + + stream->synced_data.has_added_trailer = true; + stream->synced_data.pending_trailer = trailer; + should_schedule_task = !stream->synced_data.is_cross_thread_work_task_scheduled; + stream->synced_data.is_cross_thread_work_task_scheduled = true; + + unlock: + s_stream_unlock_synced_data(stream); + } /* END CRITICAL SECTION */ + + if (error_code) { + AWS_LOGF_ERROR( + AWS_LS_HTTP_STREAM, + "id=%p: Failed to add trailer, error %d (%s)", + (void *)stream_base, + error_code, + aws_error_name(error_code)); + + aws_h1_trailer_destroy(trailer); + return aws_raise_error(error_code); + } + + AWS_LOGF_TRACE(AWS_LS_HTTP_STREAM, "id=%p: Adding trailer to stream", (void *)stream); + + if (should_schedule_task) { + /* Keep stream alive until task completes */ + aws_atomic_fetch_add(&stream->base.refcount, 1); + AWS_LOGF_TRACE(AWS_LS_HTTP_STREAM, "id=%p: Scheduling stream cross-thread work task.", (void *)stream_base); + aws_channel_schedule_task_now( + stream->base.owning_connection->channel_slot->channel, &stream->cross_thread_work_task); + } else { + AWS_LOGF_TRACE( + AWS_LS_HTTP_STREAM, "id=%p: Stream cross-thread work task was already scheduled.", (void *)stream_base); + } + + return AWS_OP_SUCCESS; +} + static const struct aws_http_stream_vtable s_stream_vtable = { .destroy = s_stream_destroy, .update_window = s_stream_update_window, .activate = aws_h1_stream_activate, .http1_write_chunk = s_stream_write_chunk, + .http1_add_trailer = s_stream_add_trailer, .http2_reset_stream = NULL, .http2_get_received_error_code = NULL, .http2_get_sent_error_code = NULL, diff --git a/source/http.c b/source/http.c index 8bb74906f..528a6c9b6 100644 --- a/source/http.c +++ b/source/http.c @@ -279,11 +279,32 @@ static void s_headers_init(struct aws_allocator *alloc) { 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_SET_COOKIE] = aws_byte_cursor_from_c_str("set-cookie"); s_header_enum_to_str[AWS_HTTP_HEADER_HOST] = aws_byte_cursor_from_c_str("host"); 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"); s_header_enum_to_str[AWS_HTTP_HEADER_TRANSFER_ENCODING] = aws_byte_cursor_from_c_str("transfer-encoding"); + s_header_enum_to_str[AWS_HTTP_HEADER_CACHE_CONTROL] = aws_byte_cursor_from_c_str("cache-control"); + s_header_enum_to_str[AWS_HTTP_HEADER_MAX_FORWARDS] = aws_byte_cursor_from_c_str("max-forwards"); + s_header_enum_to_str[AWS_HTTP_HEADER_PRAGMA] = aws_byte_cursor_from_c_str("pragma"); + s_header_enum_to_str[AWS_HTTP_HEADER_RANGE] = aws_byte_cursor_from_c_str("range"); + s_header_enum_to_str[AWS_HTTP_HEADER_TE] = aws_byte_cursor_from_c_str("te"); + s_header_enum_to_str[AWS_HTTP_HEADER_CONTENT_ENCODING] = aws_byte_cursor_from_c_str("content-encoding"); + s_header_enum_to_str[AWS_HTTP_HEADER_CONTENT_TYPE] = aws_byte_cursor_from_c_str("content-type"); + s_header_enum_to_str[AWS_HTTP_HEADER_CONTENT_RANGE] = aws_byte_cursor_from_c_str("content-range"); + s_header_enum_to_str[AWS_HTTP_HEADER_TRAILER] = aws_byte_cursor_from_c_str("trailer"); + s_header_enum_to_str[AWS_HTTP_HEADER_WWW_AUTHENTICATE] = aws_byte_cursor_from_c_str("www-authenticate"); + s_header_enum_to_str[AWS_HTTP_HEADER_AUTHORIZATION] = aws_byte_cursor_from_c_str("authorization"); + s_header_enum_to_str[AWS_HTTP_HEADER_PROXY_AUTHENTICATE] = aws_byte_cursor_from_c_str("proxy-authenticate"); + s_header_enum_to_str[AWS_HTTP_HEADER_PROXY_AUTHORIZATION] = aws_byte_cursor_from_c_str("proxy-authorization"); + s_header_enum_to_str[AWS_HTTP_HEADER_AGE] = aws_byte_cursor_from_c_str("age"); + s_header_enum_to_str[AWS_HTTP_HEADER_EXPIRES] = aws_byte_cursor_from_c_str("expires"); + s_header_enum_to_str[AWS_HTTP_HEADER_DATE] = aws_byte_cursor_from_c_str("date"); + s_header_enum_to_str[AWS_HTTP_HEADER_LOCATION] = aws_byte_cursor_from_c_str("location"); + s_header_enum_to_str[AWS_HTTP_HEADER_RETRY_AFTER] = aws_byte_cursor_from_c_str("retry-after"); + s_header_enum_to_str[AWS_HTTP_HEADER_VARY] = aws_byte_cursor_from_c_str("vary"); + s_header_enum_to_str[AWS_HTTP_HEADER_WARNING] = aws_byte_cursor_from_c_str("warning"); s_init_str_to_enum_hash_table( &s_header_str_to_enum, diff --git a/source/request_response.c b/source/request_response.c index 68822f936..b9dc7f6ad 100644 --- a/source/request_response.c +++ b/source/request_response.c @@ -573,6 +573,23 @@ int aws_http1_stream_write_chunk(struct aws_http_stream *http1_stream, const str return http1_stream->vtable->http1_write_chunk(http1_stream, options); } +int aws_http1_stream_add_chunked_trailer( + struct aws_http_stream *http1_stream, + const struct aws_http_headers *trailing_headers) { + AWS_PRECONDITION(http1_stream); + AWS_PRECONDITION(http1_stream->vtable); + AWS_PRECONDITION(trailing_headers); + if (!http1_stream->vtable->http1_add_trailer) { + AWS_LOGF_TRACE( + AWS_LS_HTTP_STREAM, + "id=%p: HTTP/1 stream only function invoked on other stream, ignoring call.", + (void *)http1_stream); + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + + return http1_stream->vtable->http1_add_trailer(http1_stream, trailing_headers); +} + struct aws_input_stream *aws_http_message_get_body_stream(const struct aws_http_message *message) { AWS_PRECONDITION(message); return message->body_stream; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 517808dbd..f511235fe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -74,6 +74,9 @@ add_test_case(h1_client_request_send_1liner) add_test_case(h1_client_request_send_headers) add_test_case(h1_client_request_send_body) add_test_case(h1_client_request_send_body_chunked) +add_test_case(h1_client_request_send_chunked_trailer) +add_test_case(h1_client_request_forbidden_trailer) +add_test_case(h1_client_request_send_empty_chunked_trailer) add_test_case(h1_client_request_send_large_body) add_test_case(h1_client_request_send_large_body_chunked) add_test_case(h1_client_request_send_large_head) diff --git a/tests/test_h1_client.c b/tests/test_h1_client.c index 8115f5d78..1f90ebeda 100644 --- a/tests/test_h1_client.c +++ b/tests/test_h1_client.c @@ -326,6 +326,409 @@ H1_CLIENT_TEST_CASE(h1_client_request_send_body_chunked) { return AWS_OP_SUCCESS; } +int chunked_test_helper( + const struct aws_byte_cursor *body, + struct aws_http_headers *trailers, + const char *expected, + struct tester tester, + struct aws_allocator *allocator) { + + /* send request */ + struct aws_http_message *request = s_new_default_chunked_put_request(allocator); + struct aws_http_make_request_options opt = { + .self_size = sizeof(opt), + .request = request, + }; + struct aws_http_stream *stream = aws_http_connection_make_request(tester.connection, &opt); + ASSERT_NOT_NULL(stream); + + /* Initialize and send the stream chunks */ + ASSERT_SUCCESS(aws_http_stream_activate(stream)); + if (body != NULL) { + struct aws_input_stream *body_stream = aws_input_stream_new_from_cursor(allocator, body); + struct aws_http1_chunk_options options = s_default_chunk_options(body_stream, body->len); + ASSERT_SUCCESS(aws_http1_stream_write_chunk(stream, &options)); + } + ASSERT_SUCCESS(aws_http1_stream_add_chunked_trailer(stream, trailers)); + ASSERT_SUCCESS(s_write_termination_chunk(allocator, stream)); + + testing_channel_drain_queued_tasks(&tester.testing_channel); + + /* check result */ + ASSERT_SUCCESS(testing_channel_check_written_messages_str(&tester.testing_channel, allocator, expected)); + + /* clean up */ + aws_http_message_destroy(request); + aws_http_stream_release(stream); + return AWS_OP_SUCCESS; +} + +int chunked_trailer_succeed( + const struct aws_byte_cursor *body, + struct aws_http_headers *trailers, + struct tester tester, + struct aws_allocator *allocator) { + + /* send request */ + struct aws_http_message *request = s_new_default_chunked_put_request(allocator); + struct aws_http_make_request_options opt = { + .self_size = sizeof(opt), + .request = request, + }; + struct aws_http_stream *stream = aws_http_connection_make_request(tester.connection, &opt); + ASSERT_NOT_NULL(stream); + + /* Initialize and send the stream chunks */ + ASSERT_SUCCESS(aws_http_stream_activate(stream)); + if (body != NULL) { + struct aws_input_stream *body_stream = aws_input_stream_new_from_cursor(allocator, body); + struct aws_http1_chunk_options options = s_default_chunk_options(body_stream, body->len); + ASSERT_SUCCESS(aws_http1_stream_write_chunk(stream, &options)); + } + + /* kind of gross, but good enough for now */ + int err = aws_http1_stream_add_chunked_trailer(stream, trailers); + if (err) { + aws_http_message_destroy(request); + aws_http_stream_release(stream); + return err; + } + ASSERT_SUCCESS(s_write_termination_chunk(allocator, stream)); + + testing_channel_drain_queued_tasks(&tester.testing_channel); + + /* clean up */ + aws_http_message_destroy(request); + aws_http_stream_release(stream); + return AWS_OP_SUCCESS; +} + +H1_CLIENT_TEST_CASE(h1_client_request_send_chunked_trailer) { + (void)ctx; + struct tester tester; + ASSERT_SUCCESS(s_tester_init(&tester, allocator)); + struct aws_http_headers *trailers = aws_http_headers_new(allocator); + const struct aws_http_header trailer = { + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("chunked"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("trailer"), + }; + const struct aws_http_header trailer1 = { + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("another"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("test"), + }; + aws_http_headers_add_header(trailers, &trailer); + aws_http_headers_add_header(trailers, &trailer1); + + /* Initialize and send the stream chunks */ + static const struct aws_byte_cursor body = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("write more tests"); + + const char *expected = "PUT /plan.txt HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "10\r\n" + "write more tests" + "\r\n" + "0\r\n" + "chunked: trailer\r\n" + "another: test\r\n" + "\r\n"; + + ASSERT_SUCCESS(chunked_test_helper(&body, trailers, expected, tester, allocator)); + /* clean up */ + aws_http_headers_release(trailers); + ASSERT_SUCCESS(s_tester_clean_up(&tester)); + return AWS_OP_SUCCESS; +} + +H1_CLIENT_TEST_CASE(h1_client_request_send_empty_chunked_trailer) { + (void)ctx; + struct tester tester; + ASSERT_SUCCESS(s_tester_init(&tester, allocator)); + struct aws_http_headers *trailers = aws_http_headers_new(allocator); + + /* Initialize and send the stream chunks */ + static const struct aws_byte_cursor body = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("write more tests"); + + const char *expected = "PUT /plan.txt HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "10\r\n" + "write more tests" + "\r\n" + "0\r\n" + "\r\n"; + + ASSERT_SUCCESS(chunked_test_helper(&body, trailers, expected, tester, allocator)); + /* clean up */ + aws_http_headers_release(trailers); + ASSERT_SUCCESS(s_tester_clean_up(&tester)); + return AWS_OP_SUCCESS; +} + +H1_CLIENT_TEST_CASE(h1_client_request_forbidden_trailer) { + (void)ctx; + struct tester tester; + ASSERT_SUCCESS(s_tester_init(&tester, allocator)); + + struct aws_http_headers *success = aws_http_headers_new(allocator); + aws_http_headers_add_header( + success, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("should"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("succeed"), + }); + struct aws_http_headers *transfer_encoding = aws_http_headers_new(allocator); + aws_http_headers_add_header( + transfer_encoding, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Transfer-Encoding"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("gzip, chunked"), + }); + struct aws_http_headers *content_length = aws_http_headers_new(allocator); + aws_http_headers_add_header( + content_length, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Content-Length"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("3495"), + }); + struct aws_http_headers *host = aws_http_headers_new(allocator); + aws_http_headers_add_header( + host, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Host"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("www.example.org"), + }); + struct aws_http_headers *cache_control = aws_http_headers_new(allocator); + aws_http_headers_add_header( + cache_control, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Cache-Control"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("private"), + }); + struct aws_http_headers *expect = aws_http_headers_new(allocator); + aws_http_headers_add_header( + expect, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Expect"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("100-continue"), + }); + struct aws_http_headers *max_forwards = aws_http_headers_new(allocator); + aws_http_headers_add_header( + max_forwards, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("max-forwards"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("123"), + }); + struct aws_http_headers *pragma = aws_http_headers_new(allocator); + aws_http_headers_add_header( + pragma, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("pragma"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("no-cache"), + }); + struct aws_http_headers *range = aws_http_headers_new(allocator); + aws_http_headers_add_header( + range, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("range"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("bytes=0-1023"), + }); + struct aws_http_headers *te = aws_http_headers_new(allocator); + aws_http_headers_add_header( + te, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("te"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("trailers, deflate;q=0.5"), + }); + struct aws_http_headers *www_authenticate = aws_http_headers_new(allocator); + aws_http_headers_add_header( + www_authenticate, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("www-authenticate"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL( + "Newauth realm=\"apps\", type=1,title=\"Login to \"apps\"\", Basic realm=\"simple\""), + }); + struct aws_http_headers *authorization = aws_http_headers_new(allocator); + aws_http_headers_add_header( + authorization, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("authorization"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("credentials"), + }); + struct aws_http_headers *proxy_authenticate = aws_http_headers_new(allocator); + aws_http_headers_add_header( + proxy_authenticate, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("proxy-authenticate"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Basic YWxhZGRpbjpvcGVuc2VzYW1l"), + }); + struct aws_http_headers *proxy_authorization = aws_http_headers_new(allocator); + aws_http_headers_add_header( + proxy_authorization, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("proxy-authorization"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("credentials"), + }); + struct aws_http_headers *set_cookie = aws_http_headers_new(allocator); + aws_http_headers_add_header( + set_cookie, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("set-cookie"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("sessionId=38afes7a8"), + }); + struct aws_http_headers *cookie = aws_http_headers_new(allocator); + aws_http_headers_add_header( + cookie, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("cookie"), + .value = + AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1"), + }); + struct aws_http_headers *age = aws_http_headers_new(allocator); + aws_http_headers_add_header( + age, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("age"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("24"), + }); + struct aws_http_headers *expires = aws_http_headers_new(allocator); + aws_http_headers_add_header( + expires, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("expires"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Wed, 21 Oct 2015 07:28:00 GMT"), + }); + struct aws_http_headers *date = aws_http_headers_new(allocator); + aws_http_headers_add_header( + date, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("date"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Wed, 21 Oct 2015 07:28:00 GMT"), + }); + struct aws_http_headers *location = aws_http_headers_new(allocator); + aws_http_headers_add_header( + location, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("location"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("/index.html"), + }); + struct aws_http_headers *retry_after = aws_http_headers_new(allocator); + aws_http_headers_add_header( + retry_after, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("retry-after"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("120"), + }); + struct aws_http_headers *vary = aws_http_headers_new(allocator); + aws_http_headers_add_header( + vary, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("vary"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("User-Agent"), + }); + + struct aws_http_headers *warning = aws_http_headers_new(allocator); + aws_http_headers_add_header( + warning, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("warning"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("110 anderson/1.3.37 \"Response is stale\""), + }); + struct aws_http_headers *content_encoding = aws_http_headers_new(allocator); + aws_http_headers_add_header( + content_encoding, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("content-encoding"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("gzip"), + }); + + struct aws_http_headers *content_type = aws_http_headers_new(allocator); + aws_http_headers_add_header( + content_type, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("content-type"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("text/html"), + }); + struct aws_http_headers *content_range = aws_http_headers_new(allocator); + aws_http_headers_add_header( + content_range, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("content-range"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("bytes 200-1000/67589"), + }); + + struct aws_http_headers *trailer = aws_http_headers_new(allocator); + aws_http_headers_add_header( + trailer, + &(struct aws_http_header){ + .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("trailer"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Expires"), + }); + + ASSERT_SUCCESS(chunked_trailer_succeed(NULL, success, tester, allocator)); + ASSERT_ERROR( + AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, transfer_encoding, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, content_length, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, host, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, cache_control, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, expect, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, max_forwards, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, pragma, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, range, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, te, tester, allocator)); + ASSERT_ERROR( + AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, www_authenticate, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, authorization, tester, allocator)); + ASSERT_ERROR( + AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, proxy_authenticate, tester, allocator)); + ASSERT_ERROR( + AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, proxy_authorization, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, set_cookie, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, cookie, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, age, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, expires, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, date, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, location, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, retry_after, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, vary, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, warning, tester, allocator)); + ASSERT_ERROR( + AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, content_encoding, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, content_type, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, content_range, tester, allocator)); + ASSERT_ERROR(AWS_ERROR_HTTP_INVALID_HEADER_FIELD, chunked_trailer_succeed(NULL, trailer, tester, allocator)); + /* clean up */ + aws_http_headers_release(success); + aws_http_headers_release(transfer_encoding); + aws_http_headers_release(content_length); + aws_http_headers_release(host); + aws_http_headers_release(cache_control); + aws_http_headers_release(expect); + aws_http_headers_release(max_forwards); + aws_http_headers_release(pragma); + aws_http_headers_release(range); + aws_http_headers_release(te); + aws_http_headers_release(www_authenticate); + aws_http_headers_release(authorization); + aws_http_headers_release(proxy_authenticate); + aws_http_headers_release(proxy_authorization); + aws_http_headers_release(set_cookie); + aws_http_headers_release(cookie); + aws_http_headers_release(age); + aws_http_headers_release(expires); + aws_http_headers_release(date); + aws_http_headers_release(location); + aws_http_headers_release(retry_after); + aws_http_headers_release(vary); + aws_http_headers_release(warning); + aws_http_headers_release(content_encoding); + aws_http_headers_release(content_type); + aws_http_headers_release(content_range); + aws_http_headers_release(trailer); + ASSERT_SUCCESS(s_tester_clean_up(&tester)); + return AWS_OP_SUCCESS; +} + H1_CLIENT_TEST_CASE(h1_client_request_send_chunked_extensions) { (void)ctx; struct tester tester;