http: implement HTTP PUT method 57/43157/8
authorAndrew Yourtchenko <[email protected]>
Fri, 13 Jun 2025 14:13:27 +0000 (16:13 +0200)
committerFlorin Coras <[email protected]>
Wed, 18 Jun 2025 01:27:56 +0000 (01:27 +0000)
This implements the HTTP PUT request with the ability
to stream the data in chunks, rather than sending
the entire request body at once.

Type: feature
Change-Id: Ib04103a4bacf76a3c0bf9483a63a2edb693276c6
Signed-off-by: Andrew Yourtchenko <[email protected]>
src/plugins/http/http.c
src/plugins/http/http.h
src/plugins/http/http1.c
src/plugins/http/http_buffer.c
src/plugins/http/http_buffer.h
src/plugins/http/http_private.h

index 951bf3a..5a61b0d 100644 (file)
@@ -28,6 +28,7 @@ static http_engine_vft_t *http_vfts;
 const http_buffer_type_t msg_to_buf_type[] = {
   [HTTP_MSG_DATA_INLINE] = HTTP_BUFFER_FIFO,
   [HTTP_MSG_DATA_PTR] = HTTP_BUFFER_PTR,
+  [HTTP_MSG_DATA_STREAMING] = HTTP_BUFFER_STREAMING,
 };
 
 void
index 434ff96..bd9e40e 100644 (file)
@@ -55,6 +55,7 @@ typedef enum http_req_method_
 {
   HTTP_REQ_GET = 0,
   HTTP_REQ_POST,
+  HTTP_REQ_PUT,
   HTTP_REQ_CONNECT,
   HTTP_REQ_UNKNOWN, /* for internal use */
 } http_req_method_t;
@@ -335,7 +336,10 @@ typedef enum http_upgrade_proto_
 typedef enum http_msg_data_type_
 {
   HTTP_MSG_DATA_INLINE,
-  HTTP_MSG_DATA_PTR
+  HTTP_MSG_DATA_PTR,
+  HTTP_MSG_DATA_STREAMING,
+  /* The value below is used for boundary checks of http_msg_data_type_t */
+  HTTP_MSG_DATA_N_TYPES,
 } http_msg_data_type_t;
 
 typedef struct http_field_line_
index f7d79b8..a0aaf06 100644 (file)
@@ -55,6 +55,17 @@ static const char *post_request_template = "POST %s HTTP/1.1\r\n"
                                           "User-Agent: %v\r\n"
                                           "Content-Length: %llu\r\n";
 
+static const char *put_request_template = "PUT %s HTTP/1.1\r\n"
+                                         "Host: %v\r\n"
+                                         "User-Agent: %v\r\n"
+                                         "Content-Length: %llu\r\n";
+
+static const char *put_chunked_request_template =
+  "PUT %s HTTP/1.1\r\n"
+  "Host: %v\r\n"
+  "User-Agent: %v\r\n"
+  "Transfer-Encoding: chunked\r\n";
+
 always_inline http_req_t *
 http1_conn_alloc_req (http_conn_t *hc)
 {
@@ -304,6 +315,12 @@ http1_parse_request_line (http_req_t *req, u8 *rx_buf, http_status_code_t *ec)
       req->method = HTTP_REQ_POST;
       req->target_path_offset = method_offset + 5;
     }
