@@ -64,6 +64,11 @@ struct elasticurl_ctx {
6464 enum aws_log_level log_level ;
6565 enum aws_http_version required_http_version ;
6666 bool exchange_completed ;
67+ bool manual_write ;
68+ bool manual_write_chunked ;
69+ int64_t manual_write_content_length ;
70+ struct aws_http_stream * stream ;
71+ bool stream_ready ;
6772};
6873
6974static void s_usage (int exit_code ) {
@@ -96,6 +101,7 @@ static void s_usage(int exit_code) {
96101 fprintf (stderr , " --version: print the version of elasticurl.\n" );
97102 fprintf (stderr , " --http2: HTTP/2 connection required\n" );
98103 fprintf (stderr , " --http1_1: HTTP/1.1 connection required\n" );
104+ fprintf (stderr , " --manual-write: interactively write request body via stdin\n" );
99105 fprintf (stderr , " -h, --help\n" );
100106 fprintf (stderr , " Display this message and quit.\n" );
101107 exit (exit_code );
@@ -125,6 +131,7 @@ static struct aws_cli_option s_long_options[] = {
125131 {"version" , AWS_CLI_OPTIONS_NO_ARGUMENT , NULL , 'V' },
126132 {"http2" , AWS_CLI_OPTIONS_NO_ARGUMENT , NULL , 'w' },
127133 {"http1_1" , AWS_CLI_OPTIONS_NO_ARGUMENT , NULL , 'W' },
134+ {"manual-write" , AWS_CLI_OPTIONS_NO_ARGUMENT , NULL , 'n' },
128135 {"help" , AWS_CLI_OPTIONS_NO_ARGUMENT , NULL , 'h' },
129136 /* Per getopt(3) the last element of the array has to be filled with all zeros */
130137 {NULL , AWS_CLI_OPTIONS_NO_ARGUMENT , NULL , 0 },
@@ -162,7 +169,7 @@ static void s_parse_options(int argc, char **argv, struct elasticurl_ctx *ctx) {
162169 while (true) {
163170 int option_index = 0 ;
164171 int c =
165- aws_cli_getopt_long (argc , argv , "a:b:c:e:f:H:d:g:j:l:m:M:GPHiko:t:v:VwWh " , s_long_options , & option_index );
172+ aws_cli_getopt_long (argc , argv , "a:b:c:e:f:H:d:g:j:l:m:M:GPHiko:t:v:VwWnh " , s_long_options , & option_index );
166173 if (c == -1 ) {
167174 break ;
168175 }
@@ -276,6 +283,9 @@ static void s_parse_options(int argc, char **argv, struct elasticurl_ctx *ctx) {
276283 ctx -> alpn = "http/1.1" ;
277284 ctx -> required_http_version = AWS_HTTP_VERSION_1_1 ;
278285 break ;
286+ case 'n' :
287+ ctx -> manual_write = true;
288+ break ;
279289 case 'h' :
280290 s_usage (0 );
281291 break ;
@@ -432,7 +442,26 @@ static struct aws_http_message *s_build_http_request(
432442 };
433443 aws_http_message_add_header (request , user_agent_header );
434444
435- if (app_ctx -> input_body ) {
445+ if (app_ctx -> manual_write ) {
446+ /* Manual write mode: set headers but no body stream.
447+ * H2 doesn't use Transfer-Encoding — just send DATA frames. */
448+ if (app_ctx -> manual_write_chunked && protocol_version != AWS_HTTP_VERSION_2 ) {
449+ struct aws_http_header te_header = {
450+ .name = aws_byte_cursor_from_c_str ("transfer-encoding" ),
451+ .value = aws_byte_cursor_from_c_str ("chunked" ),
452+ };
453+ aws_http_message_add_header (request , te_header );
454+ } else if (!app_ctx -> manual_write_chunked ) {
455+ char content_length [64 ];
456+ AWS_ZERO_ARRAY (content_length );
457+ snprintf (content_length , sizeof (content_length ), "%" PRIi64 , app_ctx -> manual_write_content_length );
458+ struct aws_http_header cl_header = {
459+ .name = aws_byte_cursor_from_c_str ("content-length" ),
460+ .value = aws_byte_cursor_from_c_str (content_length ),
461+ };
462+ aws_http_message_add_header (request , cl_header );
463+ }
464+ } else if (app_ctx -> input_body ) {
436465 int64_t data_len = 0 ;
437466 if (aws_input_stream_get_length (app_ctx -> input_body , & data_len )) {
438467 fprintf (stderr , "failed to get length of input stream.\n" );
@@ -522,6 +551,7 @@ static void s_on_signing_complete(struct aws_http_message *request, int error_co
522551 .on_response_header_block_done = s_on_incoming_header_block_done_fn ,
523552 .on_response_body = s_on_incoming_body_fn ,
524553 .on_complete = s_on_stream_complete_fn ,
554+ .use_manual_data_writes = app_ctx -> manual_write ,
525555 };
526556
527557 app_ctx -> response_code_written = false;
@@ -533,6 +563,15 @@ static void s_on_signing_complete(struct aws_http_message *request, int error_co
533563 }
534564 aws_http_stream_activate (stream );
535565
566+ if (app_ctx -> manual_write ) {
567+ /* Store stream and signal main thread to begin interactive writes */
568+ app_ctx -> stream = stream ;
569+ aws_mutex_lock (& app_ctx -> mutex );
570+ app_ctx -> stream_ready = true;
571+ aws_mutex_unlock (& app_ctx -> mutex );
572+ aws_condition_variable_notify_all (& app_ctx -> c_var );
573+ }
574+
536575 /* Connection will stay alive until stream completes */
537576 aws_http_connection_release (app_ctx -> connection );
538577 app_ctx -> connection = NULL ;
@@ -554,6 +593,103 @@ static bool s_completion_predicate(void *arg) {
554593 return app_ctx -> exchange_completed ;
555594}
556595
596+ static bool s_stream_ready_predicate (void * arg ) {
597+ struct elasticurl_ctx * app_ctx = arg ;
598+ return app_ctx -> stream_ready || app_ctx -> exchange_completed ;
599+ }
600+
601+ struct manual_write_ctx {
602+ struct aws_allocator * allocator ;
603+ uint8_t * data ;
604+ struct aws_input_stream * stream ;
605+ };
606+
607+ static void s_on_manual_write_complete (struct aws_http_stream * stream , int error_code , void * user_data ) {
608+ (void )stream ;
609+ (void )error_code ;
610+ struct manual_write_ctx * ctx = user_data ;
611+ aws_input_stream_release (ctx -> stream );
612+ aws_mem_release (ctx -> allocator , ctx -> data );
613+ aws_mem_release (ctx -> allocator , ctx );
614+ }
615+
616+ static void s_manual_write_loop (struct elasticurl_ctx * app_ctx ) {
617+ /* Wait for stream to be activated */
618+ aws_mutex_lock (& app_ctx -> mutex );
619+ aws_condition_variable_wait_pred (& app_ctx -> c_var , & app_ctx -> mutex , s_stream_ready_predicate , app_ctx );
620+ aws_mutex_unlock (& app_ctx -> mutex );
621+
622+ if (app_ctx -> exchange_completed ) {
623+ return ;
624+ }
625+
626+ int64_t bytes_sent = 0 ;
627+ char line_buf [4096 ];
628+
629+ fprintf (stderr , "Enter data (empty line to finish):\n" );
630+ while (fgets (line_buf , sizeof (line_buf ), stdin )) {
631+ /* Strip trailing newline */
632+ size_t len = strlen (line_buf );
633+ if (len > 0 && line_buf [len - 1 ] == '\n' ) {
634+ line_buf [-- len ] = '\0' ;
635+ }
636+
637+ /* Empty line = done */
638+ if (len == 0 ) {
639+ break ;
640+ }
641+
642+ /* Heap-allocate data so it outlives this stack frame */
643+ uint8_t * heap_data = aws_mem_calloc (app_ctx -> allocator , 1 , len );
644+ memcpy (heap_data , line_buf , len );
645+
646+ struct aws_byte_cursor data_cursor = aws_byte_cursor_from_array (heap_data , len );
647+ struct aws_input_stream * data_stream =
648+ aws_input_stream_new_from_cursor (app_ctx -> allocator , & data_cursor );
649+
650+ struct manual_write_ctx * write_ctx = aws_mem_calloc (app_ctx -> allocator , 1 , sizeof (struct manual_write_ctx ));
651+ write_ctx -> allocator = app_ctx -> allocator ;
652+ write_ctx -> data = heap_data ;
653+ write_ctx -> stream = data_stream ;
654+
655+ struct aws_http_stream_write_data_options write_opts = {
656+ .data = data_stream ,
657+ .end_stream = false,
658+ .on_complete = s_on_manual_write_complete ,
659+ .user_data = write_ctx ,
660+ };
661+
662+ if (aws_http_stream_write_data (app_ctx -> stream , & write_opts )) {
663+ fprintf (stderr , "write_data failed: %s\n" , aws_error_debug_str (aws_last_error ()));
664+ aws_input_stream_release (data_stream );
665+ aws_mem_release (app_ctx -> allocator , heap_data );
666+ aws_mem_release (app_ctx -> allocator , write_ctx );
667+ break ;
668+ }
669+
670+ bytes_sent += (int64_t )len ;
671+ fprintf (stderr , "Sent %zu bytes (total: %" PRIi64 ")\n" , len , bytes_sent );
672+ }
673+
674+ /* Send final write */
675+ struct aws_byte_cursor empty_cursor = aws_byte_cursor_from_c_str ("" );
676+ struct aws_input_stream * empty_stream =
677+ aws_input_stream_new_from_cursor (app_ctx -> allocator , & empty_cursor );
678+
679+ struct aws_http_stream_write_data_options final_opts = {
680+ .data = empty_stream ,
681+ .end_stream = true,
682+ };
683+
684+ if (aws_http_stream_write_data (app_ctx -> stream , & final_opts )) {
685+ fprintf (stderr , "final write_data failed: %s\n" , aws_error_debug_str (aws_last_error ()));
686+ } else {
687+ fprintf (stderr , "Stream complete. Sent %" PRIi64 " bytes.\n" , bytes_sent );
688+ }
689+
690+ aws_input_stream_release (empty_stream );
691+ }
692+
557693int main (int argc , char * * argv ) {
558694 struct aws_allocator * allocator = aws_default_allocator ();
559695
@@ -579,6 +715,24 @@ int main(int argc, char **argv) {
579715
580716 s_parse_options (argc , argv , & app_ctx );
581717
718+ /* Interactive prompt for manual-write mode */
719+ if (app_ctx .manual_write ) {
720+ if (!strcmp (app_ctx .verb , "GET" )) {
721+ app_ctx .verb = "POST" ;
722+ }
723+ fprintf (stderr , "Manual write mode enabled.\n" );
724+ fprintf (stderr , "Content-Length (leave empty for chunked transfer encoding): " );
725+ char cl_buf [64 ];
726+ if (fgets (cl_buf , sizeof (cl_buf ), stdin ) && cl_buf [0 ] != '\n' ) {
727+ app_ctx .manual_write_content_length = (int64_t )atoll (cl_buf );
728+ app_ctx .manual_write_chunked = false;
729+ fprintf (stderr , "Using Content-Length: %" PRIi64 "\n" , app_ctx .manual_write_content_length );
730+ } else {
731+ app_ctx .manual_write_chunked = true;
732+ fprintf (stderr , "Using chunked transfer encoding.\n" );
733+ }
734+ }
735+
582736 struct aws_logger logger ;
583737 AWS_ZERO_STRUCT (logger );
584738
@@ -728,6 +882,9 @@ int main(int argc, char **argv) {
728882 http_client_options .prior_knowledge_http2 = true;
729883 }
730884 aws_http_client_connect (& http_client_options );
885+ if (app_ctx .manual_write ) {
886+ s_manual_write_loop (& app_ctx );
887+ }
731888 aws_mutex_lock (& app_ctx .mutex );
732889 aws_condition_variable_wait_pred (& app_ctx .c_var , & app_ctx .mutex , s_completion_predicate , & app_ctx );
733890 aws_mutex_unlock (& app_ctx .mutex );
0 commit comments