Rewrite this to be easier to understand and more correct, and make the
authorDan Winship <danw@src.gnome.org>
Sat, 6 Jan 2007 19:24:44 +0000 (19:24 +0000)
committerDan Winship <danw@src.gnome.org>
Sat, 6 Jan 2007 19:24:44 +0000 (19:24 +0000)
* libsoup/soup-headers.c (soup_headers_parse): Rewrite this to be
easier to understand and more correct, and make the "str" param
const rather than overwriting it during parsing.
(soup_headers_parse_request, soup_headers_parse_response):
Likewise, make "str" param const. Fix the doc comment to describe
the correct constraint on str. Make the parsing slightly more
lenient as per sections 4.1 and 19.3 of RFC 2616.

* tests/header-parsing.c: new regression test, for Request-Line,
Status-Line, and message-header parsing.

Inspired by #391970 (crash in SoupServer when certain invalid
requests are received).

svn path=/trunk/; revision=905

ChangeLog
libsoup/soup-headers.c
libsoup/soup-headers.h
tests/Makefile.am
tests/header-parsing.c [new file with mode: 0644]

index d379088..7937ce0 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+2007-01-06  Dan Winship  <danw@novell.com>
+
+       * libsoup/soup-headers.c (soup_headers_parse): Rewrite this to be
+       easier to understand and more correct, and make the "str" param
+       const rather than overwriting it during parsing.
+       (soup_headers_parse_request, soup_headers_parse_response):
+       Likewise, make "str" param const. Fix the doc comment to describe
+       the correct constraint on str. Make the parsing slightly more
+       lenient as per sections 4.1 and 19.3 of RFC 2616.
+
+       * tests/header-parsing.c: new regression test, for Request-Line,
+       Status-Line, and message-header parsing.
+
+       Inspired by #391970 (crash in SoupServer when certain invalid
+       requests are received).
+
 2006-12-05  Dan Winship  <danw@novell.com>
 
        * libsoup/soup-message.c (soup_message_set_uri): Remove the calls
index 14e75d6..3467132 100644 (file)
 #include "soup-headers.h"
 #include "soup-misc.h"
 
-/*
- * "HTTP/1.1 200 OK\r\nContent-Length: 1234\r\n          567\r\n\r\n"
- *                     ^             ^ ^    ^            ^   ^
- *                     |             | |    |            |   |
- *                    key            0 val  0          val+  0
- *                                         , <---memmove-...
- * 
- * key: "Content-Length"
- * val: "1234, 567"
- */
 static gboolean
