]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-http: Improved message header and body parsing for better RFC compliance.
authorStephan Bosch <stephan@rename-it.nl>
Sun, 15 Sep 2013 00:39:45 +0000 (03:39 +0300)
committerStephan Bosch <stephan@rename-it.nl>
Sun, 15 Sep 2013 00:39:45 +0000 (03:39 +0300)
Added pre-parsed transfer-encoding and connection header content (array) to
parsed message struct. Fixed message body handling for when both
transfer-encoding and content-length headers are missing. Now duplicates of
unique important message headers yield an error.

src/lib-http/http-message-parser.c
src/lib-http/http-message-parser.h
src/lib-http/http-request-parser.c
src/lib-http/http-request-parser.h
src/lib-http/http-response-parser.c
src/lib-http/http-response.h
src/lib-http/http-transfer.h

index 5669969b9a742ab38448e630f91b1f783f912b80..c0996c138f9dfae81e2c7282e79cfc35c8464480 100644 (file)
@@ -49,6 +49,7 @@ void http_message_parser_restart(struct http_message_parser *parser,
        }
        parser->msg.date = (time_t)-1;
        p_array_init(&parser->msg.headers, parser->msg.pool, 32);
+       p_array_init(&parser->msg.connection_options, parser->msg.pool, 4);
 }
 
 int http_message_parse_version(struct http_message_parser *parser)
