Add support for chunk-encoded client request bodies
authorFabian Keil <fk@fabiankeil.de>
Fri, 7 Dec 2012 12:43:55 +0000 (12:43 +0000)
committerFabian Keil <fk@fabiankeil.de>
Fri, 7 Dec 2012 12:43:55 +0000 (12:43 +0000)
It's simplistic and requires the whole request body to be buffered
before the request is forwarded. If the buffer limit is reached,
the request is rejected.

Previously chunk-encoded request bodies weren't guaranteed
to be forwarded correctly, so this can also be considered a
bug fix although chunk-encoded request bodies aren't commonly
used in the real world.

jcc.c
parsers.c
parsers.h
project.h

diff --git a/jcc.c b/jcc.c
index 919e199..6967ad8 100644 (file)
--- a/jcc.c
+++ b/jcc.c
@@ -1,4 +1,4 @@
-const char jcc_rcs[] = "$Id: jcc.c,v 1.417 2012/11/24 14:04:29 fabiankeil Exp $";
+const char jcc_rcs[] = "$Id: jcc.c,v 1.418 2012/12/07 12:43:05 fabiankeil Exp $";
 /*********************************************************************
  *
  * File        :  $Source: /cvsroot/ijbswa/current/jcc.c,v $
@@ -268,6 +268,13 @@ static const char CLIENT_CONNECTION_TIMEOUT_RESPONSE[] =
    "Connection: close\r\n\r\n"
    "The connection timed out because the client request didn't arrive in time.\r\n";
 
+static const char CLIENT_BODY_PARSE_ERROR_RESPONSE[] =
+   "HTTP/1.1 400 Failed reading client body\r\n"
+   "Proxy-Agent: Privoxy " VERSION "\r\n"
+   "Content-Type: text/plain\r\n"
+   "Connection: close\r\n\r\n"
+   "Failed parsing or buffering the chunk-encoded client body.\r\n";
+
 /* A function to crunch a response */
 typedef struct http_response *(*crunch_func_ptr)(struct client_state *);
 
@@ -1273,6 +1280,146 @@ static char *get_request_line(struct client_state *csp)
 
 }
 