-soup_headers_parse (char       *str, 
+soup_headers_parse (const char *str, 
                    int         len, 
                    GHashTable *dest)
 {
-       char *key = NULL, *val = NULL, *end = NULL;
-       int offset = 0, lws = 0;
-
-       key = strstr (str, "\r\n");
-       key += 2;
-
-       /* join continuation headers, using a comma */
-       while ((key = strstr (key, "\r\n"))) {
-               key += 2;
-               offset = key - str;
-
-               if (!*key)
-                       break;
-
-               /* check if first character on the line is whitespace */
-               if (*key == ' ' || *key == '\t') {
-                       key -= 2;
-
-                       /* eat any trailing space from the previous line*/
-                       while (key [-1] == ' ' || key [-1] == '\t') key--;
-
-                       /* count how many characters are whitespace */
-                       lws = strspn (key, " \t\r\n");
-
-                       /* if continuation line, replace whitespace with ", " */
-                       if (key [-1] != ':') {
-                               lws -= 2;
-                               key [0] = ',';
-                               key [1] = ' ';
-                       }
-
-                       g_memmove (key, &key [lws], len - offset - lws);
-               }
-       }
-
-       key = str;
-
-       /* set eos for header key and value and add to hashtable */
-        while ((key = strstr (key, "\r\n"))) {
-               GSList *exist_hdrs;
-               
-               /* set end of last val, or end of http reason phrase */
-                key [0] = '\0';
-               key += 2;
-
-               if (!*key)
-                       break;
-
-                val = strchr (key, ':'); /* find start of val */
+       const char *end = str + len;
+       const char *name_start, *name_end, *value_start, *value_end;
+       char *name, *value, *eol, *sol;
+       GSList *hdrs;
+
+       /* As per RFC 2616 section 19.3, we treat '\n' as the
+        * line terminator, and '\r', if it appears, merely as
+        * ignorable trailing whitespace.
+        */
+
+       /* Skip over the Request-Line / Status-Line */
+       value_end = memchr (str, '\n', len);
+       if (!value_end)
+               return FALSE;
 
-               if (!val || val > strchr (key, '\r'))
+       while (value_end < end - 1) {
+               name_start = value_end + 1;
+               name_end = memchr (name_start, ':', end - name_start);
+               if (!name_end)
                        return FALSE;
 
-               /* set end of key */
-               val [0] = '\0';
-               
-               val++;
-               val += strspn (val, " \t");  /* skip whitespace */
-
-               /* find the end of the value */
-               end = strstr (val, "\r\n");
-               if (!end)
+               /* Find the end of the value; ie, an end-of-line that
+                * isn't followed by a continuation line.
+                */
+               value_end = memchr (name_start, '\n', end - name_start);
+               if (!value_end || value_end < name_end)
                        return FALSE;
+               while (value_end != end - 1 &&
+                      (*(value_end + 1) == ' ' || *(value_end + 1) == '\t')) {
+                       value_end = memchr (value_end + 1, '\n', end - value_end);
+                       if (!value_end)
+                               return FALSE;
+               }
 
-               exist_hdrs = g_hash_table_lookup (dest, key);
-               exist_hdrs = g_slist_append (exist_hdrs, 
-                                            g_strndup (val, end - val));
-
-               if (!exist_hdrs->next)
-                       g_hash_table_insert (dest, g_strdup (key), exist_hdrs);
+               name = g_strndup (name_start, name_end - name_start);
+
+               value_start = name_end + 1;
+               while (value_start < value_end &&
+                      (*value_start == ' ' || *value_start == '\t' ||
+                       *value_start == '\r' || *value_start == '\n'))
+                       value_start++;
+               value = g_strndup (value_start, value_end - value_start);
+
+               /* Collapse continuation lines inside value */
+               while ((eol = strchr (value, '\n'))) {
+                       /* find start of next line */
+                       sol = eol + 1;
+                       while (*sol == ' ' || *sol == '\t')
+                               sol++;
+
+                       /* back up over trailing whitespace on current line */
+                       while (eol[-1] == ' ' || eol[-1] == '\t' || eol[-1] == '\r')
+                               eol--;
+
+                       /* Delete all but one SP */
+                       *eol = ' ';
+                       g_memmove (eol + 1, sol, strlen (sol) + 1);
+               }
 
-               key = end;
+               /* clip trailing whitespace */
+               eol = strchr (value, '\0');
+               while (eol > value &&
+                      (eol[-1] == ' ' || eol[-1] == '\t' || eol[-1] == '\r'))
+                       eol--;
+               *eol = '\0';
+
+               hdrs = g_hash_table_lookup (dest, name);
+               hdrs = g_slist_append (hdrs, value);
+               if (!hdrs->next)
+                       g_hash_table_insert (dest, name, hdrs);
+               else
+                       g_free (name);
         }
 
        return TRUE;
@@ -108,7 +98,7 @@ soup_headers_parse (char       *str,
 /**
  * soup_headers_parse_request:
  * @str: the header string (including the trailing blank line)
- * @len: length of @str
+ * @len: length of @str up to (but not including) the terminating blank line.
  * @dest: #GHashTable to store the header values in
  * @req_method: if non-%NULL, will be filled in with the request method
  * @req_path: if non-%NULL, will be filled in with the request path
@@ -117,60 +107,82 @@ soup_headers_parse (char       *str,
  * Parses the headers of an HTTP request in @str and stores the
  * results in @req_method, @req_path, @ver, and @dest.
  *
- * @len must be the length of @str only up to (and including) the
- * terminating blank line. Parts of @str up to that point will be
- * overwritten during parsing.
- *
  * Return value: success or failure.
  **/
 gboolean
-soup_headers_parse_request (char             *str, 
+soup_headers_parse_request (const char       *str, 
                            int               len, 
                            GHashTable       *dest, 
                            char            **req_method,
                            char            **req_path,
                            SoupHttpVersion  *ver) 
 {
-       gulong http_major, http_minor;
-       char *s1, *s2, *cr, *p;
+       const char *method, *method_end, *path, *path_end, *version, *headers;
+       int minor_version;
 
        if (!str || !*str)
                return FALSE;
 
-       cr = memchr (str, '\r', len);
-       if (!cr)
+       /* RFC 2616 4.1 "servers SHOULD ignore any empty line(s)
+        * received where a Request-Line is expected."
+        */
+       while (*str == '\r' || *str == '\n') {
+               str++;
+               len--;
+       }
+
+       /* RFC 2616 19.3 "[servers] SHOULD accept any amount of SP or
+        * HT characters between [Request-Line] fields"
+        */
+
+       method = method_end = str;
+       while (method_end < str + len && *method_end != ' ' && *method_end != '\t')
+               method_end++;
+       if (method_end >= str + len)
+               return FALSE;
+
+       path = method_end;
+       while (path < str + len && (*path == ' ' || *path == '\t'))
+               path++;
+       if (path >= str + len)
                return FALSE;
 
-       s1 = memchr (str, ' ', cr - str);
-       if (!s1)
+       path_end = path;
+       while (path_end < str + len && *path_end != ' ' && *path_end != '\t')
+               path_end++;
+       if (path_end >= str + len)
                return FALSE;
-       s2 = memchr (s1 + 1, ' ', cr - (s1 + 1));
-       if (!s2)
+
+       version = path_end;
+       while (version < str + len && (*version == ' ' || *version == '\t'))
+               version++;
+       if (version + 8 >= str + len)
                return FALSE;
 
-       if (strncmp (s2, " HTTP/", 6) != 0)
+       /* FIXME: we want SoupServer to return
+        * SOUP_STATUS_HTTP_VERSION_NOT_SUPPORTED here
+        */
+       if (strncmp (version, "HTTP/1.", 7) != 0)
                return FALSE;
-       http_major = strtoul (s2 + 6, &p, 10);
-       if (*p != '.')
+       minor_version = version[7] - '0';
+       if (minor_version < 0 || minor_version > 1)
                return FALSE;
-       http_minor = strtoul (p + 1, &p, 10);
-       if (p != cr)
+
+       headers = version + 8;
+       if (headers < str + len && *headers == '\r')
+               headers++;
+       if (headers >= str + len || *headers != '\n')
                return FALSE;
 
        if (!soup_headers_parse (str, len, dest)) 
                return FALSE;
 
        if (req_method)
-               *req_method = g_strndup (str, s1 - str);
+               *req_method = g_strndup (method, method_end - method);
        if (req_path)
-               *req_path = g_strndup (s1 + 1, s2 - (s1 + 1));
-
-       if (ver) {
-               if (http_major == 1 && http_minor == 1) 
-                       *ver = SOUP_HTTP_1_1;
-               else 
-                       *ver = SOUP_HTTP_1_0;
-       }
+               *req_path = g_strndup (path, path_end - path);
+       if (ver)
+               *ver = (minor_version == 0) ? SOUP_HTTP_1_0 : SOUP_HTTP_1_1;
 
        return TRUE;
 }
@@ -184,7 +196,8 @@ soup_headers_parse_request (char             *str,
  * phrase
  *
  * Parses the HTTP Status-Line string in @status_line into @ver,
- * @status_code, and @reason_phrase.
+ * @status_code, and @reason_phrase. @status_line must be terminated by
+ * either '\0' or '\r\n'.
  *
  * Return value: %TRUE if @status_line was parsed successfully.
  **/
@@ -194,29 +207,42 @@ soup_headers_parse_status_line (const char       *status_line,
                                guint            *status_code,
                                char            **reason_phrase)
 {
-       guint http_major, http_minor, code;
-       guint phrase_start = 0;
-
-       if (sscanf (status_line, 
-                   "HTTP/%1u.%1u %3u %n", 
-                   &http_major,
-                   &http_minor,
-                   &code, 
-                   &phrase_start) < 3 || !phrase_start)
-               return FALSE;
-
-       if (ver) {
-               if (http_major == 1 && http_minor == 1) 
-                       *ver = SOUP_HTTP_1_1;
-               else 
-                       *ver = SOUP_HTTP_1_0;
-       }
+       guint minor_version, code;
+       const char *code_start, *code_end, *phrase_start, *phrase_end;
 
+       if (strncmp (status_line, "HTTP/1.", 7) != 0)
+               return FALSE;
+       minor_version = status_line[7] - '0';
+       if (minor_version < 0 || minor_version > 1)
+               return FALSE;
+       if (ver)
+               *ver = (minor_version == 0) ? SOUP_HTTP_1_0 : SOUP_HTTP_1_1;
+
+       code_start = status_line + 8;
+       while (*code_start == ' ' || *code_start == '\t')
+               code_start++;
+       code_end = code_start;
+       while (*code_end >= '0' && *code_end <= '9')
+               code_end++;
+       if (code_end != code_start + 3)
+               return FALSE;
+       code = atoi (code_start);
+       if (code < 100 || code > 599)
+               return FALSE;
        if (status_code)
                *status_code = code;
 
+       phrase_start = code_end;
+       while (*phrase_start == ' ' || *phrase_start == '\t')
+               phrase_start++;
+       phrase_end = strchr (phrase_start, '\n');
+       if (!phrase_end)
+               return FALSE;
+       while (phrase_end > phrase_start &&
+              (phrase_end[-1] == '\r' || phrase_end[-1] == ' ' || phrase_end[-1] == '\t'))
+               phrase_end--;
        if (reason_phrase)
-               *reason_phrase = g_strdup (status_line + phrase_start);
+               *reason_phrase = g_strndup (phrase_start, phrase_end - phrase_start);
 
        return TRUE;
 }
@@ -224,7 +250,7 @@ soup_headers_parse_status_line (const char       *status_line,
 /**
  * soup_headers_parse_response:
  * @str: the header string (including the trailing blank line)
- * @len: length of @str
+ * @len: length of @str up to (but not including) the terminating blank line.
  * @dest: #GHashTable to store the header values in
  * @ver: if non-%NULL, will be filled in with the HTTP version
  * @status_code: if non-%NULL, will be filled in with the status code
@@ -234,21 +260,17 @@ soup_headers_parse_status_line (const char       *status_line,
  * Parses the headers of an HTTP response in @str and stores the
  * results in @ver, @status_code, @reason_phrase, and @dest.
  *
- * @len must be the length of @str only up to (and including) the
- * terminating blank line. Parts of @str up to that point will be
- * overwritten during parsing.
- *
  * Return value: success or failure.
  **/
 gboolean
-soup_headers_parse_response (char             *str, 
+soup_headers_parse_response (const char       *str, 
                             int               len, 
                             GHashTable       *dest,
                             SoupHttpVersion  *ver,
                             guint            *status_code,
                             char            **reason_phrase)
 {
-       if (!str || !*str || len < sizeof ("HTTP/0.0 000 A\r\n\r\n"))
+       if (!str || !*str)
                return FALSE;
 
        if (!soup_headers_parse (str, len, dest)) 
index 174edd7..ea30b62 100644 (file)
@@ -11,7 +11,7 @@
 
 /* HTTP Header Parsing */
 
-gboolean    soup_headers_parse_request      (char             *str, 
+gboolean    soup_headers_parse_request      (const char       *str, 
                                             int               len, 
                                             GHashTable       *dest, 
                                             char            **req_method,
@@ -23,7 +23,7 @@ gboolean    soup_headers_parse_status_line  (const char        *status_line,
                                             guint            *status_code,
                                             char            **reason_phrase);
 
-gboolean    soup_headers_parse_response     (char             *str, 
+gboolean    soup_headers_parse_response     (const char       *str, 
                                             int               len, 
                                             GHashTable       *dest,
                                             SoupHttpVersion  *ver,
index 8eefbf1..e146a9a 100644 (file)
@@ -12,6 +12,7 @@ noinst_PROGRAMS =     \
        dns             \
        get             \
        getbug          \
+       header-parsing  \
        revserver       \
        simple-httpd    \
        simple-proxy    \
@@ -24,6 +25,7 @@ dict_SOURCES = dict.c
 dns_SOURCES = dns.c
 get_SOURCES = get.c
 getbug_SOURCES = getbug.c
+header_parsing_SOURCES = header-parsing.c
 revserver_SOURCES = revserver.c
 simple_httpd_SOURCES = simple-httpd.c
 simple_proxy_SOURCES = simple-proxy.c
@@ -37,7 +39,7 @@ if HAVE_XMLRPC_EPI_PHP
 XMLRPC_TESTS = xmlrpc-test
 endif
 
-TESTS = date uri-parsing $(APACHE_TESTS) $(XMLRPC_TESTS)
+TESTS = date header-parsing uri-parsing $(APACHE_TESTS) $(XMLRPC_TESTS)
 
 EXTRA_DIST =           \
        libsoup.supp    \
diff --git a/tests/header-parsing.c b/tests/header-parsing.c
new file mode 100644 (file)
index 0000000..4e4d467
--- /dev/null
@@ -0,0 +1,621 @@
+#include <config.h>
+
+#include <stdio.h>
+#include <string.h>
+
+#include "libsoup/soup-message.h"
+#include "libsoup/soup-headers.h"
+
+struct RequestTest {
+       char *description;
+       char *request;
+       int length;
+       char *method, *path;
+       SoupHttpVersion version;
+       struct {
+               char *name, *value;
+       } headers[4];
+} reqtests[] = {
+       /**********************/
+       /*** VALID REQUESTS ***/
+       /**********************/
+
+       { "HTTP 1.0 request with no headers",
+         "GET / HTTP/1.0\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_0,
+         { { NULL } }
+       },
+
+       { "Req w/ 1 header",
+         "GET / HTTP/1.1\r\nHost: example.com\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 1 header, no leading whitespace",
+         "GET / HTTP/1.1\r\nHost:example.com\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 1 header including trailing whitespace",
+         "GET / HTTP/1.1\r\nHost: example.com \r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 1 header, wrapped",
+         "GET / HTTP/1.1\r\nFoo: bar\r\n baz\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Foo", "bar baz" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 1 header, wrapped with additional whitespace",
+         "GET / HTTP/1.1\r\nFoo: bar \r\n  baz\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Foo", "bar baz" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 1 header, wrapped with tab",
+         "GET / HTTP/1.1\r\nFoo: bar\r\n\tbaz\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Foo", "bar baz" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 1 header, wrapped before value",
+         "GET / HTTP/1.1\r\nFoo:\r\n bar baz\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Foo", "bar baz" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 1 header with empty value",
+         "GET / HTTP/1.1\r\nHost:\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 2 headers",
+         "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { "Connection", "close" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 3 headers",
+         "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nBlah: blah\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { "Connection", "close" },
+           { "Blah", "blah" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 3 headers, 1st wrapped",
+         "GET / HTTP/1.1\r\nFoo: bar\r\n baz\r\nConnection: close\r\nBlah: blah\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Foo", "bar baz" },
+           { "Connection", "close" },
+           { "Blah", "blah" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 3 headers, 2nd wrapped",
+         "GET / HTTP/1.1\r\nConnection: close\r\nBlah: blah\r\nFoo: bar\r\n baz\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Connection", "close" },
+           { "Foo", "bar baz" },
+           { "Blah", "blah" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ 3 headers, 3rd wrapped",
+         "GET / HTTP/1.1\r\nConnection: close\r\nBlah: blah\r\nFoo: bar\r\n baz\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Connection", "close" },
+           { "Blah", "blah" },
+           { "Foo", "bar baz" },
+           { NULL }
+         }
+       },
+
+       /****************************/
+       /*** RECOVERABLE REQUESTS ***/
+       /****************************/
+
+       /* RFC 2616 section 4.1 says we SHOULD accept this */
+
+       { "Spurious leading CRLF",
+         "\r\nGET / HTTP/1.1\r\nHost: example.com\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { NULL }
+         }
+       },
+
+       /* RFC 2616 section 19.3 says we SHOULD accept these */
+
+       { "LF instead of CRLF after header",
+         "GET / HTTP/1.1\nHost: example.com\nConnection: close\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { "Connection", "close" },
+           { NULL }
+         }
+       },
+
+       { "LF instead of CRLF after Request-Line",
+         "GET / HTTP/1.1\nHost: example.com\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { NULL }
+         }
+       },
+
+       { "Req w/ incorrect whitespace in Request-Line",
+         "GET  /\tHTTP/1.1\r\nHost: example.com\r\n", -1,
+         "GET", "/", SOUP_HTTP_1_1,
+         { { "Host", "example.com" },
+           { NULL }
+         }
+       },
+
+       /************************/
+       /*** INVALID REQUESTS ***/
+       /************************/
+
+       { "HTTP 0.9 request; not supported",
+         "GET /\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "HTTP 1.2 request; not supported (no such thing)",
+         "GET / HTTP/1.2\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "Non-HTTP request",
+         "GET / SOUP/1.1\r\nHost: example.com\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "Junk after Request-Line",
+         "GET / HTTP/1.1 blah\r\nHost: example.com\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "NUL in Method",
+         "G\x00T / HTTP/1.1\r\nHost: example.com\r\n", 37,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "NUL in Path",
+         "GET /\x00 HTTP/1.1\r\nHost: example.com\r\n", 38,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "NUL in Header",
+         "GET / HTTP/1.1\r\nHost: example\x00com\r\n", 37,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "Header line with no ':'",
+         "GET / HTTP/1.1\r\nHost example.com\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "No terminating CRLF",
+         "GET / HTTP/1.1\r\nHost: example.com", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "Blank line before headers",
+         "GET / HTTP/1.1\r\n\r\nHost: example.com\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "Blank line in headers",
+         "GET / HTTP/1.1\r\nHost: example.com\r\n\r\nConnection: close\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+       { "Blank line after headers",
+         "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n", -1,
+         NULL, NULL, -1,
+         { { NULL } }
+       },
+
+};
+static const int num_reqtests = G_N_ELEMENTS (reqtests);
+
+struct ResponseTest {
+       char *description;
+       char *response;
+       int length;
+       SoupHttpVersion version;
+       guint status_code;
+       char *reason_phrase;
+       struct {
+               char *name, *value;
+       } headers[4];
+} resptests[] = {
+       /***********************/
+       /*** VALID RESPONSES ***/
+       /***********************/
+
+       { "HTTP 1.0 response w/ no headers",
+         "HTTP/1.0 200 ok\r\n", -1,
+         SOUP_HTTP_1_0, SOUP_STATUS_OK, "ok",
+         { { NULL } }
+       },
+
+       { "HTTP 1.1 response w/ no headers",
+         "HTTP/1.1 200 ok\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok",
+         { { NULL } }
+       },
+
+       { "Response w/ multi-word Reason-Phrase",
+         "HTTP/1.1 400 bad request\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_BAD_REQUEST, "bad request",
+         { { NULL } }
+       },
+
+       { "Response w/ 1 header",
+         "HTTP/1.1 200 ok\r\nFoo: bar\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok",
+         { { "Foo", "bar" },
+           { NULL }
+         }
+       },
+
+       { "Response w/ 2 headers",
+         "HTTP/1.1 200 ok\r\nFoo: bar\r\nBaz: quux\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok",
+         { { "Foo", "bar" },
+           { "Baz", "quux" },
+           { NULL }
+         }
+       },
+
+       { "Response w/ no reason phrase",
+         "HTTP/1.1 200 \r\nFoo: bar\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "",
+         { { "Foo", "bar" },
+           { NULL }
+         }
+       },
+
+       /*****************************/
+       /*** RECOVERABLE RESPONSES ***/
+       /*****************************/
+
+       /* RFC 2616 section 19.3 says we SHOULD accept these */
+
+       { "Response w/ LF instead of CRLF after Status-Line",
+         "HTTP/1.1 200 ok\nFoo: bar\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok",
+         { { "Foo", "bar" },
+           { NULL }
+         }
+       },
+
+       { "Response w/ incorrect spacing in Status-Line",
+         "HTTP/1.1  200\tok\r\nFoo: bar\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok",
+         { { "Foo", "bar" },
+           { NULL }
+         }
+       },
+
+       { "Response w/ no reason phrase or preceding SP",
+         "HTTP/1.1 200\r\nFoo: bar\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "",
+         { { "Foo", "bar" },
+           { NULL }
+         }
+       },
+
+       { "Response w/ no whitespace after status code",
+         "HTTP/1.1 200ok\r\nFoo: bar\r\n", -1,
+         SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok",
+         { { "Foo", "bar" },
+           { NULL }
+         }
+       },
+
+       /*************************/
+       /*** INVALID RESPONSES ***/
+       /*************************/
+
+       { "Invalid HTTP version",
+         "HTTP/1.2 200 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Non-HTTP response",
+         "SOUP/1.1 200 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Non-numeric status code",
+         "HTTP/1.1 XXX OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "No status code",
+         "HTTP/1.1 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "One-digit status code",
+         "HTTP/1.1 2 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Two-digit status code",
+         "HTTP/1.1 20 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Four-digit status code",
+         "HTTP/1.1 2000 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Status code < 100",
+         "HTTP/1.1 001 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Status code > 599",
+         "HTTP/1.1 600 OK\r\nFoo: bar\r\n", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "NUL in Reason Phrase",
+         "HTTP/1.1 200 O\x00K\r\nFoo: bar\r\n", 28,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "NUL in Header",
+         "HTTP/1.1 200 OK\r\nFoo: b\x00ar\r\n", 28,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+};
+static const int num_resptests = G_N_ELEMENTS (resptests);
+
+static void
+print_header (gpointer key, gpointer value, gpointer data)
+{
+       GSList *values = value;
+       printf ("              '%s': '%s'\n",
+               (char *)key, (char*)values->data);
+}
+
+static void
+free_headers (gpointer value)
+{
+       GSList *headers = value;
+
+       /* We know that there are no duplicate headers in any of the
+        * test cases, so...
+        */
+       g_free (headers->data);
+       g_slist_free (headers);
+}
+
+static int
+do_request_tests (void)
+{
+       int i, len, h, errors;
+       char *method, *path;
+       GSList *values;
+       SoupHttpVersion version;
+       GHashTable *headers;
+
+       printf ("Request tests\n");
+       for (i = 0; i < num_reqtests; i++) {
+               gboolean ok = TRUE;
+
+               printf ("%2d. %s (%s): ", i + 1, reqtests[i].description,
+                       reqtests[i].method ? "should parse" : "should NOT parse");
+
+               headers = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                g_free, free_headers);
+               method = path = NULL;
+
+               if (reqtests[i].length == -1)
+                       len = strlen (reqtests[i].request);
+               else
+                       len = reqtests[i].length;
+               if (soup_headers_parse_request (reqtests[i].request, len,
+                                               headers, &method, &path,
+                                               &version)) {
+                       if ((reqtests[i].method && strcmp (reqtests[i].method, method) != 0) || !reqtests[i].method)
+                               ok = FALSE;
+                       if ((reqtests[i].path && strcmp (reqtests[i].path, path) != 0) || !reqtests[i].path)
+                               ok = FALSE;
+                       if (reqtests[i].version != version)
+                               ok = FALSE;
+
+                       for (h = 0; reqtests[i].headers[h].name; h++) {
+                               values = g_hash_table_lookup (headers, reqtests[i].headers[h].name);
+                               if (!values || values->next ||
+                                   strcmp (reqtests[i].headers[h].value, values->data) != 0)
+                                       ok = FALSE;
+                       }
+                       if (g_hash_table_size (headers) != h)
+                               ok = FALSE;
+               } else {
+                       if (reqtests[i].method)
+                               ok = FALSE;
+               }
+
+               if (ok)
+                       printf ("OK!\n");
+               else {
+                       printf ("BAD!\n");
+                       errors++;
+                       if (reqtests[i].method) {
+                               printf ("    expected: '%s' '%s' 'HTTP/1.%d'\n",
+                                       reqtests[i].method, reqtests[i].path,
+                                       reqtests[i].version);
+                               for (h = 0; reqtests[i].headers[h].name; h++) {
+                                       printf ("              '%s': '%s'\n",
+                                               reqtests[i].headers[h].name,
+                                               reqtests[i].headers[h].value);
+                               }
+                       } else
+                               printf ("    expected: parse error\n");
+                       if (method) {
+                               printf ("         got: '%s' '%s' 'HTTP/1.%d'\n",
+                                       method, path, version);
+                               g_hash_table_foreach (headers, print_header, NULL);
+                       } else
+                               printf ("         got: parse error\n");
+               }
+
+               g_free (method);
+               g_free (path);
+               g_hash_table_destroy (headers);
+       }
+       printf ("\n");
+
+       return errors;
+}
+
+static int
+do_response_tests (void)
+{
+       int i, len, h, errors;
+       guint status_code;
+       char *reason_phrase;
+       GSList *values;
+       SoupHttpVersion version;
+       GHashTable *headers;
+
+       printf ("Response tests\n");
+       for (i = 0; i < num_resptests; i++) {
+               gboolean ok = TRUE;
+
+               printf ("%2d. %s (%s): ", i + 1, resptests[i].description,
+                       resptests[i].reason_phrase ? "should parse" : "should NOT parse");
+
+               headers = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                g_free, free_headers);
+               reason_phrase = NULL;
+
+               if (resptests[i].length == -1)
+                       len = strlen (resptests[i].response);
+               else
+                       len = resptests[i].length;
+               if (soup_headers_parse_response (resptests[i].response, len,
+                                                headers, &version,
+                                                &status_code, &reason_phrase)) {
+                       if (resptests[i].version != version)
+                               ok = FALSE;
+                       if (resptests[i].status_code != status_code)
+                               ok = FALSE;
+                       if ((resptests[i].reason_phrase && strcmp (resptests[i].reason_phrase, reason_phrase) != 0) || !resptests[i].reason_phrase)
+                               ok = FALSE;
+
+                       for (h = 0; resptests[i].headers[h].name; h++) {
+                               values = g_hash_table_lookup (headers, resptests[i].headers[h].name);
+                               if (!values || values->next ||
+                                   strcmp (resptests[i].headers[h].value, values->data) != 0)
+                                       ok = FALSE;
+                       }
+                       if (g_hash_table_size (headers) != h)
+                               ok = FALSE;
+               } else {
+                       if (resptests[i].reason_phrase)
+                               ok = FALSE;
+               }
+
+               if (ok)
+                       printf ("OK!\n");
+               else {
+                       printf ("BAD!\n");
+                       errors++;
+                       if (resptests[i].reason_phrase) {
+                               printf ("    expected: 'HTTP/1.%d' '%03d' '%s'\n",
+                                       resptests[i].version,
+                                       resptests[i].status_code,
+                                       resptests[i].reason_phrase);
+                               for (h = 0; resptests[i].headers[h].name; h++) {
+                                       printf ("              '%s': '%s'\n",
+                                               resptests[i].headers[h].name,
+                                               resptests[i].headers[h].value);
+                               }
+                       } else
+                               printf ("    expected: parse error\n");
+                       if (reason_phrase) {
+                               printf ("         got: 'HTTP/1.%d' '%03d' '%s'\n",
+                                       version, status_code, reason_phrase);
+                               g_hash_table_foreach (headers, print_header, NULL);
+                       } else
+                               printf ("         got: parse error\n");
+               }
+
+               g_free (reason_phrase);
+               g_hash_table_destroy (headers);
+       }
+       printf ("\n");
+
+       return errors;
+}
+
+int
+main (int argc, char **argv)
+{
+       int errors;
+
+       errors = do_request_tests ();
+       errors += do_response_tests ();
+
+       printf ("%d errors\n", errors);
+       return errors;
+}