http: supported chunked Transfer-Encoding
authorScott Griepentrog <sgriepentrog@digium.com>
Fri, 17 Jan 2014 20:51:19 +0000 (20:51 +0000)
committerScott Griepentrog <sgriepentrog@digium.com>
Fri, 17 Jan 2014 20:51:19 +0000 (20:51 +0000)
This change implements support for HTTP Transfer-Encoding
chunked in both JSON and Form (post vars) body content. A
new function ast_http_get_contents() handles both regular
and chunked mode body, returning after the entire body is
received.

(closes issue ASTERISK-23068)
Reported by: Matt Jordan
Review: https://reviewboard.asterisk.org/r/3125/
........

Merged revisions 405861 from http://svn.asterisk.org/svn/asterisk/branches/12

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@405862 65c4cc65-6c06-0410-ace0-fbb531ad65f3

main/http.c

index af1ef81..e867de1 100644 (file)
@@ -688,11 +688,187 @@ static const char *get_transfer_encoding(struct ast_variable *headers)
        return get_header(headers, "Transfer-Encoding");
 }
 
+/*!
+ * \brief decode chunked mode hexadecimal value
+ *
+ * \param s string to decode
+ * \param len length of string
+ * \return integer value or -1 for decode error
+ */
+static int chunked_atoh(const char *s, int len)
+{
+       int value = 0;
+       char c;
+
+       if (*s < '0') {
+               /* zero value must be 0\n not just \n */
+               return -1;
+       }
+
+       while (len--)
+       {
+               if (*s == '\x0D') {
+                       return value;
+               }
+               value <<= 4;
+               c = *s++;
+               if (c >= '0' && c <= '9') {
+                       value += c - '0';
+                       continue;
+               }
+               if (c >= 'a' && c <= 'f') {
+                       value += 10 + c - 'a';
+                       continue;
+               }
+               if (c >= 'A' && c <= 'F') {
+                       value += 10 + c - 'A';
+                       continue;
+               }
+               /* invalid character */
+               return -1;
+       }
+       /* end of string */
+       return -1;
+}
+
+/*!
+ * \brief Returns the contents (body) of the HTTP request
+ *
+ * \param return_length ptr to int that returns content length
+ * \param aser HTTP TCP/TLS session object
+ * \param headers List of HTTP headers
+ * \return ptr to content (zero terminated) or NULL on failure
+ * \note Since returned ptr is malloc'd, it should be free'd by caller
+ */
+static char *ast_http_get_contents(int *return_length,
+       struct ast_tcptls_session_instance *ser, struct ast_variable *headers)
+{
+       const char *transfer_encoding;
+       int res;
+       int content_length = 0;
+       int chunk_length;
+       char chunk_header[8];
+       int bufsize = 250;
+       char *buf;
+
+       transfer_encoding = get_transfer_encoding(headers);
+
+       if (ast_strlen_zero(transfer_encoding) ||
+               strcasecmp(transfer_encoding, "chunked") != 0) {
+               /* handle regular non-chunked content */
+               content_length = get_content_length(headers);
+               if (content_length <= 0) {
+                       /* no content - not an error */
+                       return NULL;
+               }
+               if (content_length > MAX_POST_CONTENT - 1) {
+                       ast_log(LOG_WARNING,
+                               "Excessively long HTTP content. (%d > %d)\n",
+                               content_length, MAX_POST_CONTENT);
+                       errno = EFBIG;
+                       return NULL;
+               }
+               buf = ast_malloc(content_length + 1);
+               if (!buf) {
+                       /* Malloc sets ENOMEM */
+                       return NULL;
+               }
+               res = fread(buf, 1, content_length, ser->f);
+               if (res < content_length) {
+                       /* Error, distinguishable by ferror() or feof(), but neither
+                        * is good. Treat either one as I/O error */
+                       ast_log(LOG_WARNING, "Short HTTP request body (%d < %d)\n",
+                               res, content_length);
+                       errno = EIO;
+                       ast_free(buf);
+                       return NULL;
+               }
+               buf[content_length] = 0;
+               *return_length = content_length;
+               return buf;
+       }
+
+       /* pre-allocate buffer */
+       buf = ast_malloc(bufsize);
+       if (!buf) {
+               return NULL;
+       }
+
+       /* handled chunked content */
+       do {
+               /* get the line of hexadecimal giving chunk size */
+               if (!fgets(chunk_header, sizeof(chunk_header), ser->f)) {
+                       ast_log(LOG_WARNING,
+                               "Short HTTP read of chunked header\n");
+                       errno = EIO;
+                       ast_free(buf);
+                       return NULL;
+               }
+               chunk_length = chunked_atoh(chunk_header, sizeof(chunk_header));
+               if (chunk_length < 0) {
+                       ast_log(LOG_WARNING, "Invalid HTTP chunk size\n");
+                       errno = EIO;
+                       ast_free(buf);
+                       return NULL;
+               }
+               if (content_length + chunk_length > MAX_POST_CONTENT - 1) {
+                       ast_log(LOG_WARNING,
+                               "Excessively long HTTP chunk. (%d + %d > %d)\n",
+                               content_length, chunk_length, MAX_POST_CONTENT);
+                       errno = EFBIG;
+                       ast_free(buf);
+                       return NULL;
+               }
+
+               /* insure buffer is large enough +1 */
+               if (content_length + chunk_length >= bufsize)
+               {
+                       bufsize *= 2;
+                       buf = ast_realloc(buf, bufsize);
+                       if (!buf) {
+                               return NULL;
+                       }
+               }
+
+               /* read the chunk */
+               res = fread(buf + content_length, 1, chunk_length, ser->f);
+               if (res < chunk_length) {
+                       ast_log(LOG_WARNING, "Short HTTP chunk read (%d < %d)\n",
+                               res, chunk_length);
+                       errno = EIO;
+                       ast_free(buf);
+                       return NULL;
+               }
+               content_length += chunk_length;
+
+               /* insure the next 2 bytes are CRLF */
+               res = fread(chunk_header, 1, 2, ser->f);
+               if (res < 2) {
+                       ast_log(LOG_WARNING,
+                               "Short HTTP chunk sync read (%d < 2)\n", res);
+                       errno = EIO;
+                       ast_free(buf);
+                       return NULL;
+               }
+               if (chunk_header[0] != 0x0D || chunk_header[1] != 0x0A) {
+                       ast_log(LOG_WARNING,
+                               "Post HTTP chunk sync bytes wrong (%d, %d)\n",
+                               chunk_header[0], chunk_header[1]);
+                       errno = EIO;
+                       ast_free(buf);
+                       return NULL;
+               }
+       } while (chunk_length);
+
+       buf[content_length] = 0;
+       *return_length = content_length;
+       return buf;
+}
+
 struct ast_json *ast_http_get_json(
        struct ast_tcptls_session_instance *ser, struct ast_variable *headers)
 {
        int content_length = 0;
-       int res;
        struct ast_json *body;
        RAII_VAR(char *, buf, NULL, ast_free);
        RAII_VAR(char *, type, get_content_type(headers), ast_free);
@@ -705,34 +881,10 @@ struct ast_json *ast_http_get_json(
                return NULL;
        }
 
-       content_length = get_content_length(headers);
-
-       if (content_length <= 0) {
-               /* No content (or streaming content). */
-               return NULL;
-       }
-
-       if (content_length > MAX_POST_CONTENT - 1) {
-               ast_log(LOG_WARNING,
-                       "Excessively long HTTP content. (%d > %d)\n",
-                       content_length, MAX_POST_CONTENT);
-               errno = EFBIG;
-               return NULL;
-       }
-
-       buf = ast_malloc(content_length);
-       if (!buf) {
-               /* Malloc sets ENOMEM */
-               return NULL;
-       }
-
-       res = fread(buf, 1, content_length, ser->f);
-       if (res < content_length) {
-               /* Error, distinguishable by ferror() or feof(), but neither
-                * is good. Treat either one as I/O error */
-               ast_log(LOG_WARNING, "Short HTTP request body (%d < %d)\n",
-                       res, content_length);
-               errno = EIO;
+       buf = ast_http_get_contents(&content_length, ser, headers);
+       if (buf == NULL)
+       {
+               /* errno already set */
                return NULL;
        }
 
@@ -758,7 +910,6 @@ struct ast_variable *ast_http_get_post_vars(
        char *var, *val;
        RAII_VAR(char *, buf, NULL, ast_free_ptr);
        RAII_VAR(char *, type, get_content_type(headers), ast_free);
-       int res;
 
        /* Use errno to distinguish errors from no params */
        errno = 0;
@@ -769,34 +920,10 @@ struct ast_variable *ast_http_get_post_vars(
                return NULL;
        }
 
-       content_length = get_content_length(headers);
-
-       if (content_length <= 0) {
-               return NULL;
-       }
-
-       if (content_length > MAX_POST_CONTENT - 1) {
-               ast_log(LOG_WARNING,
-                       "Excessively long HTTP content. (%d > %d)\n",
-                       content_length, MAX_POST_CONTENT);
-               errno = EFBIG;
-               return NULL;
-       }
-
-       buf = ast_malloc(content_length + 1);
-       if (!buf) {
-               /* malloc sets errno to ENOMEM */
-               return NULL;
-       }
-
-       res = fread(buf, 1, content_length, ser->f);
-       if (res < content_length) {
-               /* Error, distinguishable by ferror() or feof(), but neither
-                * is good. Treat either one as I/O error */
-               errno = EIO;
+       buf = ast_http_get_contents(&content_length, ser, headers);
+       if (buf == NULL) {
                return NULL;
        }
-       buf[content_length] = '\0';
 
        while ((val = strsep(&buf, "&"))) {
                var = strsep(&val, "=");
@@ -1191,7 +1318,8 @@ static void *httpd_helper_thread(void *data)
         * RFC 2616, section 3.6, we should respond with a 501 for any transfer-
         * codings we don't understand.
         */
-       if (strcasecmp(transfer_encoding, "identity") != 0) {
+       if (strcasecmp(transfer_encoding, "identity") != 0 &&
+               strcasecmp(transfer_encoding, "chunked") != 0) {
                /* Transfer encodings not supported */
                ast_http_error(ser, 501, "Unimplemented", "Unsupported Transfer-Encoding.");
                goto done;