#include <config.h>

#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "libsoup/soup-message.h"
#include "libsoup/soup-headers.h"

gboolean debug = FALSE;

static void
dprintf (const char *format, ...)
{
	va_list args;

	if (!debug)
		return;

	va_start (args, format);
	vprintf (format, args);
	va_end (args);
}

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 }
	  }
	},

	{ "Req w/ incorrect whitespace after Request-Line",
	  "GET / HTTP/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;
	dprintf ("              '%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 = 0;
	char *method, *path;
	GSList *values;
	SoupHttpVersion version;
	GHashTable *headers;

	dprintf ("Request tests\n");
	for (i = 0; i < 1; i++) {
		gboolean ok = TRUE;

		dprintf ("%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)
			dprintf ("OK!\n");
		else {
			dprintf ("BAD!\n");
			errors++;
			if (reqtests[i].method) {
				dprintf ("    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++) {
					dprintf ("              '%s': '%s'\n",
						 reqtests[i].headers[h].name,
						 reqtests[i].headers[h].value);
				}
			} else
				dprintf ("    expected: parse error\n");
			if (method) {
				dprintf ("         got: '%s' '%s' 'HTTP/1.%d'\n",
					method, path, version);
				g_hash_table_foreach (headers, print_header, NULL);
			} else
				dprintf ("         got: parse error\n");
		}

		g_free (method);
		g_free (path);
		g_hash_table_destroy (headers);
	}
	dprintf ("\n");

	return errors;
}

static int
do_response_tests (void)
{
	int i, len, h, errors = 0;
	guint status_code;
	char *reason_phrase;
	GSList *values;
	SoupHttpVersion version;
	GHashTable *headers;

	dprintf ("Response tests\n");
	for (i = 0; i < num_resptests; i++) {
		gboolean ok = TRUE;

		dprintf ("%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)
			dprintf ("OK!\n");
		else {
			dprintf ("BAD!\n");
			errors++;
			if (resptests[i].reason_phrase) {
				dprintf ("    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++) {
					dprintf ("              '%s': '%s'\n",
						 resptests[i].headers[h].name,
						 resptests[i].headers[h].value);
				}
			} else
				dprintf ("    expected: parse error\n");
			if (reason_phrase) {
				dprintf ("         got: 'HTTP/1.%d' '%03d' '%s'\n",
					 version, status_code, reason_phrase);
				g_hash_table_foreach (headers, print_header, NULL);
			} else
				dprintf ("         got: parse error\n");
		}

		g_free (reason_phrase);
		g_hash_table_destroy (headers);
	}
	dprintf ("\n");

	return errors;
}

int
main (int argc, char **argv)
{
	int opt, errors;

	while ((opt = getopt (argc, argv, "d")) != -1) {
		switch (opt) {
		case 'd':
			debug = TRUE;
			break;
		default:
			fprintf (stderr, "Usage: %s [-d]\n", argv[0]);
			return 1;
		}
	}

	errors = do_request_tests ();
	errors += do_response_tests ();

	dprintf ("\n");
	if (errors) {
		printf ("header-parsing: %d error(s). Run with '-d' for details\n",
			errors);
	} else
		printf ("header-parsing: OK\n");
	return errors;
}


syntax highlighted by Code2HTML, v. 0.9.1