/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ #include "test-utils.h" typedef struct { const char *name, *value; } Header; static struct RequestTest { const char *description; const char *request; int length; guint status; const char *method, *path; SoupHTTPVersion version; Header headers[10]; } reqtests[] = { /**********************/ /*** VALID REQUESTS ***/ /**********************/ { "HTTP 1.0 request with no headers", "GET / HTTP/1.0\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_0, { { NULL } } }, { "Req w/ 1 header", "GET / HTTP/1.1\r\nHost: example.com\r\n", -1, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Connection", "close" }, { "Blah", "blah" }, { "Foo", "bar baz" }, { 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, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Connection", "close" }, { "Blah", "blah" }, { "Foo", "bar baz" }, { NULL } } }, { "Req w/ same header multiple times", "GET / HTTP/1.1\r\nFoo: bar\r\nFoo: baz\r\nFoo: quux\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Foo", "bar, baz, quux" }, { NULL } } }, { "Connection header on HTTP/1.0 message", "GET / HTTP/1.0\r\nFoo: bar\r\nConnection: Bar, Quux\r\nBar: baz\r\nQuux: foo\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_0, { { "Foo", "bar" }, { "Connection", "Bar, Quux" }, { NULL } } }, { "GET with full URI", "GET http://example.com HTTP/1.1\r\n", -1, SOUP_STATUS_OK, "GET", "http://example.com", SOUP_HTTP_1_1, { { NULL } } }, { "GET with full URI in upper-case", "GET HTTP://example.com HTTP/1.1\r\n", -1, SOUP_STATUS_OK, "GET", "HTTP://example.com", SOUP_HTTP_1_1, { { NULL } } }, /* It's better for this to be passed through: this means a SoupServer * could implement ftp-over-http proxying, for instance */ { "GET with full URI of unrecognised scheme", "GET AbOuT: HTTP/1.1\r\n", -1, SOUP_STATUS_OK, "GET", "AbOuT:", SOUP_HTTP_1_1, { { 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, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Host", "example.com" }, { NULL } } }, /* RFC 2616 section 3.1 says we MUST accept this */ { "HTTP/01.01 request", "GET / HTTP/01.01\r\nHost: example.com\r\n", -1, SOUP_STATUS_OK, "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\r\nHost: example.com\nConnection: close\n", -1, SOUP_STATUS_OK, "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, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Host", "example.com" }, { NULL } } }, { "Mixed CRLF/LF", "GET / HTTP/1.1\r\na: b\r\nc: d\ne: f\r\ng: h\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "a", "b" }, { "c", "d" }, { "e", "f" }, { "g", "h" }, { NULL } } }, { "Req w/ incorrect whitespace in Request-Line", "GET /\tHTTP/1.1\r\nHost: example.com\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Host", "example.com" }, { NULL } } }, { "Req w/ incorrect whitespace after Request-Line", "GET / HTTP/1.1 \r\nHost: example.com\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Host", "example.com" }, { NULL } } }, /* If the request/status line is parseable, then we * just ignore any invalid-looking headers after that. * (qv bug 579318). */ { "Req w/ mangled header", "GET / HTTP/1.1\r\nHost: example.com\r\nFoo one\r\nBar: two\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Host", "example.com" }, { "Bar", "two" }, { NULL } } }, { "First header line is continuation", "GET / HTTP/1.1\r\n b\r\nHost: example.com\r\nc: d\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "Host", "example.com" }, { "c", "d" }, { NULL } } }, { "Zero-length header name", "GET / HTTP/1.1\r\na: b\r\n: example.com\r\nc: d\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "a", "b" }, { "c", "d" }, { NULL } } }, { "CR in header name", "GET / HTTP/1.1\r\na: b\r\na\rb: cd\r\nx\r: y\r\n\rz: w\r\nc: d\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "a", "b" }, { "c", "d" }, { NULL } } }, { "CR in header value", "GET / HTTP/1.1\r\na: b\r\nHost: example\rcom\r\np: \rq\r\ns: t\r\r\nc: d\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "a", "b" }, { "Host", "example com" }, /* CR in the middle turns to space */ { "p", "q" }, /* CR at beginning is ignored */ { "s", "t" }, /* CR at end is ignored */ { "c", "d" }, { NULL } } }, { "Tab in header name", "GET / HTTP/1.1\r\na: b\r\na\tb: cd\r\nx\t: y\r\np: q\r\n\tz: w\r\nc: d\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "a", "b" }, /* Tab anywhere in the header name causes it to be * ignored... except at beginning of line where it's a * continuation line */ { "p", "q z: w" }, { "c", "d" }, { NULL } } }, { "Tab in header value", "GET / HTTP/1.1\r\na: b\r\nab: c\td\r\nx: \ty\r\nz: w\t\r\nc: d\r\n", -1, SOUP_STATUS_OK, "GET", "/", SOUP_HTTP_1_1, { { "a", "b" }, { "ab", "c\td" }, /* internal tab preserved */ { "x", "y" }, /* leading tab ignored */ { "z", "w" }, /* trailing tab ignored */ { "c", "d" }, { NULL } } }, /************************/ /*** INVALID REQUESTS ***/ /************************/ { "HTTP 0.9 request; not supported", "GET /\r\n", -1, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "HTTP 1.2 request (no such thing)", "GET / HTTP/1.2\r\n", -1, SOUP_STATUS_HTTP_VERSION_NOT_SUPPORTED, NULL, NULL, -1, { { NULL } } }, { "HTTP 2000 request (no such thing)", "GET / HTTP/2000.0\r\n", -1, SOUP_STATUS_HTTP_VERSION_NOT_SUPPORTED, NULL, NULL, -1, { { NULL } } }, { "Non-HTTP request", "GET / SOUP/1.1\r\nHost: example.com\r\n", -1, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "Junk after Request-Line", "GET / HTTP/1.1 blah\r\nHost: example.com\r\n", -1, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "NUL in Method", "G\x00T / HTTP/1.1\r\nHost: example.com\r\n", 37, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "NUL at beginning of Method", "\x00 / HTTP/1.1\r\nHost: example.com\r\n", 35, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "NUL in Path", "GET /\x00 HTTP/1.1\r\nHost: example.com\r\n", 38, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "NUL in header name", "GET / HTTP/1.1\r\n\x00: silly\r\n", 37, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "NUL in header value", "GET / HTTP/1.1\r\nHost: example\x00com\r\n", 37, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "No terminating CRLF", "GET / HTTP/1.1\r\nHost: example.com", -1, SOUP_STATUS_BAD_REQUEST, NULL, NULL, -1, { { NULL } } }, { "Unrecognized expectation", "GET / HTTP/1.1\r\nHost: example.com\r\nExpect: the-impossible\r\n", -1, SOUP_STATUS_EXPECTATION_FAILED, NULL, NULL, -1, { { NULL } } } }; static const int num_reqtests = G_N_ELEMENTS (reqtests); static struct ResponseTest { const char *description; const char *response; int length; SoupHTTPVersion version; guint status_code; const char *reason_phrase; Header headers[10]; } 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/ same header multiple times", "HTTP/1.1 200 ok\r\nFoo: bar\r\nFoo: baz\r\nFoo: 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 } } }, { "Connection header on HTTP/1.0 message", "HTTP/1.0 200 ok\r\nFoo: bar\r\nConnection: Bar\r\nBar: quux\r\n", -1, SOUP_HTTP_1_0, SOUP_STATUS_OK, "ok", { { "Foo", "bar" }, { "Connection", "Bar" }, { NULL } } }, /* Tests from Cockpit */ { "Response w/ 3 headers, check case-insensitivity", "HTTP/1.0 200 ok\r\nHeader1: value3\r\nHeader2: field\r\nHead3: Another \r\n", -1, SOUP_HTTP_1_0, SOUP_STATUS_OK, "ok", { { "header1", "value3" }, { "Header2", "field" }, { "hEAD3", "Another" }, { "Something else", NULL }, { NULL } } }, /*****************************/ /*** RECOVERABLE RESPONSES ***/ /*****************************/ /* RFC 2616 section 3.1 says we MUST accept this */ { "HTTP/01.01 response", "HTTP/01.01 200 ok\r\nFoo: bar\r\n", -1, SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok", { { "Foo", "bar" }, { NULL } } }, /* 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 } } }, /* Shoutcast support */ { "Shoutcast server not-quite-HTTP", "ICY 200 OK\r\nFoo: bar\r\n", -1, SOUP_HTTP_1_0, SOUP_STATUS_OK, "OK", { { "Foo", "bar" }, { NULL } } }, /* qv bug 579318, do_bad_header_tests() below */ { "Response w/ mangled header", "HTTP/1.1 200 ok\r\nFoo: one\r\nBar two:2\r\nBaz: three\r\n", -1, SOUP_HTTP_1_1, SOUP_STATUS_OK, "ok", { { "Foo", "one" }, { "Baz", "three" }, { NULL } } }, /* qv bug 602863 */ { "HTTP 1.1 response with leading line break", "\nHTTP/1.1 200 ok\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 at start", "\x00HTTP/1.1 200 OK\r\nFoo: bar\r\n", 28, -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 name", "HTTP/1.1 200 OK\r\nF\x00oo: bar\r\n", 28, -1, 0, NULL, { { NULL } } }, { "NUL in header value", "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 struct QValueTest { const char *header_value; const char *acceptable[7]; const char *unacceptable[2]; } qvaluetests[] = { { "text/plain; q=0.5, text/html,\t text/x-dvi; q=0.8, text/x-c", { "text/html", "text/x-c", "text/x-dvi", "text/plain", NULL }, { NULL }, }, { "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5", { "text/html;level=1", "text/html", "*/*", "text/html;level=2", "text/*", NULL }, { NULL } }, { "gzip;q=1.0, identity; q=0.5, *;q=0", { "gzip", "identity", NULL }, { "*", NULL }, } }; static const int num_qvaluetests = G_N_ELEMENTS (qvaluetests); static void check_headers (Header *headers, SoupMessageHeaders *hdrs) { GSList *header_names, *h; SoupMessageHeadersIter iter; const char *name, *value; int i; header_names = NULL; soup_message_headers_iter_init (&iter, hdrs); while (soup_message_headers_iter_next (&iter, &name, &value)) { if (!g_slist_find_custom (header_names, name, (GCompareFunc)strcmp)) header_names = g_slist_append (header_names, (char *)name); } for (i = 0, h = header_names; headers[i].name && h; i++, h = h->next) { g_assert (g_ascii_strcasecmp (h->data, headers[i].name) == 0); value = soup_message_headers_get_list (hdrs, headers[i].name); g_assert_cmpstr (value, ==, headers[i].value); } /* If we have remaining fields to check, they should return NULL */ for (; headers[i].name; i++) { value = soup_message_headers_get_list (hdrs, headers[i].name); g_assert_null (value); } g_assert_null (headers[i].name); g_assert_null (h); g_slist_free (header_names); } static void do_request_tests (void) { int i, len; char *method, *path; SoupHTTPVersion version; SoupMessageHeaders *headers; guint status; for (i = 0; i < num_reqtests; i++) { debug_printf (1, "%2d. %s (%s)\n", i + 1, reqtests[i].description, soup_status_get_phrase (reqtests[i].status)); headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST); method = path = NULL; if (reqtests[i].length == -1) len = strlen (reqtests[i].request); else len = reqtests[i].length; status = soup_headers_parse_request (reqtests[i].request, len, headers, &method, &path, &version); g_assert_cmpint (status, ==, reqtests[i].status); if (SOUP_STATUS_IS_SUCCESSFUL (status)) { g_assert_cmpstr (method, ==, reqtests[i].method); g_assert_cmpstr (path, ==, reqtests[i].path); g_assert_cmpint (version, ==, reqtests[i].version); check_headers (reqtests[i].headers, headers); } g_free (method); g_free (path); soup_message_headers_free (headers); } } static void do_response_tests (void) { int i, len; guint status_code; char *reason_phrase; SoupHTTPVersion version; SoupMessageHeaders *headers; for (i = 0; i < num_resptests; i++) { debug_printf (1, "%2d. %s (%s)\n", i + 1, resptests[i].description, resptests[i].reason_phrase ? "should parse" : "should NOT parse"); headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE); 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)) { g_assert_cmpint (version, ==, resptests[i].version); g_assert_cmpint (status_code, ==, resptests[i].status_code); g_assert_cmpstr (reason_phrase, ==, resptests[i].reason_phrase); check_headers (resptests[i].headers, headers); } else g_assert_null (resptests[i].reason_phrase); g_free (reason_phrase); soup_message_headers_free (headers); } } static void do_qvalue_tests (void) { int i, j; GSList *acceptable, *unacceptable, *iter; for (i = 0; i < num_qvaluetests; i++) { debug_printf (1, "%2d. %s:\n", i + 1, qvaluetests[i].header_value); unacceptable = NULL; acceptable = soup_header_parse_quality_list (qvaluetests[i].header_value, &unacceptable); debug_printf (1, " acceptable: "); if (acceptable) { for (iter = acceptable, j = 0; iter; iter = iter->next, j++) { debug_printf (1, "%s ", (char *)iter->data); g_assert_cmpstr (iter->data, ==, qvaluetests[i].acceptable[j]); } debug_printf (1, "\n"); soup_header_free_list (acceptable); } else debug_printf (1, "(none)\n"); debug_printf (1, " unacceptable: "); if (unacceptable) { for (iter = unacceptable, j = 0; iter; iter = iter->next, j++) { debug_printf (1, "%s ", (char *)iter->data); g_assert_cmpstr (iter->data, ==, qvaluetests[i].unacceptable[j]); } debug_printf (1, "\n"); soup_header_free_list (unacceptable); } else debug_printf (1, "(none)\n"); } } #define RFC5987_TEST_FILENAME "t\xC3\xA9st.txt" #define RFC5987_TEST_FALLBACK_FILENAME "test.txt" #define RFC5987_TEST_HEADER_ENCODED "attachment; filename*=UTF-8''t%C3%A9st.txt" #define RFC5987_TEST_HEADER_UTF8 "attachment; filename*=UTF-8''t%C3%A9st.txt; filename=\"test.txt\"" #define RFC5987_TEST_HEADER_ISO "attachment; filename=\"test.txt\"; filename*=iso-8859-1''t%E9st.txt" #define RFC5987_TEST_HEADER_FALLBACK "attachment; filename*=Unknown''t%FF%FF%FFst.txt; filename=\"test.txt\"" static void do_content_disposition_tests (void) { SoupMessageHeaders *hdrs; GHashTable *params; const char *header, *filename; char *disposition; SoupBuffer *buffer; SoupMultipart *multipart; SoupMessageBody *body; hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART); params = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_insert (params, "filename", RFC5987_TEST_FILENAME); soup_message_headers_set_content_disposition (hdrs, "attachment", params); g_hash_table_destroy (params); header = soup_message_headers_get_one (hdrs, "Content-Disposition"); g_assert_cmpstr (header, ==, RFC5987_TEST_HEADER_ENCODED); /* UTF-8 decoding */ soup_message_headers_clear (hdrs); soup_message_headers_append (hdrs, "Content-Disposition", RFC5987_TEST_HEADER_UTF8); if (!soup_message_headers_get_content_disposition (hdrs, &disposition, ¶ms)) { soup_test_assert (FALSE, "UTF-8 decoding FAILED"); return; } g_free (disposition); filename = g_hash_table_lookup (params, "filename"); g_assert_cmpstr (filename, ==, RFC5987_TEST_FILENAME); g_hash_table_destroy (params); /* ISO-8859-1 decoding */ soup_message_headers_clear (hdrs); soup_message_headers_append (hdrs, "Content-Disposition", RFC5987_TEST_HEADER_ISO); if (!soup_message_headers_get_content_disposition (hdrs, &disposition, ¶ms)) { soup_test_assert (FALSE, "iso-8859-1 decoding FAILED"); return; } g_free (disposition); filename = g_hash_table_lookup (params, "filename"); g_assert_cmpstr (filename, ==, RFC5987_TEST_FILENAME); g_hash_table_destroy (params); /* Fallback */ soup_message_headers_clear (hdrs); soup_message_headers_append (hdrs, "Content-Disposition", RFC5987_TEST_HEADER_FALLBACK); if (!soup_message_headers_get_content_disposition (hdrs, &disposition, ¶ms)) { soup_test_assert (FALSE, "fallback decoding FAILED"); return; } g_free (disposition); filename = g_hash_table_lookup (params, "filename"); g_assert_cmpstr (filename, ==, RFC5987_TEST_FALLBACK_FILENAME); g_hash_table_destroy (params); soup_message_headers_free (hdrs); /* Ensure that soup-multipart always quotes filename (bug 641280) */ multipart = soup_multipart_new (SOUP_FORM_MIME_TYPE_MULTIPART); buffer = soup_buffer_new (SOUP_MEMORY_STATIC, "foo", 3); soup_multipart_append_form_file (multipart, "test", "token", "text/plain", buffer); soup_buffer_free (buffer); hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART); body = soup_message_body_new (); soup_multipart_to_message (multipart, hdrs, body); soup_message_headers_free (hdrs); soup_multipart_free (multipart); buffer = soup_message_body_flatten (body); soup_message_body_free (body); g_assert_true (strstr (buffer->data, "filename=\"token\"")); soup_buffer_free (buffer); } #define CONTENT_TYPE_TEST_MIME_TYPE "text/plain" #define CONTENT_TYPE_TEST_ATTRIBUTE "charset" #define CONTENT_TYPE_TEST_VALUE "US-ASCII" #define CONTENT_TYPE_TEST_HEADER "text/plain; charset=US-ASCII" #define CONTENT_TYPE_BAD_HEADER "plain text, not text/html" static void do_content_type_tests (void) { SoupMessageHeaders *hdrs; GHashTable *params; const char *header, *mime_type; hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART); params = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_insert (params, CONTENT_TYPE_TEST_ATTRIBUTE, CONTENT_TYPE_TEST_VALUE); soup_message_headers_set_content_type (hdrs, CONTENT_TYPE_TEST_MIME_TYPE, params); g_hash_table_destroy (params); header = soup_message_headers_get_one (hdrs, "Content-Type"); g_assert_cmpstr (header, ==, CONTENT_TYPE_TEST_HEADER); soup_message_headers_clear (hdrs); soup_message_headers_append (hdrs, "Content-Type", CONTENT_TYPE_TEST_MIME_TYPE); /* Add a second Content-Type header: should be ignored */ soup_message_headers_append (hdrs, "Content-Type", CONTENT_TYPE_TEST_MIME_TYPE); mime_type = soup_message_headers_get_content_type (hdrs, ¶ms); g_assert_cmpstr (mime_type, ==, CONTENT_TYPE_TEST_MIME_TYPE); g_assert_cmpint (g_hash_table_size (params), ==, 0); if (params) g_hash_table_destroy (params); soup_message_headers_clear (hdrs); soup_message_headers_append (hdrs, "Content-Type", CONTENT_TYPE_BAD_HEADER); mime_type = soup_message_headers_get_content_type (hdrs, ¶ms); g_assert_null (mime_type); soup_message_headers_free (hdrs); } struct { const char *name, *value; } test_params[] = { { "one", "foo" }, { "two", "test with spaces" }, { "three", "test with \"quotes\" and \\s" }, { "four", NULL }, { "five", "test with \xC3\xA1\xC3\xA7\xC4\x89\xC3\xA8\xC3\xB1\xC5\xA3\xC5\xA1" } }; #define TEST_PARAMS_RESULT "one=foo, two=\"test with spaces\", three=\"test with \\\"quotes\\\" and \\\\s\", four, five*=UTF-8''test%20with%20%C3%A1%C3%A7%C4%89%C3%A8%C3%B1%C5%A3%C5%A1" static void do_append_param_tests (void) { GString *params; int i; params = g_string_new (NULL); for (i = 0; i < G_N_ELEMENTS (test_params); i++) { if (i > 0) g_string_append (params, ", "); soup_header_g_string_append_param (params, test_params[i].name, test_params[i].value); } g_assert_cmpstr (params->str, ==, TEST_PARAMS_RESULT); g_string_free (params, TRUE); } static const struct { const char *description, *name, *value; } bad_headers[] = { { "Empty name", "", "value" }, { "Name with spaces", "na me", "value" }, { "Name with colon", "na:me", "value" }, { "Name with CR", "na\rme", "value" }, { "Name with LF", "na\nme", "value" }, { "Name with tab", "na\tme", "value" }, { "Value with CR", "name", "val\rue" }, { "Value with LF", "name", "val\nue" }, { "Value with LWS", "name", "val\r\n ue" } }; static void do_bad_header_tests (void) { SoupMessageHeaders *hdrs; int i; hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART); for (i = 0; i < G_N_ELEMENTS (bad_headers); i++) { debug_printf (1, " %s\n", bad_headers[i].description); g_test_expect_message ("libsoup", G_LOG_LEVEL_CRITICAL, "*soup_message_headers_append*assertion*failed*"); soup_message_headers_append (hdrs, bad_headers[i].name, bad_headers[i].value); g_test_assert_expected_messages (); } soup_message_headers_free (hdrs); } int main (int argc, char **argv) { int ret; test_init (argc, argv, NULL); g_test_add_func ("/header-parsing/request", do_request_tests); g_test_add_func ("/header-parsing/response", do_response_tests); g_test_add_func ("/header-parsing/qvalue", do_qvalue_tests); g_test_add_func ("/header-parsing/content-disposition", do_content_disposition_tests); g_test_add_func ("/header-parsing/content-type", do_content_type_tests); g_test_add_func ("/header-parsing/append-param", do_append_param_tests); g_test_add_func ("/header-parsing/bad", do_bad_header_tests); ret = g_test_run (); test_cleanup (); return ret; }