summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Winship <danw@src.gnome.org>2007-01-06 19:24:44 +0000
committerDan Winship <danw@src.gnome.org>2007-01-06 19:24:44 +0000
commit536d7139dedeb86cfe19fe1b5de4ab240aae0c06 (patch)
tree0fc95d9c0b7a8f8f02ea4ab25c2f4771eb69bcfe
parented822899a0a752fea5eb44f2f4ea4eaaed9d1176 (diff)
downloadlibsoup-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--ChangeLog16
-rw-r--r--libsoup/soup-headers.c284
-rw-r--r--libsoup/soup-headers.h4
-rw-r--r--tests/Makefile.am4
-rw-r--r--tests/header-parsing.c621
5 files changed, 795 insertions, 134 deletions
diff --git a/ChangeLog b/ChangeLog
index d379088f..7937ce06 100644
--- 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
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;
+}