@@ -127,14 +127,6 @@ static int s_scan_outgoing_headers(
127127 return aws_raise_error (AWS_ERROR_HTTP_INVALID_HEADER_FIELD );
128128 }
129129
130- if (encoder_message -> has_chunked_encoding_header && has_body_stream ) {
131- AWS_LOGF_ERROR (
132- AWS_LS_HTTP_STREAM ,
133- "id=static: Both Transfer-Encoding chunked header and body stream is set. "
134- "chunked data must use the chunk API to write the body stream." );
135- return aws_raise_error (AWS_ERROR_HTTP_INVALID_BODY_STREAM );
136- }
137-
138130 if (body_headers_forbidden && (encoder_message -> content_length > 0 || has_transfer_encoding_header )) {
139131 AWS_LOGF_ERROR (
140132 AWS_LS_HTTP_STREAM ,
@@ -663,7 +655,7 @@ static int s_encode_stream(
663655 err = aws_input_stream_get_status (stream , & status );
664656 if (err ) {
665657 ENCODER_LOGF (
666- TRACE ,
658+ ERROR ,
667659 encoder ,
668660 "Failed to query body stream status, error %d (%s)" ,
669661 aws_last_error (),
@@ -729,7 +721,10 @@ static int s_state_fn_head(struct aws_h1_encoder *encoder, struct aws_byte_buf *
729721
730722 /* Pick next state */
731723 if (encoder -> message -> body && encoder -> message -> content_length ) {
732- return s_switch_state (encoder , AWS_H1_ENCODER_STATE_UNCHUNKED_BODY );
724+ return s_switch_state (encoder , AWS_H1_ENCODER_STATE_UNCHUNKED_BODY_STREAM );
725+
726+ } else if (encoder -> message -> body && encoder -> message -> has_chunked_encoding_header ) {
727+ return s_switch_state (encoder , AWS_H1_ENCODER_STATE_CHUNKED_BODY_STREAM );
733728
734729 } else if (encoder -> message -> has_chunked_encoding_header ) {
735730 return s_switch_state (encoder , AWS_H1_ENCODER_STATE_CHUNK_NEXT );
@@ -739,8 +734,8 @@ static int s_state_fn_head(struct aws_h1_encoder *encoder, struct aws_byte_buf *
739734 }
740735}
741736
742- /* Write out body (not using chunked encoding). */
743- static int s_state_fn_unchunked_body (struct aws_h1_encoder * encoder , struct aws_byte_buf * dst ) {
737+ /* Write out body with known Content-Length (not using chunked encoding). */
738+ static int s_state_fn_unchunked_body_stream (struct aws_h1_encoder * encoder , struct aws_byte_buf * dst ) {
744739 bool done ;
745740 if (s_encode_stream (encoder , dst , encoder -> message -> body , encoder -> message -> content_length , & done )) {
746741 return AWS_OP_ERR ;
@@ -755,6 +750,133 @@ static int s_state_fn_unchunked_body(struct aws_h1_encoder *encoder, struct aws_
755750 return s_switch_state (encoder , AWS_H1_ENCODER_STATE_DONE );
756751}
757752
753+ /* Write out body (of unknown Content-Length) using chunked encoding.
754+ * Each pass through this state writes out 1 chunk of body data (or nothing at all). */
755+ static int s_state_fn_chunked_body_stream (struct aws_h1_encoder * encoder , struct aws_byte_buf * dst ) {
756+
757+ /* Each chunk is prefixed with: CHUNK-LENGTH-IN-ASCII-HEX CRLF
758+ * and suffixed with: CRLF
759+ *
760+ * When reading from the stream, we don't know how much data we'll get,
761+ * but the length needs to go in the prefix, before the data!
762+ * Therefore, leave space at start of dst buffer for the prefix,
763+ * we'll go back and write it AFTER streaming the body data.
764+ * Leave space at the end for the suffix too.
765+ *
766+ * Use a predictable length for the prefix.
767+ * 8 hex chars (i.e. "000000F7") seems reasonable (4 is too small, 16 is ridiculous, dynamic is complicated). */
768+ enum { padded_hex_len = 8 }; /* enum, because it's used as size for stack array */
769+ const char * padded_hex_fmt = "%08zX" ;
770+ const size_t max_hex_value_given_padding = UINT32_MAX ; /* fits in 8 chars */
771+ const size_t chunk_prefix_len = padded_hex_len + CRLF_SIZE ;
772+ const size_t chunk_suffix_len = CRLF_SIZE ;
773+
774+ /* If dst buffer nearly full, don't bother reading from stream.
775+ * Remain in this state and we'll get a fresh buffer next tick. */
776+ const size_t dont_bother_if_space_less_than = 128 ; /* magic number, seems reasonable */
777+ AWS_ASSERT (dont_bother_if_space_less_than > chunk_prefix_len + chunk_suffix_len );
778+ if (dst -> capacity - dst -> len < dont_bother_if_space_less_than ) {
779+ /* If this buffer is empty, and still not big enough, just give up.
780+ * Probably never happens, but g_aws_channel_max_fragment_size can theoretically be tweaked by user. */
781+ if (dst -> len == 0 ) {
782+ AWS_LOGF_ERROR (
783+ AWS_LS_HTTP_STREAM , "id=%p Channel max fragment size is too small." , (void * )encoder -> current_stream );
784+ return aws_raise_error (AWS_ERROR_INVALID_STATE );
785+ }
786+
787+ /* Remain in this state and we'll get a fresh buffer next tick */
788+ return AWS_OP_SUCCESS ;
789+ }
790+
791+ /* Use a sub-buffer to limit where body can go.
792+ * Body will go after chunk-prefix, and needs to leave enough space for chunk-suffix afterwards. */
793+ uint8_t * body_sub_buf_start = dst -> buffer + dst -> len + chunk_prefix_len ;
794+ uint8_t * body_sub_buf_end = dst -> buffer + dst -> capacity - chunk_suffix_len ;
795+ struct aws_byte_buf body_sub_buf =
796+ aws_byte_buf_from_empty_array (body_sub_buf_start , body_sub_buf_end - body_sub_buf_start );
797+ /* We set aside a fixed number of bytes to encode the length, don't read more than that */
798+ body_sub_buf .capacity = aws_min_size (body_sub_buf .capacity , max_hex_value_given_padding );
799+
800+ /* Stream body into sub-buffer */
801+ ENCODER_LOG (TRACE , encoder , "Reading from body stream." );
802+ if (aws_input_stream_read (encoder -> message -> body , & body_sub_buf ) != AWS_OP_SUCCESS ) {
803+ ENCODER_LOGF (
804+ ERROR ,
805+ encoder ,
806+ "Failed to read body stream, error %d (%s)" ,
807+ aws_last_error (),
808+ aws_error_name (aws_last_error ()));
809+ return AWS_OP_ERR ;
810+ }
811+
812+ /* If ANY body data was streamed, then write in chunk prefix and suffix.
813+ *
814+ * (else no body data streamed, so dst remains untouched. Maybe we've
815+ * reached end of stream, maybe user just doesn't have data yet to send) */
816+ if (body_sub_buf .len > 0 ) {
817+ encoder -> chunk_count ++ ;
818+ ENCODER_LOGF (
819+ TRACE , encoder , "Sending chunk #%" PRIu64 " with size %zu" , encoder -> chunk_count , body_sub_buf .len );
820+ bool wrote_all = true;
821+
822+ /* Write chunk-prefix: LENGTH-IN-HEX CRLF */
823+ char hexbuf [padded_hex_len + 1 ] = {0 };
824+ AWS_ASSERT (body_sub_buf .len <= max_hex_value_given_padding ); /* guaranteed, b/c we clamped .capacity earlier */
825+ snprintf (hexbuf , sizeof (hexbuf ), padded_hex_fmt , body_sub_buf .len );
826+
827+ wrote_all &= aws_byte_buf_write_from_whole_cursor (dst , aws_byte_cursor_from_c_str (hexbuf ));
828+ wrote_all &= s_write_crlf (dst );
829+
830+ /* Increment dst->len, since we already copied body in there via sub-buffer */
831+ AWS_ASSERT (dst -> buffer + dst -> len == body_sub_buf_start ); /* written chunk-prefix should end at body start */
832+ dst -> len += body_sub_buf .len ; /* safe b/c we clamped body_sub_buf.capacity earlier */
833+
834+ /* Write chunk-suffix: CRLF */
835+ wrote_all &= s_write_crlf (dst );
836+
837+ AWS_ASSERT (wrote_all ); /* everything should have fit, we did a lot of math and clamping to guarantee it */
838+ (void )wrote_all ;
839+ }
840+
841+ /* If body stream has ended: switch states.
842+ * As an optimization, we only do this check when the stream didn't 100% fill the buffer */
843+ if (body_sub_buf .len < body_sub_buf .capacity ) {
844+ struct aws_stream_status stream_status ;
845+ if (aws_input_stream_get_status (encoder -> message -> body , & stream_status ) != AWS_OP_SUCCESS ) {
846+ ENCODER_LOGF (
847+ ERROR ,
848+ encoder ,
849+ "Failed to query body stream status, error %d (%s)" ,
850+ aws_last_error (),
851+ aws_error_name (aws_last_error ()));
852+ return AWS_OP_ERR ;
853+ }
854+
855+ if (stream_status .is_end_of_stream ) {
856+ encoder -> chunk_count ++ ;
857+ ENCODER_LOGF (TRACE , encoder , "Sending last chunk #%" PRIu64 , encoder -> chunk_count );
858+ return s_switch_state (encoder , AWS_H1_ENCODER_STATE_CHUNKED_BODY_STREAM_LAST_CHUNK );
859+ }
860+ }
861+
862+ /* Remain in state until done streaming body */
863+ return AWS_OP_SUCCESS ;
864+ }
865+
866+ /* Note: this state is ONLY used when streaming a body of unknown Content-Length.
867+ * It is NOT used when the write_chunk() API is being used. */
868+ static int s_state_fn_chunked_body_stream_last_chunk (struct aws_h1_encoder * encoder , struct aws_byte_buf * dst ) {
869+
870+ struct aws_byte_cursor last_chunk = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL ("0\r\n" );
871+ if (aws_byte_buf_write_from_whole_cursor (dst , last_chunk ) == true) {
872+ ENCODER_LOG (TRACE , encoder , "Last chunk complete" );
873+ return s_switch_state (encoder , AWS_H1_ENCODER_STATE_CHUNK_TRAILER );
874+ } else {
875+ /* Remain in state until there's enough space to write */
876+ return AWS_OP_SUCCESS ;
877+ }
878+ }
879+
758880/* Select next chunk to work on.
759881 * Encoder is essentially "paused" here if no chunks are available. */
760882static int s_state_fn_chunk_next (struct aws_h1_encoder * encoder , struct aws_byte_buf * dst ) {
@@ -773,7 +895,7 @@ static int s_state_fn_chunk_next(struct aws_h1_encoder *encoder, struct aws_byte
773895 ENCODER_LOGF (
774896 TRACE ,
775897 encoder ,
776- "Begin sending chunk %zu with size %" PRIu64 ,
898+ "Begin sending chunk #%" PRIu64 " with size %" PRIu64 ,
777899 encoder -> chunk_count ,
778900 encoder -> current_chunk -> data_size );
779901
@@ -871,7 +993,10 @@ struct encoder_state_def {
871993static struct encoder_state_def s_encoder_states [] = {
872994 [AWS_H1_ENCODER_STATE_INIT ] = {.fn = s_state_fn_init , .name = "INIT" },
873995 [AWS_H1_ENCODER_STATE_HEAD ] = {.fn = s_state_fn_head , .name = "HEAD" },
874- [AWS_H1_ENCODER_STATE_UNCHUNKED_BODY ] = {.fn = s_state_fn_unchunked_body , .name = "BODY" },
996+ [AWS_H1_ENCODER_STATE_UNCHUNKED_BODY_STREAM ] = {.fn = s_state_fn_unchunked_body_stream , .name = "BODY" },
997+ [AWS_H1_ENCODER_STATE_CHUNKED_BODY_STREAM ] = {.fn = s_state_fn_chunked_body_stream , .name = "CHUNKED_BODY_STREAM" },
998+ [AWS_H1_ENCODER_STATE_CHUNKED_BODY_STREAM_LAST_CHUNK ] =
999+ {.fn = s_state_fn_chunked_body_stream_last_chunk , .name = "LAST_CHUNK" },
8751000 [AWS_H1_ENCODER_STATE_CHUNK_NEXT ] = {.fn = s_state_fn_chunk_next , .name = "CHUNK_NEXT" },
8761001 [AWS_H1_ENCODER_STATE_CHUNK_LINE ] = {.fn = s_state_fn_chunk_line , .name = "CHUNK_LINE" },
8771002 [AWS_H1_ENCODER_STATE_CHUNK_BODY ] = {.fn = s_state_fn_chunk_body , .name = "CHUNK_BODY" },
0 commit comments