@@ -106,10 +107,24 @@ http_message_parse_header(struct http_message_parser *parser, const char *name,
        memcpy(value, data, size);
        hdr->size = size;
 
+       /* https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-23
+            Section 3.2.2:
+
+          A sender MUST NOT generate multiple header fields with the same field
+          name in a message unless either the entire field value for that
+          header field is defined as a comma-separated list [i.e., #(values)]
+          or the header field is a well-known exception.
+        */
+
        switch (name[0]) {
        case 'C': case 'c':
+               /* Connection: */
                if (strcasecmp(name, "Connection") == 0) {
+                       const char **opt_idx;
                        const char *option;
+                       unsigned int num_tokens = 0;
+
+                       /* Multiple Connection headers are allowed and combined into one */
 
                        /* Connection        = 1#connection-option
                                 connection-option = token
@@ -118,24 +133,42 @@ http_message_parse_header(struct http_message_parser *parser, const char *name,
                        for (;;) {
                                if (http_parse_token_list_next(&hparser, &option) <= 0)
                                        break;
-                               if (strcasecmp(option, "close") == 0) {
+                               num_tokens++;
+                               if (strcasecmp(option, "close") == 0)
                                        parser->msg.connection_close = TRUE;
-                                       break; // not interested in any other options
-                               }
+                               opt_idx = array_append_space(&parser->msg.connection_options);
+                               *opt_idx = p_strdup(parser->msg.pool, option);
+                       }
+
+                       if (hparser.cur < hparser.end || num_tokens == 0) {
+                               *error_r = "Invalid Connection header";
+                               return -1;
                        }
+
                        return 0;
                }
+               /* Content-Length: */
                if (strcasecmp(name, "Content-Length") == 0) {
+                       if (parser->msg.have_content_length) {
+                               *error_r = "Duplicate Content-Length header";
+                               return -1;
+                       }
                        /* Content-Length = 1*DIGIT */
                        if (str_to_uoff(hdr->value, &parser->msg.content_length) < 0) {
                                *error_r = "Invalid Content-Length header";
                                return -1;
                        }
+                       parser->msg.have_content_length = TRUE;
                        return 0;
                }
                break;
        case 'D': case 'd':
                if (strcasecmp(name, "Date") == 0) {
+                       if (parser->msg.date != (time_t)-1) {
+                               *error_r = "Duplicate Date header";
+                               return -1;
+                       }
+
                        /* Date = HTTP-date */
                        (void)http_date_parse(data, size, &parser->msg.date);
                        return 0;
@@ -143,15 +176,102 @@ http_message_parse_header(struct http_message_parser *parser, const char *name,
                break;
        case 'L': case 'l':
                if (strcasecmp(name, "Location") == 0) {
+                       /* FIXME: move this to response parser */
                        /* Location = URI-reference (not parsed here) */
                        parser->msg.location = hdr->value;
                        return 0;
                }
                break;
        case 'T': case 't':
+               /* Transfer-Encoding: */
                if (strcasecmp(name, "Transfer-Encoding") == 0) {
-                       /* Transfer-Encoding = 1#transfer-coding */
-                       parser->msg.transfer_encoding = hdr->value;
+                       const char *trenc = NULL;
+       
+                       /* Multiple Transfer-Encoding headers are allowed and combined into one */
+                       if (!array_is_created(&parser->msg.transfer_encoding))
+                               p_array_init(&parser->msg.transfer_encoding, parser->msg.pool, 4);
+
+                       /* Transfer-Encoding  = 1#transfer-coding 
+                                transfer-coding    = "chunked" / "compress" / "deflate" / "gzip"
+                                                     / transfer-extension
+                                transfer-extension = token *( OWS ";" OWS transfer-parameter )
+                                transfer-parameter = attribute BWS "=" BWS value
+                                attribute          = token
+                                value              = word
+                       */
+                       http_parser_init(&hparser, data, size);
+                       for (;;) {
+                               /* transfer-coding */
+                               if (http_parse_token(&hparser, &trenc) > 0) {
+                                       struct http_transfer_coding *coding;
+                                       bool parse_error;
+
+                                       coding = array_append_space(&parser->msg.transfer_encoding);
+                                       coding->name = p_strdup(parser->msg.pool, trenc);
+               
+                                       /* *( OWS ";" OWS transfer-parameter ) */
+                                       parse_error = FALSE;
+                                       for (;;) {
+                                               struct http_transfer_param *param;
+                                               const char *attribute, *value;
+
+                                               /* OWS ";" OWS */
+                                               http_parse_ows(&hparser);
+                                               if (hparser.cur >= hparser.end || *hparser.cur != ';')
+                                                       break;
+                                               hparser.cur++;
+                                               http_parse_ows(&hparser);
+
+                                               /* attribute */
+                                               if (http_parse_token(&hparser, &attribute) <= 0) {
+                                                       parse_error = TRUE;
+                                                       break;
+                                               }
+
+                                               /* BWS "=" BWS */
+                                               http_parse_ows(&hparser);
+                                               if (hparser.cur >= hparser.end || *hparser.cur != '=') {
+                                                       parse_error = TRUE;
+                                                       break;
+                                               }
+                                               hparser.cur++;
+                                               http_parse_ows(&hparser);
+
+                                               /* value */
+                                               if (http_parse_word(&hparser, &value) <= 0) {
+                                                       parse_error = TRUE;
+                                                       break;
+                                               }
+               
+                                               if (!array_is_created(&coding->parameters))
+                                                       p_array_init(&coding->parameters, parser->msg.pool, 2);
+                                               param = array_append_space(&coding->parameters);
+                                               param->attribute = p_strdup(parser->msg.pool, attribute);
+                                               param->value = p_strdup(parser->msg.pool, value);
+                                       }
+                                       if (parse_error)
+                                               break;
+                                       
+                               } else {
+                                       /* http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-23
+                                                        Appendix B:
+
+                                                For compatibility with legacy list rules, recipients SHOULD accept
+                                                empty list elements.
+                                        */
+                               }
+                               http_parse_ows(&hparser);
+                               if (hparser.cur >= hparser.end || *hparser.cur != ',')
+                                       break;
+                               hparser.cur++;
+                               http_parse_ows(&hparser);
+                       }
+
+                       if (hparser.cur < hparser.end ||
+                               array_count(&parser->msg.transfer_encoding) == 0) {
+                               *error_r = "Invalid Transfer-Encoding header";
+                               return -1;
+                       }
                        return 0;
                }
                break;
@@ -188,40 +308,105 @@ int http_message_parse_headers(struct http_message_parser *parser,
        return ret;
 }
 
-int http_message_parse_body(struct http_message_parser *parser,
+
+int http_message_parse_body(struct http_message_parser *parser, bool request,
                            const char **error_r)
 {
-       struct http_parser hparser;
+       *error_r = NULL;
+
+       if (array_is_created(&parser->msg.transfer_encoding)) {
+               const struct http_transfer_coding *coding;
+
+               bool chunked_last = FALSE;
 
-       if (parser->msg.content_length > 0) {
+               array_foreach(&parser->msg.transfer_encoding, coding) {
+                       if (strcasecmp(coding->name, "chunked") == 0) {
+                               chunked_last = TRUE;
+               
+                               if (*error_r == NULL && array_is_created(&coding->parameters) &&
+                                       array_count(&coding->parameters) > 0) {
+                                       const struct http_transfer_param *param =
+                                               array_idx(&coding->parameters, 0);
+                                       
+                                       *error_r = t_strdup_printf("Unexpected parameter `%s' specified"
+                                               "for the `%s' transfer coding", param->attribute, coding->name);
+                               }
+                       } else if (chunked_last) {
+                               *error_r = "Chunked Transfer-Encoding must be last";
+                               return -1;
+                       } else if (*error_r == NULL) {
+                               *error_r = t_strdup_printf(
+                               "Unknown transfer coding `%s'", coding->name);
+               }
+       }
+
+               if (chunked_last) {     
+                       parser->payload =
+                               http_transfer_chunked_istream_create(parser->input);
+               } else if (!request) {
+                       /*  https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-23
+                             Section 3.3.3.:
+
+                           If a Transfer-Encoding header field is present in a response and
+                           the chunked transfer coding is not the final encoding, the
+                           message body length is determined by reading the connection until
+                           it is closed by the server.
+                        */
+                       parser->payload =
+                                       i_stream_create_limit(parser->input, (size_t)-1);
+               } else {
+                       /* https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-23
+                             Section 3.3.3.:
+
+                          If a Transfer-Encoding header field is present in a request and the
+                          chunked transfer coding is not the final encoding, the message body
+                          length cannot be determined reliably; the server MUST respond with
+                          the 400 (Bad Request) status code and then close the connection.
+                        */
+                       *error_r = "Final Transfer-Encoding in request is not `chunked'";
+                       return -1;
+               }
+
+               /* https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-23
+                    Section 3.3.3.:
+
+                        If a message is received with both a Transfer-Encoding and a
+       Content-Length header field, the Transfer-Encoding overrides the
+       Content-Length.  Such a message might indicate an attempt to
+       perform request or response smuggling (bypass of security-related
+       checks on message routing or content) and thus ought to be
+       handled as an error.  A sender MUST remove the received Content-
+       Length field prior to forwarding such a message downstream.
+                */
+               if (parser->msg.have_content_length) {
+                       ARRAY_TYPE(http_response_header) *headers = &parser->msg.headers;
+                       const struct http_response_header *hdr;
+
+                       array_foreach(headers, hdr) {
+                               if (strcasecmp(hdr->key, "Content-Length") == 0) {
+                                       array_delete(headers, array_foreach_idx(headers, hdr), 1);
+                                       break;
+                               }
+                       }
+               }
+       } else if (parser->msg.content_length > 0) {
                /* Got explicit message size from Content-Length: header */
                parser->payload =
                        i_stream_create_limit(parser->input,
                                              parser->msg.content_length);
-       } else if (parser->msg.transfer_encoding != NULL) {
-               const char *tenc;
-
-               /* Transfer-Encoding = 1#transfer-coding
-                  transfer-coding    = "chunked" / "compress" / "deflate" / "gzip"
-                                     / transfer-extension       ;  [FIXME]
-                  transfer-extension = token *( OWS ";" OWS transfer-parameter )
-               */
-               http_parser_init(&hparser,
-                                (const unsigned char *)parser->msg.transfer_encoding,
-                                strlen(parser->msg.transfer_encoding));
-               for (;;) {
-                       if (http_parse_token_list_next(&hparser, &tenc) <= 0)
-                               break;
-                       if (strcasecmp(tenc, "chunked") == 0) {
-                               parser->payload =
-                                       http_transfer_chunked_istream_create(parser->input);
-                               break; // FIXME
-                       } else {
-                               *error_r = t_strdup_printf(
-                                       "Unknown Transfer-Encoding `%s'", tenc);
-                               return -1;
-                       }
-               }
+       } else if (!parser->msg.have_content_length && !request) {
+               /* https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-23
+                    Section 3.3.3.:
+
+                  If this is a request message and none of the above are true, then
+                  the message body length is zero (no message body is present).
+
+                  Otherwise, this is a response message without a declared message
+                  body length, so the message body length is determined by the
+                  number of octets received prior to the server closing the connection.
+                */
+               parser->payload =
+                       i_stream_create_limit(parser->input, (size_t)-1);
        }
        return 0;
 }
index 026991de926d87126d3ad65d9212b6a2477415ca..1b41dd34e4c5a7c0401ce06ba264160115cc1b52 100644 (file)
@@ -2,6 +2,7 @@
 #define HTTP_MESSAGE_PARSER_H
 
 #include "http-response.h"
+#include "http-transfer.h"
 
 struct http_message {
        pool_t pool;
@@ -14,9 +15,11 @@ struct http_message {
 
        uoff_t content_length;
        const char *location;
-       const char *transfer_encoding;
+       ARRAY_TYPE(http_transfer_coding) transfer_encoding;
+       ARRAY_TYPE(const_string) connection_options;
 
        unsigned int connection_close:1;
+       unsigned int have_content_length:1;
 };
 
 struct http_message_parser {
@@ -42,7 +45,7 @@ int http_message_parse_finish_payload(struct http_message_parser *parser,
 int http_message_parse_version(struct http_message_parser *parser);
 int http_message_parse_headers(struct http_message_parser *parser,
                               const char **error_r);
-int http_message_parse_body(struct http_message_parser *parser,
+int http_message_parse_body(struct http_message_parser *parser, bool request,
                            const char **error_r);
 
 #endif
index 9aa3e816bda6d65f9f81af9e9c5bd6ad127dfb47..cb3fb546a6ff308a156687547c9925c3e371005a 100644 (file)
@@ -270,7 +270,7 @@ int http_request_parse_next(struct http_request_parser *parser,
        } 
        if ((ret = http_message_parse_headers(&parser->parser, error_r)) <= 0)
                return ret;
-       if (http_message_parse_body(&parser->parser, error_r) < 0)
+       if (http_message_parse_body(&parser->parser, TRUE, error_r) < 0)
                return -1;
        parser->state = HTTP_REQUEST_PARSE_STATE_INIT;
 
@@ -282,6 +282,7 @@ int http_request_parse_next(struct http_request_parser *parser,
        request->date = parser->parser.msg.date;
        request->payload = parser->parser.payload;
        request->headers = parser->parser.msg.headers;
+       request->connection_options = parser->parser.msg.connection_options;
        request->connection_close = parser->parser.msg.connection_close;
        return 1;
 }
index f285101a6a75bb3dc37d574f8c7e62ae83be9d4c..676e1570c8b6f74afeb3e645b01fd797df9cbec1 100644 (file)
@@ -14,6 +14,7 @@ struct http_request {
        struct istream *payload;
 
        ARRAY_TYPE(http_response_header) headers;
+       ARRAY_TYPE(const_string) connection_options;
 
        unsigned int connection_close:1;
 };
index 43847895739a0f39cf485a47fe57f187e83aa816..77b7fddf7de9dcc1df9b35e483931c98b30a2904 100644 (file)
@@ -279,7 +279,7 @@ int http_response_parse_next(struct http_response_parser *parser,
 
        if (!no_payload) {
                /* [ message-body ] */
-               if (http_message_parse_body(&parser->parser, error_r) < 0)
+               if (http_message_parse_body(&parser->parser, FALSE, error_r) < 0)
                        return -1;
        }
        parser->state = HTTP_RESPONSE_PARSE_STATE_INIT;
@@ -293,6 +293,7 @@ int http_response_parse_next(struct http_response_parser *parser,
        response->date = parser->parser.msg.date;
        response->payload = parser->parser.payload;
        response->headers = parser->parser.msg.headers;
+       response->connection_options = parser->parser.msg.connection_options;
        response->connection_close = parser->parser.msg.connection_close;
        return 1;
 }
index 5be5cd44b39e040368c1505f0d5186f23000a13b..16002ae4e3d218bb7a80aeb3591801f1291431a5 100644 (file)
@@ -21,6 +21,7 @@ struct http_response {
        struct istream *payload;
 
        ARRAY_TYPE(http_response_header) headers;
+       ARRAY_TYPE(const_string) connection_options;
 
        unsigned int connection_close:1;
 };
index d4181f85f6210364ef6e9a478aecc5c581301e49..c7da0daf464bff9d618dcba6175cd27db173afb0 100644 (file)
@@ -1,6 +1,20 @@
 #ifndef HTTP_TRANSFER_H
 #define HTTP_TRANSFER_H
 
+struct http_transfer_param {
+       const char *attribute;
+       const char *value;
+};
+ARRAY_DEFINE_TYPE(http_transfer_param, struct http_transfer_param);
+
+struct http_transfer_coding {
+       const char *name;
+       ARRAY_TYPE(http_transfer_param) parameters;
+
+};
+ARRAY_DEFINE_TYPE(http_transfer_coding, struct http_transfer_coding);
+
+
 // FIXME: we currently lack a means to get error strings from the input stream
 
 struct istream *