+enum chunk_status
+{
+   CHUNK_STATUS_MISSING_DATA,
+   CHUNK_STATUS_BODY_COMPLETE,
+   CHUNK_STATUS_PARSE_ERROR
+};
+
+
+/*********************************************************************
+ *
+ * Function    :  chunked_body_is_complete
+ *
+ * Description :  Figures out wheter or not a chunked body is complete.
+ *
+ *                Currently it always starts at the beginning of the
+ *                buffer which is somewhat wasteful and prevents Privoxy
+ *                from starting to forward the correctly parsed chunks
+ *                as soon as theoretically possible.
+ *
+ *                Should be modified to work with a common buffer,
+ *                and allow the caller to skip already parsed chunks.
+ *
+ *                This would allow the function to be used for unbuffered
+ *                response bodies as well.
+ *
+ * Parameters  :
+ *          1  :  iob = Buffer with the body to check.
+ *          2  :  length = Length of complete body
+ *
+ * Returns     :  Enum with the result of the check.
+ *
+ *********************************************************************/
+static enum chunk_status chunked_body_is_complete(struct iob *iob, size_t *length)
+{
+   unsigned int chunksize;
+   char *p = iob->cur;
+
+   do
+   {
+      /*
+       * We need at least a single digit, followed by "\r\n",
+       * followed by an unknown amount of data, followed by "\r\n".
+       */
+      if (p + 5 > iob->eod)
+      {
+         return CHUNK_STATUS_MISSING_DATA;
+      }
+      if (sscanf(p, "%x", &chunksize) != 1)
+      {
+         return CHUNK_STATUS_PARSE_ERROR;
+      }
+
+      /*
+       * We want at least a single digit, followed by "\r\n",
+       * followed by the specified amount of data, followed by "\r\n".
+       */
+      if (p + chunksize + 5 > iob->eod)
+      {
+         return CHUNK_STATUS_MISSING_DATA;
+      }
+
+      /* Skip chunk-size. */
+      p = strstr(p, "\r\n");
+      if (NULL == p)
+      {
+         return CHUNK_STATUS_PARSE_ERROR;
+      }
+      /*
+       * Skip "\r\n", the chunk data and another "\r\n".
+       * Moving p to either the beginning of the next chunk-size
+       * or one byte beyond the end of the chunked data.
+       */
+      p += 2 + chunksize + 2;
+   } while (chunksize > 0U);
+
+   *length = (size_t)(p - iob->cur);
+   assert(*length <= (size_t)(iob->eod - iob->cur));
+   assert(p <= iob->eod);
+
+   return CHUNK_STATUS_BODY_COMPLETE;
+
+}
+
+
+/*********************************************************************
+ *
+ * Function    : receive_chunked_client_request_body
+ *
+ * Description : Read the chunk-encoded client request body.
+ *               Failures are dealt with.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *
+ * Returns     :  JB_ERR_OK or JB_ERR_PARSE
+ *
+ *********************************************************************/
+static jb_err receive_chunked_client_request_body(struct client_state *csp)
+{
+   size_t body_length;
+   enum chunk_status status;
+
+   while (CHUNK_STATUS_MISSING_DATA ==
+      (status = chunked_body_is_complete(csp->client_iob,&body_length)))
+   {
+      char buf[BUFFER_SIZE];
+      int len;
+
+      if (!data_is_available(csp->cfd, csp->config->socket_timeout))
+      {
+         log_error(LOG_LEVEL_ERROR,
+            "Timeout while waiting for the client body.");
+         break;
+      }
+      len = read_socket(csp->cfd, buf, sizeof(buf) - 1);
+      if (len <= 0)
+      {
+         log_error(LOG_LEVEL_ERROR, "Read the client body failed: %E");
+         break;
+      }
+      if (add_to_iob(csp->client_iob, csp->config->buffer_limit, buf, len))
+      {
+         break;
+      }
+   }
+   if (status != CHUNK_STATUS_BODY_COMPLETE)
+   {
+      write_socket(csp->cfd, CLIENT_BODY_PARSE_ERROR_RESPONSE,
+         strlen(CLIENT_BODY_PARSE_ERROR_RESPONSE));
+      log_error(LOG_LEVEL_CLF,
+         "%s - - [%T] \"Failed reading chunked client body\" 400 0", csp->ip_addr_str);
+      return JB_ERR_PARSE;
+   }
+   log_error(LOG_LEVEL_CONNECT,
+      "Chunked client body completely read. Length: %d", body_length);
+   csp->expected_client_content_length = body_length;
+
+   return JB_ERR_OK;
+
+}
 
 /*********************************************************************
  *
@@ -1402,6 +1549,14 @@ static jb_err receive_client_request(struct client_state *csp)
       }
       else
       {
+         if (!strncmpic(p, "Transfer-Encoding:", 18))
+         {
+            /*
+             * XXX: should be called through sed()
+             *      but currently can't.
+             */
+            client_transfer_encoding(csp, &p);
+         }
          /*
           * We were able to read a complete
           * header and can finally enlist it.
@@ -1495,7 +1650,21 @@ static jb_err parse_client_request(struct client_state *csp)
 
    if (csp->http->ssl == 0)
    {
-      csp->expected_client_content_length = get_expected_content_length(csp->headers);
+      /*
+       * This whole block belongs to chat() but currently
+       * has to be executed before sed().
+       */
+      if (csp->flags & CSP_FLAG_CHUNKED_CLIENT_BODY)
+      {
+         if (receive_chunked_client_request_body(csp) != JB_ERR_OK)
+         {
+            return JB_ERR_PARSE;
+         }
+      }
+      else
+      {
+         csp->expected_client_content_length = get_expected_content_length(csp->headers);
+      }
       verify_request_length(csp);
    }
 #endif /* def FEATURE_CONNECTION_KEEP_ALIVE */
