diff options
author | Dan Winship <danw@src.gnome.org> | 2007-01-06 19:24:44 +0000 |
---|---|---|
committer | Dan Winship <danw@src.gnome.org> | 2007-01-06 19:24:44 +0000 |
commit | 536d7139dedeb86cfe19fe1b5de4ab240aae0c06 (patch) | |
tree | 0fc95d9c0b7a8f8f02ea4ab25c2f4771eb69bcfe | |
parent | ed822899a0a752fea5eb44f2f4ea4eaaed9d1176 (diff) | |
download | libsoup-536d7139dedeb86cfe19fe1b5de4ab240aae0c06.tar.gz |
Rewrite this to be easier to understand and more correct, and make the
* 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
-rw-r--r-- | ChangeLog | 16 | ||||
-rw-r--r-- | libsoup/soup-headers.c | 284 | ||||
-rw-r--r-- | libsoup/soup-headers.h | 4 | ||||
-rw-r--r-- | tests/Makefile.am | 4 | ||||
-rw-r--r-- | tests/header-parsing.c | 621 |
5 files changed, 795 insertions, 134 deletions
@@ -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 diff --git a/libsoup/soup-headers.c b/libsoup/soup-headers.c index 14e75d6a..3467132f 100644 --- a/libsoup/soup-headers.c +++ b/libsoup/soup-headers.c @@ -13,93 +13,83 @@ #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)) diff --git a/libsoup/soup-headers.h b/libsoup/soup-headers.h index 174edd71..ea30b627 100644 --- a/libsoup/soup-headers.h +++ b/libsoup/soup-headers.h @@ -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, diff --git a/tests/Makefile.am b/tests/Makefile.am index 8eefbf16..e146a9aa 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -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 index 00000000..4e4d4677 --- /dev/null +++ b/tests/header-parsing.c @@ -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; +} |