changeset 15195:70305d850220

Adds HTTP URL parse support.
author Stephan Bosch <stephan@rename-it.nl>
date Wed, 10 Oct 2012 23:57:56 +0300
parents b8929da80876
children d927aaaf9252
files configure.in src/Makefile.am src/lib-http/Makefile.am src/lib-http/http-url.c src/lib-http/http-url.h src/lib-http/test-http-url.c
diffstat 6 files changed, 834 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/configure.in	Wed Oct 10 23:56:01 2012 +0300
+++ b/configure.in	Wed Oct 10 23:57:56 2012 +0300
@@ -2754,6 +2754,7 @@
 src/lib-dict/Makefile
 src/lib-dns/Makefile
 src/lib-fs/Makefile
+src/lib-http/Makefile
 src/lib-imap/Makefile
 src/lib-imap-storage/Makefile
 src/lib-imap-client/Makefile
--- a/src/Makefile.am	Wed Oct 10 23:56:01 2012 +0300
+++ b/src/Makefile.am	Wed Oct 10 23:57:56 2012 +0300
@@ -9,6 +9,7 @@
 	lib-imap \
 	lib-imap-storage \
 	lib-master \
+	lib-http \
 	lib-dict \
 	lib-settings \
 	lib-ssl-iostream
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-http/Makefile.am	Wed Oct 10 23:57:56 2012 +0300
@@ -0,0 +1,37 @@
+noinst_LTLIBRARIES = libhttp.la
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib \
+	-I$(top_srcdir)/src/lib-test \
+	-I$(top_srcdir)/src/lib-dns \
+	-I$(top_srcdir)/src/lib-ssl-iostream
+
+libhttp_la_SOURCES = \
+	http-url.c
+
+headers = \
+	http-url.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+	test-http-url
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+	../lib-test/libtest.la \
+	../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_http_url_SOURCES = test-http-url.c
+test_http_url_LDADD = http-url.lo  $(test_libs)
+test_http_url_DEPENDENCIES = $(test_deps)
+
+check: check-am check-test
+check-test: all-am
+	for bin in $(test_programs); do \
+	  if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+	done
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-http/http-url.c	Wed Oct 10 23:57:56 2012 +0300
@@ -0,0 +1,287 @@
+/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "net.h"
+#include "uri-util.h"
+#include "http-url.h"
+
+/*
+ * HTTP URL parser
+ */
+
+struct http_url_parser {
+	struct uri_parser parser;
+
+	enum http_url_parse_flags flags;
+
+	struct http_url *url;
+	struct http_url *base;
+
+	unsigned int relative:1;
+};
+
+static bool http_url_do_parse(struct http_url_parser *url_parser)
+{
+	struct uri_parser *parser = &url_parser->parser;
+	struct http_url *url = url_parser->url, *base = url_parser->base;
+	struct uri_authority auth;
+	const char *const *path;
+	bool relative = TRUE, have_path = FALSE;
+	int path_relative;
+	const char *part;
+	int ret;
+
+	/* RFC 2616 - Hypertext Transfer Protocol, Section 3.2:
+	 *   
+	 * http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
+	 * 
+	 * Translated to RFC 3986:
+	 *
+	 * absolute-http-URL = "http:" "//" host [ ":" port ] path-absolute
+	 *                       ["?" query] [ "#" fragment ]
+	 * relative-http-ref = relative-http-part [ "?" query ] [ "#" fragment ]
+	 * relative-http-part = "//" host [ ":" port ] path-abempty
+	 *                      / path-absolute
+	 *                      / path-noscheme
+	 *                      / path-empty
+	 */
+
+	/* "http:" / "https:" */
+	if ((url_parser->flags & HTTP_URL_PARSE_SCHEME_EXTERNAL) == 0) {
+		const char *scheme;
+
+		if ((ret = uri_parse_scheme(parser, &scheme)) < 0)
+			return FALSE;
+		else if (ret > 0) {
+			if (strcasecmp(scheme, "https") == 0) {
+				if (url != NULL)
+					url->have_ssl = TRUE;
+			} else if (strcasecmp(scheme, "http") != 0) {
+				parser->error = "Not an HTTP URL";
+				return FALSE;
+			}
+			relative = FALSE;
+		}
+	} else {
+		relative = FALSE;
+	}
+
+	/* "//" host [ ":" port ] */
+	if ((ret = uri_parse_authority(parser, &auth)) < 0)
+		return FALSE;
+	if (ret > 0) {
+		if (auth.enc_userinfo != NULL) {
+			/* http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-20
+
+				 Section 2.8.1:
+
+			   {...} Senders MUST NOT include a userinfo subcomponent (and its "@"
+			   delimiter) when transmitting an "http" URI in a message. Recipients
+			   of HTTP messages that contain a URI reference SHOULD parse for the
+			   existence of userinfo and treat its presence as an error, likely
+			   indicating that the deprecated subcomponent is being used to
+			   obscure the authority for the sake of phishing attacks.
+			 */
+			parser->error = "HTTP URL does not allow `userinfo@' part";
+			return FALSE;
+		}
+		relative = FALSE;
+	} else if (!relative) {
+		parser->error = "Absolute HTTP URL requires `//' after `http:'";
+		return FALSE;
+	}
+
+	if (ret > 0 && url != NULL) {
+		url->host_name = auth.host_literal;
+		url->host_ip = auth.host_ip;
+		url->have_host_ip = auth.have_host_ip;
+		url->port = auth.port;
+		url->have_port = auth.have_port;
+	}
+
+	/* path-abempty / path-absolute / path-noscheme / path-empty */
+	if ((ret = uri_parse_path(parser, &path_relative, &path)) < 0)
+		return FALSE;
+
+	/* Relative URLs are only valid when we have a base URL */
+	if (relative) {
+		if (base == NULL) {
+			parser->error = "Relative URL not allowed";
+			return FALSE;
+		} else if (url != NULL) {
+			url->host_name = p_strdup_empty(parser->pool, base->host_name); 
+			url->host_ip = base->host_ip;
+			url->have_host_ip = base->have_host_ip;
+			url->port = base->port;
+			url->have_port = base->have_port;
+		}
+
+		url_parser->relative = TRUE;
+	}
+
+	/* Resolve path */
+	if (ret > 0) {
+		string_t *fullpath;
+
+		have_path = TRUE;
+
+		if (url != NULL)
+			fullpath = t_str_new(256);
+
+		if (relative && path_relative > 0 && base->path != NULL) {
+			const char *pbegin = base->path;
+			const char *pend = base->path + strlen(base->path);
+			const char *p = pend - 1;
+
+			i_assert(*pbegin == '/');
+
+			/* discard trailing segments of base path based on how many effective
+			   leading '..' segments were found in the relative path.
+			 */
+			while (path_relative > 0 && p > pbegin) {
+				while (p > pbegin && *p != '/') p--;
+				if (p >= pbegin) {
+					pend = p;
+					path_relative--;
+				}
+				if (p > pbegin) p--;
+			}
+
+			if (url != NULL && pend > pbegin)
+				str_append_n(fullpath, pbegin, pend-pbegin);
+		}
+	
+		/* append relative path */
+		while (*path != NULL) {
+			if (!uri_data_decode(parser, *path, NULL, &part))
+				return FALSE;
+
+			if (url != NULL) {
+				str_append_c(fullpath, '/');
+				str_append(fullpath, part);
+			}	
+			path++;
+		}
+
+		if (url != NULL)
+			url->path = str_c(fullpath);
+	} else if (relative && url != NULL) {
+		url->path = base->path;
+	}
+	
+	/* [ "?" query ] */
+	if ((ret = uri_parse_query(parser, &part)) < 0)
+		return FALSE;
+	if (ret > 0) {
+		if (!uri_data_decode(parser, part, NULL, NULL)) // check only
+			return FALSE;
+		if (url != NULL)
+			url->enc_query = p_strdup(parser->pool, part);
+	} else if (relative && !have_path && url != NULL) {
+		url->enc_query = p_strdup(parser->pool, base->enc_query);		
+	}
+
+	/* [ "#" fragment ] */
+	if ((ret = uri_parse_fragment(parser, &part)) < 0) 
+		return FALSE;
+	if (ret > 0) {	
+		if ((url_parser->flags & HTTP_URL_ALLOW_FRAGMENT_PART) == 0) {
+			parser->error = "URL fragment not allowed for HTTP URL in this context";
+			return FALSE;
+		}
+		if (!uri_data_decode(parser, part, NULL, NULL)) // check only
+			return FALSE;
+		if (url != NULL)
+			url->enc_fragment =  p_strdup(parser->pool, part);
+	} else if (relative && !have_path && url != NULL) {
+		url->enc_fragment = p_strdup(parser->pool, base->enc_fragment);		
+	}
+
+	if (parser->cur != parser->end) {
+		parser->error = "HTTP URL contains invalid character.";
+		return FALSE;
+	}
+	return TRUE;
+}
+
+/* Public API */
+
+int http_url_parse(const char *url, struct http_url *base,
+		   enum http_url_parse_flags flags,
+		   struct http_url **url_r, const char **error_r)
+{
+	struct http_url_parser url_parser;
+
+	/* base != NULL indicates whether relative URLs are allowed. However, certain
+	   flags may also dictate whether relative URLs are allowed/required. */
+	i_assert((flags & HTTP_URL_PARSE_SCHEME_EXTERNAL) == 0 || base == NULL);
+
+	memset(&url_parser, '\0', sizeof(url_parser));
+	uri_parser_init(&url_parser.parser, pool_datastack_create(), url);
+
+	url_parser.url = t_new(struct http_url, 1);
+	url_parser.base = base;
+	url_parser.flags = flags;
+
+	if (!http_url_do_parse(&url_parser)) {
+		*error_r = url_parser.parser.error;
+		return -1;
+	}
+	*url_r = url_parser.url;
+	return 0;
+}
+
+/*
+ * HTTP URL construction
+ */
+
+const char *http_url_create(const struct http_url *url)
+{
+	string_t *urlstr = t_str_new(512);
+
+	/* scheme */
+	uri_append_scheme(urlstr, "http");
+	str_append(urlstr, "//");
+
+	/* host:port */
+	if (url->host_name != NULL) {
+		/* assume IPv6 literal if starts with '['; avoid encoding */
+		if (*url->host_name == '[')
+			str_append(urlstr, url->host_name);
+		else
+			uri_append_host_name(urlstr, url->host_name);
+	} else if (url->have_host_ip) {
+		uri_append_host_ip(urlstr, &url->host_ip);
+	} else
+		i_unreached();
+	if (url->have_port)
+		uri_append_port(urlstr, url->port);
+
+	if (url->path == NULL || *url->path == '\0') {
+		/* Older syntax of RFC 2616 requires this slash at all times for an
+			 absolute URL
+		 */
+		str_append_c(urlstr, '/');
+	} else {
+		uri_append_path_data(urlstr, "", url->path);
+	}
+
+	/* query (pre-encoded) */
+	if (url->enc_query != NULL) {
+		str_append_c(urlstr, '?');
+		str_append(urlstr, url->enc_query);
+	}
+
+	/* fragment */
+	if (url->enc_fragment != NULL) {
+		str_append_c(urlstr, '#');
+		str_append(urlstr, url->enc_fragment);
+	}
+
+	return str_c(urlstr);
+}
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-http/http-url.h	Wed Oct 10 23:57:56 2012 +0300
@@ -0,0 +1,46 @@
+#ifndef HTTP_URL_H
+#define HTTP_URL_H
+
+struct http_url {
+	/* server */
+	const char *host_name;
+	struct ip_addr host_ip;
+	in_port_t port;
+
+	/* path */
+	const char *path;
+
+	/* ?query (still encoded) */
+	const char *enc_query;
+
+	/* #fragment (still encoded) */
+	const char *enc_fragment;
+
+	unsigned int have_host_ip:1; /* URL uses IP address */
+	unsigned int have_port:1;
+	unsigned int have_ssl:1;
+};
+
+/*
+ * HTTP URL parsing
+ */
+
+enum http_url_parse_flags {
+	/* Scheme part 'http:' is already parsed externally. This implies that
+	   this is an absolute HTTP URL. */
+	HTTP_URL_PARSE_SCHEME_EXTERNAL	= 0x01,
+	/* Allow '#fragment' part in URL */
+	HTTP_URL_ALLOW_FRAGMENT_PART = 0x02
+};
+
+int http_url_parse(const char *url, struct http_url *base,
+		   enum http_url_parse_flags flags,
+		   struct http_url **url_r, const char **error_r);
+
+/*
+ * HTTP URL construction
+ */
+
+const char *http_url_create(const struct http_url *url);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-http/test-http-url.c	Wed Oct 10 23:57:56 2012 +0300
@@ -0,0 +1,462 @@
+/* Copyright (c) 2009-2012 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "http-url.h"
+#include "test-common.h"
+
+struct valid_http_url_test {
+	const char *url;
+	enum http_url_parse_flags flags;
+	struct http_url url_base;
+
+	struct http_url url_parsed;
+};
+
+/* Valid HTTP URL tests */
+static struct valid_http_url_test valid_url_tests[] = {
+	/* Generic tests */
+	{
+		.url = "http://localhost",
+		.url_parsed = {
+			.host_name = "localhost" }
+	},{
+		.url = "http://www.%65%78%61%6d%70%6c%65.com",
+		.url_parsed = {
+			.host_name = "www.example.com" }
+	},{
+		.url = "http://www.dovecot.org:8080",
+		.url_parsed = {
+			.host_name = "www.dovecot.org",
+			.port = 8080, .have_port = TRUE }
+	},{
+		.url = "http://127.0.0.1",
+		.url_parsed = {
+			.host_name = "127.0.0.1",
+			.have_host_ip = TRUE }
+#ifdef HAVE_IPV6
+	},{
+		.url = "http://[::1]",
+		.url_parsed = {
+			.host_name = "[::1]",
+			.have_host_ip = TRUE }
+	},{
+		.url = "http://[::1]:8080",
+		.url_parsed = {
+			.host_name = "[::1]",
+			.have_host_ip = TRUE,
+			.port = 8080, .have_port = TRUE }
+#endif
+	},{
+		.url = "http://www.example.com/"
+			"?question=What%20are%20you%20doing%3f&answer=Nothing.",
+		.url_parsed = {
+			.path = "/",
+			.host_name = "www.example.com",
+			.enc_query = "question=What%20are%20you%20doing%3f&answer=Nothing." }
+	},{
+		.url = "http://www.example.com/#Status%20of%20development",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+		.url_parsed = {
+			.path = "/",
+			.host_name = "www.example.com",
+			.enc_fragment = "Status%20of%20development" }
+
+	
+	/* RFC 3986, Section 5.4. Reference Resolution Examples 
+	 *
+	 * Within a representation with a well defined base URI of
+	 *
+	 *  http://a/b/c/d;p?q
+	 *
+	 * a relative reference is transformed to its target URI as follows.
+	 *
+	 * 5.4.1. Normal Examples
+	 */
+	},{ // "g"             =  "http://a/b/c/g"
+		.url = "g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g" }
+	},{ // "./g"           =  "http://a/b/c/g"
+		.url = "./g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g" }
+	},{ // "g/"            =  "http://a/b/c/g/"
+		.url = "g/",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g/" }
+	},{ // "/g"            =  "http://a/g"
+		.url = "/g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/g" }
+	},{ // "//g"           =  "http://g"
+		.url = "//g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "g" }
+	},{ // "?y"            =  "http://a/b/c/d;p?y"
+		.url = "?y",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "y" }
+	},{ // "g?y"           =  "http://a/b/c/g?y"
+		.url = "g?y",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g", .enc_query = "y" }
+	},{ // "#s"            =  "http://a/b/c/d;p?q#s"
+		.url = "#s",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q",
+			.enc_fragment = "s" }
+	},{ // "g#s"           =  "http://a/b/c/g#s"
+		.url = "g#s",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g", .enc_fragment = "s" }
+
+	},{ // "g?y#s"         =  "http://a/b/c/g?y#s"
+		.url = "g?y#s",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g", .enc_query = "y",
+			.enc_fragment = "s" }
+	},{ // ";x"            =  "http://a/b/c/;x"
+		.url = ";x",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/;x" }
+	},{ // "g;x"           =  "http://a/b/c/g;x"
+		.url = "g;x",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g;x" }
+
+	},{ // "g;x?y#s"       =  "http://a/b/c/g;x?y#s"
+		.url = "g;x?y#s",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g;x", .enc_query = "y",
+			.enc_fragment = "s" }
+	},{ // ""              =  "http://a/b/c/d;p?q"
+		.url = "",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" }
+	},{ // "."             =  "http://a/b/c/"
+		.url = ".",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/" }
+	},{ // "./"            =  "http://a/b/c/"
+		.url = "./",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/" }
+	},{ // ".."            =  "http://a/b/"
+		.url = "..",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/" }
+	},{ // "../"           =  "http://a/b/"
+		.url = "../",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/" }
+	},{ // "../g"          =  "http://a/b/g"
+		.url = "../g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/g" }
+	},{ // "../.."         =  "http://a/"
+		.url = "../..",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/" }
+	},{ // "../../"        =  "http://a/"
+		.url = "../../",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/" }
+	},{ // "../../g"       =  "http://a/g"
+		.url = "../../g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/g" }
+
+	/* 5.4.2. Abnormal Examples
+	 */
+	},{ // "../../../g"    =  "http://a/g"
+		.url = "../../../g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/g" }
+	},{ // "../../../../g" =  "http://a/g"
+		.url = "../../../../g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/g" }
+	},{ // "/./g"          =  "http://a/g"
+		.url = "/./g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/g" }
+	},{ // "/../g"         =  "http://a/g"
+		.url = "/../g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/g" }
+	},{ // "g."            =  "http://a/b/c/g."
+		.url = "g.",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g." }
+	},{ // ".g"            =  "http://a/b/c/.g"
+		.url = ".g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/.g" }
+	},{ // "g.."           =  "http://a/b/c/g.."
+		.url = "g..",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g.." }
+	},{ // "..g"           =  "http://a/b/c/..g"
+		.url = "..g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/..g" }
+	},{ // "./../g"        =  "http://a/b/g"
+		.url = "./../g",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/g" }
+	},{ // "./g/."         =  "http://a/b/c/g/"
+		.url = "./g/.",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g/" }
+	},{ // "g/./h"         =  "http://a/b/c/g/h"
+		.url = "g/./h",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g/h" }
+	},{ // "g/../h"        =  "http://a/b/c/h"
+		.url = "g/../h",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/h" }
+	},{ // "g;x=1/./y"     =  "http://a/b/c/g;x=1/y"
+		.url = "g;x=1/./y",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g;x=1/y" }
+	},{ // "g;x=1/../y"    =  "http://a/b/c/y"
+		.url = "g;x=1/../y",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/y" }
+	},{ // "g?y/./x"       =  "http://a/b/c/g?y/./x"
+		.url = "g?y/./x",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g", .enc_query = "y/./x" }
+	},{ // "g?y/../x"      =  "http://a/b/c/g?y/../x"
+		.url = "g?y/../x",
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed = { .host_name = "a", .path = "/b/c/g", .enc_query = "y/../x" }
+	},{ // "g#s/./x"       =  "http://a/b/c/g#s/./x"
+		.url = "g#s/./x",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed =
+			{ .host_name = "a", .path = "/b/c/g", .enc_fragment = "s/./x" }
+	},{ // "g#s/../x"      =  "http://a/b/c/g#s/../x"	
+		.url = "g#s/../x",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+		.url_base = { .host_name = "a", .path = "/b/c/d;p", .enc_query = "q" },
+		.url_parsed =
+			{ .host_name = "a", .path = "/b/c/g", .enc_fragment = "s/../x" }
+	}
+};
+
+static unsigned int valid_url_test_count = N_ELEMENTS(valid_url_tests);
+
+static void test_http_url_valid(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < valid_url_test_count; i++) T_BEGIN {
+		const char *url = valid_url_tests[i].url;
+		enum http_url_parse_flags flags = valid_url_tests[i].flags;
+		struct http_url *urlt = &valid_url_tests[i].url_parsed;
+		struct http_url *urlb = &valid_url_tests[i].url_base;
+		struct http_url *urlp;
+		const char *error = NULL;
+
+		test_begin(t_strdup_printf("http url valid [%d]", i));
+
+		if (urlb->host_name == NULL) urlb = NULL;
+		if (http_url_parse(url, urlb, flags, &urlp, &error) < 0)
+			urlp = NULL;
+
+		test_out_reason(t_strdup_printf("http_url_parse(%s)",
+			valid_url_tests[i].url), urlp != NULL, error);
+		if (urlp != NULL) {
+			if (urlp->host_name == NULL || urlt->host_name == NULL) {
+				test_out(t_strdup_printf("url->host_name = %s", urlp->host_name),
+					urlp->host_name == urlt->host_name);
+			} else {
+				test_out(t_strdup_printf("url->host_name = %s", urlp->host_name),
+					strcmp(urlp->host_name, urlt->host_name) == 0);
+			}
+			if (!urlp->have_port) {
+				test_out("url->port = (unspecified)",
+					urlp->have_port == urlt->have_port);
+			} else {
+				test_out(t_strdup_printf("url->port = %u", urlp->port),
+					urlp->have_port == urlt->have_port && urlp->port == urlt->port);
+			}
+			if (!urlp->have_host_ip) {
+				test_out("url->host_ip = (unspecified)",
+					urlp->have_host_ip == urlt->have_host_ip);
+			} else {
+				test_out("url->host_ip = (valid)",
+					urlp->have_host_ip == urlt->have_host_ip);
+			}
+			if (urlp->path == NULL || urlt->path == NULL) {
+				test_out(t_strdup_printf("url->path = %s", urlp->path),
+					urlp->path == urlt->path);
+			} else {
+				test_out(t_strdup_printf("url->path = %s", urlp->path),
+					strcmp(urlp->path, urlt->path) == 0);
+			}
+			if (urlp->enc_query == NULL || urlt->enc_query == NULL) {
+				test_out(t_strdup_printf(
+						"url->enc_query = %s", urlp->enc_query),
+					urlp->enc_query == urlt->enc_query);
+			} else {
+				test_out(t_strdup_printf(
+						"url->enc_query = %s", urlp->enc_query),
+					strcmp(urlp->enc_query, urlt->enc_query) == 0);
+			}
+			if (urlp->enc_fragment == NULL || urlt->enc_fragment == NULL) {
+				test_out(t_strdup_printf(
+						"url->enc_fragment = %s", urlp->enc_fragment),
+					urlp->enc_fragment == urlt->enc_fragment);
+			} else {
+				test_out(t_strdup_printf(
+						"url->enc_fragment = %s", urlp->enc_fragment),
+					strcmp(urlp->enc_fragment, urlt->enc_fragment) == 0);
+			}
+		}
+
+		test_end();
+	} T_END;
+}
+
+struct invalid_http_url_test {
+	const char *url;
+	enum http_url_parse_flags flags;
+	struct http_url url_base;
+};
+
+static struct invalid_http_url_test invalid_url_tests[] = {
+	{
+		.url = "imap://example.com/INBOX"
+	},{
+		.url = "http:/www.example.com"
+	},{
+		.url = ""
+	},{
+		.url = "/index.html"
+	},{
+		.url = "http://www.example.com/index.html\""
+	},{
+		.url = "http:///dovecot.org"
+	},{
+		.url = "http://[]/index.html"
+	},{
+		.url = "http://[v08.234:232:234:234:2221]/index.html"
+#ifdef HAVE_IPV6
+	},{
+		.url = "http://[1::34a:34:234::6]/index.html"
+#endif
+	},{
+		.url = "http://example%a.com/index.html"
+	},{
+		.url = "http://example.com%/index.html"
+	},{
+		.url = "http://example%00.com/index.html"
+	},{
+		.url = "http://example.com:65539/index.html"
+	},{
+		.url = "http://example.com/settings/%00/"
+	},{
+		.url = "http://example.com/settings/%0r/"
+	},{
+		.url = "http://example.com/settings/misc/%/"
+	},{
+		.url = "http://example.com/?%00"
+	},{
+		.url = "http://www.example.com/network.html#IMAP_Server"
+	},{
+		.url = "http://example.com/#%00",
+		.flags = HTTP_URL_ALLOW_FRAGMENT_PART
+	}
+};
+
+static unsigned int invalid_url_test_count = N_ELEMENTS(invalid_url_tests);
+
+static void test_http_url_invalid(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < invalid_url_test_count; i++) T_BEGIN {
+		const char *url = invalid_url_tests[i].url;
+		enum http_url_parse_flags flags = invalid_url_tests[i].flags;
+		struct http_url *urlb = &invalid_url_tests[i].url_base;
+		struct http_url *urlp;
+		const char *error = NULL;
+
+		if (urlb->host_name == NULL)
+			urlb = NULL;
+
+		test_begin(t_strdup_printf("http url invalid [%d]", i));
+
+		if (http_url_parse(url, urlb, flags, &urlp, &error) < 0)
+			urlp = NULL;
+		test_out_reason(t_strdup_printf("parse %s", url), urlp == NULL, error);
+
+		test_end();
+	} T_END;
+
+}
+
+static const char *parse_create_url_tests[] = {
+	"http://www.example.com/",
+	"http://10.0.0.1/",
+#ifdef HAVE_IPV6
+	"http://[::1]/",
+#endif
+	"http://www.example.com:993/",
+	"http://www.example.com/index.html",
+	"http://www.example.com/settings/index.html",
+	"http://ww.%23example.com/",
+	"http://www.example.com/%23shared/news",
+	"http://www.example.com/query.php?name=Hendrik%20Visser",
+	"http://www.example.com/network.html#IMAP%20Server",
+};
+
+static unsigned int
+parse_create_url_test_count = N_ELEMENTS(parse_create_url_tests);
+
+static void test_http_url_parse_create(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < parse_create_url_test_count; i++) T_BEGIN {
+		const char *url = parse_create_url_tests[i];
+		struct http_url *urlp;
+		const char *error = NULL;
+
+		test_begin(t_strdup_printf("http url parse/create [%d]", i));
+
+		if (http_url_parse
+			(url, NULL, HTTP_URL_ALLOW_FRAGMENT_PART, &urlp, &error) < 0)
+			urlp = NULL;
+		test_out_reason(t_strdup_printf("parse  %s", url), urlp != NULL, error);
+		if (urlp != NULL) {
+			const char *urlnew = http_url_create(urlp);
+			test_out(t_strdup_printf
+				("create %s", urlnew), strcmp(url, urlnew) == 0);
+		}
+
+		test_end();
+	} T_END;
+
+}
+
+int main(void)
+{
+	static void (*test_functions[])(void) = {
+		test_http_url_valid,
+		test_http_url_invalid,
+		test_http_url_parse_create,
+		NULL
+	};
+	return test_run(test_functions);
+}
+