+  else if (!memcmp (rx_buf + method_offset, "PUT ", 4))
+    {
+      HTTP_DBG (0, "PUT method");
+      req->method = HTTP_REQ_PUT;
+      req->target_path_offset = method_offset + 4;
+    }
   else if (!memcmp (rx_buf + method_offset, "CONNECT ", 8))
     {
       HTTP_DBG (0, "CONNECT method");
@@ -1249,7 +1266,7 @@ http1_req_state_wait_app_reply (http_conn_t *hc, http_req_t *req,
 
   http_get_app_msg (req, &msg);
 
-  if (msg.data.type > HTTP_MSG_DATA_PTR)
+  if (msg.data.type >= HTTP_MSG_DATA_N_TYPES)
     {
       clib_warning ("no data");
       sc = HTTP_STATUS_INTERNAL_ERROR;
@@ -1363,7 +1380,7 @@ http1_req_state_wait_app_method (http_conn_t *hc, http_req_t *req,
 
   http_get_app_msg (req, &msg);
 
-  if (msg.data.type > HTTP_MSG_DATA_PTR)
+  if (msg.data.type >= HTTP_MSG_DATA_N_TYPES)
     {
       clib_warning ("no data");
       goto error;
@@ -1431,6 +1448,55 @@ http1_req_state_wait_app_method (http_conn_t *hc, http_req_t *req,
       next_state = HTTP_REQ_STATE_APP_IO_MORE_DATA;
       sm_result = HTTP_SM_CONTINUE;
     }
+  else if (msg.method_type == HTTP_REQ_PUT)
+    {
+      /* Check if this is a streaming PUT */
+      if (msg.data.type == HTTP_MSG_DATA_STREAMING)
+       {
+         /*
+          * Streaming PUT with chunked transfer encoding
+          */
+         request = format (request, put_chunked_request_template,
+                           /* target */
+                           target,
+                           /* Host */
+                           hc->host,
+                           /* User-Agent */
+                           hc->app_name);
+
+         http_req_tx_buffer_init (req, &msg);
+
+         /* For streaming, we need a different state */
+         next_state = HTTP_REQ_STATE_APP_IO_MORE_STREAMING_DATA;
+         sm_result = HTTP_SM_CONTINUE;
+       }
+      else
+       {
+         if (!msg.data.body_len)
+           {
+             clib_warning ("PUT request should include data");
+             goto error;
+           }
+         /*
+          * Regular PUT with Content-Length
+          */
+         request = format (request, put_request_template,
+                           /* target */
+                           target,
+                           /* Host */
+                           hc->host,
+                           /* User-Agent */
+                           hc->app_name,
+                           /* Content-Length */
+                           msg.data.body_len);
+
+         http_req_tx_buffer_init (req, &msg);
+
+         next_state = HTTP_REQ_STATE_APP_IO_MORE_DATA;
+         sm_result = HTTP_SM_CONTINUE;
+       }
+    }
+
   else
     {
       clib_warning ("unsupported method %d", msg.method_type);
@@ -1517,6 +1583,75 @@ check_fifo:
   return HTTP_SM_STOP;
 }
 
+static http_sm_result_t
+http1_req_state_app_io_more_streaming_data (http_conn_t *hc, http_req_t *req,
+                                           transport_send_params_t *sp)
+{
+  u32 max_write, chunk_size, n_segs, n_written = 0;
+  http_buffer_t *hb = &req->tx_buf;
+  svm_fifo_seg_t *seg;
+  int finished = 0;
+  int chunk_sz_value_headroom = 20;
+  u8 chunk_hdr[32];
+  int hdr_len;
+
+  ASSERT (hb->type == HTTP_BUFFER_STREAMING);
+
+  /* For streaming, check if we have data available */
+  max_write = http_io_ts_max_write (hc, sp);
+  /*
+   * do not drain more than we are going to write at a max - which
+   * is max_write minus chunk_sz_value_headroom (overhead for the chunk
+   * size value) bytes to leave the room for chunk headers.
+   */
+  if (max_write < chunk_sz_value_headroom)
+    {
+      HTTP_DBG (1, "ts tx fifo full - before write");
+      goto check_fifo;
+    }
+  chunk_size = http_buffer_get_segs (hb, max_write - chunk_sz_value_headroom,
+                                    &seg, &n_segs);
+  if (chunk_size == 0)
+    {
+      /* No data available right now, wait for more */
+      HTTP_DBG (1, "streaming: no data available");
+      return HTTP_SM_STOP;
+    }
+
+  /* Write chunk size in hex */
+  hdr_len =
+    snprintf ((char *) chunk_hdr, sizeof (chunk_hdr), "%x\r\n", chunk_size);
+  http_io_ts_write (hc, chunk_hdr, hdr_len, sp);
+
+  /* Write chunk data */
+  n_written = http_io_ts_write_segs (hc, seg, n_segs, sp);
+
+  /* Write chunk trailer */
+  http_io_ts_write (hc, (u8 *) "\r\n", 2, sp);
+
+  http_buffer_drain (hb, n_written);
+
+  finished = http_buffer_bytes_left (hb) == 0;
+  if (finished)
+    {
+      /* Send final chunk (0-sized) */
+      http_io_ts_write (hc, (u8 *) "0\r\n\r\n", 5, sp);
+
+      /* Finished transaction:
+       * server back to HTTP_REQ_STATE_WAIT_TRANSPORT_METHOD
+       * client to HTTP_REQ_STATE_WAIT_TRANSPORT_REPLY */
+      http_req_state_change (req, (hc->flags & HTTP_CONN_F_IS_SERVER) ?
+                                   HTTP_REQ_STATE_WAIT_TRANSPORT_METHOD :
+                                   HTTP_REQ_STATE_WAIT_TRANSPORT_REPLY);
+      http_buffer_free (hb);
+    }
+  http_io_ts_after_write (hc, finished);
+
+check_fifo:
+  http1_check_and_deschedule (hc, req, sp);
+  return HTTP_SM_STOP;
+}
+
 static http_sm_result_t
 http1_req_state_tunnel_tx (http_conn_t *hc, http_req_t *req,
                           transport_send_params_t *sp)
@@ -1614,6 +1749,7 @@ static http_sm_handler tx_state_funcs[HTTP_REQ_N_STATES] = {
   http1_req_state_app_io_more_data,
   http1_req_state_tunnel_tx,
   http1_req_state_udp_tunnel_tx,
+  http1_req_state_app_io_more_streaming_data,
 };
 
 static http_sm_handler rx_state_funcs[HTTP_REQ_N_STATES] = {
@@ -1626,6 +1762,7 @@ static http_sm_handler rx_state_funcs[HTTP_REQ_N_STATES] = {
   0, /* app io more data */
   http1_req_state_tunnel_rx,
   http1_req_state_udp_tunnel_rx,
+  0, /* app io more streaming data */
 };
 
 static_always_inline int
index 493abb3..87a1f80 100644 (file)
@@ -16,7 +16,7 @@
 #include <http/http_buffer.h>
 #include <http/http.h>
 
-static http_buffer_vft_t buf_vfts[HTTP_BUFFER_PTR + 1];
+static http_buffer_vft_t buf_vfts[HTTP_BUFFER_N_TYPES];
 
 #define HTTP_BUFFER_REGISTER_VFT(type, vft)                                   \
   static void __attribute__ ((constructor)) http_buf_init_##type (void)       \
@@ -206,6 +206,104 @@ const static http_buffer_vft_t buf_ptr_vft = {
 
 HTTP_BUFFER_REGISTER_VFT (HTTP_BUFFER_PTR, buf_ptr_vft);
 
+typedef struct http_buffer_streaming_
+{
+  svm_fifo_t *src;
+  svm_fifo_seg_t *segs;
+  u64 total_len; /* total expected length (can be ~0 for unknown) */
+  u64 sent;     /* bytes sent so far */
+} http_buffer_streaming_t;
+
+STATIC_ASSERT (sizeof (http_buffer_streaming_t) <= HTTP_BUFFER_DATA_SZ,
+              "buf data");
+
+static void
+buf_streaming_init (http_buffer_t *hb, void *data, u64 len)
+{
+  svm_fifo_t *f = (svm_fifo_t *) data;
+  http_buffer_streaming_t *bs;
+
+  bs = (http_buffer_streaming_t *) &hb->data;
+
+  bs->total_len = len;
+  bs->sent = 0;
+  bs->src = f;
+  bs->segs = 0;
+}
+
+static void
+buf_streaming_free (http_buffer_t *hb)
+{
+  http_buffer_streaming_t *bs = (http_buffer_streaming_t *) &hb->data;
+
+  bs->src = 0;
+  vec_free (bs->segs);
+}
+
+static u32
+buf_streaming_get_segs (http_buffer_t *hb, u32 max_len, svm_fifo_seg_t **fs,
+                       u32 *n_segs)
+{
+  http_buffer_streaming_t *bs = (http_buffer_streaming_t *) &hb->data;
+
+  u32 _n_segs = 5;
+  int len;
+
+  /* For streaming, we send whatever is available */
+  u32 available = svm_fifo_max_dequeue (bs->src);
+  if (available == 0)
+    return 0;
+
+  max_len = clib_min (available, max_len);
+
+  vec_validate (bs->segs, _n_segs - 1);
+
+  len = svm_fifo_segments (bs->src, 0, bs->segs, &_n_segs, max_len);
+  if (len < 0)
+    return 0;
+
+  *n_segs = _n_segs;
+
+  HTTP_DBG (1, "streaming: available to send %u n_segs %u", len, *n_segs);
+
+  *fs = bs->segs;
+  return len;
+}
+
+static u32
+buf_streaming_drain (http_buffer_t *hb, u32 len)
+{
+  http_buffer_streaming_t *bs = (http_buffer_streaming_t *) &hb->data;
+
+  bs->sent += len;
+  svm_fifo_dequeue_drop (bs->src, len);
+  HTTP_DBG (1, "streaming: drained %u total sent %lu", len, bs->sent);
+
+  return len;
+}
+
+static u64
+buf_streaming_bytes_left (http_buffer_t *hb)
+{
+  http_buffer_streaming_t *bs = (http_buffer_streaming_t *) &hb->data;
+  if (bs->total_len == ~0)
+    {
+      return ~0;
+    }
+
+  return (bs->total_len > bs->sent ? (bs->total_len - bs->sent) : 0);
+}
+
+const static http_buffer_vft_t buf_streaming_vft = {
+  .init = buf_streaming_init,
+  .free = buf_streaming_free,
+  .get_segs = buf_streaming_get_segs,
+  .drain = buf_streaming_drain,
+  .bytes_left = buf_streaming_bytes_left,
+};
+
+HTTP_BUFFER_REGISTER_VFT (HTTP_BUFFER_STREAMING, buf_streaming_vft);
+
 void
 http_buffer_init (http_buffer_t *hb, http_buffer_type_t type, svm_fifo_t *f,
                  u64 data_len)
index 6176f10..d487c56 100644 (file)
@@ -24,6 +24,10 @@ typedef enum http_buffer_type_
 {
   HTTP_BUFFER_FIFO,
   HTTP_BUFFER_PTR,
+  HTTP_BUFFER_STREAMING,
+  /* the value below is used to size the structures indexed by
+     http_buffer_type_t */
+  HTTP_BUFFER_N_TYPES,
 } http_buffer_type_t;
 
 typedef struct http_buffer_vft_ http_buffer_vft_t;
index a7898cf..5ad940d 100644 (file)
@@ -65,7 +65,8 @@ typedef enum http_conn_state_
   _ (5, WAIT_APP_REPLY, "wait app reply")                                     \
   _ (6, APP_IO_MORE_DATA, "app io more data")                                 \
   _ (7, TUNNEL, "tunnel")                                                     \
-  _ (8, UDP_TUNNEL, "udp tunnel")
+  _ (8, UDP_TUNNEL, "udp tunnel")                                             \
+  _ (9, APP_IO_MORE_STREAMING_DATA, "app io more streaming data")
 
 typedef enum http_req_state_
 {