index 70a57a2..96fab7f 100644 (file)
--- a/parsers.c
+++ b/parsers.c
@@ -1,4 +1,4 @@
-const char parsers_rcs[] = "$Id: parsers.c,v 1.268 2012/11/24 14:07:57 fabiankeil Exp $";
+const char parsers_rcs[] = "$Id: parsers.c,v 1.269 2012/11/24 14:09:11 fabiankeil Exp $";
 /*********************************************************************
  *
  * File        :  $Source: /cvsroot/ijbswa/current/parsers.c,v $
@@ -198,6 +198,9 @@ static const struct parsers client_patterns[] = {
    { "Request-Range:",           14,   client_range },
    { "If-Range:",                 9,   client_range },
    { "X-Filter:",                 9,   client_x_filter },
+#if 0
+   { "Transfer-Encoding:",       18,   client_transfer_encoding },
+#endif
    { "*",                         0,   crunch_client_header },
    { "*",                         0,   filter_header },
    { NULL,                        0,   NULL }
@@ -1979,6 +1982,38 @@ static jb_err client_proxy_connection(struct client_state *csp, char **header)
 #endif  /* def FEATURE_CONNECTION_KEEP_ALIVE */
 
 
+/*********************************************************************
+ *
+ * Function    :  client_transfer_encoding
+ *
+ * Description :  Raise the CSP_FLAG_CHUNKED_CLIENT_BODY flag if
+ *                the request body is "chunked"
+ *
+ *                XXX: Currently not called through sed() as we
+ *                     need the flag earlier on. Should be fixed.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *          2  :  header = On input, pointer to header to modify.
+ *                On output, pointer to the modified header, or NULL
+ *                to remove the header.  This function frees the
+ *                original string if necessary.
+ *
+ * Returns     :  JB_ERR_OK on success, or
+ *
+ *********************************************************************/
+jb_err client_transfer_encoding(struct client_state *csp, char **header)
+{
+   if (strstr(*header, "chunked"))
+   {
+      csp->flags |= CSP_FLAG_CHUNKED_CLIENT_BODY;
+      log_error(LOG_LEVEL_HEADER, "Expecting chunked client body");
+   }
+
+   return JB_ERR_OK;
+}
+
+
 /*********************************************************************
  *
  * Function    :  crumble
index ce774c9..7f958a5 100644 (file)
--- a/parsers.h
+++ b/parsers.h
@@ -1,6 +1,6 @@
 #ifndef PARSERS_H_INCLUDED
 #define PARSERS_H_INCLUDED
-#define PARSERS_H_VERSION "$Id: parsers.h,v 1.53 2012/10/21 12:39:27 fabiankeil Exp $"
+#define PARSERS_H_VERSION "$Id: parsers.h,v 1.54 2012/10/21 12:58:03 fabiankeil Exp $"
 /*********************************************************************
  *
  * File        :  $Source: /cvsroot/ijbswa/current/parsers.h,v $
@@ -65,6 +65,7 @@ extern jb_err update_server_headers(struct client_state *csp);
 extern void get_http_time(int time_offset, char *buf, size_t buffer_size);
 extern jb_err get_destination_from_headers(const struct list *headers, struct http_request *http);
 extern unsigned long long get_expected_content_length(struct list *headers);
+extern jb_err client_transfer_encoding(struct client_state *csp, char **header);
 
 #ifdef FEATURE_FORCE_LOAD
 extern int strclean(char *string, const char *substring);
index e308ce5..eba76a6 100644 (file)
--- a/project.h
+++ b/project.h
@@ -1,7 +1,7 @@
 #ifndef PROJECT_H_INCLUDED
 #define PROJECT_H_INCLUDED
 /** Version string. */
-#define PROJECT_H_VERSION "$Id: project.h,v 1.192 2012/11/11 12:38:42 fabiankeil Exp $"
+#define PROJECT_H_VERSION "$Id: project.h,v 1.193 2012/11/24 13:55:51 fabiankeil Exp $"
 /*********************************************************************
  *
  * File        :  $Source: /cvsroot/ijbswa/current/project.h,v $
@@ -836,6 +836,12 @@ struct reusable_connection
  */
 #define CSP_FLAG_PIPELINED_REQUEST_WAITING          0x00800000U
 
+/**
+ * Flag for csp->flags: Set if the client body is chunk-encoded
+ */
+#define CSP_FLAG_CHUNKED_CLIENT_BODY                0x01000000U
+
+
 /*
  * Flags for use in return codes of child processes
  */