1
0

2 Коммиты 224495a8c6 ... 3f1b906df7

Автор SHA1 Сообщение Дата
  221V 3f1b906df7 rm diet-ng - part 2 1 неделя назад
  221V 83ba2d447a rm diet-ng - part 1 - add vibe-d-http, deps 1 неделя назад
32 измененных файлов с 16946 добавлено и 3 удалено
  1. 6 1
      vtest/dub.json
  2. 0 2
      vtest/dub.selections.json
  3. 112 0
      vtest/source/vibe/http/auth/basic_auth.d
  4. 320 0
      vtest/source/vibe/http/auth/digest_auth.d
  5. 1300 0
      vtest/source/vibe/http/client.d
  6. 1107 0
      vtest/source/vibe/http/common.d
  7. 52 0
      vtest/source/vibe/http/dist.d
  8. 732 0
      vtest/source/vibe/http/fileserver.d
  9. 169 0
      vtest/source/vibe/http/form.d
  10. 21 0
      vtest/source/vibe/http/internal/basic_auth_client.d
  11. 665 0
      vtest/source/vibe/http/internal/http1/server.d
  12. 79 0
      vtest/source/vibe/http/internal/http2/error.d
  13. 708 0
      vtest/source/vibe/http/internal/http2/exchange.d
  14. 471 0
      vtest/source/vibe/http/internal/http2/frame.d
  15. 142 0
      vtest/source/vibe/http/internal/http2/hpack/decoder.d
  16. 126 0
      vtest/source/vibe/http/internal/http2/hpack/encoder.d
  17. 30 0
      vtest/source/vibe/http/internal/http2/hpack/exception.d
  18. 264 0
      vtest/source/vibe/http/internal/http2/hpack/hpack.d
  19. 2807 0
      vtest/source/vibe/http/internal/http2/hpack/huffman.d
  20. 411 0
      vtest/source/vibe/http/internal/http2/hpack/tables.d
  21. 14 0
      vtest/source/vibe/http/internal/http2/hpack/util.d
  22. 276 0
      vtest/source/vibe/http/internal/http2/multiplexing.d
  23. 971 0
      vtest/source/vibe/http/internal/http2/server.d
  24. 382 0
      vtest/source/vibe/http/internal/http2/settings.d
  25. 38 0
      vtest/source/vibe/http/internal/utils.d
  26. 259 0
      vtest/source/vibe/http/log.d
  27. 300 0
      vtest/source/vibe/http/proxy.d
  28. 1431 0
      vtest/source/vibe/http/router.d
  29. 2013 0
      vtest/source/vibe/http/server.d
  30. 294 0
      vtest/source/vibe/http/session.d
  31. 155 0
      vtest/source/vibe/http/status.d
  32. 1291 0
      vtest/source/vibe/http/websockets.d

+ 6 - 1
vtest/dub.json

@@ -6,7 +6,12 @@
   "dflags": ["-w", "-O", "-static"],
   "targetType": "executable",
   "dependencies": {
-    "vibe-d": "~>0.9",
+    "vibe-inet:crypto": "~>1.1.2",
+    "vibe-inet": "~>1.1.2",
+    "vibe-stream:tls": "~>1.1.1",
+    "vibe-stream": "~>1.1.1",
+    "vibe-inet:textfilter": "~>1.1.2",
+    
     "mustache-d": "~>0.1.5"
   },
   

+ 0 - 2
vtest/dub.selections.json

@@ -1,7 +1,6 @@
 {
 	"fileVersion": 1,
 	"versions": {
-		"diet-ng": "1.8.4",
 		"eventcore": "0.9.35",
 		"mir-linux-kernel": "1.2.1",
 		"mustache-d": "0.1.5",
@@ -11,7 +10,6 @@
 		"taggedalgebraic": "0.11.23",
 		"vibe-container": "1.6.2",
 		"vibe-core": "2.11.0",
-		"vibe-d": "0.10.2",
 		"vibe-http": "1.2.2",
 		"vibe-inet": "1.1.2",
 		"vibe-serialization": "1.1.1",

+ 112 - 0
vtest/source/vibe/http/auth/basic_auth.d

@@ -0,0 +1,112 @@
+/**
+	Implements HTTP Basic Auth.
+
+	Copyright: © 2012 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.http.auth.basic_auth;
+
+import vibe.http.server;
+import vibe.core.log;
+
+import std.base64;
+import std.exception;
+import std.string;
+
+@safe:
+
+
+/**
+	Returns a request handler that enforces request to be authenticated using HTTP Basic Auth.
+*/
+HTTPServerRequestDelegateS performBasicAuth(string realm, PasswordVerifyCallback pwcheck)
+{
+	void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	@safe {
+		if (!checkBasicAuth(req, pwcheck)) {
+			res.statusCode = HTTPStatus.unauthorized;
+			res.contentType = "text/plain";
+			res.headers["WWW-Authenticate"] = "Basic realm=\""~realm~"\"";
+			res.bodyWriter.write("Authorization required");
+		}
+	}
+	return &handleRequest;
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+HTTPServerRequestDelegateS performBasicAuth(string realm, bool delegate(string, string) @system pwcheck)
+@system {
+	return performBasicAuth(realm, (u, p) @trusted => pwcheck(u, p));
+}
+
+
+/**
+	Enforces HTTP Basic Auth authentication on the given req/res pair.
+
+	Params:
+		req = Request object that is to be checked
+		res = Response object that will be used for authentication errors
+		realm = HTTP Basic Auth realm reported to the client
+		pwcheck = A delegate queried for validating user/password pairs
+
+	Returns: Returns the name of the authenticated user.
+
+	Throws: Throws a HTTPStatusExeption in case of an authentication failure.
+*/
+string performBasicAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, string realm, scope PasswordVerifyCallback pwcheck)
+{
+	if (checkBasicAuth(req, pwcheck))
+		return req.username;
+
+	res.headers["WWW-Authenticate"] = "Basic realm=\""~realm~"\"";
+	throw new HTTPStatusException(HTTPStatus.unauthorized);
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+string performBasicAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, string realm, scope bool delegate(string, string) @system pwcheck)
+@system {
+	return performBasicAuth(req, res, realm, (u, p) @trusted => pwcheck(u, p));
+}
+
+
+/**
+	Checks for valid HTTP Basic Auth authentication on the given request.
+
+	Upon successful authorization, the name of the authorized user will
+	be stored in `req.username`.
+
+	Params:
+		req = Request object that is to be checked
+		pwcheck = A delegate queried for validating user/password pairs
+
+	Returns: Returns `true` $(I iff) a valid Basic Auth header is present
+		and the credentials were verified successfully by the validation
+		callback.
+
+	Throws: Throws a `HTTPStatusExeption` with `HTTPStatusCode.badRequest`
+		if the "Authorization" header is malformed.
+*/
+bool checkBasicAuth(scope HTTPServerRequest req, scope PasswordVerifyCallback pwcheck)
+{
+	auto pauth = "Authorization" in req.headers;
+	if (pauth && (*pauth).startsWith("Basic ")) {
+		string user_pw = () @trusted { return cast(string)Base64.decode((*pauth)[6 .. $]); } ();
+
+		auto idx = user_pw.indexOf(":");
+		enforceBadRequest(idx >= 0, "Invalid auth string format!");
+		string user = user_pw[0 .. idx];
+		string password = user_pw[idx+1 .. $];
+
+		if (pwcheck(user, password)) {
+			req.username = user;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static import vibe.http.internal.basic_auth_client;
+
+alias addBasicAuth = vibe.http.internal.basic_auth_client.addBasicAuth;
+
+alias PasswordVerifyCallback = bool delegate(string user, string password);

+ 320 - 0
vtest/source/vibe/http/auth/digest_auth.d

@@ -0,0 +1,320 @@
+/**
+	Implements HTTP Digest Authentication.
+
+	This is a minimal implementation based on RFC 2069.
+
+	Copyright: © 2015 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Kai Nacke
+*/
+module vibe.http.auth.digest_auth;
+
+import vibe.core.log;
+import vibe.crypto.cryptorand;
+import vibe.http.server;
+import vibe.inet.url;
+
+import std.base64;
+import std.datetime;
+import std.digest.md;
+import std.exception;
+import std.string;
+
+@safe:
+
+enum NonceState { Valid, Expired, Invalid }
+
+class DigestAuthInfo
+{
+	@safe:
+
+	string realm;
+	ubyte[16] secret;
+	Duration timeout;
+
+	this()
+	{
+		secureRNG.read(secret[]);
+		timeout = 300.seconds;
+	}
+
+	string createNonce(scope const HTTPServerRequest req)
+	{
+		auto now = Clock.currTime(UTC()).stdTime();
+		auto time = () @trusted { return *cast(ubyte[now.sizeof]*)&now; } ();
+		MD5 md5;
+		md5.put(time);
+		md5.put(secret);
+		auto data = md5.finish();
+		return Base64.encode(time ~ data);
+	}
+
+	NonceState checkNonce(string nonce, scope const HTTPServerRequest req)
+	{
+		auto now = Clock.currTime(UTC()).stdTime();
+		ubyte[] decoded = Base64.decode(nonce);
+		if (decoded.length != now.sizeof + secret.length) return NonceState.Invalid;
+		auto timebytes = decoded[0 .. now.sizeof];
+		auto time = () @trusted { return (cast(typeof(now)[])timebytes)[0]; } ();
+		if (timeout.total!"hnsecs" + time < now) return NonceState.Expired;
+		MD5 md5;
+		md5.put(timebytes);
+		md5.put(secret);
+		auto data = md5.finish();
+		if (data[] != decoded[now.sizeof .. $]) return NonceState.Invalid;
+		return NonceState.Valid;
+	}
+}
+
+unittest
+{
+	auto authInfo = new DigestAuthInfo;
+	auto req = createTestHTTPServerRequest(URL("http://localhost/"));
+	auto nonce = authInfo.createNonce(req);
+	assert(authInfo.checkNonce(nonce, req) == NonceState.Valid);
+}
+
+private bool checkDigest(scope HTTPServerRequest req, DigestAuthInfo info, scope DigestHashCallback pwhash, out bool stale, out string username)
+{
+	stale = false;
+	username = "";
+	auto pauth = "Authorization" in req.headers;
+
+	if (pauth && (*pauth).startsWith("Digest ")) {
+		string realm, nonce, response, uri, algorithm;
+		foreach (param; split((*pauth)[7 .. $], ",")) {
+			auto kv = split(param, "=");
+			switch (kv[0].strip().toLower()) {
+				default: break;
+				case "realm": realm = param.stripLeft()[7..$-1]; break;
+				case "username": username = param.stripLeft()[10..$-1]; break;
+				case "nonce": nonce = kv[1][1..$-1]; break;
+				case "uri": uri = param.stripLeft()[5..$-1]; break;
+				case "response": response = kv[1][1..$-1]; break;
+				case "algorithm": algorithm = kv[1][1..$-1]; break;
+			}
+		}
+
+		if (realm != info.realm)
+			return false;
+		if (algorithm !is null && algorithm != "MD5")
+			return false;
+
+		auto nonceState = info.checkNonce(nonce, req);
+		if (nonceState != NonceState.Valid) {
+			stale = nonceState == NonceState.Expired;
+			return false;
+		}
+
+		auto ha1 = pwhash(realm, username);
+		auto ha2 = toHexString!(LetterCase.lower)(md5Of(httpMethodString(req.method) ~ ":" ~ uri));
+		auto calcresponse = toHexString!(LetterCase.lower)(md5Of(ha1 ~ ":" ~ nonce ~ ":" ~ ha2 ));
+		if (response[] == calcresponse[])
+			return true;
+	}
+	return false;
+}
+
+/**
+	Returns a request handler that enforces request to be authenticated using HTTP Digest Auth.
+*/
+HTTPServerRequestDelegate performDigestAuth(DigestAuthInfo info, scope DigestHashCallback pwhash)
+{
+	void handleRequest(HTTPServerRequest req, HTTPServerResponse res)
+	@safe {
+		bool stale;
+		string username;
+		if (checkDigest(req, info, pwhash, stale, username)) {
+			req.username = username;
+			return ;
+		}
+
+		// else output an error page
+		res.statusCode = HTTPStatus.unauthorized;
+		res.contentType = "text/plain";
+		res.headers["WWW-Authenticate"] = "Digest realm=\""~info.realm~"\", nonce=\""~info.createNonce(req)~"\", stale="~(stale?"true":"false");
+		res.bodyWriter.write("Authorization required");
+	}
+	return &handleRequest;
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+HTTPServerRequestDelegate performDigestAuth(DigestAuthInfo info, scope string delegate(string, string) @system pwhash)
+@system {
+	return performDigestAuth(info, (r, u) @trusted => pwhash(r, u));
+}
+
+/**
+	Enforces HTTP Digest Auth authentication on the given req/res pair.
+
+	Params:
+		req = Request object that is to be checked
+		res = Response object that will be used for authentication errors
+		info = Digest authentication info object
+		pwhash = A delegate queried for returning the digest password
+
+	Returns: Returns the name of the authenticated user.
+
+	Throws: Throws a HTTPStatusExeption in case of an authentication failure.
+*/
+string performDigestAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, DigestAuthInfo info, scope DigestHashCallback pwhash)
+{
+	bool stale;
+	string username;
+	if (checkDigest(req, info, pwhash, stale, username))
+		return username;
+
+	res.headers["WWW-Authenticate"] = "Digest realm=\""~info.realm~"\", nonce=\""~info.createNonce(req)~"\", stale="~(stale?"true":"false");
+	throw new HTTPStatusException(HTTPStatus.unauthorized);
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+string performDigestAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, DigestAuthInfo info, scope string delegate(string, string) @system pwhash)
+@system {
+	return performDigestAuth(req, res, info, (r, u) @trusted => pwhash(r, u));
+}
+
+/**
+	Creates the digest password from the user name, realm and password.
+
+	Params:
+		realm = The realm
+		user = The user name
+		password = The plain text password
+
+	Returns: Returns the digest password
+*/
+string createDigestPassword(string realm, string user, string password)
+{
+	return toHexString!(LetterCase.lower)(md5Of(user ~ ":" ~ realm ~ ":" ~ password)).dup;
+}
+
+alias DigestHashCallback = string delegate(string realm, string user);
+
+/// Structure which describes requirements of the digest authentication - see https://tools.ietf.org/html/rfc2617
+struct DigestAuthParams {
+	enum Qop { none = 0, auth = 1, auth_int = 2 }
+	enum Algorithm { none = 0, md5 = 1, md5_sess = 2 }
+
+	string realm, domain, nonce, opaque;
+	Algorithm algorithm = Algorithm.md5;
+	bool stale;
+	Qop qop;
+
+	/// Parses WWW-Authenticate header value with the digest parameters
+	this(string auth) {
+		import std.algorithm : splitter;
+
+		assert(auth.startsWith("Digest "), "Correct Digest authentication request not provided");
+
+		foreach (param; auth["Digest ".length..$].splitter(','))
+		{
+			auto idx = param.indexOf("=");
+			if (idx <= 0) {
+				logError("Invalid parameter in auth header: %s (%s)", param, auth);
+				continue;
+			}
+			auto k = param[0..idx];
+			auto v = param[idx+1..$];
+			switch (k.strip().toLower()) {
+				default: break;
+				case "realm": realm = v[1..$-1]; break;
+				case "domain": domain = v[1..$-1]; break;
+				case "nonce": nonce = v[1..$-1]; break;
+				case "opaque": opaque = v[1..$-1]; break;
+				case "stale": stale = v.toLower() == "true"; break;
+				case "algorithm":
+					switch (v) {
+						default: break;
+						case "MD5": algorithm = Algorithm.md5; break;
+						case "MD5-sess": algorithm = Algorithm.md5_sess; break;
+					}
+					break;
+				case "qop":
+					foreach (q; v[1..$-1].splitter(',')) {
+						switch (q) {
+							default: break;
+							case "auth": qop |= Qop.auth; break;
+							case "auth-int": qop |= Qop.auth_int; break;
+						}
+					}
+					break;
+			}
+		}
+	}
+}
+
+/**
+	Creates the digest authorization request header.
+
+	Params:
+		method = HTTP method (required only when some qop is requested)
+		username = user name
+		password = user password
+		url = requested url
+		auth = value from the WWW-Authenticate response header
+		cnonce = client generated unique data string (required only when some qop is requested)
+		nc = the count of requests sent by the client (required only when some qop is requested)
+		entityBody = request entity body required only if qop==auth-int
+*/
+auto createDigestAuthHeader(U)(HTTPMethod method, U url, string username, string password, DigestAuthParams auth,
+	string cnonce = null, int nc = 0, in ubyte[] entityBody = null)
+if (is(U == string) || is(U == URL)) {
+
+	import std.array : appender;
+	import std.format : formattedWrite;
+
+	auto getHA1(string username, string password, string realm, string nonce = null, string cnonce = null) {
+
+		assert((nonce is null && cnonce is null) || (nonce !is null && cnonce !is null));
+
+		auto ha1 = toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", username, realm, password))).dup;
+		if (nonce !is null) ha1 = toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", ha1, nonce, cnonce))).dup;
+		return ha1;
+	}
+
+	auto getHA2(HTTPMethod method, string uri, in ubyte[] ebody = null) {
+		return ebody is null
+			? toHexString!(LetterCase.lower)(md5Of(format("%s:%s", method, uri))).dup
+			: toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", method, uri, toHexString!(LetterCase.lower)(md5Of(ebody)).dup))).dup;
+	}
+
+	static if (is(U == string)) auto uri = URL(url).pathString;
+	else auto uri = url.pathString;
+
+	auto dig = appender!string();
+	dig ~= "Digest ";
+	dig ~= `username="`; dig ~= username; dig ~= `", `;
+	dig ~= `realm="`; dig ~= auth.realm; dig ~= `", `;
+	dig ~= `nonce="`; dig ~= auth.nonce; dig ~= `", `;
+	dig ~= `uri="`; dig ~= uri; dig ~= `", `;
+	if (auth.opaque.length) { dig ~= `opaque="`; dig ~= auth.opaque; dig ~= `", `; }
+
+	//choose one of provided qop
+	DigestAuthParams.Qop qop;
+	if ((auth.qop & DigestAuthParams.Qop.auth) == DigestAuthParams.Qop.auth) qop = DigestAuthParams.Qop.auth;
+	else if ((auth.qop & DigestAuthParams.Qop.auth_int) == DigestAuthParams.Qop.auth_int) qop = DigestAuthParams.Qop.auth_int;
+
+	if (qop != DigestAuthParams.Qop.none) {
+		assert(cnonce !is null, "cnonce is required");
+		assert(nc != 0, "nc is required");
+
+		dig ~= `qop="`; dig ~= qop == DigestAuthParams.Qop.auth ? "auth" : "auth-int"; dig ~= `", `;
+		dig ~= `cnonce="`; dig ~= cnonce; dig ~= `", `;
+		dig ~= `nc="`; dig.formattedWrite("%08x", nc); dig ~= `", `;
+	}
+
+	auto ha1 = auth.algorithm == DigestAuthParams.Algorithm.md5_sess
+		? getHA1(username, password, auth.realm, auth.nonce, cnonce)
+		: getHA1(username, password, auth.realm);
+
+	auto ha2 = qop != DigestAuthParams.Qop.auth_int
+		? getHA2(method, uri)
+		: getHA2(method, uri, entityBody);
+
+	auto resp = qop == DigestAuthParams.Qop.none
+		? toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", ha1, auth.nonce, ha2))).dup
+		: toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%08x:%s:%s:%s", ha1, auth.nonce, nc, cnonce, qop == DigestAuthParams.Qop.auth ? "auth" : "auth-int" , ha2))).dup;
+
+	dig ~= `response="`; dig ~= resp; dig ~= `"`;
+
+	return dig.data;
+}

+ 1300 - 0
vtest/source/vibe/http/client.d

@@ -0,0 +1,1300 @@
+/**
+	A simple HTTP/1.1 client implementation.
+
+	Copyright: © 2012-2014 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig, Jan Krüger
+*/
+module vibe.http.client;
+
+public import vibe.core.net;
+public import vibe.http.common;
+public import vibe.inet.url;
+
+import vibe.container.dictionarylist;
+import vibe.container.internal.utilallocator;
+import vibe.container.ringbuffer : RingBuffer;
+import vibe.core.connectionpool;
+import vibe.core.core;
+import vibe.core.log;
+import vibe.data.json;
+import vibe.inet.message;
+import vibe.inet.url;
+import vibe.stream.counting;
+import vibe.stream.tls;
+import vibe.stream.operations;
+import vibe.stream.wrapper : createConnectionProxyStream;
+import vibe.stream.zlib;
+import vibe.internal.freelistref;
+import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy;
+
+import core.exception : AssertError;
+import std.algorithm : splitter;
+import std.array;
+import std.conv;
+import std.encoding : sanitize;
+import std.exception;
+import std.format;
+import std.string;
+import std.typecons;
+import std.datetime;
+import std.socket : AddressFamily;
+
+version(Posix)
+{
+	version = UnixSocket;
+}
+
+
+/**************************************************************************************************/
+/* Public functions                                                                               */
+/**************************************************************************************************/
+@safe:
+
+/**
+	Performs a synchronous HTTP request on the specified URL.
+
+	The requester parameter allows to customize the request and to specify the request body for
+	non-GET requests before it is sent. A response object is then returned or passed to the
+	responder callback synchronously.
+
+	This function is a low-level HTTP client facility. It will not perform automatic redirect,
+	caching or similar tasks. For a high-level download facility (similar to cURL), see the
+	`vibe.inet.urltransfer` module.
+
+	Note that it is highly recommended to use one of the overloads that take a responder callback,
+	as they can avoid some memory allocations and are safe against accidentally leaving stale
+	response objects (objects whose response body wasn't fully read). For the returning overloads
+	of the function it is recommended to put a `scope(exit)` right after the call in which
+	`HTTPClientResponse.dropBody` is called to avoid this.
+
+	See_also: `vibe.inet.urltransfer.download`
+*/
+HTTPClientResponse requestHTTP(string url, scope void delegate(scope HTTPClientRequest req) requester = null, const(HTTPClientSettings) settings = defaultSettings)
+{
+	return requestHTTP(URL.parse(url), requester, settings);
+}
+/// ditto
+HTTPClientResponse requestHTTP(URL url, scope void delegate(scope HTTPClientRequest req) requester = null, const(HTTPClientSettings) settings = defaultSettings)
+{
+	auto cli = connectHTTP(url, settings);
+	auto res = cli.request(
+		(scope req){ httpRequesterDg(req, url, settings, requester); },
+	);
+
+	// make sure the connection stays locked if the body still needs to be read
+	if( res.m_client ) res.lockedConnection = cli;
+
+	logTrace("Returning HTTPClientResponse for conn %s", () @trusted { return cast(void*)res.lockedConnection.__conn; } ());
+	return res;
+}
+/// ditto
+void requestHTTP(string url, scope void delegate(scope HTTPClientRequest req) requester, scope void delegate(scope HTTPClientResponse req) responder, const(HTTPClientSettings) settings = defaultSettings)
+{
+	requestHTTP(URL(url), requester, responder, settings);
+}
+/// ditto
+void requestHTTP(URL url, scope void delegate(scope HTTPClientRequest req) requester, scope void delegate(scope HTTPClientResponse req) responder, const(HTTPClientSettings) settings = defaultSettings)
+{
+	auto cli = connectHTTP(url, settings);
+	cli.request(
+		(scope req){ httpRequesterDg(req, url, settings, requester); },
+		responder
+	);
+	assert(!cli.m_requesting, "HTTP client still requesting after return!?");
+	assert(!cli.m_responding, "HTTP client still responding after return!?");
+}
+
+private bool isTLSRequired(in URL url, in HTTPClientSettings settings)
+{
+	version(UnixSocket) {
+		enforce(url.schema == "http" || url.schema == "https" || url.schema == "http+unix" || url.schema == "https+unix", "URL schema must be http(s) or http(s)+unix.");
+	} else {
+		enforce(url.schema == "http" || url.schema == "https", "URL schema must be http(s).");
+	}
+	enforce(url.host.length > 0, "URL must contain a host name.");
+	bool use_tls;
+
+	if (settings.proxyURL.schema !is null)
+		use_tls = settings.proxyURL.schema == "https";
+	else
+	{
+		version(UnixSocket)
+			use_tls = url.schema == "https" || url.schema == "https+unix";
+		else
+			use_tls = url.schema == "https";
+	}
+
+	return use_tls;
+}
+
+private void httpRequesterDg(scope HTTPClientRequest req, in URL url, in HTTPClientSettings settings, scope void delegate(scope HTTPClientRequest req) requester)
+{
+	import std.algorithm.searching : canFind;
+	import vibe.http.internal.basic_auth_client: addBasicAuth;
+
+	if (url.localURI.length) {
+		assert(url.path.absolute, "Request URL path must be absolute.");
+		req.requestURL = url.localURI;
+	}
+
+	if (settings.proxyURL.schema !is null)
+		req.requestURL = url.toString(); // proxy exception to the URL representation
+
+	// IPv6 addresses need to be put into brackets
+	auto hoststr = url.host.canFind(':') ? "["~url.host~"]" : url.host;
+
+	// Provide port number when it is not the default one (RFC2616 section 14.23)
+	if (url.port && url.port != url.defaultPort)
+		req.headers["Host"] = format("%s:%d", hoststr, url.port);
+	else
+		req.headers["Host"] = hoststr;
+
+	if ("authorization" !in req.headers && url.username != "")
+		req.addBasicAuth(url.username, url.password);
+
+	if (requester) () @trusted { requester(req); } ();
+}
+
+/** Posts a simple JSON request. Note that the server www.example.org does not
+	exists, so there will be no meaningful result.
+*/
+unittest {
+	import vibe.core.log;
+	import vibe.http.client;
+	import vibe.stream.operations;
+
+	void test()
+	{
+		requestHTTP("http://www.example.org/",
+			(scope req) {
+				req.method = HTTPMethod.POST;
+				//req.writeJsonBody(["name": "My Name"]);
+			},
+			(scope res) {
+				logInfo("Response: %s", res.bodyReader.readAllUTF8());
+			}
+		);
+	}
+}
+
+
+/**
+	Returns a HTTPClient proxy object that is connected to the specified host.
+
+	Internally, a connection pool is used to reuse already existing connections. Note that
+	usually requestHTTP should be used for making requests instead of manually using a
+	HTTPClient to do so.
+*/
+auto connectHTTP(string host, ushort port = 0, bool use_tls = false, const(HTTPClientSettings) settings = null)
+{
+	auto sttngs = settings ? settings : defaultSettings;
+
+	if (port == 0) port = use_tls ? 443 : 80;
+	auto ckey = ConnInfo(host, sttngs.tlsPeerName, port, use_tls, sttngs.proxyURL.host, sttngs.proxyURL.port, sttngs.networkInterface);
+
+	ConnectionPool!HTTPClient pool;
+	s_connections.opApply((ref c) @safe {
+		if (c[0] == ckey)
+			pool = c[1];
+		return 0;
+	});
+
+	if (!pool) {
+		logDebug("Create HTTP client pool %s(%s):%s %s proxy %s:%d", host, sttngs.tlsPeerName, port, use_tls, sttngs.proxyURL.host, sttngs.proxyURL.port);
+		pool = new ConnectionPool!HTTPClient({
+				auto ret = new HTTPClient;
+				ret.connect(host, port, use_tls, sttngs);
+				return ret;
+			});
+		if (s_connections.full) s_connections.removeFront();
+		s_connections.put(tuple(ckey, pool));
+	}
+
+	return pool.lockConnection();
+}
+
+/// Ditto
+auto connectHTTP(URL url, const(HTTPClientSettings) settings = null)
+{
+	const use_tls = isTLSRequired(url, settings);
+	return connectHTTP(url.getFilteredHost, url.port, use_tls, settings);
+}
+
+static ~this()
+{
+	foreach (ci; s_connections) {
+		ci[1].removeUnused((conn) {
+			conn.disconnect();
+		});
+	}
+}
+
+private struct ConnInfo { string host; string tlsPeerName; ushort port; bool useTLS; string proxyIP; ushort proxyPort; NetworkAddress bind_addr; }
+private static RingBuffer!(Tuple!(ConnInfo, ConnectionPool!HTTPClient), 16) s_connections;
+
+
+/**************************************************************************************************/
+/* Public types                                                                                   */
+/**************************************************************************************************/
+
+/**
+	Defines an HTTP/HTTPS proxy request or a connection timeout for an HTTPClient.
+*/
+class HTTPClientSettings {
+	URL proxyURL;
+	Duration defaultKeepAliveTimeout = 10.seconds;
+
+	/// Timeout for establishing a connection to the server
+	Duration connectTimeout = Duration.max;
+
+	/// Timeout during read operations on the underyling transport
+	Duration readTimeout = Duration.max;
+
+	/// Forces a specific network interface to use for outgoing connections.
+	NetworkAddress networkInterface = anyAddress;
+
+	/// Can be used to force looking up IPv4/IPv6 addresses for host names.
+	AddressFamily dnsAddressFamily = AddressFamily.UNSPEC;
+
+	/** Allows to customize the TLS context before connecting to a server.
+
+		Note that this overrides a callback set with `HTTPClient.setTLSContextSetup`.
+	*/
+	void delegate(TLSContext ctx) @safe nothrow tlsContextSetup;
+
+	/**
+		TLS Peer name override.
+
+		Allows to customize the tls peer name sent to server during the TLS connection setup (SNI)
+	*/
+	string tlsPeerName;
+
+	@property HTTPClientSettings dup()
+	const @safe {
+		auto ret = new HTTPClientSettings;
+		ret.proxyURL = this.proxyURL;
+		ret.connectTimeout = this.connectTimeout;
+		ret.readTimeout = this.readTimeout;
+		ret.networkInterface = this.networkInterface;
+		ret.dnsAddressFamily = this.dnsAddressFamily;
+		ret.tlsContextSetup = this.tlsContextSetup;
+		ret.tlsPeerName = this.tlsPeerName;
+		return ret;
+	}
+}
+
+///
+unittest {
+	void test() {
+
+		HTTPClientSettings settings = new HTTPClientSettings;
+		settings.proxyURL = URL.parse("http://proxyuser:proxypass@192.168.2.50:3128");
+		settings.defaultKeepAliveTimeout = 0.seconds; // closes connection immediately after receiving the data.
+		requestHTTP("http://www.example.org",
+					(scope req){
+			req.method = HTTPMethod.GET;
+		},
+		(scope res){
+			logInfo("Headers:");
+			foreach (key, ref value; res.headers.byKeyValue) {
+				logInfo("%s: %s", key, value);
+			}
+			logInfo("Response: %s", res.bodyReader.readAllUTF8());
+		}, settings);
+
+	}
+}
+
+version (Have_vibe_core)
+unittest { // test connect timeout
+	import std.conv : to;
+	import vibe.core.stream : pipe, nullSink;
+
+	HTTPClientSettings settings = new HTTPClientSettings;
+	settings.connectTimeout = 50.msecs;
+
+	// Use an IP address that is guaranteed to be unassigned globally to force
+	// a timeout (see RFC 3330)
+	auto cli = connectHTTP("192.0.2.0", 80, false, settings);
+	auto timer = setTimer(500.msecs, { assert(false, "Connect timeout occurred too late"); });
+	scope (exit) timer.stop();
+
+	try {
+		cli.request(
+			(scope req) { assert(false, "Expected no connection"); },
+			(scope res) { assert(false, "Expected no response"); }
+		);
+		assert(false, "Response read expected to fail due to timeout");
+	} catch(Exception e) {}
+}
+
+unittest { // test read timeout
+	import std.conv : to;
+	import vibe.core.stream : pipe, nullSink;
+
+	version (VibeLibasyncDriver) {
+		logInfo("Skipping HTTP client read timeout test due to buggy libasync driver.");
+	} else {
+		HTTPClientSettings settings = new HTTPClientSettings;
+		settings.readTimeout = 50.msecs;
+
+		auto l = listenTCP(0, (conn) {
+			try conn.pipe(nullSink);
+			catch (Exception e) assert(false, e.msg);
+			conn.close();
+		}, "127.0.0.1");
+
+		auto cli = connectHTTP("127.0.0.1", l.bindAddress.port, false, settings);
+		auto timer = setTimer(500.msecs, { assert(false, "Read timeout occurred too late"); });
+		scope (exit) {
+			timer.stop();
+			l.stopListening();
+			cli.disconnect();
+			sleep(10.msecs); // allow the read connection end to fully close
+		}
+
+		try {
+			cli.request(
+				(scope req) { req.method = HTTPMethod.GET; },
+				(scope res) { assert(false, "Expected no response"); }
+			);
+			assert(false, "Response read expected to fail due to timeout");
+		} catch(Exception e) {}
+	}
+}
+
+
+/**
+	Implementation of a HTTP 1.0/1.1 client with keep-alive support.
+
+	Note that it is usually recommended to use requestHTTP for making requests as that will use a
+	pool of HTTPClient instances to keep the number of connection establishments low while not
+	blocking requests from different tasks.
+*/
+final class HTTPClient {
+	@safe:
+
+	enum maxHeaderLineLength = 4096;
+
+	private {
+		Rebindable!(const(HTTPClientSettings)) m_settings;
+		string m_server;
+		string m_tlsPeerName;
+		ushort m_port;
+		bool m_useTLS;
+		TCPConnection m_conn;
+		InterfaceProxy!Stream m_stream;
+		TLSStream m_tlsStream;
+		TLSContext m_tls;
+		static __gshared m_userAgent = "vibe.d/"~vibeVersionString~" (HTTPClient, +http://vibed.org/)";
+		static __gshared void function(TLSContext) ms_tlsSetup;
+		bool m_requesting = false, m_responding = false;
+		SysTime m_keepAliveLimit;
+		Duration m_keepAliveTimeout;
+	}
+
+	/** Get the current settings for the HTTP client. **/
+	@property const(HTTPClientSettings) settings() const {
+		return m_settings;
+	}
+
+	/**
+		Sets the default user agent string for new HTTP requests.
+	*/
+	static void setUserAgentString(string str) @trusted { m_userAgent = str; }
+
+	/**
+		Sets a callback that will be called for every TLS context that is created.
+
+		Setting such a callback is useful for adjusting the validation parameters
+		of the TLS context.
+	*/
+	static void setTLSSetupCallback(void function(TLSContext) @safe func) @trusted { ms_tlsSetup = func; }
+
+	/**
+		Sets up this HTTPClient to connect to a specific server.
+
+		This method may only be called if any previous connection has been closed.
+
+		The actual connection is deferred until a request is initiated (using `HTTPClient.request`).
+	*/
+	void connect(string server, ushort port = 80, bool use_tls = false, const(HTTPClientSettings) settings = defaultSettings)
+	{
+		assert(!m_conn);
+		assert(port != 0);
+		disconnect();
+		m_conn = TCPConnection.init;
+		m_settings = settings;
+		m_keepAliveTimeout = settings.defaultKeepAliveTimeout;
+		m_keepAliveLimit = Clock.currTime(UTC()) + m_keepAliveTimeout;
+		m_server = server;
+		m_tlsPeerName = settings.tlsPeerName.length ? settings.tlsPeerName : server;
+		m_port = port;
+		m_useTLS = use_tls;
+		if (use_tls) {
+			m_tls = createTLSContext(TLSContextKind.client);
+			// this will be changed to trustedCert once a proper root CA store is available by default
+			m_tls.peerValidationMode = TLSPeerValidationMode.none;
+			if (settings.tlsContextSetup) settings.tlsContextSetup(m_tls);
+			else () @trusted { if (ms_tlsSetup) ms_tlsSetup(m_tls); } ();
+		}
+	}
+
+	/**
+		Forcefully closes the TCP connection.
+
+		Before calling this method, be sure that no request is currently being processed.
+	*/
+	void disconnect()
+	nothrow {
+		if (m_conn) {
+			version (Have_vibe_core) {}
+			else scope(failure) assert(false);
+
+			if (m_conn.connected) {
+				try m_stream.finalize();
+				catch (Exception e) logDebug("Failed to finalize connection stream when closing HTTP client connection: %s", e.msg);
+				m_conn.close();
+			}
+
+			if (m_useTLS) () @trusted { return destroy(m_stream); } ();
+			m_stream = InterfaceProxy!Stream.init;
+			() @trusted { return destroy(m_conn); } ();
+			m_conn = TCPConnection.init;
+		}
+	}
+
+	private void doProxyRequest(T, U)(ref T res, U requester, ref bool close_conn, ref bool has_body)
+	@trusted { // scope new
+		import std.conv : to;
+		scope request_allocator = createRequestAllocator();
+		scope (exit) freeRequestAllocator(request_allocator);
+
+		res.dropBody();
+		scope(failure)
+			res.disconnect();
+		if (res.statusCode != 407) {
+			throw new HTTPStatusException(HTTPStatus.internalServerError, "Proxy returned Proxy-Authenticate without a 407 status code.");
+		}
+
+		// send the request again with the proxy authentication information if available
+		if (m_settings.proxyURL.username is null) {
+			throw new HTTPStatusException(HTTPStatus.proxyAuthenticationRequired, "Proxy Authentication Required.");
+		}
+
+		m_responding = false;
+		close_conn = false;
+		bool found_proxy_auth;
+
+		foreach (string proxyAuth; res.headers.getAll("Proxy-Authenticate"))
+		{
+			if (proxyAuth.length >= "Basic".length && proxyAuth[0.."Basic".length] == "Basic")
+			{
+				found_proxy_auth = true;
+				break;
+			}
+		}
+
+		if (!found_proxy_auth)
+		{
+			throw new HTTPStatusException(HTTPStatus.notAcceptable, "The Proxy Server didn't allow Basic Authentication");
+		}
+
+		SysTime connected_time;
+		has_body = doRequestWithRetry(requester, true, close_conn, connected_time);
+		m_responding = true;
+
+		static if (is(T == HTTPClientResponse))
+			res = new HTTPClientResponse(this, close_conn);
+		else
+			res = scoped!HTTPClientResponse(this, close_conn);
+
+		res.initialize(has_body, request_allocator, connected_time);
+
+		if (res.headers.get("Proxy-Authenticate", null) !is null){
+			res.dropBody();
+			throw new HTTPStatusException(HTTPStatus.proxyAuthenticationRequired, "Proxy Authentication Failed.");
+		}
+
+	}
+
+	/**
+		Performs a HTTP request.
+
+		`requester` is called first to populate the request with headers and the desired
+		HTTP method and version. After a response has been received it is then passed
+		to the caller which can in turn read the reponse body. Any part of the body
+		that has not been processed will automatically be consumed and dropped.
+
+		Note that the `requester` callback might be invoked multiple times in the event
+		that a request has to be resent due to a connection failure.
+
+		Also note that the second form of this method (returning a `HTTPClientResponse`) is
+		not recommended to use as it may accidentially block a HTTP connection when
+		only part of the response body was read and also requires a heap allocation
+		for the response object. The callback based version on the other hand uses
+		a stack allocation and guarantees that the request has been fully processed
+		once it has returned.
+	*/
+	void request(scope void delegate(scope HTTPClientRequest req) requester, scope void delegate(scope HTTPClientResponse) responder)
+	@trusted { // scope new
+		scope request_allocator = createRequestAllocator();
+		scope (exit) freeRequestAllocator(request_allocator);
+
+		scope (failure) {
+			m_responding = false;
+			disconnect();
+		}
+
+		bool close_conn;
+		SysTime connected_time;
+		bool has_body = doRequestWithRetry(requester, false, close_conn, connected_time);
+
+		m_responding = true;
+		auto res = scoped!HTTPClientResponse(this, close_conn);
+		res.initialize(has_body, request_allocator, connected_time);
+
+		// proxy implementation
+		if (res.headers.get("Proxy-Authenticate", null) !is null) {
+			doProxyRequest(res, requester, close_conn, has_body);
+		}
+
+		Exception user_exception;
+		while (true)
+		{
+			try responder(res);
+			catch (Exception e) {
+				logDebug("Error while handling response: %s", e.toString().sanitize());
+				user_exception = e;
+			}
+			if (res.statusCode < 200) {
+				// just an informational status -> read and handle next response
+				if (m_responding) res.dropBody();
+				if (m_conn) {
+					res = scoped!HTTPClientResponse(this, close_conn);
+					res.initialize(has_body, request_allocator, connected_time);
+					continue;
+				}
+			}
+			if (m_responding) {
+				logDebug("Failed to handle the complete response of the server - disconnecting.");
+				res.disconnect();
+			}
+			assert(!m_responding, "Still in responding state after finalizing the response!?");
+
+			if (user_exception || res.headers.get("Connection") == "close")
+				disconnect();
+			break;
+		}
+		if (user_exception) throw user_exception;
+	}
+
+	/// ditto
+	HTTPClientResponse request(scope void delegate(HTTPClientRequest) requester)
+	{
+		bool close_conn;
+		SysTime connected_time;
+		scope (failure) {
+			m_responding = false;
+			disconnect();
+		}
+		bool has_body = doRequestWithRetry(requester, false, close_conn, connected_time);
+		m_responding = true;
+		auto res = new HTTPClientResponse(this, close_conn);
+		res.initialize(has_body, () @trusted { return vibeThreadAllocator(); } (), connected_time);
+
+		// proxy implementation
+		if (res.headers.get("Proxy-Authenticate", null) !is null) {
+			doProxyRequest(res, requester, close_conn, has_body);
+		}
+
+		return res;
+	}
+
+	private bool doRequestWithRetry(scope void delegate(HTTPClientRequest req) requester, bool confirmed_proxy_auth /* basic only */, out bool close_conn, out SysTime connected_time)
+	{
+		if (m_conn && m_conn.connected && Clock.currTime(UTC()) > m_keepAliveLimit){
+			logDebug("Disconnected to avoid timeout");
+			disconnect();
+		}
+
+		// check if this isn't the first request on a connection
+		bool is_persistent_request = m_conn && m_conn.connected;
+
+		// retry the request if the connection gets closed prematurely and this is a persistent request
+		bool has_body;
+		foreach (i; 0 .. is_persistent_request ? 2 : 1) {
+		 	connected_time = Clock.currTime(UTC());
+
+			close_conn = false;
+			has_body = doRequest(requester, close_conn, false, connected_time);
+
+			logTrace("HTTP client waiting for response");
+			if (!m_stream.empty) break;
+		}
+		return has_body;
+	}
+
+	private bool doRequest(scope void delegate(HTTPClientRequest req) requester, ref bool close_conn, bool confirmed_proxy_auth = false /* basic only */, SysTime connected_time = Clock.currTime(UTC()))
+	{
+		assert(!m_requesting, "Interleaved HTTP client requests detected!");
+		assert(!m_responding, "Interleaved HTTP client request/response detected!");
+
+		m_requesting = true;
+		scope(exit) m_requesting = false;
+
+		if (!m_conn || !m_conn.connected || m_conn.waitForDataEx(0.seconds) == WaitForDataStatus.noMoreData) {
+			if (m_conn)
+				disconnect(); // make sure all resources are freed
+
+			if (m_settings.proxyURL.host !is null){
+
+				enum AddressType {
+					IPv4,
+					IPv6,
+					Host
+				}
+
+				static AddressType getAddressType(string host){
+					import std.regex : regex, Captures, Regex, matchFirst;
+
+					static IPv4Regex = regex(`^\s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\s*$`, ``);
+					static IPv6Regex = regex(`^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$`, ``);
+
+					if (!matchFirst(host, IPv4Regex).empty)
+					{
+						return AddressType.IPv4;
+					}
+					else if (!matchFirst(host, IPv6Regex).empty)
+					{
+						return AddressType.IPv6;
+					}
+					else
+					{
+						return AddressType.Host;
+					}
+				}
+
+				import std.functional : memoize;
+				alias findAddressType = memoize!getAddressType;
+
+				bool use_dns;
+				if (() @trusted { return findAddressType(m_settings.proxyURL.host); } () == AddressType.Host)
+				{
+					use_dns = true;
+				}
+
+				NetworkAddress proxyAddr = resolveHost(m_settings.proxyURL.host, m_settings.dnsAddressFamily, use_dns,
+					m_settings.connectTimeout);
+				proxyAddr.port = m_settings.proxyURL.port;
+				m_conn = connectTCPWithTimeout(proxyAddr, m_settings.networkInterface, m_settings.connectTimeout);
+			}
+			else {
+				version(UnixSocket)
+				{
+					import core.sys.posix.sys.un;
+					import core.sys.posix.sys.socket;
+					import std.regex : regex, Captures, Regex, matchFirst, ctRegex;
+					import core.stdc.string : strcpy;
+
+					NetworkAddress addr;
+					if (m_server[0] == '/')
+					{
+						addr.family = AF_UNIX;
+						sockaddr_un* s = addr.sockAddrUnix();
+						enforce(s.sun_path.length > m_server.length, "Unix sockets cannot have that long a name.");
+						s.sun_family = AF_UNIX;
+						() @trusted { strcpy(cast(char*)s.sun_path.ptr,m_server.toStringz()); } ();
+					} else
+					{
+						addr = resolveHost(m_server, m_settings.dnsAddressFamily, true, m_settings.connectTimeout);
+						addr.port = m_port;
+					}
+					m_conn = connectTCPWithTimeout(addr, m_settings.networkInterface, m_settings.connectTimeout);
+				} else
+				{
+					auto addr = resolveHost(m_server, m_settings.dnsAddressFamily, true, m_settings.connectTimeout);
+					addr.port = m_port;
+					m_conn = connectTCPWithTimeout(addr, m_settings.networkInterface, m_settings.connectTimeout);
+				}
+			}
+
+			if (m_settings.readTimeout != Duration.max)
+				m_conn.readTimeout = m_settings.readTimeout;
+
+			m_stream = m_conn;
+			if (m_useTLS) {
+				try m_tlsStream = createTLSStream(m_conn, m_tls, TLSStreamState.connecting, m_tlsPeerName, m_conn.remoteAddress);
+				catch (Exception e) {
+					m_conn.close();
+					m_conn = TCPConnection.init;
+					throw e;
+				}
+				m_stream = m_tlsStream;
+			}
+		}
+
+		return () @trusted { // scoped
+			auto req = scoped!HTTPClientRequest(m_stream, m_conn);
+			if (m_useTLS)
+				req.m_peerCertificate = m_tlsStream.peerCertificate;
+
+			req.headers["User-Agent"] = m_userAgent;
+			if (m_settings.proxyURL.host !is null){
+				req.headers["Proxy-Connection"] = "keep-alive";
+				if (confirmed_proxy_auth)
+				{
+					import std.base64;
+					ubyte[] user_pass = cast(ubyte[])(m_settings.proxyURL.username ~ ":" ~ m_settings.proxyURL.password);
+
+					req.headers["Proxy-Authorization"] = "Basic " ~ cast(string) Base64.encode(user_pass);
+				}
+			}
+			else {
+				req.headers["Connection"] = "keep-alive";
+			}
+			req.headers["Accept-Encoding"] = "gzip, deflate";
+			req.headers["Host"] = m_server;
+			requester(req);
+
+			if (req.httpVersion == HTTPVersion.HTTP_1_0)
+				close_conn = true;
+			else  if (m_settings.proxyURL.host !is null)
+				close_conn = req.headers.get("Proxy-Connection", "keep-alive") != "keep-alive";
+			else
+				close_conn = req.headers.get("Connection", "keep-alive") != "keep-alive";
+
+			req.finalize();
+
+			return req.method != HTTPMethod.HEAD;
+		} ();
+	}
+}
+
+private auto connectTCPWithTimeout(NetworkAddress addr, NetworkAddress bind_address, Duration timeout)
+{
+	auto ret = connectTCP(addr, bind_address, timeout);
+
+	// Avoid additional latency in the request-response cycle
+	ret.tcpNoDelay = true;
+
+	return ret;
+}
+
+/**
+	Represents a HTTP client request (as sent to the server).
+*/
+final class HTTPClientRequest : HTTPRequest {
+	import vibe.internal.array : FixedAppender;
+
+	private {
+		InterfaceProxy!OutputStream m_bodyWriter;
+		FreeListRef!ChunkedOutputStream m_chunkedStream;
+		bool m_headerWritten = false;
+		FixedAppender!(string, 22) m_contentLengthBuffer;
+		TCPConnection m_rawConn;
+		TLSCertificateInformation m_peerCertificate;
+	}
+
+
+	/// private
+	this(InterfaceProxy!Stream conn, TCPConnection raw_conn)
+	{
+		super(conn);
+		m_rawConn = raw_conn;
+	}
+
+	@property NetworkAddress localAddress() const { return m_rawConn.localAddress; }
+	@property NetworkAddress remoteAddress() const { return m_rawConn.remoteAddress; }
+
+	@property ref inout(TLSCertificateInformation) peerCertificate() inout { return m_peerCertificate; }
+
+	/**
+		Accesses the Content-Length header of the request.
+
+		Negative values correspond to an unset Content-Length header.
+	*/
+	@property long contentLength() const { return headers.get("Content-Length", "-1").to!long(); }
+	/// ditto
+	@property void contentLength(long value)
+	{
+		if (value >= 0) headers["Content-Length"] = clengthString(value);
+		else if ("Content-Length" in headers) headers.remove("Content-Length");
+	}
+
+	/**
+		Writes the whole request body at once using raw bytes.
+	*/
+	void writeBody(RandomAccessStream data)
+	{
+		writeBody(data, data.size - data.tell());
+	}
+	/// ditto
+	void writeBody(InputStream data)
+	{
+		data.pipe(bodyWriter);
+		finalize();
+	}
+	/// ditto
+	void writeBody(InputStream data, ulong length)
+	{
+		headers["Content-Length"] = clengthString(length);
+		data.pipe(bodyWriter, length);
+		finalize();
+	}
+	/// ditto
+	void writeBody(in ubyte[] data, string content_type = null)
+	{
+		if( content_type != "" ) headers["Content-Type"] = content_type;
+		headers["Content-Length"] = clengthString(data.length);
+		bodyWriter.write(data);
+		finalize();
+	}
+
+	/**
+		Writes the request body as JSON data.
+	*/
+	void writeJsonBody(T)(T data, bool allow_chunked = false)
+	{
+		import vibe.stream.wrapper : streamOutputRange;
+
+		headers["Content-Type"] = "application/json; charset=UTF-8";
+
+		// set an explicit content-length field if chunked encoding is not allowed
+		if (!allow_chunked) {
+			import vibe.internal.rangeutil;
+			long length = 0;
+			auto counter = () @trusted { return RangeCounter(&length); } ();
+			() @trusted { serializeToJson(counter, data); } ();
+			headers["Content-Length"] = clengthString(length);
+		}
+
+		auto rng = streamOutputRange!1024(bodyWriter);
+		() @trusted { serializeToJson(&rng, data); } ();
+		rng.flush();
+		finalize();
+	}
+
+	/** Writes the request body as form data.
+	*/
+	void writeFormBody(T)(T key_value_map)
+	{
+		import vibe.inet.webform : formEncode;
+		import vibe.stream.wrapper : streamOutputRange;
+
+		import vibe.internal.rangeutil;
+		long length = 0;
+		auto counter = () @trusted { return RangeCounter(&length); } ();
+		counter.formEncode(key_value_map);
+		headers["Content-Length"] = clengthString(length);
+		headers["Content-Type"] = "application/x-www-form-urlencoded";
+		auto dst = streamOutputRange!1024(bodyWriter);
+		() @trusted { return &dst; } ().formEncode(key_value_map);
+	}
+
+	///
+	unittest {
+		void test(HTTPClientRequest req) {
+			req.writeFormBody(["foo": "bar"]);
+		}
+	}
+
+	void writePart(MultiPart part)
+	{
+		assert(false, "TODO");
+	}
+
+	/**
+		An output stream suitable for writing the request body.
+
+		The first retrieval will cause the request header to be written, make sure
+		that all headers are set up in advance.s
+	*/
+	@property InterfaceProxy!OutputStream bodyWriter()
+	{
+		if (m_bodyWriter) return m_bodyWriter;
+
+		assert(!m_headerWritten, "Trying to write request body after body was already written.");
+
+		if (httpVersion != HTTPVersion.HTTP_1_0
+			&& "Content-Length" !in headers && "Transfer-Encoding" !in headers
+			&& headers.get("Connection", "") != "close")
+		{
+			headers["Transfer-Encoding"] = "chunked";
+		}
+
+		writeHeader();
+		m_bodyWriter = m_conn;
+
+		if (headers.get("Transfer-Encoding", null) == "chunked") {
+			m_chunkedStream = createChunkedOutputStreamFL(m_bodyWriter);
+			m_bodyWriter = m_chunkedStream;
+		}
+
+		return m_bodyWriter;
+	}
+
+	private void writeHeader()
+	{
+		import vibe.stream.wrapper;
+
+		assert(!m_headerWritten, "HTTPClient tried to write headers twice.");
+		m_headerWritten = true;
+
+		auto output = streamOutputRange!1024(m_conn);
+
+		formattedWrite(() @trusted { return &output; } (), "%s %s %s\r\n", httpMethodString(method), requestURL, getHTTPVersionString(httpVersion));
+		logTrace("--------------------");
+		logTrace("HTTP client request:");
+		logTrace("--------------------");
+		logTrace("%s", this);
+		foreach (k, v; headers.byKeyValue) {
+			() @trusted { formattedWrite(&output, "%s: %s\r\n", k, v); } ();
+			logTrace("%s: %s", k, v);
+		}
+		output.put("\r\n");
+		logTrace("--------------------");
+	}
+
+	private void finalize()
+	{
+		// test if already finalized
+		if (m_headerWritten && !m_bodyWriter)
+			return;
+
+		// force the request to be sent
+		if (!m_headerWritten) writeHeader();
+		else {
+			bodyWriter.flush();
+			if (m_chunkedStream) {
+				m_bodyWriter.finalize();
+				m_conn.flush();
+			}
+			m_bodyWriter = typeof(m_bodyWriter).init;
+			m_conn = typeof(m_conn).init;
+		}
+	}
+
+	private string clengthString(ulong len)
+	{
+		m_contentLengthBuffer.clear();
+		() @trusted { formattedWrite(&m_contentLengthBuffer, "%s", len); } ();
+		return () @trusted { return m_contentLengthBuffer.data; } ();
+	}
+}
+
+
+/**
+	Represents a HTTP client response (as received from the server).
+*/
+final class HTTPClientResponse : HTTPResponse {
+	@safe:
+
+	private {
+		HTTPClient m_client;
+		LockedConnection!HTTPClient lockedConnection;
+		FreeListRef!LimitedInputStream m_limitedInputStream;
+		FreeListRef!ChunkedInputStream m_chunkedInputStream;
+		FreeListRef!ZlibInputStream m_zlibInputStream;
+		FreeListRef!EndCallbackInputStream m_endCallback;
+		InterfaceProxy!InputStream m_bodyReader;
+		bool m_closeConn;
+		int m_maxRequests;
+	}
+
+	/// Contains the keep-alive 'max' parameter, indicates how many requests a client can
+	/// make before the server closes the connection.
+	@property int maxRequests() const {
+		return m_maxRequests;
+	}
+
+
+	/// All cookies that shall be set on the client for this request
+	override @property ref DictionaryList!Cookie cookies() {
+		if ("Set-Cookie" in this.headers && m_cookies.length == 0) {
+			foreach (cookieString; this.headers.getAll("Set-Cookie")) {
+				auto cookie = parseHTTPCookie(cookieString);
+				if (cookie[0].length)
+					m_cookies[cookie[0]] = cookie[1];
+			}
+		}
+		return m_cookies;
+	}
+
+	/// private
+	this(HTTPClient client, bool close_conn)
+	nothrow {
+		m_client = client;
+		m_closeConn = close_conn;
+	}
+
+	private void initialize(Allocator)(bool has_body, Allocator alloc, SysTime connected_time = Clock.currTime(UTC()))
+	{
+		scope(failure) finalize(true);
+
+		// read and parse status line ("HTTP/#.# #[ $]\r\n")
+		logTrace("HTTP client reading status line");
+		string stln = () @trusted { return cast(string)m_client.m_stream.readLine(HTTPClient.maxHeaderLineLength, "\r\n", alloc); } ();
+		logTrace("stln: %s", stln);
+		this.httpVersion = parseHTTPVersion(stln);
+
+		enforce(stln.startsWith(" "));
+		stln = stln[1 .. $];
+		this.statusCode = parse!int(stln);
+		if( stln.length > 0 ){
+			enforce(stln.startsWith(" "));
+			stln = stln[1 .. $];
+			this.statusPhrase = stln;
+		}
+
+		// read headers until an empty line is hit
+		parseRFC5322Header(m_client.m_stream, this.headers, HTTPClient.maxHeaderLineLength, alloc, false);
+
+		logTrace("---------------------");
+		logTrace("HTTP client response:");
+		logTrace("---------------------");
+		logTrace("%s", this);
+		foreach (k, v; this.headers.byKeyValue)
+			logTrace("%s: %s", k, v);
+		logTrace("---------------------");
+		Duration server_timeout;
+		bool has_server_timeout;
+		if (auto pka = "Keep-Alive" in this.headers) {
+			foreach(s; splitter(*pka, ',')){
+				auto pair = s.splitter('=');
+				auto name = pair.front.strip();
+				pair.popFront();
+				if (icmp(name, "timeout") == 0) {
+					has_server_timeout = true;
+					server_timeout = pair.front.to!int().seconds;
+				} else if (icmp(name, "max") == 0) {
+					m_maxRequests = pair.front.to!int();
+				}
+			}
+		}
+		Duration elapsed = Clock.currTime(UTC()) - connected_time;
+		if (this.headers.get("Connection") == "close") {
+			// this header will trigger m_client.disconnect() in m_client.doRequest() when it goes out of scope
+		} else if (has_server_timeout && m_client.m_keepAliveTimeout > server_timeout) {
+			m_client.m_keepAliveLimit = Clock.currTime(UTC()) + server_timeout - elapsed;
+		} else if (this.httpVersion == HTTPVersion.HTTP_1_1) {
+			m_client.m_keepAliveLimit = Clock.currTime(UTC()) + m_client.m_keepAliveTimeout;
+		}
+
+		if (!has_body) finalize();
+	}
+
+	~this()
+	{
+		debug if (m_client) {
+			import core.stdc.stdio;
+			printf("WARNING: HTTPClientResponse not fully processed before being finalized\n");
+		}
+	}
+
+	/**
+		An input stream suitable for reading the response body.
+	*/
+	@property InterfaceProxy!InputStream bodyReader()
+	{
+		if( m_bodyReader ) return m_bodyReader;
+
+		assert (m_client, "Response was already read or no response body, may not use bodyReader.");
+
+		// prepare body the reader
+		if (auto pte = "Transfer-Encoding" in this.headers) {
+			enforce(*pte == "chunked");
+			m_chunkedInputStream = createChunkedInputStreamFL(m_client.m_stream);
+			m_bodyReader = this.m_chunkedInputStream;
+		} else if (auto pcl = "Content-Length" in this.headers) {
+			m_limitedInputStream = createLimitedInputStreamFL(m_client.m_stream, to!ulong(*pcl));
+			m_bodyReader = m_limitedInputStream;
+		} else if (isKeepAliveResponse) {
+			m_limitedInputStream = createLimitedInputStreamFL(m_client.m_stream, 0);
+			m_bodyReader = m_limitedInputStream;
+		} else {
+			m_bodyReader = m_client.m_stream;
+		}
+
+		if( auto pce = "Content-Encoding" in this.headers ){
+			if( *pce == "deflate" ){
+				m_zlibInputStream = createDeflateInputStreamFL(m_bodyReader);
+				m_bodyReader = m_zlibInputStream;
+			} else if( *pce == "gzip" || *pce == "x-gzip"){
+				m_zlibInputStream = createGzipInputStreamFL(m_bodyReader);
+				m_bodyReader = m_zlibInputStream;
+			}
+			else enforce(*pce == "identity" || *pce == "", "Unsuported content encoding: "~*pce);
+		}
+
+		// be sure to free resouces as soon as the response has been read
+		m_endCallback = createEndCallbackInputStreamFL(m_bodyReader, &this.finalize);
+		m_bodyReader = m_endCallback;
+
+		return m_bodyReader;
+	}
+
+	/**
+		Provides unsafe means to read raw data from the connection.
+
+		No transfer decoding and no content decoding is done on the data.
+
+		Not that the provided delegate must read the whole stream,
+		as the state of the response is unknown after raw bytes have been
+		taken. Failure to read the right amount of data will lead to
+		protocol corruption in later requests.
+	*/
+	void readRawBody(scope void delegate(scope InterfaceProxy!InputStream stream) @safe del)
+	{
+		assert(!m_bodyReader, "May not mix use of readRawBody and bodyReader.");
+		del(interfaceProxy!InputStream(m_client.m_stream));
+		finalize();
+	}
+	/// ditto
+	static if (!is(InputStream == InterfaceProxy!InputStream))
+	void readRawBody(scope void delegate(scope InputStream stream) @safe del)
+	{
+		import vibe.internal.interfaceproxy : asInterface;
+
+		assert(!m_bodyReader, "May not mix use of readRawBody and bodyReader.");
+		del(m_client.m_stream.asInterface!(.InputStream));
+		finalize();
+	}
+
+	/**
+		Reads the whole response body and tries to parse it as JSON.
+	*/
+	Json readJson(){
+		auto bdy = bodyReader.readAllUTF8();
+		return () @trusted { return parseJson(bdy); } ();
+	}
+
+	/**
+		Reads and discards the response body.
+	*/
+	void dropBody()
+	{
+		if (m_client) {
+			if( bodyReader.empty ){
+				finalize();
+			} else {
+				bodyReader.pipe(nullSink);
+				assert(!lockedConnection.__conn);
+			}
+		}
+	}
+
+	/**
+		Forcefully terminates the connection regardless of the current state.
+
+		Note that this will only actually disconnect if the request has not yet
+		been fully processed. If the whole body was already read, the
+		connection is not owned by the current request operation anymore and
+		cannot be accessed. Use a "Connection: close" header instead in this
+		case to let the server close the connection.
+	*/
+	void disconnect()
+	{
+		finalize(true);
+	}
+
+	/**
+		Switches the connection to a new protocol and returns the resulting ConnectionStream.
+
+		The caller caller gets ownership of the ConnectionStream and is responsible
+		for closing it.
+
+		Notice:
+			When using the overload that returns a `ConnectionStream`, the caller
+			must make sure that the stream is not used after the
+			`HTTPClientRequest` has been destroyed.
+
+		Params:
+			new_protocol = The protocol to which the connection is expected to
+				upgrade. Should match the Upgrade header of the request. If an
+				empty string is passed, the "Upgrade" header will be ignored and
+				should be checked by other means.
+	*/
+	ConnectionStream switchProtocol(string new_protocol)
+	{
+		enforce(statusCode == HTTPStatus.switchingProtocols, "Server did not send a 101 - Switching Protocols response");
+		string *resNewProto = "Upgrade" in headers;
+		enforce(resNewProto, "Server did not send an Upgrade header");
+		enforce(!new_protocol.length || !icmp(*resNewProto, new_protocol),
+			"Expected Upgrade: " ~ new_protocol ~", received Upgrade: " ~ *resNewProto);
+		auto stream = createConnectionProxyStream!(typeof(m_client.m_stream), typeof(m_client.m_conn))(m_client.m_stream, m_client.m_conn);
+		m_closeConn = true; // cannot reuse connection for further requests!
+		return stream;
+	}
+	/// ditto
+	void switchProtocol(string new_protocol, scope void delegate(ConnectionStream str) @safe del)
+	{
+		enforce(statusCode == HTTPStatus.switchingProtocols, "Server did not send a 101 - Switching Protocols response");
+		string *resNewProto = "Upgrade" in headers;
+		enforce(resNewProto, "Server did not send an Upgrade header");
+		enforce(!new_protocol.length || !icmp(*resNewProto, new_protocol),
+			"Expected Upgrade: " ~ new_protocol ~", received Upgrade: " ~ *resNewProto);
+		auto stream = createConnectionProxyStream(m_client.m_stream, m_client.m_conn);
+		scope (exit) () @trusted { destroy(stream); } ();
+		m_closeConn = true;
+		del(stream);
+	}
+
+	private @property isKeepAliveResponse()
+	const {
+		string conn;
+		if (this.httpVersion == HTTPVersion.HTTP_1_0) {
+			// Workaround for non-standard-conformant servers - for example see #1780
+			auto pcl = "Content-Length" in this.headers;
+			if (pcl) conn = this.headers.get("Connection", "close");
+			else return false; // can't use keepalive when no content length is set
+		}
+		else conn = this.headers.get("Connection", "keep-alive");
+		return icmp(conn, "close") != 0;
+	}
+
+	private void finalize()
+	{
+		finalize(m_closeConn);
+	}
+
+	private void finalize(bool disconnect)
+	{
+		// ignore duplicate and too early calls to finalize
+		// (too early happesn for empty response bodies)
+		if (!m_client) return;
+
+		auto cli = m_client;
+		m_client = null;
+		cli.m_responding = false;
+		destroy(m_bodyReader);
+		destroy(m_endCallback);
+		destroy(m_zlibInputStream);
+		destroy(m_chunkedInputStream);
+		destroy(m_limitedInputStream);
+		if (disconnect) cli.disconnect();
+		destroy(lockedConnection);
+	}
+}
+
+/** Returns clean host string. In case of unix socket it performs urlDecode on host. */
+package auto getFilteredHost(URL url)
+{
+	version(UnixSocket)
+	{
+		import vibe.textfilter.urlencode : urlDecode;
+		if (url.schema == "https+unix" || url.schema == "http+unix")
+			return urlDecode(url.host);
+		else
+			return url.host;
+	} else
+		return url.host;
+}
+
+// This object is a placeholder and should to never be modified.
+package @property const(HTTPClientSettings) defaultSettings()
+@trusted nothrow {
+	__gshared HTTPClientSettings ret = new HTTPClientSettings;
+	return ret;
+}

+ 1107 - 0
vtest/source/vibe/http/common.d

@@ -0,0 +1,1107 @@
+/**
+	Common classes for HTTP clients and servers.
+
+	Copyright: © 2012-2015 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig, Jan Krüger
+*/
+module vibe.http.common;
+
+public import vibe.http.status;
+
+import vibe.container.dictionarylist;
+import vibe.container.internal.appender;
+import vibe.container.internal.utilallocator;
+import vibe.core.log;
+import vibe.core.net;
+import vibe.inet.message;
+import vibe.stream.operations;
+import vibe.textfilter.urlencode : urlEncode, urlDecode;
+import vibe.internal.freelistref;
+import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy;
+
+import std.algorithm;
+import std.array;
+import std.conv;
+import std.datetime;
+import std.exception;
+import std.format;
+import std.range : isOutputRange;
+import std.string;
+import std.typecons;
+import std.uni: asLowerCase, sicmp;
+
+
+enum HTTPVersion {
+	HTTP_1_0,
+	HTTP_1_1,
+	HTTP_2
+}
+
+
+enum HTTPMethod {
+	// HTTP standard, RFC 2616
+	GET,
+	HEAD,
+	PUT,
+	POST,
+	PATCH,
+	DELETE,
+	OPTIONS,
+	TRACE,
+	CONNECT,
+
+	// WEBDAV extensions, RFC 2518
+	PROPFIND,
+	PROPPATCH,
+	MKCOL,
+	COPY,
+	MOVE,
+	LOCK,
+	UNLOCK,
+
+	// Versioning Extensions to WebDAV, RFC 3253
+	VERSIONCONTROL,
+	REPORT,
+	CHECKOUT,
+	CHECKIN,
+	UNCHECKOUT,
+	MKWORKSPACE,
+	UPDATE,
+	LABEL,
+	MERGE,
+	BASELINECONTROL,
+	MKACTIVITY,
+
+	// Ordered Collections Protocol, RFC 3648
+	ORDERPATCH,
+
+	// Access Control Protocol, RFC 3744
+	ACL
+}
+
+
+/**
+	Returns the string representation of the given HttpMethod.
+*/
+string httpMethodString(HTTPMethod m)
+@safe nothrow {
+	switch(m){
+		case HTTPMethod.BASELINECONTROL: return "BASELINE-CONTROL";
+		case HTTPMethod.VERSIONCONTROL: return "VERSION-CONTROL";
+		default:
+			try return to!string(m);
+			catch (Exception e) assert(false, e.msg);
+	}
+}
+
+/**
+	Returns the HttpMethod value matching the given HTTP method string.
+*/
+HTTPMethod httpMethodFromString(string str)
+@safe {
+	switch(str){
+		default: throw new Exception("Invalid HTTP method: "~str);
+		// HTTP standard, RFC 2616
+		case "GET": return HTTPMethod.GET;
+		case "HEAD": return HTTPMethod.HEAD;
+		case "PUT": return HTTPMethod.PUT;
+		case "POST": return HTTPMethod.POST;
+		case "PATCH": return HTTPMethod.PATCH;
+		case "DELETE": return HTTPMethod.DELETE;
+		case "OPTIONS": return HTTPMethod.OPTIONS;
+		case "TRACE": return HTTPMethod.TRACE;
+		case "CONNECT": return HTTPMethod.CONNECT;
+
+		// WEBDAV extensions, RFC 2518
+		case "PROPFIND": return HTTPMethod.PROPFIND;
+		case "PROPPATCH": return HTTPMethod.PROPPATCH;
+		case "MKCOL": return HTTPMethod.MKCOL;
+		case "COPY": return HTTPMethod.COPY;
+		case "MOVE": return HTTPMethod.MOVE;
+		case "LOCK": return HTTPMethod.LOCK;
+		case "UNLOCK": return HTTPMethod.UNLOCK;
+
+		// Versioning Extensions to WebDAV, RFC 3253
+		case "VERSION-CONTROL": return HTTPMethod.VERSIONCONTROL;
+		case "REPORT": return HTTPMethod.REPORT;
+		case "CHECKOUT": return HTTPMethod.CHECKOUT;
+		case "CHECKIN": return HTTPMethod.CHECKIN;
+		case "UNCHECKOUT": return HTTPMethod.UNCHECKOUT;
+		case "MKWORKSPACE": return HTTPMethod.MKWORKSPACE;
+		case "UPDATE": return HTTPMethod.UPDATE;
+		case "LABEL": return HTTPMethod.LABEL;
+		case "MERGE": return HTTPMethod.MERGE;
+		case "BASELINE-CONTROL": return HTTPMethod.BASELINECONTROL;
+		case "MKACTIVITY": return HTTPMethod.MKACTIVITY;
+
+		// Ordered Collections Protocol, RFC 3648
+		case "ORDERPATCH": return HTTPMethod.ORDERPATCH;
+
+		// Access Control Protocol, RFC 3744
+		case "ACL": return HTTPMethod.ACL;
+	}
+}
+
+unittest
+{
+	assert(httpMethodString(HTTPMethod.GET) == "GET");
+	assert(httpMethodString(HTTPMethod.UNLOCK) == "UNLOCK");
+	assert(httpMethodString(HTTPMethod.VERSIONCONTROL) == "VERSION-CONTROL");
+	assert(httpMethodString(HTTPMethod.BASELINECONTROL) == "BASELINE-CONTROL");
+	assert(httpMethodFromString("GET") == HTTPMethod.GET);
+	assert(httpMethodFromString("UNLOCK") == HTTPMethod.UNLOCK);
+	assert(httpMethodFromString("VERSION-CONTROL") == HTTPMethod.VERSIONCONTROL);
+}
+
+
+/**
+	Utility function that throws a HTTPStatusException if the _condition is not met.
+*/
+T enforceHTTP(T)(T condition, HTTPStatus statusCode, lazy string message = null, string file = __FILE__, typeof(__LINE__) line = __LINE__)
+{
+	return enforce(condition, new HTTPStatusException(statusCode, message, file, line));
+}
+
+/**
+	Utility function that throws a HTTPStatusException with status code "400 Bad Request" if the _condition is not met.
+*/
+T enforceBadRequest(T)(T condition, lazy string message = null, string file = __FILE__, typeof(__LINE__) line = __LINE__)
+{
+	return enforceHTTP(condition, HTTPStatus.badRequest, message, file, line);
+}
+
+
+/**
+	Represents an HTTP request made to a server.
+*/
+class HTTPRequest {
+	@safe:
+
+	protected {
+		InterfaceProxy!Stream m_conn;
+	}
+
+	public {
+		/// The HTTP protocol version used for the request
+		HTTPVersion httpVersion = HTTPVersion.HTTP_1_1;
+
+		/// The HTTP _method of the request
+		HTTPMethod method = HTTPMethod.GET;
+
+		/** The request URI
+
+			Note that the request URI usually does not include the global
+			'http://server' part, but only the local path and a query string.
+			A possible exception is a proxy server, which will get full URLs.
+		*/
+		string requestURI = "/";
+
+		/// Compatibility alias - scheduled for deprecation
+		alias requestURL = requestURI;
+
+		/// All request _headers
+		InetHeaderMap headers;
+	}
+
+	protected this(InterfaceProxy!Stream conn)
+	{
+		m_conn = conn;
+	}
+
+	protected this()
+	{
+	}
+
+	scope:
+
+	public override string toString()
+	{
+		return httpMethodString(method) ~ " " ~ requestURL ~ " " ~ getHTTPVersionString(httpVersion);
+	}
+
+	/** Shortcut to the 'Host' header (always present for HTTP 1.1)
+	*/
+	@property string host() const { auto ph = "Host" in headers; return ph ? *ph : null; }
+	/// ditto
+	@property void host(string v) { headers["Host"] = v; }
+
+	/** Returns the mime type part of the 'Content-Type' header.
+
+		This function gets the pure mime type (e.g. "text/plain")
+		without any supplimentary parameters such as "charset=...".
+		Use contentTypeParameters to get any parameter string or
+		headers["Content-Type"] to get the raw value.
+	*/
+	@property string contentType()
+	const {
+		auto pv = "Content-Type" in headers;
+		if( !pv ) return null;
+		auto idx = std.string.indexOf(*pv, ';');
+		return idx >= 0 ? (*pv)[0 .. idx] : *pv;
+	}
+	/// ditto
+	@property void contentType(string ct) { headers["Content-Type"] = ct; }
+
+	/** Returns any supplementary parameters of the 'Content-Type' header.
+
+		This is a semicolon separated ist of key/value pairs. Usually, if set,
+		this contains the character set used for text based content types.
+	*/
+	@property string contentTypeParameters()
+	const {
+		auto pv = "Content-Type" in headers;
+		if( !pv ) return null;
+		auto idx = std.string.indexOf(*pv, ';');
+		return idx >= 0 ? (*pv)[idx+1 .. $] : null;
+	}
+
+	/** Determines if the connection persists across requests.
+	*/
+	@property bool persistent() const
+	{
+		auto ph = "connection" in headers;
+		switch(httpVersion) {
+			case HTTPVersion.HTTP_1_0:
+				if (ph && asLowerCase(*ph).equal("keep-alive")) return true;
+				return false;
+			case HTTPVersion.HTTP_1_1:
+				if (ph && !(asLowerCase(*ph).equal("keep-alive"))) return false;
+				return true;
+			default:
+				return false;
+		}
+	}
+}
+
+
+/**
+	Represents the HTTP response from the server back to the client.
+*/
+class HTTPResponse {
+	@safe:
+
+	protected DictionaryList!Cookie m_cookies;
+
+	public {
+		/// The protocol version of the response - should not be changed
+		HTTPVersion httpVersion = HTTPVersion.HTTP_1_1;
+
+		/// The status code of the response, 200 by default
+		int statusCode = HTTPStatus.ok;
+
+		/** The status phrase of the response
+
+			If no phrase is set, a default one corresponding to the status code will be used.
+		*/
+		string statusPhrase;
+
+		/// The response header fields
+		InetHeaderMap headers;
+
+		/// All cookies that shall be set on the client for this request
+		@property ref DictionaryList!Cookie cookies() return scope { return m_cookies; }
+	}
+
+	scope:
+
+	public override string toString()
+	{
+		auto app = appender!string();
+		formattedWrite(app, "%s %d %s", getHTTPVersionString(this.httpVersion), this.statusCode, this.statusPhrase);
+		return app.data;
+	}
+
+	/** Shortcut to the "Content-Type" header
+	*/
+	@property string contentType() const { auto pct = "Content-Type" in headers; return pct ? *pct : "application/octet-stream"; }
+	/// ditto
+	@property void contentType(string ct) { headers["Content-Type"] = ct; }
+}
+
+
+/**
+	Respresents a HTTP response status.
+
+	Throwing this exception from within a request handler will produce a matching error page.
+*/
+class HTTPStatusException : Exception {
+	pure nothrow @safe @nogc:
+
+	private {
+		int m_status;
+	}
+
+	this(int status, string message = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
+	{
+		super(message.length ? message : httpStatusText(status), file, line, next);
+		m_status = status;
+	}
+
+	/// The HTTP status code
+	@property int status() const { return m_status; }
+
+	string debugMessage;
+}
+
+
+final class MultiPart {
+	string contentType;
+
+	InputStream stream;
+	//JsonValue json;
+	string[string] form;
+}
+
+/**
+ * Returns:
+ *     The version string corresponding to the `ver`,
+ *     suitable for usage in the start line of the request.
+ */
+string getHTTPVersionString(HTTPVersion ver)
+nothrow pure @nogc @safe {
+	final switch(ver){
+		case HTTPVersion.HTTP_1_0: return "HTTP/1.0";
+		case HTTPVersion.HTTP_1_1: return "HTTP/1.1";
+		case HTTPVersion.HTTP_2: return "HTTP/2";
+	}
+}
+
+
+HTTPVersion parseHTTPVersion(ref string str)
+@safe {
+	enforceBadRequest(str.startsWith("HTTP/1."));
+	str = str[7 .. $];
+	int minorVersion = parse!int(str);
+
+	enforceBadRequest( minorVersion == 0 || minorVersion == 1 );
+	return minorVersion == 0 ? HTTPVersion.HTTP_1_0 : HTTPVersion.HTTP_1_1;
+}
+
+
+/**
+	Takes an input stream that contains data in HTTP chunked format and outputs the raw data.
+*/
+final class ChunkedInputStream : InputStream
+{
+	@safe:
+
+	private {
+		InterfaceProxy!InputStream m_in;
+		ulong m_bytesInCurrentChunk = 0;
+	}
+
+	/// private
+	this(InterfaceProxy!InputStream stream, bool dummy)
+	{
+		assert(!!stream);
+		m_in = stream;
+		readChunk();
+	}
+
+	@property bool empty() const { return m_bytesInCurrentChunk == 0; }
+
+	@property ulong leastSize() const { return m_bytesInCurrentChunk; }
+
+	@property bool dataAvailableForRead() { return m_bytesInCurrentChunk > 0 && m_in.dataAvailableForRead; }
+
+	const(ubyte)[] peek()
+	{
+		auto dt = m_in.peek();
+		return dt[0 .. min(dt.length, m_bytesInCurrentChunk)];
+	}
+
+	size_t read(scope ubyte[] dst, IOMode mode)
+	{
+		enforceBadRequest(!empty, "Read past end of chunked stream.");
+		size_t nbytes = 0;
+
+		while (dst.length > 0) {
+			enforceBadRequest(m_bytesInCurrentChunk > 0, "Reading past end of chunked HTTP stream.");
+
+			auto sz = cast(size_t)min(m_bytesInCurrentChunk, dst.length);
+			m_in.read(dst[0 .. sz]);
+			dst = dst[sz .. $];
+			m_bytesInCurrentChunk -= sz;
+			nbytes += sz;
+
+			// FIXME: this blocks, but shouldn't for IOMode.once/immediat
+			if( m_bytesInCurrentChunk == 0 ){
+				// skip current chunk footer and read next chunk
+				ubyte[2] crlf;
+				m_in.read(crlf);
+				enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n');
+				readChunk();
+			}
+
+			if (mode != IOMode.all) break;
+		}
+
+		return nbytes;
+	}
+
+	alias read = InputStream.read;
+
+	private void readChunk()
+	{
+		assert(m_bytesInCurrentChunk == 0);
+		// read chunk header
+		logTrace("read next chunk header");
+		auto ln = () @trusted { return cast(string)m_in.readLine(); } ();
+		logTrace("got chunk header: %s", ln);
+		m_bytesInCurrentChunk = parse!ulong(ln, 16u);
+
+		if( m_bytesInCurrentChunk == 0 ){
+			// empty chunk denotes the end
+			// skip final chunk footer
+			ubyte[2] crlf;
+			m_in.read(crlf);
+			enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n');
+		}
+	}
+}
+
+/// Creates a new `ChunkedInputStream` instance.
+ChunkedInputStream chunkedInputStream(IS)(IS source_stream) if (isInputStream!IS)
+{
+	return new ChunkedInputStream(interfaceProxy!InputStream(source_stream), true);
+}
+
+/// Creates a new `ChunkedInputStream` instance.
+FreeListRef!ChunkedInputStream createChunkedInputStreamFL(IS)(IS source_stream) if (isInputStream!IS)
+{
+	return () @trusted { return FreeListRef!ChunkedInputStream(interfaceProxy!InputStream(source_stream), true); } ();
+}
+
+
+/**
+	Outputs data to an output stream in HTTP chunked format.
+*/
+final class ChunkedOutputStream : OutputStream {
+	@safe:
+
+	alias ChunkExtensionCallback = string delegate(in ubyte[] data);
+	private {
+		InterfaceProxy!OutputStream m_out;
+		AllocAppender!(ubyte[]) m_buffer;
+		size_t m_maxBufferSize = 4*1024;
+		bool m_finalized = false;
+		ChunkExtensionCallback m_chunkExtensionCallback = null;
+	}
+
+	/// private
+	this(Allocator)(InterfaceProxy!OutputStream stream, Allocator alloc, bool dummy)
+	{
+		m_out = stream;
+		m_buffer = AllocAppender!(ubyte[])(alloc);
+	}
+
+	/** Maximum buffer size used to buffer individual chunks.
+
+		A size of zero means unlimited buffer size. Explicit flush is required
+		in this case to empty the buffer.
+	*/
+	@property size_t maxBufferSize() const { return m_maxBufferSize; }
+	/// ditto
+	@property void maxBufferSize(size_t bytes) { m_maxBufferSize = bytes; if (m_buffer.data.length >= m_maxBufferSize) flush(); }
+
+	/** A delegate used to specify the extensions for each chunk written to the underlying stream.
+
+	 	The delegate has to be of type `string delegate(in const(ubyte)[] data)` and gets handed the
+	 	data of each chunk before it is written to the underlying stream. If it's return value is non-empty,
+	 	it will be added to the chunk's header line.
+
+	 	The returned chunk extension string should be of the format `key1=value1;key2=value2;[...];keyN=valueN`
+	 	and **not contain any carriage return or newline characters**.
+
+	 	Also note that the delegate should accept the passed data through a scoped argument. Thus, **no references
+	 	to the provided data should be stored in the delegate**. If the data has to be stored for later use,
+	 	it needs to be copied first.
+	 */
+	@property ChunkExtensionCallback chunkExtensionCallback() const { return m_chunkExtensionCallback; }
+	/// ditto
+	@property void chunkExtensionCallback(ChunkExtensionCallback cb) { m_chunkExtensionCallback = cb; }
+
+	private void append(scope void delegate(scope ubyte[] dst) @safe del, size_t nbytes)
+	{
+		assert(del !is null);
+		auto sz = nbytes;
+		if (m_maxBufferSize > 0 && m_maxBufferSize < m_buffer.data.length + sz)
+			sz = m_maxBufferSize - min(m_buffer.data.length, m_maxBufferSize);
+
+		if (sz > 0)
+		{
+			m_buffer.reserve(sz);
+			() @trusted {
+				m_buffer.append((scope ubyte[] dst) {
+					debug assert(dst.length >= sz);
+					del(dst[0..sz]);
+					return sz;
+				});
+			} ();
+		}
+	}
+
+	static if (is(typeof(.OutputStream.outputStreamVersion)) && .OutputStream.outputStreamVersion > 1) {
+		override size_t write(scope const(ubyte)[] bytes_, IOMode mode) { return doWrite(bytes_, mode); }
+	} else {
+		override size_t write(in ubyte[] bytes_, IOMode mode) { return doWrite(bytes_, mode); }
+	}
+
+	alias write = OutputStream.write;
+
+	private size_t doWrite(scope const(ubyte)[] bytes_, IOMode mode)
+	{
+		assert(!m_finalized);
+		const(ubyte)[] bytes = bytes_;
+		size_t nbytes = 0;
+		while (bytes.length > 0) {
+			append((scope ubyte[] dst) {
+					auto n = dst.length;
+					dst[] = bytes[0..n];
+					bytes = bytes[n..$];
+					nbytes += n;
+				}, bytes.length);
+			if (mode == IOMode.immediate) break;
+			if (mode == IOMode.once && nbytes > 0) break;
+			if (bytes.length > 0)
+				flush();
+		}
+		return nbytes;
+	}
+
+	void flush()
+	{
+		assert(!m_finalized);
+		auto data = m_buffer.data();
+		if( data.length ){
+			writeChunk(data);
+		}
+		m_out.flush();
+		() @trusted { m_buffer.reset(AppenderResetMode.reuseData); } ();
+	}
+
+	void finalize()
+	{
+		if (m_finalized) return;
+		flush();
+		() @trusted { m_buffer.reset(AppenderResetMode.freeData); } ();
+		m_finalized = true;
+		writeChunk([]);
+		m_out.flush();
+	}
+
+	private void writeChunk(in ubyte[] data)
+	{
+		import vibe.stream.wrapper;
+		auto rng = streamOutputRange(m_out);
+		formattedWrite(() @trusted { return &rng; } (), "%x", data.length);
+		if (m_chunkExtensionCallback !is null)
+		{
+			rng.put(';');
+			auto extension = m_chunkExtensionCallback(data);
+			assert(!extension.startsWith(';'));
+			debug assert(extension.indexOf('\r') < 0);
+			debug assert(extension.indexOf('\n') < 0);
+			rng.put(extension);
+		}
+		rng.put("\r\n");
+		rng.put(data);
+		rng.put("\r\n");
+	}
+}
+
+/// Creates a new `ChunkedInputStream` instance.
+ChunkedOutputStream createChunkedOutputStream(OS)(OS destination_stream) if (isOutputStream!OS)
+{
+	return createChunkedOutputStream(destination_stream, theAllocator());
+}
+/// ditto
+ChunkedOutputStream createChunkedOutputStream(OS, Allocator)(OS destination_stream, Allocator allocator) if (isOutputStream!OS)
+{
+	return new ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true);
+}
+
+/// Creates a new `ChunkedOutputStream` instance.
+FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS)(OS destination_stream) if (isOutputStream!OS)
+{
+	return createChunkedOutputStreamFL(destination_stream, theAllocator());
+}
+/// ditto
+FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS, Allocator)(OS destination_stream, Allocator allocator) if (isOutputStream!OS)
+{
+	return FreeListRef!ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true);
+}
+
+/// Parses the cookie from a header field, returning the name of the cookie.
+/// Implements an algorithm equivalent to https://tools.ietf.org/html/rfc6265#section-5.2
+/// Returns: the cookie name as return value, populates the dst argument or allocates on the GC for the tuple overload.
+string parseHTTPCookie(string header_string, scope Cookie dst)
+@safe
+in {
+	assert(dst !is null);
+} do {
+	if (!header_string.length)
+		return typeof(return).init;
+
+	auto parts = header_string.splitter(';');
+	auto idx = parts.front.indexOf('=');
+	if (idx == -1)
+		return typeof(return).init;
+
+	auto name = parts.front[0 .. idx].strip();
+	dst.m_value = parts.front[name.length + 1 .. $].strip();
+	parts.popFront();
+
+	if (!name.length)
+		return typeof(return).init;
+
+	foreach(part; parts) {
+		if (!part.length)
+			continue;
+
+		idx = part.indexOf('=');
+		if (idx == -1) {
+			idx = part.length;
+		}
+		auto key = part[0 .. idx].strip();
+		auto value = part[min(idx + 1, $) .. $].strip();
+
+		try {
+			if (key.sicmp("httponly") == 0) {
+				dst.m_httpOnly = true;
+			} else if (key.sicmp("secure") == 0) {
+				dst.m_secure = true;
+			} else if (key.sicmp("expires") == 0) {
+				// RFC 822 got updated by RFC 1123 (which is to be used) but is valid for this
+				// this parsing is just for validation
+				parseRFC822DateTimeString(value);
+				dst.m_expires = value;
+			} else if (key.sicmp("max-age") == 0) {
+				if (value.length && value[0] != '-')
+					dst.m_maxAge = value.to!long;
+			} else if (key.sicmp("domain") == 0) {
+				if (value.length && value[0] == '.')
+					value = value[1 .. $]; // the leading . must be stripped (5.2.3)
+
+				enforce!ConvException(value.all!(a => a >= 32), "Cookie Domain must not contain any control characters");
+				dst.m_domain = value.toLower; // must be lower (5.2.3)
+			} else if (key.sicmp("path") == 0) {
+				if (value.length && value[0] == '/') {
+					enforce!ConvException(value.all!(a => a >= 32), "Cookie Path must not contain any control characters");
+					dst.m_path = value;
+				} else {
+					dst.m_path = null;
+				}
+			} // else extension value...
+		} catch (DateTimeException) {
+		} catch (ConvException) {
+		}
+		// RFC 6265 says to ignore invalid values on all of these fields
+	}
+	return name;
+}
+
+/// ditto
+Tuple!(string, Cookie) parseHTTPCookie(string header_string)
+@safe {
+	Cookie cookie = new Cookie();
+	auto name = parseHTTPCookie(header_string, cookie);
+	return tuple(name, cookie);
+}
+
+final class Cookie {
+	@safe:
+
+	private {
+		string m_value;
+		string m_domain;
+		string m_path;
+		string m_expires;
+		long m_maxAge;
+		bool m_secure;
+		bool m_httpOnly;
+		SameSite m_sameSite;
+	}
+
+	enum Encoding {
+		url,
+		raw,
+		none = raw
+	}
+
+	enum SameSite {
+		default_,
+		lax,
+		none,
+		strict,
+	}
+
+	/// Cookie payload
+	@property void value(string value) { m_value = urlEncode(value); }
+	/// ditto
+	@property string value() const { return urlDecode(m_value); }
+
+	/// Undecoded cookie payload
+	@property void rawValue(string value) { m_value = value; }
+	/// ditto
+	@property string rawValue() const { return m_value; }
+
+	/// The domain for which the cookie is valid
+	@property void domain(string value) { m_domain = value; }
+	/// ditto
+	@property string domain() const { return m_domain; }
+
+	/// The path/local URI for which the cookie is valid
+	@property void path(string value) { m_path = value; }
+	/// ditto
+	@property string path() const { return m_path; }
+
+	/// Expiration date of the cookie
+	@property void expires(string value) { m_expires = value; }
+	/// ditto
+	@property void expires(SysTime value) { m_expires = value.toRFC822DateTimeString(); }
+	/// ditto
+	@property string expires() const { return m_expires; }
+
+	/** Maximum life time of the cookie
+
+		This is the modern variant of `expires`. For backwards compatibility it
+		is recommended to set both properties, or to use the `expire` method.
+	*/
+	@property void maxAge(long value) { m_maxAge = value; }
+	/// ditto
+	@property void maxAge(Duration value) { m_maxAge = value.total!"seconds"; }
+	/// ditto
+	@property long maxAge() const { return m_maxAge; }
+
+	/** Require a secure connection for transmission of this cookie
+	*/
+	@property void secure(bool value) { m_secure = value; }
+	/// ditto
+	@property bool secure() const { return m_secure; }
+
+	/** Prevents access to the cookie from scripts.
+	*/
+	@property void httpOnly(bool value) { m_httpOnly = value; }
+	/// ditto
+	@property bool httpOnly() const { return m_httpOnly; }
+
+	/** Prevent cross-site request forgery.
+	*/
+	@property void sameSite(Cookie.SameSite value) { m_sameSite = value; }
+	/// ditto
+	@property Cookie.SameSite sameSite() const { return m_sameSite; }
+
+	/** Sets the "expires" and "max-age" attributes to limit the life time of
+		the cookie.
+	*/
+	void expire(Duration max_age)
+	{
+		this.expires = Clock.currTime(UTC()) + max_age;
+		this.maxAge = max_age;
+	}
+	/// ditto
+	void expire(SysTime expire_time)
+	{
+		this.expires = expire_time;
+		this.maxAge = expire_time - Clock.currTime(UTC());
+	}
+
+	/// Sets the cookie value encoded with the specified encoding.
+	void setValue(string value, Encoding encoding)
+	{
+		final switch (encoding) {
+			case Encoding.url: m_value = urlEncode(value); break;
+			case Encoding.none: validateValue(value); m_value = value; break;
+		}
+	}
+
+	/// Writes out the full cookie in HTTP compatible format.
+	void writeString(R)(R dst, string name)
+		if (isOutputRange!(R, char))
+	{
+		import vibe.textfilter.urlencode;
+		dst.put(name);
+		dst.put('=');
+		validateValue(this.value);
+		dst.put(this.value);
+		if (this.domain && this.domain != "") {
+			dst.put("; Domain=");
+			dst.put(this.domain);
+		}
+		if (this.path != "") {
+			dst.put("; Path=");
+			dst.put(this.path);
+		}
+		if (this.expires != "") {
+			dst.put("; Expires=");
+			dst.put(this.expires);
+		}
+		if (this.maxAge) dst.formattedWrite("; Max-Age=%s", this.maxAge);
+		if (this.secure) dst.put("; Secure");
+		if (this.httpOnly) dst.put("; HttpOnly");
+		with(Cookie.SameSite)
+		final switch(this.sameSite) {
+			case default_: break;
+			case lax: dst.put("; SameSite=Lax"); break;
+			case strict: dst.put("; SameSite=Strict"); break;
+			case none: dst.put("; SameSite=None"); break;
+		}
+	}
+
+	private static void validateValue(string value)
+	{
+		enforce(!value.canFind(';') && !value.canFind('"'));
+	}
+}
+
+unittest {
+	import std.exception : assertThrown;
+
+	auto c = new Cookie;
+	c.value = "foo";
+	assert(c.value == "foo");
+	assert(c.rawValue == "foo");
+
+	c.value = "foo$";
+	assert(c.value == "foo$");
+	assert(c.rawValue == "foo%24", c.rawValue);
+
+	c.value = "foo&bar=baz?";
+	assert(c.value == "foo&bar=baz?");
+	assert(c.rawValue == "foo%26bar%3Dbaz%3F", c.rawValue);
+
+	c.setValue("foo%", Cookie.Encoding.raw);
+	assert(c.rawValue == "foo%");
+	assertThrown(c.value);
+
+	assertThrown(c.setValue("foo;bar", Cookie.Encoding.raw));
+
+	auto tup = parseHTTPCookie("foo=bar; HttpOnly; Secure; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=60000; Domain=foo.com; Path=/users");
+	assert(tup[0] == "foo");
+	assert(tup[1].value == "bar");
+	assert(tup[1].httpOnly == true);
+	assert(tup[1].secure == true);
+	assert(tup[1].expires == "Wed, 09 Jun 2021 10:18:14 GMT");
+	assert(tup[1].maxAge == 60000L);
+	assert(tup[1].domain == "foo.com");
+	assert(tup[1].path == "/users");
+
+	tup = parseHTTPCookie("SESSIONID=0123456789ABCDEF0123456789ABCDEF; Path=/site; HttpOnly");
+	assert(tup[0] == "SESSIONID");
+	assert(tup[1].value == "0123456789ABCDEF0123456789ABCDEF");
+	assert(tup[1].httpOnly == true);
+	assert(tup[1].secure == false);
+	assert(tup[1].expires == "");
+	assert(tup[1].maxAge == 0);
+	assert(tup[1].domain == "");
+	assert(tup[1].path == "/site");
+
+	tup = parseHTTPCookie("invalid");
+	assert(!tup[0].length);
+
+	tup = parseHTTPCookie("valid=");
+	assert(tup[0] == "valid");
+	assert(tup[1].value == "");
+
+	tup = parseHTTPCookie("valid=;Path=/bar;Path=foo;Expires=14   ; Something   ; Domain=..example.org");
+	assert(tup[0] == "valid");
+	assert(tup[1].value == "");
+	assert(tup[1].httpOnly == false);
+	assert(tup[1].secure == false);
+	assert(tup[1].expires == "");
+	assert(tup[1].maxAge == 0);
+	assert(tup[1].domain == ".example.org"); // spec says you must strip only the first leading dot
+	assert(tup[1].path == "");
+}
+
+
+/**
+*/
+struct CookieValueMap {
+	@safe:
+
+	struct Cookie {
+		/// Name of the cookie
+		string name;
+
+		/// The raw cookie value as transferred over the wire
+		string rawValue;
+
+		this(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url)
+		{
+			this.name = name;
+			this.setValue(value, encoding);
+		}
+
+		/// Treats the value as URL encoded
+		string value() const { return urlDecode(rawValue); }
+		/// ditto
+		void value(string val) { rawValue = urlEncode(val); }
+
+		/// Sets the cookie value, applying the specified encoding.
+		void setValue(string value, .Cookie.Encoding encoding = .Cookie.Encoding.url)
+		{
+			final switch (encoding) {
+				case .Cookie.Encoding.none: this.rawValue = value; break;
+				case .Cookie.Encoding.url: this.rawValue = urlEncode(value); break;
+			}
+		}
+	}
+
+	private {
+		Cookie[] m_entries;
+	}
+
+	auto length(){
+		return m_entries.length;
+	}
+
+	string get(string name, string def_value = null)
+	const {
+		foreach (ref c; m_entries)
+			if (c.name == name)
+				return c.value;
+		return def_value;
+	}
+
+	string[] getAll(string name)
+	const {
+		string[] ret;
+		foreach(c; m_entries)
+			if( c.name == name )
+				ret ~= c.value;
+		return ret;
+	}
+
+	void add(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url){
+		m_entries ~= Cookie(name, value, encoding);
+	}
+
+	void opIndexAssign(string value, string name)
+	{
+		m_entries ~= Cookie(name, value);
+	}
+
+	string opIndex(string name)
+	const {
+		import core.exception : RangeError;
+		foreach (ref c; m_entries)
+			if (c.name == name)
+				return c.value;
+		throw new RangeError("Non-existent cookie: "~name);
+	}
+
+	int opApply(scope int delegate(ref Cookie) @safe del)
+	{
+		foreach(ref c; m_entries)
+			if( auto ret = del(c) )
+				return ret;
+		return 0;
+	}
+
+	int opApply(scope int delegate(ref Cookie) @safe del)
+	const {
+		foreach(Cookie c; m_entries)
+			if( auto ret = del(c) )
+				return ret;
+		return 0;
+	}
+
+	int opApply(scope int delegate(string name, string value) @safe del)
+	{
+		foreach(ref c; m_entries)
+			if( auto ret = del(c.name, c.value) )
+				return ret;
+		return 0;
+	}
+
+	int opApply(scope int delegate(string name, string value) @safe del)
+	const {
+		foreach(Cookie c; m_entries)
+			if( auto ret = del(c.name, c.value) )
+				return ret;
+		return 0;
+	}
+
+	auto opBinaryRight(string op)(string name) if(op == "in")
+	{
+		return Ptr(&this, name);
+	}
+
+	auto opBinaryRight(string op)(string name) const if(op == "in")
+	{
+		return const(Ptr)(&this, name);
+	}
+
+	private static struct Ref {
+		private {
+			CookieValueMap* map;
+			string name;
+		}
+
+		@property string get() const { return (*map)[name]; }
+		void opAssign(string newval) {
+			foreach (ref c; *map)
+				if (c.name == name) {
+					c.value = newval;
+					return;
+				}
+			assert(false);
+		}
+		alias get this;
+	}
+	private static struct Ptr {
+		private {
+			CookieValueMap* map;
+			string name;
+		}
+		bool opCast() const {
+			foreach (ref c; map.m_entries)
+				if (c.name == name)
+					return true;
+			return false;
+		}
+		inout(Ref) opUnary(string op : "*")() inout { return inout(Ref)(map, name); }
+	}
+}
+
+unittest {
+	CookieValueMap m;
+	m["foo"] = "bar;baz%1";
+	assert(m["foo"] == "bar;baz%1");
+
+	m["foo"] = "bar";
+	assert(m.getAll("foo") == ["bar;baz%1", "bar"]);
+
+	assert("foo" in m);
+	if (auto val = "foo" in m) {
+		assert(*val == "bar;baz%1");
+	} else assert(false);
+	*("foo" in m) = "baz";
+	assert(m["foo"] == "baz");
+}
+
+
+package auto createRequestAllocator()
+{
+	import vibe.container.internal.utilallocator: RegionListAllocator;
+
+	static if (is(RegionListAllocator!(shared(GCAllocator), true) == struct)) {
+		version (VibeManualMemoryManagement)
+			return allocatorObject(RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance));
+		else
+			return allocatorObject(RegionListAllocator!(shared(GCAllocator), true)(1024, GCAllocator.instance));
+	} else {
+		version (VibeManualMemoryManagement)
+			return new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance);
+		else
+			return new RegionListAllocator!(shared(GCAllocator), true)(1024, GCAllocator.instance);
+	}
+}
+
+package void freeRequestAllocator(Allocator)(ref Allocator alloc)
+{
+	destroy(alloc);
+}

+ 52 - 0
vtest/source/vibe/http/dist.d

@@ -0,0 +1,52 @@
+/**
+	Interface for the VibeDist load balancer
+
+	Copyright: © 2012-2013 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig, Jan Krüger
+*/
+module vibe.http.dist;
+
+import vibe.core.log;
+import vibe.data.json;
+import vibe.inet.url;
+import vibe.http.client;
+import vibe.http.server;
+
+import std.conv;
+import std.exception;
+import std.process;
+
+
+/**
+	Listens for HTTP connections on the specified load balancer using the given HTTP server settings.
+
+	This function is usable as direct replacement of listenHTTP
+*/
+HTTPListener listenHTTPDist(HTTPServerSettings settings, HTTPServerRequestDelegate handler, string balancer_address, ushort balancer_port = 11000)
+@safe {
+	Json regmsg = Json.emptyObject;
+	regmsg["host_name"] = settings.hostName;
+	regmsg["port"] = settings.port;
+	regmsg["ssl_settings"] = "";
+	regmsg["pid"] = thisProcessID;
+	//regmsg.sslContext = settings.sslContext; // TODO: send key/cert contents
+
+	HTTPServerSettings local_settings = settings.dup;
+	local_settings.bindAddresses = ["127.0.0.1"];
+	local_settings.port = 0;
+	local_settings.disableDistHost = true;
+	auto ret = listenHTTP(local_settings, handler);
+
+	requestHTTP(URL("http://"~balancer_address~":"~to!string(balancer_port)~"/register"), (scope req){
+			logInfo("Listening for VibeDist connections on port %d", req.localAddress.port);
+			regmsg["local_address"] = "127.0.0.1";
+			regmsg["local_port"] = req.localAddress.port;
+			req.method = HTTPMethod.POST;
+			req.writeJsonBody(regmsg);
+		}, (scope res){
+			enforce(res.statusCode == HTTPStatus.ok, "Failed to register with load balancer.");
+		});
+
+	return ret;
+}

+ 732 - 0
vtest/source/vibe/http/fileserver.d

@@ -0,0 +1,732 @@
+/**
+	A static HTTP file server.
+
+	Copyright: © 2012-2015 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.http.fileserver;
+
+import vibe.core.file;
+import vibe.core.log;
+import vibe.core.stream : RandomAccessStream, pipe;
+import vibe.http.server;
+import vibe.inet.message;
+import vibe.inet.mimetypes;
+import vibe.inet.url;
+import vibe.internal.interfaceproxy;
+
+import std.ascii : isWhite;
+import std.algorithm;
+import std.conv;
+import std.datetime;
+import std.digest.md;
+import std.exception;
+import std.range : popFront, empty, drop;
+import std.string;
+import std.typecons : Flag, Yes, No;
+
+@safe:
+
+
+/**
+	Returns a request handler that serves files from the specified directory.
+
+	See `sendFile` for more information.
+
+	Params:
+		local_path = Path to the folder to serve files from.
+		settings = Optional settings object enabling customization of how
+			the files get served.
+
+	Returns:
+		A request delegate is returned, which is suitable for registering in
+		a `URLRouter` or for passing to `listenHTTP`.
+
+	See_Also: `serveStaticFile`, `sendFile`
+*/
+HTTPServerRequestDelegateS serveStaticFiles(NativePath local_path, HTTPFileServerSettings settings = null)
+{
+	import std.range.primitives : front;
+	if (!settings) settings = new HTTPFileServerSettings;
+	if (!settings.serverPathPrefix.endsWith("/")) settings.serverPathPrefix ~= "/";
+
+	void callback(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	@safe {
+		string srv_path;
+		if (auto pp = "pathMatch" in req.params) srv_path = *pp;
+		else if (req.requestPath != InetPath.init) srv_path = (cast(PosixPath)req.requestPath).toString();
+		else srv_path = req.requestURL;
+
+		if (!srv_path.startsWith(settings.serverPathPrefix)) {
+			logDebug("path '%s' not starting with '%s'", srv_path, settings.serverPathPrefix);
+			return;
+		}
+
+		auto rel_path = srv_path[settings.serverPathPrefix.length .. $];
+		auto rpath = PosixPath(rel_path);
+		logTrace("Processing '%s'", srv_path);
+
+		rpath.normalize();
+		logDebug("Path '%s' -> '%s'", rel_path, rpath.toNativeString());
+		if (rpath.absolute) {
+			logDebug("Path is absolute, not responding");
+			return;
+		} else if (!rpath.empty && rpath.bySegment.front.name == "..")
+			return; // don't respond to relative paths outside of the root path
+
+		sendFileImpl(req, res, local_path ~ rpath, settings);
+	}
+
+	return &callback;
+}
+/// ditto
+HTTPServerRequestDelegateS serveStaticFiles(string local_path, HTTPFileServerSettings settings = null)
+{
+	return serveStaticFiles(NativePath(local_path), settings);
+}
+
+///
+unittest {
+	import vibe.http.fileserver;
+	import vibe.http.router;
+	import vibe.http.server;
+
+	void setupServer()
+	{
+		auto router = new URLRouter;
+		// add other routes here
+		router.get("*", serveStaticFiles("public/"));
+
+		auto settings = new HTTPServerSettings;
+		listenHTTP(settings, router);
+	}
+}
+
+/** This example serves all files in the "public" sub directory
+	with an added prefix "static/" so that they don't interfere
+	with other registered routes.
+*/
+unittest {
+	import vibe.http.fileserver;
+	import vibe.http.router;
+	import vibe.http.server;
+
+	void setupRoutes()
+	{
+	 	auto router = new URLRouter;
+		// add other routes here
+
+		auto fsettings = new HTTPFileServerSettings;
+		fsettings.serverPathPrefix = "/static";
+		router.get("/static/*", serveStaticFiles("public/", fsettings));
+
+		auto settings = new HTTPServerSettings;
+		listenHTTP(settings, router);
+	}
+}
+
+
+/**
+	Returns a request handler that serves a specific file on disk.
+
+	See `sendFile` for more information.
+
+	Params:
+		local_path = Path to the file to serve.
+		settings = Optional settings object enabling customization of how
+			the file gets served.
+
+	Returns:
+		A request delegate is returned, which is suitable for registering in
+		a `URLRouter` or for passing to `listenHTTP`.
+
+	See_Also: `serveStaticFiles`, `sendFile`
+*/
+HTTPServerRequestDelegateS serveStaticFile(NativePath local_path, HTTPFileServerSettings settings = null)
+{
+	if (!settings) settings = new HTTPFileServerSettings;
+	assert(settings.serverPathPrefix == "/", "serverPathPrefix is not supported for single file serving.");
+
+	void callback(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	{
+		sendFileImpl(req, res, local_path, settings);
+	}
+
+	return &callback;
+}
+/// ditto
+HTTPServerRequestDelegateS serveStaticFile(string local_path, HTTPFileServerSettings settings = null)
+{
+	return serveStaticFile(NativePath(local_path), settings);
+}
+
+
+/**
+	Sends a file to the given HTTP server response object.
+
+	When serving a file, certain request headers are supported to avoid sending
+	the file if the client has it already cached. These headers are
+	`"If-Modified-Since"` and `"If-None-Match"`. The client will be delivered
+	with the necessary `"Etag"` (generated from size and last modification time
+	of the file) and `"Last-Modified"` headers.
+
+	The cache control directives `"Expires"` and/or `"Cache-Control"` will also be
+	emitted if the `HTTPFileServerSettings.maxAge` field is set to a positive
+	duration and/or `HTTPFileServerSettings.cacheControl` has been set.
+
+	Finally, HEAD requests will automatically be handled without reading the
+	actual file contents. Am empty response body is written instead.
+
+	Params:
+		req = The incoming HTTP request - cache and modification headers of the
+			request can influence the generated response.
+		res = The response object to write to.
+		path = Path to the file to be sent.
+		settings = Optional settings object enabling customization of how the
+			file gets served.
+*/
+void sendFile(scope HTTPServerRequest req, scope HTTPServerResponse res, NativePath path, HTTPFileServerSettings settings = null)
+{
+	static HTTPFileServerSettings default_settings;
+	if (!settings) {
+		if (!default_settings) default_settings = new HTTPFileServerSettings;
+		settings = default_settings;
+	}
+
+	sendFileImpl(req, res, path, settings);
+}
+
+
+/**
+	Configuration options for the static file server.
+*/
+class HTTPFileServerSettings {
+	/// Prefix of the request path to strip before looking up files
+	string serverPathPrefix = "/";
+
+	/// Maximum cache age to report to the client (zero by default)
+	Duration maxAge = 0.seconds;
+
+	/** Cache control to control where cache can be saved, if at all, such as
+		proxies, the storage, etc.
+
+		Leave null or empty to not emit any cache control directives other than
+		max-age if maxAge is set.
+
+		Common values include: public for making a shared resource cachable across
+		multiple users or private for a response that should only be cached for a
+		single user.
+
+		See https://developer.mozilla.org/de/docs/Web/HTTP/Headers/Cache-Control
+	*/
+	string cacheControl = null;
+
+	/// General options
+	HTTPFileServerOption options = HTTPFileServerOption.defaults; /// additional options
+
+	/** Maps from encoding scheme (e.g. "gzip") to file extension.
+
+		If a request accepts a supported encoding scheme, then the file server
+		will look for a file with the extension as a suffix and, if that exists,
+		sends it as the encoded representation instead of sending the original
+		file.
+
+		Example:
+			---
+			settings.encodingFileExtension["gzip"] = ".gz";
+			---
+	*/
+	string[string] encodingFileExtension;
+
+	/**
+		Called just before headers and data are sent.
+		Allows headers to be customized, or other custom processing to be performed.
+
+		Note: Any changes you make to the response, physicalPath, or anything
+		else during this function will NOT be verified by Vibe.d for correctness.
+		Make sure any alterations you make are complete and correct according to HTTP spec.
+	*/
+	void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res, ref string physicalPath) preWriteCallback = null;
+
+	this()
+	{
+	}
+
+	this(string path_prefix)
+	{
+		this();
+		serverPathPrefix = path_prefix;
+	}
+}
+
+
+/**
+   Additional options for the static file server.
+ */
+enum HTTPFileServerOption {
+	none = 0,
+	/// respond with 404 if a file was not found
+	failIfNotFound = 1 << 0,
+	/// serve index.html for directories
+	serveIndexHTML = 1 << 1,
+	/// default options are serveIndexHTML
+	defaults = serveIndexHTML,
+}
+
+
+private void sendFileImpl(scope HTTPServerRequest req, scope HTTPServerResponse res, NativePath path, HTTPFileServerSettings settings = null)
+{
+	auto pathstr = path.toNativeString();
+
+	// return if the file does not exist
+	if (!existsFile(pathstr)){
+		if (settings.options & HTTPFileServerOption.failIfNotFound)
+			throw new HTTPStatusException(HTTPStatus.notFound);
+		return;
+	}
+
+	FileInfo dirent;
+	try dirent = getFileInfo(pathstr);
+	catch(Exception){
+		throw new HTTPStatusException(HTTPStatus.internalServerError, "Failed to get information for the file due to a file system error.");
+	}
+
+	if (dirent.isDirectory) {
+		if (settings.options & HTTPFileServerOption.serveIndexHTML)
+			return sendFileImpl(req, res, path ~ "index.html", settings);
+		logDebugV("Hit directory when serving files, ignoring: %s", pathstr);
+		if (settings.options & HTTPFileServerOption.failIfNotFound)
+			throw new HTTPStatusException(HTTPStatus.notFound);
+		return;
+	}
+
+	if (handleCacheFile(req, res, dirent, settings.cacheControl, settings.maxAge)) {
+		return;
+	}
+
+	auto mimetype = res.headers.get("Content-Type", getMimeTypeForFile(pathstr));
+
+	// avoid double-compression
+	if ("Content-Encoding" in res.headers && isCompressedFormat(mimetype))
+		res.headers.remove("Content-Encoding");
+
+	if (!("Content-Type" in res.headers))
+		res.headers["Content-Type"] = mimetype;
+
+	res.headers.addField("Accept-Ranges", "bytes");
+	RangeSpec range;
+	if (auto prange = "Range" in req.headers) {
+		range = parseRangeHeader(*prange, dirent.size, res);
+
+		// potential integer overflow with rangeEnd - rangeStart == size_t.max is intended. This only happens with empty files, the + 1 will then put it back to 0
+		res.headers["Content-Length"] = to!string(range.max - range.min);
+		res.headers["Content-Range"] = "bytes %s-%s/%s".format(range.min, range.max - 1, dirent.size);
+		res.statusCode = HTTPStatus.partialContent;
+	} else res.headers["Content-Length"] = dirent.size.to!string;
+
+	// check for already encoded file if configured
+	string encodedFilepath;
+	auto pce = "Content-Encoding" in res.headers;
+	if (pce) {
+		if (auto pfe = *pce in settings.encodingFileExtension) {
+			assert((*pfe).length > 0);
+			auto p = pathstr ~ *pfe;
+			if (existsFile(p))
+				encodedFilepath = p;
+		}
+	}
+	if (encodedFilepath.length) {
+		auto origLastModified = dirent.timeModified.toUTC();
+
+		try dirent = getFileInfo(encodedFilepath);
+		catch(Exception){
+			throw new HTTPStatusException(HTTPStatus.internalServerError, "Failed to get information for the file due to a file system error.");
+		}
+
+		// encoded file must be younger than original else warn
+		if (dirent.timeModified.toUTC() >= origLastModified){
+			logTrace("Using already encoded file '%s' -> '%s'", path, encodedFilepath);
+			path = NativePath(encodedFilepath);
+			res.headers["Content-Length"] = to!string(dirent.size);
+		} else {
+			logWarn("Encoded file '%s' is older than the original '%s'. Ignoring it.", encodedFilepath, path);
+			encodedFilepath = null;
+		}
+	}
+
+	if(settings.preWriteCallback)
+		settings.preWriteCallback(req, res, pathstr);
+
+	// for HEAD responses, stop here
+	if( res.isHeadResponse() ){
+		res.writeVoidBody();
+		assert(res.headerWritten);
+		logDebug("sent file header %d, %s!", dirent.size, res.headers["Content-Type"]);
+		return;
+	}
+
+	// else write out the file contents
+	//logTrace("Open file '%s' -> '%s'", srv_path, pathstr);
+	FileStream fil;
+	try {
+		fil = openFile(path);
+	} catch( Exception e ){
+		// TODO: handle non-existant files differently than locked files?
+		logDebug("Failed to open file %s: %s", pathstr, () @trusted { return e.toString(); } ());
+		return;
+	}
+	scope(exit) fil.close();
+
+	if (range.max > range.min) {
+		fil.seek(range.min);
+		fil.pipe(res.bodyWriter, range.max - range.min);
+		logTrace("partially sent file %d-%d, %s!", range.min, range.max - 1, res.headers["Content-Type"]);
+	} else {
+		if (pce && !encodedFilepath.length)
+			fil.pipe(res.bodyWriter);
+		else res.writeRawBody(fil);
+		logTrace("sent file %d, %s!", fil.size, res.headers["Content-Type"]);
+	}
+}
+
+/**
+	Calls $(D handleCache) with prefilled etag and lastModified value based on a file.
+
+	See_Also: handleCache
+
+	Returns: $(D true) if the cache was already handled and no further response must be sent or $(D false) if a response must be sent.
+*/
+bool handleCacheFile(scope HTTPServerRequest req, scope HTTPServerResponse res,
+		string file, string cache_control = null, Duration max_age = Duration.zero)
+{
+	return handleCacheFile(req, res, NativePath(file), cache_control, max_age);
+}
+
+/// ditto
+bool handleCacheFile(scope HTTPServerRequest req, scope HTTPServerResponse res,
+		NativePath file, string cache_control = null, Duration max_age = Duration.zero)
+{
+	if (!existsFile(file)) {
+		return false;
+	}
+
+	FileInfo ent;
+	try {
+		ent = getFileInfo(file);
+	} catch (Exception) {
+		throw new HTTPStatusException(HTTPStatus.internalServerError,
+				"Failed to get information for the file due to a file system error.");
+	}
+
+	return handleCacheFile(req, res, ent, cache_control, max_age);
+}
+
+/// ditto
+bool handleCacheFile(scope HTTPServerRequest req, scope HTTPServerResponse res,
+		FileInfo dirent, string cache_control = null, Duration max_age = Duration.zero)
+{
+	import std.bitmanip : nativeToLittleEndian;
+	import std.digest.md : MD5, toHexString;
+
+	SysTime lastModified = dirent.timeModified;
+	const weak = cast(Flag!"weak") dirent.isDirectory;
+	auto etag = ETag.md5(weak, lastModified.stdTime.nativeToLittleEndian, dirent.size.nativeToLittleEndian);
+
+	return handleCache(req, res, etag, lastModified, cache_control, max_age);
+}
+
+/**
+	Processes header tags in a request and writes responses given on requested cache status.
+
+	Params:
+		req = the client request used to determine cache control flow.
+		res = the response to write cache headers to.
+		etag = if set to anything except .init, adds a Etag header to the response and enables handling of If-Match and If-None-Match cache control request headers.
+		last_modified = if set to anything except .init, adds a Last-Modified header to the response and enables handling of If-Modified-Since and If-Unmodified-Since cache control request headers.
+		cache_control = if set, adds or modifies the Cache-Control header in the response to this string. Might get an additional max-age value appended if max_age is set.
+		max_age = optional duration to set the Expires header and Cache-Control max-age part to. (if no existing `max-age=` part is given in the cache_control parameter)
+
+	Returns: $(D true) if the cache was already handled and no further response must be sent or $(D false) if a response must be sent.
+*/
+bool handleCache(scope HTTPServerRequest req, scope HTTPServerResponse res, ETag etag,
+		SysTime last_modified, string cache_control = null, Duration max_age = Duration.zero)
+{
+	// https://tools.ietf.org/html/rfc7232#section-4.1
+	// and
+	// https://tools.ietf.org/html/rfc7232#section-6
+	string lastModifiedString;
+	if (last_modified != SysTime.init) {
+		lastModifiedString = toRFC822DateTimeString(last_modified);
+		res.headers["Last-Modified"] = lastModifiedString;
+	}
+
+	if (etag != ETag.init) {
+		res.headers["Etag"] = etag.toString;
+	}
+
+	if (max_age > Duration.zero) {
+		res.headers["Expires"] = toRFC822DateTimeString(Clock.currTime(UTC()) + max_age);
+	}
+
+	if (cache_control.length) {
+		if (max_age > Duration.zero && !cache_control.canFind("max-age=")) {
+			res.headers["Cache-Control"] = cache_control
+				~ ", max-age=" ~ to!string(max_age.total!"seconds");
+		} else {
+			res.headers["Cache-Control"] = cache_control;
+		}
+	} else if (max_age > Duration.zero) {
+		res.headers["Cache-Control"] = text("max-age=", max_age.total!"seconds");
+	}
+
+	// https://tools.ietf.org/html/rfc7232#section-3.1
+	string ifMatch = req.headers.get("If-Match");
+	if (ifMatch.length) {
+		if (!cacheMatch(ifMatch, etag, No.allowWeak)) {
+			res.statusCode = HTTPStatus.preconditionFailed;
+			res.writeVoidBody();
+			return true;
+		}
+	}
+	else if (last_modified != SysTime.init) {
+		// https://tools.ietf.org/html/rfc7232#section-3.4
+		string ifUnmodifiedSince = req.headers.get("If-Unmodified-Since");
+		if (ifUnmodifiedSince.length) {
+			const check = lastModifiedString != ifUnmodifiedSince
+				|| last_modified > parseRFC822DateTimeString(ifUnmodifiedSince);
+			if (check) {
+				res.statusCode = HTTPStatus.preconditionFailed;
+				res.writeVoidBody();
+				return true;
+			}
+		}
+	}
+
+	// https://tools.ietf.org/html/rfc7232#section-3.2
+	string ifNoneMatch = req.headers.get("If-None-Match");
+	if (ifNoneMatch.length) {
+		if (cacheMatch(ifNoneMatch, etag, Yes.allowWeak)) {
+			if (req.method.among!(HTTPMethod.GET, HTTPMethod.HEAD))
+				res.statusCode = HTTPStatus.notModified;
+			else
+				res.statusCode = HTTPStatus.preconditionFailed;
+			res.writeVoidBody();
+			return true;
+		}
+	}
+	else if (last_modified != SysTime.init && req.method.among!(HTTPMethod.GET, HTTPMethod.HEAD)) {
+		// https://tools.ietf.org/html/rfc7232#section-3.3
+		string ifModifiedSince = req.headers.get("If-Modified-Since");
+		if (ifModifiedSince.length) {
+			const check = lastModifiedString == ifModifiedSince ||
+				last_modified <= parseRFC822DateTimeString(ifModifiedSince);
+			if (check) {
+				res.statusCode = HTTPStatus.notModified;
+				res.writeVoidBody();
+				return true;
+			}
+		}
+	}
+
+	// TODO: support If-Range here
+
+	return false;
+}
+
+/**
+	Represents an Entity-Tag value for use inside HTTP Cache headers.
+
+	Standards: https://tools.ietf.org/html/rfc7232#section-2.3
+*/
+struct ETag
+{
+	bool weak;
+	string tag;
+
+	static ETag parse(string s)
+	{
+		enforce!ConvException(s.endsWith('"'));
+
+		if (s.startsWith(`W/"`)) {
+			ETag ret = { weak: true, tag: s[3 .. $ - 1] };
+			return ret;
+		} else if (s.startsWith('"')) {
+			ETag ret;
+			ret.tag = s[1 .. $ - 1];
+			return ret;
+		} else {
+			throw new ConvException(`ETag didn't start with W/" nor with " !`);
+		}
+	}
+
+	string toString() const @property
+	{
+		return text(weak ? `W/"` : `"`, tag, '"');
+	}
+
+	/**
+		Encodes the bytes with URL Base64 to a human readable string and returns an ETag struct wrapping it.
+	 */
+	static ETag fromBytesBase64URLNoPadding(scope const(ubyte)[] bytes, Flag!"weak" weak = No.weak)
+	{
+		import std.base64 : Base64URLNoPadding;
+
+		return ETag(weak, Base64URLNoPadding.encode(bytes).idup);
+	}
+
+	/**
+		Hashes the input bytes with md5 and returns an URL Base64 encoded representation as ETag.
+	 */
+	static ETag md5(T...)(Flag!"weak" weak, T data)
+	{
+		import std.digest.md : md5Of;
+
+		return fromBytesBase64URLNoPadding(md5Of(data), weak);
+	}
+}
+
+/**
+	Matches a given match expression with a specific ETag. Can allow or disallow weak ETags and supports multiple tags.
+
+	Standards: https://tools.ietf.org/html/rfc7232#section-2.3.2
+*/
+bool cacheMatch(string match, ETag etag, Flag!"allowWeak" allow_weak)
+{
+	if (match == "*") {
+		return true;
+	}
+
+	if ((etag.weak && !allow_weak) || !match.length) {
+		return false;
+	}
+
+	auto allBytes = match.representation;
+	auto range = allBytes;
+
+	while (!range.empty)
+	{
+		range = range.stripLeft!isWhite;
+		bool isWeak = range.skipOver("W/");
+		if (!range.skipOver('"'))
+			return false; // malformed
+
+		auto end = range.countUntil('"');
+		if (end == -1)
+			return false; // malformed
+
+		const check = range[0 .. end];
+		range = range[end .. $];
+
+		if (allow_weak || !isWeak) {
+			if (check == etag.tag) {
+				return true;
+			}
+		}
+
+		range.skipOver('"');
+		range = range.stripLeft!isWhite;
+
+		if (!range.skipOver(","))
+			return false; // malformed
+	}
+
+	return false;
+}
+
+unittest
+{
+	// from RFC 7232 Section 2.3.2
+	// +--------+--------+-------------------+-----------------+
+	// | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison |
+	// +--------+--------+-------------------+-----------------+
+	// | W/"1"  | W/"1"  | no match          | match           |
+	// | W/"1"  | W/"2"  | no match          | no match        |
+	// | W/"1"  | "1"    | no match          | match           |
+	// | "1"    | "1"    | match             | match           |
+	// +--------+--------+-------------------+-----------------+
+
+	assert(!cacheMatch(`W/"1"`, ETag(Yes.weak, "1"), No.allowWeak));
+	assert( cacheMatch(`W/"1"`, ETag(Yes.weak, "1"), Yes.allowWeak));
+
+	assert(!cacheMatch(`W/"1"`, ETag(Yes.weak, "2"), No.allowWeak));
+	assert(!cacheMatch(`W/"1"`, ETag(Yes.weak, "2"), Yes.allowWeak));
+
+	assert(!cacheMatch(`W/"1"`, ETag(No.weak, "1"), No.allowWeak));
+	assert( cacheMatch(`W/"1"`, ETag(No.weak, "1"), Yes.allowWeak));
+
+	assert(cacheMatch(`"1"`, ETag(No.weak, "1"), No.allowWeak));
+	assert(cacheMatch(`"1"`, ETag(No.weak, "1"), Yes.allowWeak));
+
+	assert(cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "xyzzy"), No.allowWeak));
+	assert(cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "xyzzy"), Yes.allowWeak));
+
+	assert(!cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "xyzzz"), No.allowWeak));
+	assert(!cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "xyzzz"), Yes.allowWeak));
+
+	assert(cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "r2d2xxxx"), No.allowWeak));
+	assert(cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "r2d2xxxx"), Yes.allowWeak));
+
+	assert(cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "c3piozzzz"), No.allowWeak));
+	assert(cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "c3piozzzz"), Yes.allowWeak));
+
+	assert(!cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, ""), No.allowWeak));
+	assert(!cacheMatch(`"xyzzy","r2d2xxxx", "c3piozzzz"`, ETag(No.weak, ""), Yes.allowWeak));
+
+	assert(!cacheMatch(`"xyzzy",W/"r2d2xxxx", "c3piozzzz"`, ETag(Yes.weak, "r2d2xxxx"), No.allowWeak));
+	assert( cacheMatch(`"xyzzy",W/"r2d2xxxx", "c3piozzzz"`, ETag(Yes.weak, "r2d2xxxx"), Yes.allowWeak));
+	assert(!cacheMatch(`"xyzzy",W/"r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "r2d2xxxx"), No.allowWeak));
+	assert( cacheMatch(`"xyzzy",W/"r2d2xxxx", "c3piozzzz"`, ETag(No.weak, "r2d2xxxx"), Yes.allowWeak));
+}
+
+private RangeSpec parseRangeHeader(string range_spec, ulong file_size, scope HTTPServerResponse res)
+{
+	RangeSpec ret;
+
+	auto range = range_spec.chompPrefix("bytes=");
+	if (range.canFind(','))
+		throw new HTTPStatusException(HTTPStatus.notImplemented);
+	auto s = range.split("-");
+	if (s.length != 2)
+		throw new HTTPStatusException(HTTPStatus.badRequest);
+
+	// https://tools.ietf.org/html/rfc7233
+	// Range can be in form "-\d", "\d-" or "\d-\d"
+	try {
+		if (s[0].length) {
+			ret.min = s[0].to!ulong;
+			ret.max = s[1].length ? s[1].to!ulong + 1 : file_size;
+		} else if (s[1].length) {
+			ret.min = file_size - min(s[1].to!ulong, file_size);
+			ret.max = file_size;
+		} else {
+			throw new HTTPStatusException(HTTPStatus.badRequest);
+		}
+	} catch (ConvException) {
+		throw new HTTPStatusException(HTTPStatus.badRequest);
+	}
+
+	if (ret.max > file_size) ret.max = file_size;
+
+	if (ret.min >= ret.max) {
+		res.headers["Content-Range"] = "bytes */%s".format(file_size);
+		throw new HTTPStatusException(HTTPStatus.rangeNotSatisfiable);
+	}
+
+	return ret;
+}
+
+unittest {
+	auto res = createTestHTTPServerResponse();
+	assertThrown(parseRangeHeader("bytes=2-1", 10, res));
+	assertThrown(parseRangeHeader("bytes=10-10", 10, res));
+	assertThrown(parseRangeHeader("bytes=0-0", 0, res));
+	assert(parseRangeHeader("bytes=10-20", 100, res) == RangeSpec(10, 21));
+	assert(parseRangeHeader("bytes=0-0", 1, res) == RangeSpec(0, 1));
+	assert(parseRangeHeader("bytes=0-20", 2, res) == RangeSpec(0, 2));
+	assert(parseRangeHeader("bytes=1-20", 2, res) == RangeSpec(1, 2));
+}
+
+private struct RangeSpec {
+	ulong min, max;
+}

+ 169 - 0
vtest/source/vibe/http/form.d

@@ -0,0 +1,169 @@
+/**
+	Convenience functions for working with web forms.
+
+	Copyright: © 2012-2015 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig, Jan Krüger
+*/
+module vibe.http.form;
+
+public import vibe.inet.webform;
+
+import vibe.http.client : HTTPClientRequest; // for writeFormBody
+import vibe.http.server;
+
+import std.array;
+import std.conv;
+import std.range;
+import std.string;
+import std.typecons : isTuple;
+
+
+/**
+	Encodes the given dictionary as URL encoded form data.
+*/
+void writeFormData(R)(R dst, in string[string] data)
+	if (isOutputRange!(R, char))
+{
+	import vibe.textfilter.urlencode;
+
+	bool first = true;
+	foreach (k, v; data) {
+		if (first) first = false;
+		else dst.put("&");
+		filterURLEncode(dst, k);
+		dst.put("=");
+		filterURLEncode(dst, v);
+	}
+}
+
+///
+unittest {
+	import std.array;
+	import vibe.core.log;
+	import vibe.http.form;
+
+	void test()
+	{
+		auto dst = appender!string();
+		dst.writeFormData(["field1": "value1", "field2": "value2"]);
+		logInfo("Form data: %s", dst.data);
+	}
+}
+
+/**
+	Encodes the given ranges of `Tuple!(string, string)` as URL encoded form data
+*/
+void writeFormData(R, PairRange)(R dst, PairRange pr)
+	if (isOutputRange!(R, char) && isTuple!(ElementType!PairRange) && ElementType!PairRange.length == 2)
+{
+	import vibe.textfilter.urlencode;
+
+   if(pr.empty) return;
+
+   auto fst = pr.front;
+   pr.popFront();
+
+   filterURLEncode(dst, fst[0]);
+   dst.put("=");
+   filterURLEncode(dst, fst[1]);
+
+	foreach (pair; pr) {
+		dst.put("&");
+		filterURLEncode(dst, pair[0]);
+		dst.put("=");
+		filterURLEncode(dst, pair[1]);
+	}
+}
+
+/**
+	Writes a `vibe.http.client.HTTPClientRequest` body as URL encoded form data.
+*/
+void writeFormBody(HTTPClientRequest req, in string[string] form)
+{
+	import vibe.http.form;
+	import vibe.stream.wrapper;
+
+	StringLengthCountingRange len;
+	writeFormData(&len, form);
+	req.contentType = "application/x-www-form-urlencoded";
+	req.contentLength = len.count;
+	auto rng = streamOutputRange(req.bodyWriter);
+	writeFormData(&rng, form);
+}
+
+///
+unittest {
+	import vibe.core.log;
+	import vibe.http.client;
+	import vibe.http.form;
+	import vibe.stream.operations;
+
+	void sendForm()
+	{
+		requestHTTP("http://example.com/form",
+			(scope req) {
+				req.method = HTTPMethod.POST;
+				req.writeFormBody(["field1": "value1", "field2": "value2"]);
+			},
+			(scope res) {
+				logInfo("Response: %s", res.bodyReader.readAllUTF8());
+			});
+	}
+}
+
+/**
+	Writes a `vibe.http.client.HTTPClientRequest` body as URL encoded form data.
+
+	Params:
+	  req  = Request object to write to.
+	  form = range of `t = Tuple!(string, string)`,
+			 where `t[0]` is the name and `t[1]` the
+			 value of a form entry.
+*/
+void writeFormBody(PairRange)(HTTPClientRequest req, PairRange form)
+   if(isTuple!(ElementType!PairRange) && ElementType!PairRange.length == 2)
+{
+	import vibe.http.form;
+	import vibe.stream.wrapper;
+
+	StringLengthCountingRange len;
+	writeFormData(&len, form.save);
+	req.contentType = "application/x-www-form-urlencoded";
+	req.contentLength = len.count;
+	auto rng = streamOutputRange(req.bodyWriter);
+	writeFormData(&rng, form);
+}
+
+///
+unittest {
+	import vibe.core.log;
+	import vibe.http.client;
+	import vibe.http.form;
+	import vibe.stream.operations;
+	import std.range;
+
+	void sendForm()
+	{
+		string[] names = ["foo", "bar", "baz"];
+		string[] values = ["1", "2", "3"];
+		auto form = zip(names, values);
+		requestHTTP("http://example.com/form",
+			(scope req) {
+				req.method = HTTPMethod.POST;
+				req.writeFormBody(form);
+			},
+			(scope res) {
+				logInfo("Response: %s", res.bodyReader.readAllUTF8());
+			});
+	}
+}
+
+
+/// private
+struct StringLengthCountingRange {
+	import std.utf;
+	size_t count = 0;
+	void put(string str) { count += str.length; }
+	void put(dchar ch) { count += codeLength!char(ch); }
+}

+ 21 - 0
vtest/source/vibe/http/internal/basic_auth_client.d

@@ -0,0 +1,21 @@
+/**
+	Implements HTTP Basic Auth for client.
+
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+*/
+module vibe.http.internal.basic_auth_client;
+
+import vibe.http.common;
+import std.base64;
+
+@safe:
+
+/**
+	Augments the given HTTP request with an HTTP Basic Auth header.
+*/
+void addBasicAuth(scope HTTPRequest req, string user, string password)
+{
+	string pwstr = user ~ ":" ~ password;
+	string authstr = () @trusted { return cast(string)Base64.encode(cast(ubyte[])pwstr); } ();
+	req.headers["Authorization"] = "Basic " ~ authstr;
+}

+ 665 - 0
vtest/source/vibe/http/internal/http1/server.d

@@ -0,0 +1,665 @@
+/**
+	A HTTP 1.1/1.0 server implementation.
+
+	Copyright: © 2012-2024 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig, Jan Krüger, Ilya Shipunov
+*/
+module vibe.http.internal.http1.server;
+
+import vibe.container.internal.appender : FixedAppender;
+import vibe.core.log;
+import vibe.core.net;
+import vibe.core.stream;
+import vibe.http.common;
+import vibe.http.server;
+import vibe.inet.message;
+import vibe.inet.url;
+import vibe.internal.freelistref;
+import vibe.internal.string : formatAlloc, icmp2;
+
+import core.time;
+import std.datetime : Clock, SysTime, UTC;
+import std.encoding : sanitize;
+import std.exception : enforce;
+import std.format : format, formattedWrite;
+
+
+/** Treats an existing connection as an HTTP connection and processes incoming
+	requests.
+
+	After all requests have been processed, the connection will be closed and
+	the function returns to the caller.
+
+	Params:
+		connection = The stream to treat as an incoming HTTP client connection.
+		context = Information about the incoming listener and available
+			virtual hosts
+*/
+void handleHTTP1Connection(TLSStreamType)(TCPConnection connection, TLSStreamType tls_stream, StreamProxy http_stream, HTTPServerContext context)
+@safe {
+
+	while (!connection.empty) {
+		HTTPServerSettings settings;
+		bool keep_alive;
+
+		static if (HaveNoTLS) {} else {
+			// handle oderly TLS shutdowns
+			if (tls_stream && tls_stream.empty) break;
+		}
+
+		() @trusted {
+			scope request_allocator = createRequestAllocator();
+			scope (exit) freeRequestAllocator(request_allocator);
+
+			handleRequest!TLSStreamType(http_stream, connection, context, settings, keep_alive, request_allocator);
+		} ();
+		if (!keep_alive) { logTrace("No keep-alive - disconnecting client."); break; }
+
+		logTrace("Waiting for next request...");
+		// wait for another possible request on a keep-alive connection
+		if (!connection.waitForData(settings.keepAliveTimeout)) {
+			if (!connection.connected) logTrace("Client disconnected.");
+			else logDebug("Keep-alive connection timed out!");
+			break;
+		}
+	}
+}
+
+
+private bool handleRequest(TLSStreamType, Allocator)(StreamProxy http_stream, TCPConnection tcp_connection, HTTPServerContext listen_info, ref HTTPServerSettings settings, ref bool keep_alive, scope Allocator request_allocator)
+@safe {
+	import vibe.container.internal.utilallocator : make, dispose;
+	import vibe.http.internal.utils : formatRFC822DateAlloc;
+	import std.algorithm.searching : canFind, startsWith;
+	import std.conv : parse, to;
+	import std.string : indexOf;
+	import vibe.core.file : existsFile, removeFile;
+
+	SysTime reqtime = Clock.currTime(UTC());
+
+	// some instances that live only while the request is running
+	FreeListRef!HTTPServerRequest req = FreeListRef!HTTPServerRequest(reqtime, listen_info.bindPort);
+	FreeListRef!TimeoutHTTPInputStream timeout_http_input_stream;
+	FreeListRef!LimitedHTTPInputStream limited_http_input_stream;
+	FreeListRef!ChunkedInputStream chunked_input_stream;
+
+	// store the IP address
+	req.clientAddress = tcp_connection.remoteAddress;
+
+	if (!listen_info.hasVirtualHosts) {
+		logWarn("Didn't find a HTTP listening context for incoming connection. Dropping.");
+		keep_alive = false;
+		return false;
+	}
+
+	// Default to the first virtual host for this listener
+	HTTPServerContext.VirtualHost context = listen_info.m_virtualHosts[0];
+	HTTPServerRequestDelegate request_task = context.requestHandler;
+	settings = context.settings;
+
+	// temporarily set to the default settings, the virtual host specific settings will be set further down
+	req.m_settings = settings;
+
+	// Create the response object
+	ConnectionStreamProxy cproxy = tcp_connection;
+	auto exchange = () @trusted { return request_allocator.make!HTTP1ServerExchange(http_stream, cproxy); } ();
+	scope (exit) () @trusted { request_allocator.dispose(exchange); } ();
+	auto res = FreeListRef!HTTPServerResponse(exchange, settings, request_allocator/*.Scoped_payload*/);
+	req.tls = res.m_tls = listen_info.tlsContext !is null;
+	if (req.tls) {
+		static if (HaveNoTLS) assert(false);
+		else {
+			static if (is(InterfaceProxy!ConnectionStream == ConnectionStream))
+				req.clientCertificate = (cast(TLSStream)http_stream).peerCertificate;
+			else
+				req.clientCertificate = http_stream.extract!TLSStreamType.peerCertificate;
+		}
+	}
+
+	// Error page handler
+	void errorOut(int code, string msg, string debug_msg, Throwable ex)
+	@safe {
+		assert(!res.headerWritten);
+
+		res.statusCode = code;
+		if (settings && settings.errorPageHandler) {
+			/*scope*/ auto err = new HTTPServerErrorInfo;
+			err.code = code;
+			err.message = msg;
+			err.debugMessage = debug_msg;
+			err.exception = ex;
+			settings.errorPageHandler_(req, res, err);
+		} else {
+			if (debug_msg.length)
+				res.writeBody(format("%s - %s\n\n%s\n\nInternal error information:\n%s", code, httpStatusText(code), msg, debug_msg));
+			else res.writeBody(format("%s - %s\n\n%s", code, httpStatusText(code), msg));
+		}
+		assert(res.headerWritten);
+	}
+
+	bool parsed = false;
+	/*bool*/ keep_alive = false;
+
+	// parse the request
+	try {
+		logTrace("reading request..");
+
+		// limit the total request time
+		InputStreamProxy reqReader = http_stream;
+		if (settings.maxRequestTime > dur!"seconds"(0) && settings.maxRequestTime != Duration.max) {
+			timeout_http_input_stream = FreeListRef!TimeoutHTTPInputStream(reqReader, settings.maxRequestTime, reqtime);
+			reqReader = timeout_http_input_stream;
+		}
+
+		// basic request parsing
+		uint h2 = parseRequestHeader(req, reqReader, request_allocator, settings.maxRequestHeaderSize, settings.maxRequestHeaderLineSize, !!(settings.options & HTTPServerOption.enableHTTP2));
+		if (h2) {
+			import vibe.http.internal.http2.server : handleHTTP2Connection;
+			import vibe.http.internal.http2.settings : HTTP2ServerContext, HTTP2Settings;
+
+			// start http/2 with prior knowledge
+			uint len = 22 - h2;
+			ubyte[] dummy; dummy.length = len;
+
+			http_stream.read(dummy); // finish reading connection preface
+			auto h2settings = HTTP2Settings();
+			auto h2context = new HTTP2ServerContext(listen_info, h2settings);
+			handleHTTP2Connection(tcp_connection, tcp_connection, h2context, true);
+			return true;
+		}
+
+		logTrace("Got request header.");
+
+		// find the matching virtual host
+		string reqhost;
+		ushort reqport = 0;
+		{
+			string s = req.host;
+			enforceHTTP(s.length > 0 || req.httpVersion <= HTTPVersion.HTTP_1_0, HTTPStatus.badRequest, "Missing Host header.");
+			if (s.startsWith('[')) { // IPv6 address
+				auto idx = s.indexOf(']');
+				enforce(idx > 0, "Missing closing ']' for IPv6 address.");
+				reqhost = s[1 .. idx];
+				s = s[idx+1 .. $];
+			} else if (s.length) { // host name or IPv4 address
+				auto idx = s.indexOf(':');
+				if (idx < 0) idx = s.length;
+				enforceHTTP(idx > 0, HTTPStatus.badRequest, "Missing Host header.");
+				reqhost = s[0 .. idx];
+				s = s[idx .. $];
+			}
+			if (s.startsWith(':')) reqport = s[1 .. $].to!ushort;
+		}
+
+		foreach (ctx; listen_info.m_virtualHosts)
+			if (icmp2(ctx.settings.hostName, reqhost) == 0 &&
+				(!reqport || reqport == ctx.settings.port))
+			{
+				context = ctx;
+				settings = ctx.settings;
+				request_task = ctx.requestHandler;
+				break;
+			}
+		req.m_settings = settings;
+		res.m_settings = settings;
+
+		// setup compressed output
+		if (settings.useCompressionIfPossible) {
+			if (auto pae = "Accept-Encoding" in req.headers) {
+				if (canFind(*pae, "gzip")) {
+					res.headers["Content-Encoding"] = "gzip";
+				} else if (canFind(*pae, "deflate")) {
+					res.headers["Content-Encoding"] = "deflate";
+				}
+			}
+		}
+
+		// limit request size
+		if (auto pcl = "Content-Length" in req.headers) {
+			string v = *pcl;
+			auto contentLength = parse!ulong(v); // DMDBUG: to! thinks there is a H in the string
+			enforceBadRequest(v.length == 0, "Invalid content-length");
+			enforceBadRequest(settings.maxRequestSize <= 0 || contentLength <= settings.maxRequestSize, "Request size too big");
+			limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, contentLength);
+		} else if (auto pt = "Transfer-Encoding" in req.headers) {
+			enforceBadRequest(icmp2(*pt, "chunked") == 0);
+			chunked_input_stream = createChunkedInputStreamFL(reqReader);
+			InputStreamProxy ciproxy = chunked_input_stream;
+			limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(ciproxy, settings.maxRequestSize, true);
+		} else {
+			limited_http_input_stream = FreeListRef!LimitedHTTPInputStream(reqReader, 0);
+		}
+		req.bodyReader = limited_http_input_stream;
+
+		// handle Expect header
+		if (auto pv = "Expect" in req.headers) {
+			if (icmp2(*pv, "100-continue") == 0) {
+				logTrace("sending 100 continue");
+				http_stream.write("HTTP/1.1 100 Continue\r\n\r\n");
+			}
+		}
+
+		// eagerly parse the URL as its lightweight and defacto @nogc
+		auto url = URL.parse(req.requestURI);
+		req.queryString = url.queryString;
+		req.username = url.username;
+		req.password = url.password;
+		req.requestPath = url.path;
+
+		// lookup the session
+		if (settings.sessionStore) {
+			// use the first cookie that contains a valid session ID in case
+			// of multiple matching session cookies
+			foreach (val; req.cookies.getAll(settings.sessionIdCookie)) {
+				req.session = settings.sessionStore.open(val);
+				res.m_session = req.session;
+				if (req.session) break;
+			}
+		}
+
+		// write default headers
+		if (req.method == HTTPMethod.HEAD) exchange.m_isHeadResponse = true;
+		if (settings.serverString.length)
+			res.headers["Server"] = settings.serverString;
+		res.headers["Date"] = formatRFC822DateAlloc(reqtime);
+		if (req.persistent)
+			res.headers["Keep-Alive"] = formatAlloc(
+				request_allocator, "timeout=%d", settings.keepAliveTimeout.total!"seconds"());
+
+		// finished parsing the request
+		parsed = true;
+		logTrace("persist: %s", req.persistent);
+		keep_alive = req.persistent;
+
+		if (context.settings.rejectConnectionPredicate !is null)
+		{
+			import std.socket : Address, parseAddress;
+
+			auto forward = req.headers.get("X-Forwarded-For", null);
+			if (forward !is null)
+			{
+				try {
+					auto ix = forward.indexOf(',');
+					if (ix != -1)
+						forward = forward[0 .. ix];
+					if (context.settings.rejectConnectionPredicate(NetworkAddress(parseAddress(forward))))
+						errorOut(HTTPStatus.forbidden,
+							httpStatusText(HTTPStatus.forbidden), null, null);
+				} catch (Exception e)
+					logTrace("Malformed X-Forwarded-For header: %s", e.msg);
+			}
+		}
+
+		// handle the request
+		logTrace("handle request (body %d)", req.bodyReader.leastSize);
+		res.httpVersion = req.httpVersion;
+		request_task(req, res);
+
+		// if no one has written anything, return 404
+		if (!res.headerWritten) {
+			string dbg_msg;
+			logDiagnostic("No response written for %s", req.requestURI);
+			if (settings.options & HTTPServerOption.errorStackTraces)
+				dbg_msg = format("No routes match path '%s'", req.requestURI);
+			errorOut(HTTPStatus.notFound, httpStatusText(HTTPStatus.notFound), dbg_msg, null);
+		}
+	} catch (HTTPStatusException err) {
+		if (!res.headerWritten) errorOut(err.status, err.msg, err.debugMessage, err);
+		else logDiagnostic("HTTPStatusException while writing the response: %s", err.msg);
+		debug logDebug("Exception while handling request %s %s: %s", req.method,
+					   req.requestURI, () @trusted { return err.toString().sanitize; } ());
+		if (!parsed || res.headerWritten || justifiesConnectionClose(err.status))
+			keep_alive = false;
+	} catch (UncaughtException e) {
+		auto status = parsed ? HTTPStatus.internalServerError : HTTPStatus.badRequest;
+		string dbg_msg;
+		if (settings.options & HTTPServerOption.errorStackTraces)
+			dbg_msg = () @trusted { return e.toString().sanitize; } ();
+		if (!res.headerWritten && tcp_connection.connected)
+			errorOut(status, httpStatusText(status), dbg_msg, e);
+		else logDiagnostic("Error while writing the response: %s", e.msg);
+		debug logDebug("Exception while handling request %s %s: %s", req.method,
+					   req.requestURI, () @trusted { return e.toString().sanitize(); } ());
+		if (!parsed || res.headerWritten || !cast(Exception)e) keep_alive = false;
+	}
+
+	if (tcp_connection.connected && keep_alive) {
+		if (req.bodyReader && !req.bodyReader.empty) {
+			req.bodyReader.pipe(nullSink);
+			logTrace("dropped body");
+		}
+	}
+
+	// finalize (e.g. for chunked encoding)
+	res.finalize();
+
+	if (exchange.m_requiresConnectionClose)
+		keep_alive = false;
+
+	// NOTE: req.m_files may or may not be parsed/filled with actual data, as
+	//       it is lazily initialized when calling the .files or .form
+	//       properties
+	foreach (k, v ; req.m_files.byKeyValue) {
+		if (existsFile(v.tempPath)) {
+			removeFile(v.tempPath);
+			logDebug("Deleted upload tempfile %s", v.tempPath.toString());
+		}
+	}
+
+	if (!req.noLog) {
+		// log the request to access log
+		foreach (log; context.loggers)
+			log.log(req, res);
+	}
+
+	//logTrace("return %s (used pool memory: %s/%s)", keep_alive, request_allocator.allocatedSize, request_allocator.totalSize);
+	logTrace("return %s", keep_alive);
+	return keep_alive != false;
+}
+
+
+private uint parseRequestHeader(InputStream, Allocator)(HTTPServerRequest req, InputStream http_stream, Allocator alloc, ulong max_header_size, size_t max_header_line_size, bool enable_http2)
+	if (isInputStream!InputStream)
+{
+	import std.string : indexOf;
+	import vibe.stream.operations : readLine;
+
+	auto stream = FreeListRef!LimitedHTTPInputStream(http_stream, max_header_size);
+
+	logTrace("HTTP server reading status line");
+	auto reqln = () @trusted { return cast(string)stream.readLine(max_header_line_size, "\r\n", alloc); }();
+
+	if(reqln == "PRI * HTTP/2.0" && enable_http2) return cast(uint)reqln.length;
+
+	logTrace("--------------------");
+	logTrace("HTTP server request:");
+	logTrace("--------------------");
+	logTrace("%s", reqln);
+
+	//Method
+	auto pos = reqln.indexOf(' ');
+	enforceBadRequest(pos >= 0, "invalid request method");
+
+	req.method = httpMethodFromString(reqln[0 .. pos]);
+	reqln = reqln[pos+1 .. $];
+	//Path
+	pos = reqln.indexOf(' ');
+	enforceBadRequest(pos >= 0, "invalid request path");
+
+	req.requestURI = reqln[0 .. pos];
+	reqln = reqln[pos+1 .. $];
+
+	req.httpVersion = parseHTTPVersion(reqln);
+
+	//headers
+	parseRFC5322Header(stream, req.headers, max_header_line_size, alloc, false);
+
+	foreach (k, v; req.headers.byKeyValue)
+		logTrace("%s: %s", k, v);
+	logTrace("--------------------");
+
+	return 0;
+}
+
+class HTTP1ServerExchange : HTTPServerExchange {
+	import vibe.stream.counting : CountingOutputStream, createCountingOutputStreamFL;
+	import vibe.stream.wrapper : createConnectionProxyStream, createConnectionProxyStreamFL;
+	import vibe.stream.zlib : ZlibOutputStream, createDeflateOutputStreamFL, createGzipOutputStreamFL;
+
+	protected {
+		StreamProxy m_conn;
+		ConnectionStreamProxy m_rawConnection;
+		bool m_isHeadResponse = false;
+		OutputStreamProxy m_bodyWriter;
+		FreeListRef!ChunkedOutputStream m_chunkedBodyWriter;
+		FreeListRef!CountingOutputStream m_countingWriter;
+		FreeListRef!ZlibOutputStream m_zlibOutputStream;
+		bool m_headerWritten = false;
+		bool m_requiresConnectionClose;
+	}
+
+	this(StreamProxy conn, ConnectionStreamProxy raw_connection)
+	@safe {
+		m_conn = conn;
+		m_rawConnection = raw_connection;
+		m_countingWriter = createCountingOutputStreamFL(conn);
+	}
+
+	override @property bool isHeadResponse() const { return m_isHeadResponse; }
+	override @property bool headerWritten() const { return m_headerWritten; }
+	override @property ulong bytesWritten() @safe const { return m_countingWriter.bytesWritten; }
+
+	override void writeBody(HTTPServerResponse res, RandomAccessStreamProxy stream)
+	{
+		assert(!m_headerWritten, "A body was already written!");
+		writeHeader(res);
+		if (m_isHeadResponse) return;
+
+		auto bytes = stream.size - stream.tell();
+		stream.pipe(m_conn);
+		m_countingWriter.increment(bytes);
+	}
+
+	override void writeBody(HTTPServerResponse res, InputStreamProxy stream, ulong num_bytes = ulong.max)
+	{
+		assert(!m_headerWritten, "A body was already written!");
+		writeHeader(res);
+		if (m_isHeadResponse) return;
+
+		if (num_bytes != ulong.max) {
+			stream.pipe(m_conn, num_bytes);
+			m_countingWriter.increment(num_bytes);
+		} else stream.pipe(m_countingWriter);
+	}
+
+	override void writeVoidBody(HTTPServerResponse res)
+	{
+		if (!isHeadResponse) {
+			assert("Content-Length" !in res.headers);
+			assert("Transfer-Encoding" !in res.headers);
+		}
+		assert(!m_headerWritten);
+		writeHeader(res);
+		m_conn.flush();
+	}
+
+	override OutputStreamProxy bodyWriter(HTTPServerResponse res)
+	{
+		import std.conv : to;
+
+		assert(!!m_conn);
+		if (m_bodyWriter) {
+			// for test responses, the body writer is pre-set, without headers
+			// being written, so we may need to do that here
+			if (!m_headerWritten) writeHeader(res);
+
+			return m_bodyWriter;
+		}
+
+		assert(!m_headerWritten, "A void body was already written!");
+		assert(res.statusCode >= 200, "1xx responses can't have body");
+
+		if (m_isHeadResponse) {
+			// for HEAD requests, we define a NullOutputWriter for convenience
+			// - no body will be written. However, the request handler should call writeVoidBody()
+			// and skip writing of the body in this case.
+			if ("Content-Length" !in res.headers)
+				res.headers["Transfer-Encoding"] = "chunked";
+			writeHeader(res);
+			m_bodyWriter = nullSink;
+			return m_bodyWriter;
+		}
+
+		if ("Content-Encoding" in res.headers && "Content-Length" in res.headers) {
+			// we do not known how large the compressed body will be in advance
+			// so remove the content-length and use chunked transfer
+			res.headers.remove("Content-Length");
+		}
+
+		if (auto pcl = "Content-Length" in res.headers) {
+			writeHeader(res);
+			m_countingWriter.writeLimit = (*pcl).to!ulong;
+			m_bodyWriter = m_countingWriter;
+		} else if (res.httpVersion <= HTTPVersion.HTTP_1_0) {
+			if ("Connection" in res.headers)
+				res.headers.remove("Connection"); // default to "close"
+			writeHeader(res);
+			m_bodyWriter = m_conn;
+		} else {
+			res.headers["Transfer-Encoding"] = "chunked";
+			writeHeader(res);
+			m_chunkedBodyWriter = createChunkedOutputStreamFL(m_countingWriter);
+			m_bodyWriter = m_chunkedBodyWriter;
+		}
+
+		if (auto pce = "Content-Encoding" in res.headers) {
+			if (icmp2(*pce, "gzip") == 0) {
+				m_zlibOutputStream = createGzipOutputStreamFL(m_bodyWriter);
+				m_bodyWriter = m_zlibOutputStream;
+			} else if (icmp2(*pce, "deflate") == 0) {
+				m_zlibOutputStream = createDeflateOutputStreamFL(m_bodyWriter);
+				m_bodyWriter = m_zlibOutputStream;
+			} else {
+				logWarn("Unsupported Content-Encoding set in response: '"~*pce~"'");
+			}
+		}
+
+		return m_bodyWriter;
+	}
+
+	override ConnectionStream switchProtocol(HTTPServerResponse res, string protocol)
+	{
+		res.statusCode = HTTPStatus.switchingProtocols;
+		if (protocol.length) res.headers["Upgrade"] = protocol;
+		writeVoidBody(res);
+		m_requiresConnectionClose = true;
+		m_headerWritten = true;
+		return createConnectionProxyStream(m_conn, m_rawConnection);
+	}
+
+	override void switchProtocol(HTTPServerResponse res, string protocol, scope void delegate(scope ConnectionStream) @safe del)
+	{
+		res.statusCode = HTTPStatus.switchingProtocols;
+		if (protocol.length) res.headers["Upgrade"] = protocol;
+		writeVoidBody(res);
+		m_requiresConnectionClose = true;
+		m_headerWritten = true;
+		() @trusted {
+			auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection);
+			del(conn);
+		} ();
+		finalize(res);
+	}
+
+	override ConnectionStream connectProxy(HTTPServerResponse res)
+	{
+		return createConnectionProxyStream(m_conn, m_rawConnection);
+	}
+
+	override void connectProxy(HTTPServerResponse res, scope void delegate(scope ConnectionStream) @safe del)
+	{
+		() @trusted {
+			auto conn = createConnectionProxyStreamFL(m_conn, m_rawConnection);
+			del(conn);
+		} ();
+		finalize(res);
+	}
+
+	void finalize(HTTPServerResponse res)
+	{
+		import std.conv : to;
+
+		if (m_zlibOutputStream) {
+			m_zlibOutputStream.finalize();
+			m_zlibOutputStream.destroy();
+		}
+		if (m_chunkedBodyWriter) {
+			m_chunkedBodyWriter.finalize();
+			m_chunkedBodyWriter.destroy();
+		}
+
+		// ignore exceptions caused by an already closed connection - the client
+		// may have closed the connection already and this doesn't usually indicate
+		// a problem.
+		if (m_rawConnection && m_rawConnection.connected) {
+			try if (m_conn) m_conn.flush();
+			catch (Exception e) logDebug("Failed to flush connection after finishing HTTP response: %s", e.msg);
+			if (!isHeadResponse && m_countingWriter.bytesWritten < res.headers.get("Content-Length", "0").to!ulong) {
+				logDebug("HTTP response only written partially before finalization. Terminating connection.");
+				m_requiresConnectionClose = true;
+			}
+
+			m_rawConnection = ConnectionStreamProxy.init;
+		}
+
+		if (m_conn) {
+			m_conn = StreamProxy.init;
+			res.m_timeFinalized = Clock.currTime(UTC());
+		}
+	}
+
+	private void writeHeader(HTTPServerResponse res)
+	@safe {
+		import vibe.stream.wrapper;
+
+		assert(!m_headerWritten, "Try to write header after body has already begun.");
+		assert(res.httpVersion != HTTPVersion.HTTP_1_0 || res.statusCode >= 200, "Informational status codes aren't supported by HTTP/1.0.");
+
+		// Don't set m_headerWritten for 1xx status codes
+		if (res.statusCode >= 200) m_headerWritten = true;
+		auto dst = streamOutputRange!1024(m_conn);
+
+		void writeLine(T...)(string fmt, T args)
+		@safe {
+			formattedWrite(() @trusted { return &dst; } (), fmt, args);
+			dst.put("\r\n");
+			logTrace(fmt, args);
+		}
+
+		logTrace("---------------------");
+		logTrace("HTTP server response:");
+		logTrace("---------------------");
+
+		// write the status line
+		writeLine("%s %d %s",
+			getHTTPVersionString(res.httpVersion),
+			res.statusCode,
+			res.statusPhrase.length ? res.statusPhrase : httpStatusText(res.statusCode));
+
+		// write all normal headers
+		foreach (k, v; res.headers.byKeyValue) {
+			dst.put(k);
+			dst.put(": ");
+			dst.put(v);
+			dst.put("\r\n");
+			logTrace("%s: %s", k, v);
+		}
+
+		logTrace("---------------------");
+
+		// write cookies
+		foreach (n, cookie; () @trusted { return res.cookies.byKeyValue; } ()) {
+			dst.put("Set-Cookie: ");
+			cookie.writeString(() @trusted { return &dst; } (), n);
+			dst.put("\r\n");
+		}
+
+		// finalize response header
+		dst.put("\r\n");
+	}
+
+	bool waitForConnectionClose(Duration timeout)
+	{
+		if (!m_rawConnection || !m_rawConnection.connected) return true;
+		m_rawConnection.waitForData(timeout);
+		return !m_rawConnection.connected;
+	}
+
+	@property bool connected()
+	const {
+		if (!m_rawConnection) return false;
+		return m_rawConnection.connected;
+	}
+}
+

+ 79 - 0
vtest/source/vibe/http/internal/http2/error.d

@@ -0,0 +1,79 @@
+module vibe.http.internal.http2.error;
+
+import vibe.http.internal.http2.hpack.exception;
+import vibe.http.internal.http2.frame;
+
+import vibe.container.internal.utilallocator;
+import vibe.core.log;
+import vibe.core.net;
+import vibe.core.core;
+import vibe.core.stream;
+import vibe.stream.tls;
+import vibe.internal.array;
+import vibe.internal.freelistref;
+import vibe.internal.interfaceproxy;
+
+import std.range;
+import std.base64;
+import std.traits;
+import std.bitmanip; // read from ubyte (decoding)
+import std.typecons;
+import std.conv : to;
+import std.exception;
+import std.algorithm : canFind; // alpn callback
+import std.algorithm.iteration;
+
+enum HTTP2Error {
+	NO_ERROR 			= 0x0,
+	PROTOCOL_ERROR 		= 0x1,
+	INTERNAL_ERROR 		= 0x2,
+	FLOW_CONTROL_ERROR 	= 0x3,
+	SETTINGS_TIMEOUT 	= 0x4,
+	STREAM_CLOSED 		= 0x5,
+	FRAME_SIZE_ERROR 	= 0x6,
+	REFUSED_STREAM 		= 0x7,
+	CANCEL 				= 0x8,
+	COMPRESSION_ERROR 	= 0x9,
+	CONNECT_ERROR 		= 0xa,
+	ENHANCE_YOUR_CALM 	= 0xb,
+	INADEQUATE_SECURITY = 0xc,
+	HTTP_1_1_REQUIRED 	= 0xd
+}
+
+enum GOAWAYFrameLength = 17;
+
+/// creates a GOAWAY frame as defined in RFC 7540, section 6.8
+void buildGOAWAYFrame(R)(ref R buf, const uint streamId, HTTP2Error error)
+@safe @nogc if (isOutputRange!(R, ubyte))
+{
+	assert(buf.length == GOAWAYFrameLength, "Unable to create GOAWAY frame");
+
+	// last stream processed by the server (client-initiated)
+	uint sid = (streamId > 1) ? streamId - 2 : 0;
+
+	buf.createHTTP2FrameHeader(8, HTTP2FrameType.GOAWAY, 0x0, 0x0);
+	buf.putBytes!4(sid & 127); // last stream ID
+	buf.putBytes!4(error);
+}
+/// ditto
+void buildGOAWAYFrame(ref ubyte[GOAWAYFrameLength] dst, uint sid, HTTP2Error code)
+@safe @nogc {
+	dst[].buildGOAWAYFrame(sid, code);
+}
+
+
+/// exceptions
+T enforceHTTP2(T)(T condition, string message = null, HTTP2Error h2e = HTTP2Error.NO_ERROR, string file = __FILE__, typeof(__LINE__) line = __LINE__) @trusted
+{
+	return enforce(condition, new HTTP2Exception(message, h2e, file, line));
+}
+
+class HTTP2Exception : Exception
+{
+	HTTP2Error code;
+
+	this(string msg, HTTP2Error h2e = HTTP2Error.NO_ERROR, string file = __FILE__, size_t line = __LINE__) {
+		code = h2e;
+		super(msg, file, line);
+	}
+}

+ 708 - 0
vtest/source/vibe/http/internal/http2/exchange.d

@@ -0,0 +1,708 @@
+module vibe.http.internal.http2.exchange;
+
+import vibe.http.internal.http2.multiplexing;
+import vibe.http.internal.http2.settings;
+import vibe.http.internal.http2.server : HTTP2ConnectionStream, HTTP2StreamState;
+import vibe.http.internal.http2.hpack.hpack;
+import vibe.http.internal.http2.hpack.tables;
+import vibe.http.internal.http2.frame;
+import vibe.http.common;
+import vibe.http.status;
+import vibe.http.server;
+
+import vibe.container.internal.utilallocator;
+import vibe.core.log;
+import vibe.core.stream;
+import vibe.core.core;
+import vibe.internal.interfaceproxy;
+import vibe.stream.tls;
+import vibe.internal.array;
+import vibe.internal.string : formatAlloc, icmp2;
+import vibe.stream.wrapper : ConnectionProxyStream, createConnectionProxyStream, createConnectionProxyStreamFL;
+import vibe.stream.memory;
+import vibe.inet.url;
+import vibe.inet.message;
+
+import std.range;
+import std.string;
+import std.conv;
+import std.traits;
+import std.typecons;
+import std.datetime;
+import std.exception;
+import std.format;
+import std.algorithm.iteration;
+import std.algorithm.mutation;
+import std.algorithm.searching;
+import std.algorithm.comparison;
+
+/**
+  * HTTP/2 message exchange module as documented in:
+  * RFC 7540 (HTTP/2) section 8
+*/
+
+enum StartLine { REQUEST, RESPONSE };
+
+private alias H2F = HTTP2HeaderTableField;
+
+alias DataOutputStream = MemoryOutputStream;
+
+/// accepts a HTTP/1.1 header list, converts it to an HTTP/2 header frame and encodes it
+ubyte[] buildHeaderFrame(alias type)(string statusLine, InetHeaderMap headers,
+		HTTP2ServerContext context, ref IndexingTable table, scope IAllocator alloc, bool
+		isTLS = true) @safe
+{
+	// frame header + frame payload
+	FixedAppender!(ubyte[], 9) hbuf;
+	auto pbuf = AllocAppender!(ubyte[])(alloc);
+	auto res = AllocAppender!(ubyte[])(alloc);
+
+	// split the start line of each req / res into pseudo-headers
+	convertStartMessage(statusLine, pbuf, table, type, isTLS);
+
+	// "Host" header does not exist in HTTP/2, use ":authority" pseudo-header
+	if("Host" in headers) {
+		headers[":authority"] = headers["Host"];
+		headers.remove("Host");
+	}
+
+	foreach(k,v; headers.byKeyValue) {
+		H2F(k.toLower,v).encodeHPACK(pbuf, table);
+	}
+
+	// TODO padding
+	if(context.next_sid == 0) context.next_sid = 1;
+
+	hbuf.createHTTP2FrameHeader(cast(uint)pbuf.data.length, HTTP2FrameType.HEADERS, 0x0, context.next_sid);
+
+	res.put(hbuf.data);
+	res.put(pbuf.data);
+	return res.data;
+}
+
+/// DITTO for first request in case of h2c
+ubyte[] buildHeaderFrame(alias type)(string statusLine, InetHeaderMap headers,
+		HTTP2ServerContext context, scope IAllocator alloc) @trusted
+{
+	return buildHeaderFrame!type(statusLine, headers, context, context.table, alloc);
+}
+
+/// generates an HTTP/2 pseudo-header representation to encode a HTTP/1.1 start message line
+private void convertStartMessage(T)(string src, ref T dst, ref IndexingTable table, StartLine type, bool isTLS = true) @safe
+{
+	void toPseudo(string buf) @safe
+	{
+		// exclude protocol version (not needed in HTTP/2)
+		if(buf != "HTTP/1.1" && buf != "HTTP/2")
+		{
+			if(type == StartLine.REQUEST) { // request
+				//	request-line = method SP request-target SP HTTP-version CRLF
+					try {
+						auto method = httpMethodFromString(buf); // might throw
+						H2F(":method", method).encodeHPACK(dst, table);
+					} catch(Exception e) {
+						H2F(":scheme", (isTLS ? "https" : "http")).encodeHPACK(dst, table);
+						H2F(":path", buf).encodeHPACK(dst, table);
+				}
+			} else if(type == StartLine.RESPONSE) { // response (status-line)
+				// status-line = HTTP-version SP status-code SP reason-phrase CRLF
+				static foreach(st; __traits(allMembers, HTTPStatus)) {
+					if(buf.isNumeric && __traits(getMember, HTTPStatus, st) == buf.to!int) {
+						mixin("H2F(\":status\",HTTPStatus."~st~").encodeHPACK(dst, table); return;");
+					}
+				}
+			}
+		}
+	}
+
+	// consider each chunk of the start message line
+	src.strip("\r\n").splitter(' ').each!(s => toPseudo(s));
+}
+
+unittest {
+	import std.experimental.allocator;
+	import std.experimental.allocator.mallocator;
+	HTTP2Settings settings;
+	HTTPServerContext ctx;
+	auto context = new HTTP2ServerContext(ctx, settings);
+	auto table = IndexingTable(settings.headerTableSize);
+	scope alloc = new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance);
+
+	string statusline = "GET / HTTP/2\r\n\r\n";
+	InetHeaderMap hmap;
+	hmap["Host"] = "www.example.com";
+	ubyte[] expected = [0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1 , 0xe3, 0xc2 , 0xe5, 0xf2 , 0x3a, 0x6b , 0xa0, 0xab , 0x90, 0xf4 , 0xff];
+	// [9..$] excludes the HTTP/2 Frame header
+	auto res = buildHeaderFrame!(StartLine.REQUEST)(statusline, hmap, context, table, alloc,
+			false)[9..$];
+	assert(res == expected);
+
+	statusline = "HTTP/2 200 OK";
+	InetHeaderMap hmap1;
+	expected = [0x88];
+	res = buildHeaderFrame!(StartLine.RESPONSE)(statusline, hmap1, context, table, alloc,
+			false)[9..$];
+
+	assert(res == expected);
+}
+
+/* ======================================================= */
+/* 					HTTP/2 REQUEST HANDLING 			   */
+/* ======================================================= */
+
+/** Similar to originalHandleRequest, adapted to HTTP/2
+  * The request is converted to HTTPServerRequest through parseHTTP2RequestHeader
+  * once the HTTPServerResponse is built, HEADERS frame and (optionally) DATA Frames are sent
+*/
+bool handleHTTP2Request(UStream)(ref HTTP2ConnectionStream!UStream stream,
+		TCPConnection tcp_connection, HTTP2ServerContext h2context,
+		HTTP2HeaderTableField[] headers, ref IndexingTable table, scope IAllocator alloc) @safe
+{
+	import vibe.http.internal.utils : formatRFC822DateAlloc;
+
+	SysTime reqtime = Clock.currTime(UTC());
+	HTTPServerContext listen_info = h2context.h1context;
+
+	// initialize request
+	auto req = () @trusted { return alloc.make!HTTPServerRequest(reqtime, listen_info.bindPort); } ();
+	scope (exit) () @trusted { alloc.dispose(req); } ();
+	// store the IP address
+	req.clientAddress = tcp_connection.remoteAddress;
+
+	if (!listen_info.hasVirtualHosts) {
+		logWarn("Didn't find a HTTP listening context for incoming connection. Dropping.");
+		return false;
+	}
+
+	// Default to the first virtual host for this listener
+	HTTPServerContext.VirtualHost context = listen_info.m_virtualHosts[0];
+	HTTPServerRequestDelegate request_task = context.requestHandler;
+	HTTPServerSettings settings = context.settings;
+
+	// temporarily set to the default settings
+	req.m_settings = settings;
+
+	// Create the response object
+	InterfaceProxy!ConnectionStream cproxy = tcp_connection;
+	InterfaceProxy!Stream cstream = stream.connection; // TCPConnection / TLSStream
+
+	// check for TLS encryption
+	bool istls;
+	static if(is(UStream : TLSStream)) {
+		istls = true;
+	} else {
+		istls = false;
+	}
+	req.tls = istls;
+
+	if (req.tls) {
+		static if (HaveNoTLS) assert(false);
+		else {
+			static if (is(InterfaceProxy!Stream == Stream))
+				req.clientCertificate = (cast(TLSStream)stream.connection).peerCertificate;
+			else static if (is(typeof(stream.connection) : TLSStream))
+				req.clientCertificate = stream.connection.peerCertificate;
+			else
+				assert(false);
+		}
+	}
+
+	bool parsed = false;
+
+	// parse request:
+	// both status line + headers (already unpacked in `headers`)
+	// defined in vibe.http.server because of protected struct HTTPServerRequest
+	parseHTTP2RequestHeader(headers, req);
+	if(req.host.empty) {
+		req.host = tcp_connection.localAddress.toString;
+	}
+
+	string reqhost;
+	ushort reqport = 0;
+	{
+		string s = req.host;
+		enforceHTTP(s.length > 0 || req.httpVersion <= HTTPVersion.HTTP_1_0, HTTPStatus.badRequest, "Missing Host header.");
+		if (s.startsWith('[')) { // IPv6 address
+			auto idx = s.indexOf(']');
+			enforce(idx > 0, "Missing closing ']' for IPv6 address.");
+			reqhost = s[1 .. idx];
+			s = s[idx+1 .. $];
+		} else if (s.length) { // host name or IPv4 address
+			auto idx = s.indexOf(':');
+			if (idx < 0) idx = s.length;
+			enforceHTTP(idx > 0, HTTPStatus.badRequest, "Missing Host header.");
+			reqhost = s[0 .. idx];
+			s = s[idx .. $];
+		}
+		if (s.startsWith(':')) reqport = s[1 .. $].to!ushort;
+	}
+	foreach (ctx; listen_info.m_virtualHosts) {
+		if (icmp2(ctx.settings.hostName, reqhost) == 0 &&
+				(!reqport || reqport == ctx.settings.port))
+		{
+			context = ctx;
+			settings = ctx.settings;
+			request_task = ctx.requestHandler;
+			break;
+		}
+	}
+	req.m_settings = settings;
+
+	auto tcpc = tcp_connection;
+
+	auto exchange = () @trusted { return alloc.make!(HTTP2ServerExchange!UStream)(stream,
+		tcpc, h2context, headers, table, alloc); } ();
+	scope (exit) () @trusted { alloc.dispose(exchange); } ();
+	auto res = () @trusted { return alloc.make!HTTPServerResponse(exchange, settings, alloc); } ();
+	scope (exit) () @trusted { alloc.dispose(res); } ();
+	res.httpVersion = HTTPVersion.HTTP_2;
+
+	// setup compressed output
+	if (settings.useCompressionIfPossible) {
+		if (auto pae = "Accept-Encoding" in req.headers) {
+			if (canFind(*pae, "gzip")) {
+				res.headers["Content-Encoding"] = "gzip";
+			} else if (canFind(*pae, "deflate")) {
+				res.headers["Content-Encoding"] = "deflate";
+			}
+		}
+	}
+
+	// handle Expect header
+	if (auto pv = "Expect" in req.headers) {
+		if (icmp2(*pv, "100-continue") == 0) {
+			logTrace("sending 100 continue");
+			InetHeaderMap hmap;
+			auto cres =	buildHeaderFrame!(StartLine.RESPONSE)(
+					"HTTP/1.1 100 Continue\r\n\r\n", hmap, h2context, table, alloc, istls);
+		}
+		assert(false); // TODO determine if actually used with HTTP/2 (PUSH_PROMISE?)
+	}
+
+	// eagerly parse the URL as its lightweight and defacto @nogc
+	auto url = URL.parse(req.requestURI);
+	req.queryString = url.queryString;
+	req.username = url.username;
+	req.password = url.password;
+	req.requestPath = url.path;
+
+	// lookup the session
+	if (settings.sessionStore) {
+		// use the first cookie that contains a valid session ID in case
+		// of multiple matching session cookies
+		foreach (val; req.cookies.getAll(settings.sessionIdCookie)) {
+			req.session = settings.sessionStore.open(val);
+			res.m_session = req.session;
+			if (req.session) break;
+		}
+	}
+
+	// write default headers
+	if (req.method == HTTPMethod.HEAD) exchange.m_isHeadResponse = true;
+	if (settings.serverString.length)
+		res.headers["Server"] = settings.serverString;
+	res.headers["Date"] = formatRFC822DateAlloc(reqtime);
+	if (req.persistent) res.headers["Keep-Alive"] = formatAlloc(alloc, "timeout=%d", settings.keepAliveTimeout.total!"seconds"());
+
+	// finished parsing the request
+	parsed = true;
+	logTrace("persist: %s", req.persistent);
+	//keep_alive = req.persistent;
+	logDebug("Received request on stream ID %d: %s %s", stream.streamId, req.method, req.requestPath);
+	foreach (k, v; req.headers.byKeyValue)
+		logDebugV("%s: %s", k, v);
+
+	// utility to format the status line
+	auto statusLine = AllocAppender!string(alloc);
+
+	void writeLine(T...)(string fmt, T args)
+		@safe {
+			formattedWrite(() @trusted { return &statusLine; } (), fmt, args);
+			statusLine.put("\r\n");
+			logTrace(fmt, args);
+		}
+
+	// header frame to be sent
+	ubyte[] headerFrame;
+
+	// handle payload (DATA frame)
+	auto dataWriter = createDataOutputStream(alloc);
+	exchange.m_bodyWriter = dataWriter;
+	h2context.next_sid = stream.streamId;
+
+	// run task (writes body)
+	request_task(req, res);
+
+	if(req.method != HTTPMethod.HEAD && dataWriter.data.length > 0) { // HEADERS + DATA
+
+		// write the status line
+		writeLine("%s %d %s",
+				getHTTPVersionString(res.httpVersion),
+				res.statusCode,
+				res.statusPhrase.length ? res.statusPhrase : httpStatusText(res.statusCode));
+
+		// build the HEADERS frame
+		() @trusted {
+			headerFrame = buildHeaderFrame!(StartLine.RESPONSE)(statusLine.data, res.headers,
+					h2context, table, alloc, istls);
+		} ();
+
+		// send HEADERS frame
+		if(headerFrame.length < h2context.settings.maxFrameSize) {
+			headerFrame[4] += 0x4; // set END_HEADERS flag (sending complete header)
+			cstream.write(headerFrame);
+
+		} else {
+			// TODO CONTINUATION frames
+			assert(false);
+		}
+
+		logDebug("Sent HEADERS frame on streamID " ~ stream.streamId.to!string);
+
+		auto tlen = dataWriter.data.length;
+
+		// multiple DATA Frames might be required
+		void sendDataTask()
+		@safe {
+			logDebug("[DATA] Starting dispatch task");
+
+			scope(exit) {
+				if(stream.state == HTTP2StreamState.HALF_CLOSED_REMOTE) {
+					stream.state = HTTP2StreamState.CLOSED;
+				} else {
+					stream.state = HTTP2StreamState.HALF_CLOSED_LOCAL;
+				}
+			}
+
+			try {
+
+				auto abort = false;
+				uint done = 0;
+
+				// window length
+				uint wlen = sendWindowLength(h2context.multiplexer,
+						stream.streamId, h2context.settings.maxFrameSize, tlen);
+
+				// until the whole payload is sent
+				while(done <= tlen) {
+					auto dataFrame = AllocAppender!(ubyte[])(alloc);
+
+					dataFrame.createHTTP2FrameHeader(
+								wlen,
+								HTTP2FrameType.DATA,
+								(done+wlen >= tlen) ? 0x1 : 0x0, // END_STREAM 0x1
+								stream.streamId
+							);
+
+					// send is over
+					if(done == tlen) {
+						logDebug("[DATA] Completed DATA frame dispatch");
+						// remove task from waiting state
+						doneCondition(h2context.multiplexer, stream.streamId);
+						closeStream(h2context.multiplexer, stream.streamId);
+						break;
+					}
+
+					// wait to resume and retry
+					if(wlen == 0) {
+						logDebug("[DATA] Dispatch task waiting for WINDOW_UPDATE");
+
+						// after 60 seconds waiting, terminate dispatch
+						() @trusted {
+							auto timer = setTimer(600.seconds, {
+									logDebug("[DATA] timer expired, aborting dispatch");
+									notifyCondition(h2context.multiplexer);
+									abort = true;
+									});
+
+							// wait until a new WINDOW_UPDATE is received (or timer expires)
+							waitCondition(h2context.multiplexer, stream.streamId);
+
+							// task resumed: cancel timer
+							if(!abort) timer.stop;
+							else return;
+						} ();
+
+						logDebug("[DATA] Dispatch task resumed");
+
+					} else {
+						// write
+
+						dataFrame.put(dataWriter.data[done..done+wlen]);
+						cstream.write(dataFrame.data);
+
+						done += wlen;
+
+						logDebug("[DATA] Sent frame chunk (%d/%d bytes) on streamID %d",
+								done, tlen, stream.streamId);
+
+						updateWindow(h2context.multiplexer, stream.streamId, wlen);
+
+						// return control to the event loop
+						yield();
+					}
+
+					// compute new window length
+					wlen = sendWindowLength(h2context.multiplexer,
+							stream.streamId, h2context.settings.maxFrameSize, tlen - done);
+				}
+
+			} catch (Exception e) {
+				logException(e, "Failed to send DATA frame");
+				return;
+			}
+		}
+
+		// spawn the asynchronous data sender
+		sendDataTask();
+
+	} else if(dataWriter.data.length > 0) { // HEAD response, HEADERS frame, no DATA
+
+		// write the status line
+		writeLine("%s %d %s",
+				getHTTPVersionString(res.httpVersion),
+				res.statusCode,
+				res.statusPhrase.length ? res.statusPhrase : httpStatusText(res.statusCode));
+
+		// build the HEADERS frame
+		() @trusted {
+			headerFrame = buildHeaderFrame!(StartLine.RESPONSE)(statusLine.data, res.headers,
+					h2context, table, alloc, istls);
+		} ();
+
+		// send HEADERS frame
+		if(headerFrame.length < h2context.settings.maxFrameSize) {
+			headerFrame[4] += 0x5; // set END_HEADERS, END_STREAM flag
+			cstream.write(headerFrame);
+		} else {
+			// TODO CONTINUATION frames
+			assert(false);
+		}
+
+		logDebug("Sent HEADERS frame on streamID " ~ stream.streamId.to!string);
+
+		logDebug("[Data] No DATA frame to send");
+
+		if(stream.state == HTTP2StreamState.HALF_CLOSED_REMOTE) {
+			stream.state = HTTP2StreamState.CLOSED;
+		} else {
+			stream.state = HTTP2StreamState.HALF_CLOSED_LOCAL;
+		}
+		closeStream(h2context.multiplexer, stream.streamId);
+
+	} else { // 404: no DATA for the given path
+
+		writeLine("%s %d %s",
+				"HTTP/2",
+				404,
+				"Not Found");
+
+		// build the HEADERS frame
+		() @trusted {
+			headerFrame = buildHeaderFrame!(StartLine.RESPONSE)(statusLine.data, res.headers,
+					h2context, table, alloc, istls);
+		} ();
+
+		if(headerFrame.length < h2context.settings.maxFrameSize) {
+			headerFrame[4] += 0x5; // set END_HEADERS, END_STREAM flag
+			cstream.write(headerFrame);
+		}
+
+		logDebug("No response: sent 404 HEADERS frame");
+
+	}
+
+	return true;
+}
+
+
+uint sendWindowLength(Mux)(ref Mux multiplexer, const uint sid, const uint maxfsize, const ulong len) @safe
+{
+	return min(connectionWindow(multiplexer), streamConnectionWindow(multiplexer, sid), maxfsize, len);
+}
+
+void updateWindow(Mux)(ref Mux multiplexer, const uint sid, const ulong sent) @safe
+{
+	auto cw = connectionWindow(multiplexer) - sent;
+	auto scw = streamConnectionWindow(multiplexer, sid) - sent;
+
+	updateConnectionWindow(multiplexer, cw);
+	updateStreamConnectionWindow(multiplexer, sid, cw);
+}
+
+private DataOutputStream createDataOutputStream(IAllocator alloc = vibeThreadAllocator())
+@safe nothrow {
+	return createMemoryOutputStream(alloc);
+}
+
+private HeaderOutputStream createHeaderOutputStream(IAllocator alloc = vibeThreadAllocator())
+@safe nothrow {
+    return new HeaderOutputStream(alloc);
+}
+
+final class HTTP2ServerExchange(UStream) : HTTPServerExchange {
+	private {
+		HTTP2ConnectionStream!UStream* m_stream;
+		TCPConnection m_connection;
+		HTTP2ServerContext m_h2context;
+		HTTP2HeaderTableField[] m_headers;
+		IndexingTable* m_table;
+		IAllocator m_allocator;
+		bool m_isHeadResponse;
+		OutputStreamProxy m_bodyWriter;
+		bool m_headerWritten = false;
+	}
+
+	this(ref HTTP2ConnectionStream!UStream stream,
+		TCPConnection tcp_connection, HTTP2ServerContext h2context,
+		HTTP2HeaderTableField[] headers, ref IndexingTable table, scope IAllocator alloc)
+	{
+		m_stream = &stream;
+		m_connection = tcp_connection;
+		m_h2context = h2context;
+		m_headers = headers;
+		m_table = &table;
+		m_allocator = alloc;
+	}
+
+	@property bool isHeadResponse() const { return m_isHeadResponse; }
+	@property bool headerWritten() const { assert(false); }
+	@property ulong bytesWritten() const { assert(false); }
+	@property bool connected() const { assert(false); }
+
+	bool waitForConnectionClose(Duration timeout)
+	{
+		assert(false);
+	}
+	void writeBody(HTTPServerResponse res, RandomAccessStreamProxy stream)
+	{
+		stream.pipe(m_bodyWriter);
+		m_bodyWriter.finalize();
+	}
+	void writeBody(HTTPServerResponse res, InputStreamProxy stream, ulong num_bytes = ulong.max)
+	{
+		stream.pipe(m_bodyWriter, num_bytes);
+		m_bodyWriter.finalize();
+	}
+	void writeVoidBody(HTTPServerResponse res)
+	{
+		m_bodyWriter.finalize();
+	}
+	OutputStreamProxy bodyWriter(HTTPServerResponse res)
+	{
+		if (!m_headerWritten)
+			writeHeader();
+		return m_bodyWriter;
+	}
+	ConnectionStream switchProtocol(HTTPServerResponse res, string protocol)
+	{
+		assert(false);
+	}
+	void switchProtocol(HTTPServerResponse res, string protocol, scope void delegate(scope ConnectionStream) @safe del)
+	{
+		assert(false);
+	}
+	ConnectionStream connectProxy(HTTPServerResponse res)
+	{
+		assert(false);
+	}
+	void connectProxy(HTTPServerResponse res, scope void delegate(scope ConnectionStream) @safe del)
+	{
+		assert(false);
+	}
+	void finalize(HTTPServerResponse res)
+	{
+		// ...
+	}
+	private void writeHeader()
+	{
+		// FIXME: Currently, the header and the body are written after the
+		//        request callback has already returned. This should be changed
+		//        to happen during the callback's execution, just like for
+		//        HTTP/1.x.
+		assert(!m_headerWritten);
+		m_headerWritten = true;
+	}
+}
+
+private final class HeaderOutputStream : OutputStream {
+@safe:
+
+    private {
+        AllocAppender!(string) m_destination;
+    }
+
+    this(IAllocator alloc)
+    nothrow {
+        m_destination = AllocAppender!(string)(alloc);
+    }
+
+    /// An array with all data written to the stream so far.
+    @property string data() @trusted nothrow { return m_destination.data(); }
+
+    /// Resets the stream to its initial state containing no data.
+    void reset(AppenderResetMode mode = AppenderResetMode.keepData)
+    @system {
+        m_destination.reset(mode);
+    }
+
+    /// Reserves space for data - useful for optimization.
+    void reserve(size_t nbytes)
+    {
+        m_destination.reserve(nbytes);
+    }
+
+    size_t write(in string bytes, IOMode)
+    {
+        () @trusted { m_destination.put(bytes); } ();
+        return bytes.length;
+	}
+	/// DITTO
+    size_t write(scope const(ubyte[]) bytes, IOMode)
+    {
+        () @trusted { m_destination.put(cast(string)bytes); } ();
+        return bytes.length;
+	}
+
+    alias write = OutputStream.write;
+
+    void flush()
+    nothrow {
+    }
+
+    void finalize()
+    nothrow {
+    }
+}
+
+void parseHTTP2RequestHeader(R)(ref R headers, HTTPServerRequest req) @safe
+{
+	import std.algorithm.searching : find, startsWith;
+	import std.algorithm.iteration : filter;
+
+	//Method
+	req.method = cast(HTTPMethod)headers.find!((h,m) => h.name == m)(":method")[0].value;
+
+	//Host
+	auto host = headers.find!((h,m) => h.name == m)(":authority");
+	if(!host.empty) req.host = cast(string)host[0].value;
+
+	//Path
+	auto pathstr = cast(string)headers.find!((h,m) => h.name == m)(":path")[0].value;
+	if(req.tls) req.requestURI = "https://" ~ req.host ~ pathstr;
+	else req.requestURI = "http://" ~ req.host ~ pathstr;
+
+	auto url = URL.parse(req.requestURI);
+	req.queryString = url.queryString;
+	req.username = url.username;
+	req.password = url.password;
+	req.requestPath = url.path;
+
+	//HTTP version
+	req.httpVersion = HTTPVersion.HTTP_2;
+
+	//headers
+	foreach(h; headers.filter!(f => !f.name.startsWith(":"))) {
+		req.headers[h.name] = cast(string)h.value;
+	}
+}

+ 471 - 0
vtest/source/vibe/http/internal/http2/frame.d

@@ -0,0 +1,471 @@
+module vibe.http.internal.http2.frame;
+
+import vibe.http.internal.http2.settings;
+import vibe.http.internal.http2.error;
+
+import vibe.internal.array;
+
+import std.typecons;
+import std.traits;
+import std.range;
+import std.array;
+import std.exception;
+import std.algorithm.iteration;
+import std.algorithm.mutation;
+
+
+/** This module implements HTTP/2 Frames, as defined in RFC 7540 under:
+  *
+  * Section 4: Frame overview, Frame header composition (octets) and their meaning
+  * https://tools.ietf.org/html/rfc7540#section-4
+  *
+  * Section 6: Frame definition according to Frame Type
+  * https://tools.ietf.org/html/rfc7540#section-6
+*/
+
+enum uint HTTP2HeaderLength = 9;
+
+enum HTTP2FrameType {
+	DATA 			= 0x0,
+	HEADERS 		= 0x1,
+	PRIORITY 		= 0x2,
+	RST_STREAM 		= 0x3,
+	SETTINGS 		= 0x4,
+	PUSH_PROMISE 	= 0x5,
+	PING 			= 0x6,
+	GOAWAY 			= 0x7,
+	WINDOW_UPDATE 	= 0x8,
+	CONTINUATION 	= 0x9
+}
+
+/*** FRAME PARSING ***/
+
+/// updated by `unpackHTTP2Frame`
+struct HTTP2FrameStreamDependency {
+	bool exclusive = false;
+	bool isPushPromise = false;
+	uint streamId = 0;
+	ubyte weight = 0;
+
+	@property bool isSet() @safe @nogc { return streamId != 0; }
+
+	void fill(R)(ref R src) @safe @nogc
+		if(is(ElementType!R : ubyte))
+	{
+		uint first = src.takeExactly(4).fromBytes(4);
+		exclusive = first & (cast(ulong)1 << 32);
+		streamId = first & ((cast(ulong)1 << 32) - 1);
+		src.popFrontN(4);
+
+		if(!isPushPromise) {
+			weight = src.front;
+			src.popFront();
+		}
+	}
+}
+
+/** unpacks a frame putting the payload in `payloadDst` and returning the header
+  * implements the checks required for each frame type (Section 6 of HTTP/2 RFC)
+  *
+  * Invoked by a possible HTTP/2 request handler, the payload is meant to be handled by
+  * the caller.
+  *
+  * Note: @nogc-compatible as long as payloadDst.put is @nogc (AllocAppender.put isn't)
+  */
+HTTP2FrameHeader unpackHTTP2Frame(R,T)(ref R payloadDst, T src, ref bool endStream, ref bool endHeaders, ref bool ack, ref HTTP2FrameStreamDependency sdep) @safe
+{
+	auto header = unpackHTTP2FrameHeader(src);
+	unpackHTTP2Frame(payloadDst, src, header, endStream, endHeaders, ack, sdep);
+	return header;
+}
+
+/// DITTO
+void unpackHTTP2Frame(R,T)(ref R payloadDst, T src, HTTP2FrameHeader header, ref bool endStream, ref bool endHeaders, ref bool ack, ref HTTP2FrameStreamDependency sdep) @safe
+{
+	size_t len = header.payloadLength;
+
+	switch(header.type) {
+		case HTTP2FrameType.DATA:
+			if(header.flags & 0x8) { // padding is set, first bit is pad length
+				len -= cast(size_t)src.front + 1;
+				src.popFront();
+				enforceHTTP2(src.length >= len, "Invalid pad length", HTTP2Error.PROTOCOL_ERROR);
+			}
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			src.popFrontN(header.payloadLength - len - 1); // remove padding
+			if(header.flags & 0x1) endStream = true;
+			break;
+
+		case HTTP2FrameType.HEADERS:
+			if(header.flags & 0x8) { // padding is set, first bit is pad length
+				len -= cast(size_t)src.front + 1;
+				src.popFront();
+				enforceHTTP2(src.length >= len, "Invalid pad length", HTTP2Error.PROTOCOL_ERROR);
+			}
+			if(header.flags & 0x20) { // priority is set, fill `sdep`
+				sdep.fill(src);
+				len -= 5;
+			}
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			src.popFrontN(header.payloadLength - len - 1); // remove padding
+			if(header.flags & 0x1) endStream = true;
+			if(header.flags & 0x4) endHeaders = true;
+			break;
+
+		case HTTP2FrameType.PRIORITY:
+			enforceHTTP2(len == 5, "Invalid PRIORITY Frame", HTTP2Error.PROTOCOL_ERROR);
+			sdep.fill(src);
+			break;
+
+		case HTTP2FrameType.RST_STREAM:
+			enforceHTTP2(len == 4, "Invalid RST_STREAM Frame", HTTP2Error.PROTOCOL_ERROR);
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			break;
+
+		case HTTP2FrameType.SETTINGS:
+			enforceHTTP2(len % 6 == 0, "Invalid SETTINGS Frame (FRAME_SIZE error)", HTTP2Error.PROTOCOL_ERROR);
+			enforceHTTP2(header.streamId == 0, "Invalid streamId for SETTINGS Frame", HTTP2Error.PROTOCOL_ERROR);
+			if(header.flags & 0x1) { // this is an ACK frame
+				enforceHTTP2(len == 0, "Invalid SETTINGS ACK Frame (FRAME_SIZE error)", HTTP2Error.PROTOCOL_ERROR);
+				ack = true;
+				break;
+			}
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			break;
+
+		case HTTP2FrameType.PUSH_PROMISE:
+			if(header.flags & 0x8) { // padding is set, first bit is pad length
+				len -= cast(size_t)src.front + 1;
+				src.popFront();
+				enforceHTTP2(src.length >= len, "Invalid pad length", HTTP2Error.PROTOCOL_ERROR);
+			}
+			sdep.isPushPromise = true;
+			sdep.fill(src);
+			len -= 4;
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			src.popFrontN(header.payloadLength - len - 1); // remove padding
+			if(header.flags & 0x4) endHeaders = true;
+			break;
+
+		case HTTP2FrameType.PING:
+			enforceHTTP2(len == 8, "Invalid PING Frame (FRAME_SIZE error)",
+					HTTP2Error.PROTOCOL_ERROR);
+			enforceHTTP2(header.streamId == 0, "Invalid streamId for PING Frame",
+					HTTP2Error.PROTOCOL_ERROR);
+			if(header.flags & 0x1) {
+				ack = true;
+			}
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			break;
+
+		case HTTP2FrameType.GOAWAY: // GOAWAY is used to close connection (in handler)
+			enforceHTTP2(len >= 8, "Invalid GOAWAY Frame (FRAME_SIZE error)",
+					HTTP2Error.PROTOCOL_ERROR);
+			enforceHTTP2(header.streamId == 0, "Invalid streamId for GOAWAY Frame",
+					HTTP2Error.PROTOCOL_ERROR);
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			break;
+
+		case HTTP2FrameType.WINDOW_UPDATE:
+			enforceHTTP2(len == 4, "Invalid WINDOW_UPDATE Frame (FRAME_SIZE error)",
+					HTTP2Error.PROTOCOL_ERROR);
+			foreach(i,b; src.takeExactly(len).enumerate) {
+				if(i == 0) b &= 0x7F; // reserved bit
+				payloadDst.put(b);
+				src.popFront();
+			}
+			break;
+
+		case HTTP2FrameType.CONTINUATION:
+			enforceHTTP2(header.streamId != 0, "Invalid streamId for CONTINUATION frame",
+					HTTP2Error.PROTOCOL_ERROR);
+			foreach(b; src.takeExactly(len)) {
+				payloadDst.put(b);
+				src.popFront();
+			}
+			if(header.flags & 0x4) endHeaders = true;
+			break;
+
+		default:
+			enforceHTTP2(false, "Invalid frame header unpacked.", HTTP2Error.PROTOCOL_ERROR);
+			break;
+	}
+}
+
+unittest {
+	import vibe.internal.array : FixedAppender;
+
+	FixedAppender!(ubyte[], 4) payloadDst;
+	bool endStream = false;
+	bool endHeaders = false;
+	bool ack = false;
+	HTTP2FrameStreamDependency sdep;
+
+
+	// DATA Frame
+	ubyte[] data = [0, 0, 4, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1];
+	payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(payloadDst.data == [1, 1, 1, 1]);
+
+	// HEADERS Frame
+	payloadDst.clear;
+	data = [0, 0, 4, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2];
+	payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(payloadDst.data == [2, 2, 2, 2]);
+
+	// PRIORITY Frame
+	payloadDst.clear;
+	data = [0, 0, 5, 2, 0, 0, 0, 0, 3, 0, 0, 0, 2, 5];
+	payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(payloadDst.data == []);
+	assert(sdep.weight == 5 &&  sdep.streamId == 2);
+
+	// RST_STREAM Frame
+	payloadDst.clear;
+	data = [0, 0, 4, 3, 0, 0, 0, 0, 4, 4, 4, 4, 4];
+	payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(payloadDst.data == [4, 4, 4, 4]);
+
+	// SETTINGS Frame
+	FixedAppender!(ubyte[], 6) settingsDst;
+	data = [0, 0, 6, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2];
+	settingsDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(settingsDst.data == [0, 1, 2, 2, 2, 2]);
+
+	// PUSH_PROMISE Frame
+	payloadDst.clear;
+	data = [0, 0, 8, 5, 0, 0, 0, 0, 5, 0, 0, 0, 2, 4, 4, 4, 4];
+	payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(payloadDst.data == [4, 4, 4, 4]);
+	assert(sdep.weight == 5 &&  sdep.streamId == 2);
+
+	// PING Frame
+	FixedAppender!(ubyte[], 8) pingDst;
+	data = [0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 4, 4, 4];
+	pingDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(pingDst.data == [0, 0, 0, 2, 4, 4, 4, 4]);
+
+	// GOAWAY Frame
+	pingDst.clear;
+	data = [0, 0, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4];
+	pingDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(pingDst.data == [0, 0, 0, 2, 0, 0, 0, 4]);
+
+	// WINDOW_UPDATE
+	payloadDst.clear;
+	data = [0, 0, 4, 8, 0, 0, 0, 0, 6, 1, 1, 1, 1];
+	payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(payloadDst.data == [1, 1, 1, 1]);
+
+	// CONTINUATION
+	payloadDst.clear;
+	data = [0, 0, 4, 9, 0, 0, 0, 0, 6, 2, 2, 2, 2];
+	payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
+	assert(payloadDst.data == [2, 2, 2, 2]);
+}
+
+/*** FRAME BUILDING ***/
+
+/// concatenates a Frame header with a Frame payload
+void buildHTTP2Frame(R,H,T)(ref R dst, ref H header, ref T payload) @safe @nogc
+	if(is(ElementType!R : ubyte) && is(ElementType!T : ubyte))
+{
+	// put header
+	static if(is(H == HTTP2FrameHeader)) {
+		assert(header.payloadLength == payload.length, "Invalid payload length");
+		dst.serializeHTTP2FrameHeader(header);
+
+	} else static if(is(ElementType!H : ubyte)) {
+		auto len = header.takeExactly(3).fromBytes(3);
+		assert(len == payload.length, "Invalid payload length");
+		foreach(b; header) dst.put(b);
+	}
+
+	// put payload
+	foreach(b; payload) dst.put(b);
+}
+
+/// DITTO
+/// @nogc-compatible if dst.put is @nogc
+void buildHTTP2Frame(R,T)(ref R dst, T payload) @safe
+{
+	payload.copy(dst);
+}
+
+unittest {
+	auto header = HTTP2FrameHeader(4, cast(HTTP2FrameType)1, 0, 5);
+	ubyte[4] payload = [0, 1, 2, 3];
+	ubyte[] bheader = [0, 0, 4, 1, 0, 0, 0, 0, 5];
+	ubyte[13] expected = [0, 0, 4, 1, 0, 0, 0, 0, 5, 0, 1, 2, 3];
+
+	BatchBuffer!(ubyte, 13) dst, ddst;
+	dst.putN(13);
+	ddst.putN(13);
+	dst.buildHTTP2Frame(header, payload);
+	ddst.buildHTTP2Frame(bheader, payload);
+
+	assert(dst.peekDst == expected);
+	assert(ddst.peekDst == expected);
+}
+
+/*** FRAME HEADER ***/
+/// header packing
+/// @nogc-compatible if dst.put is @nogc
+void createHTTP2FrameHeader(R)(ref R dst, const uint len, const HTTP2FrameType type, const ubyte flags, const uint sid) @safe
+{
+	dst.serialize(HTTP2FrameHeader(len, type, flags, sid));
+}
+
+/// serializing
+void serializeHTTP2FrameHeader(R)(ref R dst, HTTP2FrameHeader header) @safe @nogc
+{
+	dst.serialize(header);
+}
+
+/// unpacking
+HTTP2FrameHeader unpackHTTP2FrameHeader(R)(scope ref R src) @safe @nogc
+{
+	scope header = HTTP2FrameHeader(src);
+	return header;
+}
+
+/** Implement an HTTP/2 Frame header
+  * The header is a 9-bit ubyte[9] string
+  */
+struct HTTP2FrameHeader
+{
+	private {
+		//ubyte[3] m_length; 			// 24-bit frame payload length
+		FixedAppender!(ubyte[], 3) m_length;
+		HTTP2FrameType m_type; 		// frame type (stored as ubyte for serialization)
+		ubyte m_flags; 				// frame flags
+		//ubyte[4] m_streamId;  		// stream id, uint (stored as ubyte for serialization)
+		FixedAppender!(ubyte[], 4) m_streamId;
+	}
+
+	this(const uint len, const HTTP2FrameType tp, const ubyte flg, const uint sid) @safe @nogc
+	{
+		assert(sid < (cast(ulong)1 << 32), "Invalid stream id");
+
+		m_length.putBytes!3(len);
+		m_type = tp;
+		m_flags = flg;
+		m_streamId.putBytes!4(sid & ((cast(ulong)1 << 32) - 1)); // reserved bit is 0
+	}
+
+	this(T)(ref T src) @safe @nogc
+		if(is(ElementType!T : ubyte))
+	{
+		m_length.put(src.take(3));
+		src.popFrontN(3);
+
+		m_type = cast(HTTP2FrameType)src.front; src.popFront;
+		m_flags = src.front; src.popFront;
+
+		m_streamId.put(src.take(1).front & 127); src.popFront; // ignore reserved bit
+		m_streamId.put(src.take(3));
+		src.popFrontN(3);
+	}
+
+	@property HTTP2FrameType type() @safe @nogc { return m_type; }
+
+	@property uint payloadLength() @safe @nogc { return m_length.data.fromBytes(3); }
+
+	@property ubyte flags() @safe @nogc { return m_flags; }
+
+	@property uint streamId() @safe @nogc { return m_streamId.data.fromBytes(4); }
+}
+
+/// convert 32-bit unsigned integer to N bytes (MSB first)
+void putBytes(uint N, R)(ref R dst, const(ulong) src) @safe @nogc
+{
+	assert(src >= 0 && src < (cast(ulong)1 << N*8), "Invalid frame payload length");
+	static if(hasLength!R) assert(dst.length >= N);
+
+	ubyte[N] buf;
+	foreach(i,ref b; buf) b = cast(ubyte)(src >> 8*(N-1-i)) & 0xff;
+
+	static if(isArray!R) {
+		dst.put(buf);
+	} else {
+		foreach(b; buf) dst.put(b);
+	}
+}
+
+/// convert a N-bytes representation MSB->LSB to uint
+uint fromBytes(R)(R src, uint n) @safe @nogc
+{
+	uint res = 0;
+	static if(isArray!R) {
+		foreach(i,b; src) res = res + (b << 8*(n-1-i));
+	} else {
+		foreach(i,b; src.enumerate.retro) res = res + (b << 8*i);
+	}
+	return res;
+}
+
+/// fill a buffer with fields from `header`
+/// @nogc-compatible if dst.put is @nogc
+private void serialize(R)(ref R dst, HTTP2FrameHeader header) @safe
+	if(isOutputRange!(R, ubyte))
+{
+	static foreach(f; __traits(allMembers, HTTP2FrameHeader)) {
+		static if(f != "__ctor" && f != "type"
+				&& f != "payloadLength" && f != "flags" && f != "streamId") {
+			static if(f == "m_length" || f == "m_streamId") {
+				mixin("dst.put(header."~f~".data);");
+			} else static if(f == "m_type") {
+				mixin("dst.put(cast(ubyte)header."~f~");");
+			} else {
+				mixin("dst.put(header."~f~");");
+			}
+		}
+	}
+}
+
+unittest {
+	import vibe.internal.array : FixedAppender;
+
+	auto header = HTTP2FrameHeader(2, cast(HTTP2FrameType)1, 0, 5);
+	ubyte[] expected = [0, 0, 2, 1, 0, 0, 0, 0, 5];
+	FixedAppender!(ubyte[], 9) dst;
+	// serialize to a ubyte[9] array
+	serialize(dst,header);
+	assert(dst.data == expected);
+
+	// test utility functions
+	FixedAppender!(ubyte[], 9) ddst;
+	ddst.createHTTP2FrameHeader(2, cast(HTTP2FrameType)1, 0, 5);
+	assert(dst.data == ddst.data);
+
+	FixedAppender!(ubyte[], 9) dddst;
+	dddst.serializeHTTP2FrameHeader(header);
+	assert(dst.data == dddst.data);
+
+	// test unpacking
+	assert(header == unpackHTTP2FrameHeader(expected));
+
+	assert(header.payloadLength == 2);
+}
+

+ 142 - 0
vtest/source/vibe/http/internal/http2/hpack/decoder.d

@@ -0,0 +1,142 @@
+module vibe.http.internal.http2.hpack.decoder;
+
+import vibe.http.internal.http2.hpack.huffman;
+import vibe.http.internal.http2.hpack.tables;
+import vibe.http.internal.http2.hpack.util;
+import vibe.http.internal.http2.hpack.exception;
+
+import vibe.internal.array : AllocAppender;
+import vibe.core.log;
+
+import std.range; // Decoder
+import std.string;
+import std.experimental.allocator;
+import std.experimental.allocator.mallocator;
+import std.exception;
+
+/** Module to implement an header decoder consistent with HPACK specifications (RFC 7541)
+  * The detailed description of the decoding process, examples and binary format details can
+  * be found at:
+  * Section 3: https://tools.ietf.org/html/rfc7541#section-3
+  * Section 6: https://tools.ietf.org/html/rfc7541#section-6
+  * Appendix C: https://tools.ietf.org/html/rfc7541#appendix-C
+*/
+alias HTTP2SettingValue = uint;
+
+void decode(I, R, T)(ref I src, ref R dst, ref IndexingTable table,  ref T alloc, ulong maxTableSize=4096) @trusted
+{
+	ubyte bbuf = src[0];
+	src = src[1..$];
+
+	if(bbuf & 128) {
+		auto res = decodeInteger(src, bbuf, 7);
+		dst.put(table[res]);
+	} else {
+		HTTP2HeaderTableField hres;
+		bool update = false;
+		auto adst = AllocAppender!string(alloc);
+
+		if (bbuf & 64) { // inserted in dynamic table
+			size_t idx = decodeInteger(src, bbuf, 6);
+			if(idx > 0) {  // name == table[index].name, value == literal
+				hres.name = table[idx].name;
+			} else {   // name == literal, value == literal
+				decodeLiteral(src, adst);
+				hres.name.setReset(adst);
+			}
+			decodeLiteral(src, adst);
+			hres.value.setReset(adst);
+			hres.index = true;
+			hres.neverIndex = false;
+
+		} else if(bbuf & 32) {
+			update = true;
+			auto nsize = decodeInteger(src, bbuf, 3);
+			enforce(nsize <= maxTableSize, "Invalid table size update");
+
+			table.updateSize(cast(HTTP2SettingValue)nsize);
+			logDebug("Updated dynamic table size to: %d octets", nsize);
+
+		} else if(bbuf & 16) { // NEVER inserted in dynamic table
+			size_t idx = decodeInteger(src, bbuf, 4);
+			if(idx > 0) {  // name == table[index].name, value == literal
+				hres.name = table[idx].name;
+			} else {   // name == literal, value == literal
+				decodeLiteral(src, adst);
+				hres.name.setReset(adst);
+			}
+			decodeLiteral(src, adst);
+			hres.value.setReset(adst);
+			hres.index = false;
+			hres.neverIndex = true;
+
+		} else { // this occourrence is not inserted in dynamic table
+			size_t idx = decodeInteger(src, bbuf, 4);
+			if(idx > 0) {  // name == table[index].name, value == literal
+				hres.name = table[idx].name;
+			} else {   // name == literal, value == literal
+				decodeLiteral(src, adst);
+				hres.name.setReset(adst);
+			}
+			decodeLiteral(src, adst);
+			hres.value.setReset(adst);
+			hres.index = hres.neverIndex = false;
+
+		}
+		assert(!(hres.index && hres.neverIndex), "Invalid header indexing information");
+
+		if(!update) dst.put(hres);
+	}
+}
+
+private void setReset(I,R)(ref I dst, ref R buf)
+	if(is(R == AllocAppender!string) || is(R == AllocAppender!(immutable(ubyte)[])))
+{
+	dst = buf.data;
+	buf.reset;
+}
+
+private size_t decodeInteger(I)(ref I src, ubyte bbuf, uint nbits) @safe @nogc
+{
+	auto res = bbuf.toInteger(8-nbits);
+
+	if (res < (1 << nbits) - 1) {
+		return res;
+	} else {
+		uint m = 0;
+		do {
+			// take another octet
+			bbuf = src[0];
+			src = src[1..$];
+			// concatenate it to the result
+			res = res + bbuf.toInteger(1)*(1 << m);
+			m += 7;
+		} while((bbuf & 128) == 128);
+		return res;
+	}
+}
+
+private void decodeLiteral(I,R)(ref I src, ref R dst) @safe
+{
+ 	enforceHPACK(!src.empty, "Invalid literal header block");
+
+	ubyte bbuf = src[0];
+	src = src[1..$];
+
+	bool huffman = (bbuf & 128) ? true : false;
+
+	assert(!src.empty, "Cannot decode from empty range block");
+
+	// take a buffer of remaining octets
+	auto vlen = decodeInteger(src, bbuf, 7); // value length
+	enforceHPACK(vlen <= src.length, "Invalid literal decoded");
+
+	auto buf = src[0..vlen];
+	src = src[vlen..$];
+
+	if(huffman) { // huffman encoded
+		decodeHuffman(buf, dst);
+	} else { // raw encoded
+		dst.put(cast(string)buf);
+	}
+}

+ 126 - 0
vtest/source/vibe/http/internal/http2/hpack/encoder.d

@@ -0,0 +1,126 @@
+module vibe.http.internal.http2.hpack.encoder;
+
+import vibe.http.internal.http2.hpack.tables;
+import vibe.http.internal.http2.hpack.huffman;
+import vibe.http.internal.http2.hpack.util;
+
+import std.range;
+import std.typecons;
+import std.conv;
+import std.array;
+
+void encode(R)(HTTP2HeaderTableField header, ref R dst, ref IndexingTable table, bool huffman = true)
+@safe
+{
+	// try to encode as integer
+	bool indexed = encodeInteger(header, dst, table, huffman);
+	// if fail, encode as literal
+	if(!indexed) encodeLiteral(header, dst, huffman);
+}
+
+/// encode a pure integer (present in table) or integer name + literal value
+private bool encodeInteger(R)(const HTTP2HeaderTableField header, ref R dst, ref IndexingTable table, bool huffman = true)
+@trusted
+{
+	// check table for indexed headers
+	size_t idx = 1;
+	bool found = false;
+	size_t partialFound = false;
+
+	while(idx < table.size) {
+		// encode both name / value as index
+		auto h = table[idx];
+		if(h.name == header.name && h.value == header.value) {
+			found = true;
+			partialFound = false;
+			break;
+			// encode name as index, value as literal
+		} else if(h.name == header.name && h.value != header.value) {
+			found = false;
+			partialFound = idx;
+		}
+		idx++;
+	}
+
+	if(found) {
+		if(idx < 127) { // can be fit in one octet
+			dst.put(cast(ubyte)(idx ^ 128));
+		} else { 		// must be split in multiple octets
+			dst.put(cast(ubyte)255);
+			idx -= 127;
+			while (idx > 127) {
+				dst.put(cast(ubyte)((idx % 128) ^ 128));
+				idx = idx / 128;
+			}
+			dst.put(cast(ubyte)(idx & 127));
+		}
+		return true;
+
+	} else if(partialFound) {
+		// encode name as index ( always smaller than 64 )
+		if(header.index) dst.put(cast(ubyte)((partialFound + 64) & 127));
+		else if (header.neverIndex) dst.put(cast(ubyte)((partialFound + 16) & 31));
+		else dst.put(cast(ubyte)(partialFound & 15));
+		// encode value as literal
+		encodeLiteralField(to!string(header.value), dst, huffman);
+
+		return true;
+	}
+
+	return false;
+}
+
+/// encode a literal field depending on its indexing requirements
+private void encodeLiteral(R)(const HTTP2HeaderTableField header, ref R dst, bool huffman = true)
+@safe
+{
+	if(header.index) dst.put(cast(ubyte)(64));
+	else if(header.neverIndex) dst.put(cast(ubyte)(16));
+	else dst.put(cast(ubyte)(0));
+
+	encodeLiteralField(to!string(header.name), dst, huffman);
+	encodeLiteralField(to!string(header.value), dst, huffman);
+}
+
+/// encode a field (name / value) using huffman or raw encoding
+private void encodeLiteralField(R)(string src, ref R dst, bool huffman = true) @safe
+{
+	if(huffman) {
+		encodeHuffman(src, dst);
+	} else {
+		auto blen = (src.length) & 127;
+		dst.put(cast(ubyte)blen);
+		dst.put(cast(ubyte[])(to!string(src).dup));
+	}
+}
+
+unittest {
+	// encode integer
+	import vibe.internal.array : BatchBuffer;
+	import vibe.http.common;
+	auto table = IndexingTable(4096);
+
+	BatchBuffer!(ubyte, 1) bres;
+	bres.putN(1);
+	ubyte[1] expected = [0x82];
+	auto hint = HTTP2HeaderTableField(":method", HTTPMethod.GET);
+
+	assert(encodeInteger(hint, bres, table));
+	assert(bres.peekDst == expected);
+}
+
+unittest {
+	// encode literal
+	// custom-key: custom-header
+	import vibe.internal.array : BatchBuffer;
+	ubyte[26] lexpected = [0x40, 0x0a, 0x63, 0x75,  0x73, 0x74,  0x6f, 0x6d,  0x2d, 0x6b,
+		0x65, 0x79, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64,
+		0x65, 0x72];
+
+	BatchBuffer!(ubyte, 26) lres;
+	lres.putN(26);
+	auto hlit = HTTP2HeaderTableField("custom-key", "custom-header");
+
+	encodeLiteral(hlit, lres, false);
+	assert(lres.peekDst == lexpected);
+}

+ 30 - 0
vtest/source/vibe/http/internal/http2/hpack/exception.d

@@ -0,0 +1,30 @@
+module vibe.http.internal.http2.hpack.exception;
+
+import std.exception;
+
+T enforceHPACK(T)(T condition, string message = null, string file = __FILE__,
+		typeof(__LINE__) line = __LINE__) @safe
+{
+	return enforce(condition, new HPACKException(message, file, line));
+}
+
+class HPACKException : Exception
+{
+	this(string msg, string file = __FILE__, size_t line = __LINE__) @safe {
+		super(msg, file, line);
+	}
+}
+
+class HPACKDecoderException : HPACKException
+{
+	this(string msg, string file = __FILE__, size_t line = __LINE__) {
+		super(msg, file, line);
+	}
+}
+
+class HPACKEncoderException : HPACKException
+{
+	this(string msg, string file = __FILE__, size_t line = __LINE__) {
+		super(msg, file, line);
+	}
+}

+ 264 - 0
vtest/source/vibe/http/internal/http2/hpack/hpack.d

@@ -0,0 +1,264 @@
+//module vibe.http.internal.hpack.hpack;
+module vibe.http.internal.http2.hpack.hpack;
+
+import vibe.http.internal.http2.hpack.encoder;
+import vibe.http.internal.http2.hpack.decoder;
+import vibe.http.internal.http2.hpack.tables;
+
+import std.range;
+import std.typecons;
+import std.array; // appender
+import std.algorithm.iteration;
+import vibe.container.internal.utilallocator: RegionListAllocator;
+
+
+/// interface for the HPACK encoder
+void encodeHPACK(I,R)(I src, ref R dst, ref IndexingTable table, bool huffman = true) @safe
+	if(is(I == HTTP2HeaderTableField) || is(ElementType!I : HTTP2HeaderTableField))
+{
+	static if(is(I == HTTP2HeaderTableField)) {
+		src.encode(dst, table, huffman);
+	} else if(is(ElementType!I : HTTP2HeaderTableField)){
+		src.each!(h => h.encode(dst, table, huffman));
+	}
+}
+
+void decodeHPACK(I,R,T)(I src, ref R dst, ref IndexingTable table, ref T alloc, uint maxTableSize = 4096) @safe
+	if(isInputRange!I && (is(ElementType!I : immutable(ubyte)) || is(ElementType!I : immutable(char))))
+{
+	while(!src.empty) src.decode(dst, table, alloc, maxTableSize);
+}
+
+/// ENCODER
+unittest {
+	//// Following examples can be found in Appendix C of the HPACK RFC
+	import vibe.http.status;
+	import vibe.http.common;
+	import std.experimental.allocator;
+	import std.experimental.allocator.gc_allocator;
+
+	auto table = IndexingTable(4096);
+	scope alloc = new RegionListAllocator!(shared(GCAllocator), false)(1024, GCAllocator.instance);
+
+	/** 1. Literal header field w. indexing (raw)
+	  * custom-key: custom-header
+	  */
+	HTTP2HeaderTableField h1 = HTTP2HeaderTableField("custom-key", "custom-header");
+	auto e1 = appender!(ubyte[]);
+	auto dec1 = appender!(HTTP2HeaderTableField[]);
+
+	h1.encodeHPACK(e1, table, false);
+	decodeHPACK(cast(immutable(ubyte)[])e1.data, dec1, table, alloc);
+	assert(dec1.data.front == h1);
+
+	/** 1bis. Literal header field w. indexing (huffman encoded)
+	  * :authority: www.example.com
+	  */
+	table.insert(HTTP2HeaderTableField(":authority", "www.example.com"));
+	HTTP2HeaderTableField h1b = HTTP2HeaderTableField(":authority", "www.example.com");
+	h1b.neverIndex = false;
+	h1b.index = true;
+	auto e1b = appender!(ubyte[]);
+	auto dec1b = appender!(HTTP2HeaderTableField[]);
+
+	h1b.encodeHPACK(e1b, table, true);
+	decodeHPACK(cast(immutable(ubyte)[])e1b.data, dec1b, table, alloc);
+	assert(dec1b.data.front == h1b);
+
+	/** 2. Literal header field without indexing (raw)
+	  * :path: /sample/path
+	  */
+	auto h2 = HTTP2HeaderTableField(":path", "/sample/path");
+	h2.neverIndex = false;
+	h2.index = false;
+	// initialize with huffman=false (can be modified by e2.huffman)
+	auto e2 = appender!(ubyte[]);
+	auto dec2 = appender!(HTTP2HeaderTableField[]);
+
+	h2.encodeHPACK(e2, table, false);
+	decodeHPACK(cast(immutable(ubyte)[])e2.data, dec2, table, alloc);
+	assert(dec2.data.front == h2);
+
+	/** 3. Literal header field never indexed (raw)
+	  * password: secret
+	  */
+	HTTP2HeaderTableField h3 = HTTP2HeaderTableField("password", "secret");
+	h3.neverIndex = true;
+	h3.index = false;
+	auto e3 = appender!(ubyte[]);
+	auto dec3 = appender!(HTTP2HeaderTableField[]);
+
+	h3.encodeHPACK(e3, table, false);
+	decodeHPACK(cast(immutable(ubyte)[])e3.data, dec3, table, alloc);
+	assert(dec3.data.front == h3);
+
+	/** 4. Indexed header field (integer)
+	  * :method: GET
+	  */
+	HTTP2HeaderTableField h4 = HTTP2HeaderTableField(":method", HTTPMethod.GET);
+	auto e4 = appender!(ubyte[]);
+	auto dec4 = appender!(HTTP2HeaderTableField[]);
+
+	h4.encodeHPACK(e4, table);
+	decodeHPACK(cast(immutable(ubyte)[])e4.data, dec4, table, alloc);
+	assert(dec4.data.front == h4);
+
+	/** 5. Full request without huffman encoding
+	  * :method: GET
+      * :scheme: http
+      * :path: /
+      * :authority: www.example.com
+      * cache-control: no-cache
+	  */
+	HTTP2HeaderTableField[] block = [
+		HTTP2HeaderTableField(":method", HTTPMethod.GET),
+		HTTP2HeaderTableField(":scheme", "http"),
+		HTTP2HeaderTableField(":path", "/"),
+		HTTP2HeaderTableField(":authority", "www.example.com"),
+		HTTP2HeaderTableField("cache-control", "no-cache")
+	];
+
+	ubyte[14] expected = [0x82, 0x86, 0x84, 0xbe, 0x58, 0x08, 0x6e, 0x6f, 0x2d, 0x63, 0x61, 0x63, 0x68, 0x65];
+	auto bres = appender!(ubyte[]);
+	block.encodeHPACK(bres, table, false);
+	assert(bres.data == expected);
+
+	/** 5. Full request with huffman encoding
+	  * :method: GET
+      * :scheme: http
+      * :path: /
+      * :authority: www.example.com
+      * cache-control: no-cache
+	  */
+	ubyte[12] eexpected = [0x82, 0x86, 0x84, 0xbe, 0x58, 0x86, 0xa8, 0xeb, 0x10, 0x64, 0x9c, 0xbf];
+	auto bbres = appender!(ubyte[]);
+	block.encodeHPACK(bbres, table, true);
+	assert(bbres.data == eexpected);
+}
+
+/// DECODER
+unittest {
+	//// Following examples can be found in Appendix C of the HPACK RFC
+
+	import std.experimental.allocator;
+	import std.experimental.allocator.gc_allocator;
+
+	auto table = IndexingTable(4096);
+	scope alloc = new RegionListAllocator!(shared(GCAllocator), false)(1024, GCAllocator.instance);
+
+	/** 1. Literal header field w. indexing (raw)
+	  * custom-key: custom-header
+	  */
+	immutable(ubyte)[] block = [0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79,
+		0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72];
+
+	//auto decoder = HeaderDecoder!(ubyte[])(block, table);
+	auto dec1 = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(dec1, table, alloc);
+	assert(dec1.data.front.name == "custom-key" && dec1.data.front.value == "custom-header");
+	// check entries to be inserted in the indexing table (dynamic)
+	assert(dec1.data.front.index);
+
+	/** 1bis. Literal header field w. indexing (huffman encoded)
+	  * :authority: www.example.com
+	  */
+	block = [0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff];
+	auto dec1b = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(dec1b, table, alloc);
+	assert(dec1b.data.front.name == ":authority" && dec1b.data.front.value == "www.example.com");
+	assert(dec1b.data.front.index);
+
+	/** 2. Literal header field without indexing (raw)
+	  * :path: /sample/path
+	  */
+	block = [0x04, 0x0c, 0x2f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x70, 0x61, 0x74, 0x68];
+	auto dec2 = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(dec2, table, alloc);
+	assert(dec2.data.front.name == ":path" && dec2.data.front.value == "/sample/path");
+
+
+	/** 3. Literal header field never indexed (raw)
+	  * password: secret
+	  */
+	block = [0x10, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x06, 0x73, 0x65,
+		  0x63, 0x72, 0x65, 0x74];
+	auto dec3 = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(dec3, table, alloc);
+	assert(dec3.data.front.name == "password" && dec3.data.front.value == "secret");
+	assert(dec3.data.front.neverIndex);
+
+
+	/** 4. Indexed header field (integer)
+	  * :method: GET
+	  */
+	import vibe.http.common;
+	block = [0x82];
+	auto dec4 = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(dec4, table, alloc);
+	assert(dec4.data.front.name == ":method" && dec4.data.front.value == HTTPMethod.GET);
+
+	/** 5. Full request without huffman encoding
+	  * :method: GET
+      * :scheme: http
+      * :path: /
+      * :authority: www.example.com
+      * cache-control: no-cache
+	  */
+	block = [0x82, 0x86, 0x84, 0xbe, 0x58, 0x08, 0x6e, 0x6f, 0x2d, 0x63, 0x61, 0x63, 0x68, 0x65];
+	table.insert(HTTP2HeaderTableField(":authority", "www.example.com"));
+	auto decR1 = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(decR1, table, alloc);
+	HTTP2HeaderTableField[] expected = [
+		HTTP2HeaderTableField(":method", HTTPMethod.GET),
+		HTTP2HeaderTableField(":scheme", "http"),
+		HTTP2HeaderTableField(":path", "/"),
+		HTTP2HeaderTableField(":authority", "www.example.com"),
+		HTTP2HeaderTableField("cache-control", "no-cache")];
+
+	foreach(i,h; decR1.data.enumerate(0)) {
+		assert(h == expected[i]);
+	}
+
+	/** 5. Full request with huffman encoding
+	  * :method: GET
+	  * :scheme: http
+	  * :path: /
+	  * :authority: www.example.com
+	  * cache-control: no-cache
+	  */
+	block = [0x82, 0x86, 0x84, 0xbe, 0x58, 0x86, 0xa8, 0xeb, 0x10, 0x64, 0x9c,0xbf];
+	auto decR2 = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(decR2, table, alloc);
+
+	foreach(i,h; decR2.data.enumerate(0)) {
+		assert(h == expected[i]);
+	}
+
+	/** Cookie header
+	  * cookie: filter=downloading
+	*/
+	auto ckexp = HTTP2HeaderTableField("cookie", "filter=downloading");
+	block = [96, 141, 148, 212, 36, 182, 65, 33, 252, 85, 65, 199, 33, 170, 155];
+	auto ckdec = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(ckdec, table, alloc);
+	assert(ckdec.data.front == ckexp);
+}
+
+
+/// Mallocator
+unittest {
+	import std.experimental.allocator;
+	import std.experimental.allocator.mallocator;
+	import std.experimental.allocator.gc_allocator;
+	auto table = IndexingTable(4096);
+	/** 1bis. Literal header field w. indexing (huffman encoded)
+	  * :authority: www.example.com
+	  */
+	immutable(ubyte)[] block = [0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff];
+	scope alloc = new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance);
+
+	auto dec1b = appender!(HTTP2HeaderTableField[]);
+	block.decodeHPACK(dec1b, table, alloc);
+	assert(dec1b.data.front.name == ":authority" && dec1b.data.front.value == "www.example.com");
+	assert(dec1b.data.front.index);
+}

+ 2807 - 0
vtest/source/vibe/http/internal/http2/hpack/huffman.d

@@ -0,0 +1,2807 @@
+module vibe.http.internal.http2.hpack.huffman;
+
+import vibe.http.internal.http2.hpack.exception;
+
+import std.range;
+
+/** Huffman encoding for HPACK header compression
+  * The huffman table specifications can be found at:
+  * Appendix B of RFC 7541: https://tools.ietf.org/html/rfc7541#appendix-B
+*/
+void encodeHuffman(I, O)(I source, ref O dst) @safe
+{
+	ulong bits;
+	int bitsLeft = 40;
+	size_t len = 0;
+
+	// compute length
+	foreach(c; source) {
+		auto e = HuffEncodeCodes[c];
+		len += e.length;
+	}
+
+	dst.put(cast(ubyte)((len/8 ^ 128) + 1));
+
+	foreach(c; source) {
+		auto e = HuffEncodeCodes[c];
+		bits |= (cast(ulong)e.code) << (bitsLeft - e.length);
+		bitsLeft -= e.length;
+		while(bitsLeft <= 32) {
+			dst.put(cast(ubyte)(bits >> 32));
+			bits <<= 8;
+			bitsLeft += 8;
+		}
+	}
+
+	if(bitsLeft != 40) {
+		bits |= ((cast(ulong)1) << bitsLeft) - 1;
+		dst.put(cast(ubyte)(bits >> 32));
+	}
+}
+
+@nogc unittest {
+	import vibe.internal.array : BatchBuffer;
+
+	string src = "www.example.com";
+	BatchBuffer!(ubyte, 13) bres;
+	bres.putN(13);
+	ubyte[13] expected = [0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff];
+	encodeHuffman(src, bres);
+	assert(bres.peekDst == expected);
+}
+
+/** Huffman decoding for HPACK header compression
+  * The huffman table specifications can be found at:
+  * Appendix B of RFC 7541: https://tools.ietf.org/html/rfc7541#appendix-B
+*/
+void decodeHuffman(I, O)(I source, ref O dst) @safe
+{
+	auto block = cast(immutable(ubyte)[])source;
+
+	char state = 0;
+	char eos = true; // termination flag
+
+	while(!block.empty) {
+		char ch = block[0];
+		block.popFront();
+		decodeSymbol(dst, state, ch >> 4, eos);
+		decodeSymbol(dst, state, ch & 0xf, eos);
+	}
+
+	enforceHPACK(eos, "Invalid encoded source");
+}
+
+private void decodeSymbol(O)(ref O decoded, ref char state, int pos, ref char eos)
+@safe
+{
+	enforceHPACK(state < 256 && pos < 16, "Invalid entry reference");
+	auto entry = HuffDecodeCodes[state][pos];
+	enforceHPACK(entry.next != state, "Invalid symbol");
+
+	if (entry.emit) { // if the symbol is terminal
+		auto sym = cast(immutable(char))entry.symbol;
+		decoded.put(sym);
+	}
+
+	state = entry.next;
+	eos = entry.ending;
+}
+
+unittest {
+	import std.array;
+	immutable ubyte[] test = [0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff];
+
+	auto dst = appender!string;
+	decodeHuffman(test, dst);
+	assert(dst.data == "www.example.com");
+
+	immutable char[] ctest = [0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff];
+	auto dst2 = appender!string;
+	decodeHuffman(ctest, dst2);
+	assert(dst2.data == "www.example.com");
+}
+
+unittest { // could be @nogc (exceptions aren't)
+	import vibe.internal.array : BatchBuffer;
+
+	immutable ubyte[12] src = [0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff];
+	BatchBuffer!(char, 15) bres;
+	bres.putN(15);
+	decodeHuffman(src, bres);
+	assert(bres.peekDst == "www.example.com");
+}
+
+private struct HuffCode {
+	char next;
+	char emit;
+	char symbol;
+	char ending;
+}
+
+private struct HuffEncCode {
+	uint code;
+	size_t length;
+}
+
+private static immutable HuffEncCode[256] HuffEncodeCodes = [
+    {0x00001ff8, 13}, {0x007fffd8, 23}, {0x0fffffe2, 28}, {0x0fffffe3, 28},
+    {0x0fffffe4, 28}, {0x0fffffe5, 28}, {0x0fffffe6, 28}, {0x0fffffe7, 28},
+    {0x0fffffe8, 28}, {0x00ffffea, 24}, {0x3ffffffc, 30}, {0x0fffffe9, 28},
+    {0x0fffffea, 28}, {0x3ffffffd, 30}, {0x0fffffeb, 28}, {0x0fffffec, 28},
+    {0x0fffffed, 28}, {0x0fffffee, 28}, {0x0fffffef, 28}, {0x0ffffff0, 28},
+    {0x0ffffff1, 28}, {0x0ffffff2, 28}, {0x3ffffffe, 30}, {0x0ffffff3, 28},
+    {0x0ffffff4, 28}, {0x0ffffff5, 28}, {0x0ffffff6, 28}, {0x0ffffff7, 28},
+    {0x0ffffff8, 28}, {0x0ffffff9, 28}, {0x0ffffffa, 28}, {0x0ffffffb, 28},
+    {0x00000014,  6}, {0x000003f8, 10}, {0x000003f9, 10}, {0x00000ffa, 12},
+    {0x00001ff9, 13}, {0x00000015,  6}, {0x000000f8,  8}, {0x000007fa, 11},
+    {0x000003fa, 10}, {0x000003fb, 10}, {0x000000f9,  8}, {0x000007fb, 11},
+    {0x000000fa,  8}, {0x00000016,  6}, {0x00000017,  6}, {0x00000018,  6},
+    {0x00000000,  5}, {0x00000001,  5}, {0x00000002,  5}, {0x00000019,  6},
+    {0x0000001a,  6}, {0x0000001b,  6}, {0x0000001c,  6}, {0x0000001d,  6},
+    {0x0000001e,  6}, {0x0000001f,  6}, {0x0000005c,  7}, {0x000000fb,  8},
+    {0x00007ffc, 15}, {0x00000020,  6}, {0x00000ffb, 12}, {0x000003fc, 10},
+    {0x00001ffa, 13}, {0x00000021,  6}, {0x0000005d,  7}, {0x0000005e,  7},
+    {0x0000005f,  7}, {0x00000060,  7}, {0x00000061,  7}, {0x00000062,  7},
+    {0x00000063,  7}, {0x00000064,  7}, {0x00000065,  7}, {0x00000066,  7},
+    {0x00000067,  7}, {0x00000068,  7}, {0x00000069,  7}, {0x0000006a,  7},
+    {0x0000006b,  7}, {0x0000006c,  7}, {0x0000006d,  7}, {0x0000006e,  7},
+    {0x0000006f,  7}, {0x00000070,  7}, {0x00000071,  7}, {0x00000072,  7},
+    {0x000000fc,  8}, {0x00000073,  7}, {0x000000fd,  8}, {0x00001ffb, 13},
+    {0x0007fff0, 19}, {0x00001ffc, 13}, {0x00003ffc, 14}, {0x00000022,  6},
+    {0x00007ffd, 15}, {0x00000003,  5}, {0x00000023,  6}, {0x00000004,  5},
+    {0x00000024,  6}, {0x00000005,  5}, {0x00000025,  6}, {0x00000026,  6},
+    {0x00000027,  6}, {0x00000006,  5}, {0x00000074,  7}, {0x00000075,  7},
+    {0x00000028,  6}, {0x00000029,  6}, {0x0000002a,  6}, {0x00000007,  5},
+    {0x0000002b,  6}, {0x00000076,  7}, {0x0000002c,  6}, {0x00000008,  5},
+    {0x00000009,  5}, {0x0000002d,  6}, {0x00000077,  7}, {0x00000078,  7},
+    {0x00000079,  7}, {0x0000007a,  7}, {0x0000007b,  7}, {0x00007ffe, 15},
+    {0x000007fc, 11}, {0x00003ffd, 14}, {0x00001ffd, 13}, {0x0ffffffc, 28},
+    {0x000fffe6, 20}, {0x003fffd2, 22}, {0x000fffe7, 20}, {0x000fffe8, 20},
+    {0x003fffd3, 22}, {0x003fffd4, 22}, {0x003fffd5, 22}, {0x007fffd9, 23},
+    {0x003fffd6, 22}, {0x007fffda, 23}, {0x007fffdb, 23}, {0x007fffdc, 23},
+    {0x007fffdd, 23}, {0x007fffde, 23}, {0x00ffffeb, 24}, {0x007fffdf, 23},
+    {0x00ffffec, 24}, {0x00ffffed, 24}, {0x003fffd7, 22}, {0x007fffe0, 23},
+    {0x00ffffee, 24}, {0x007fffe1, 23}, {0x007fffe2, 23}, {0x007fffe3, 23},
+    {0x007fffe4, 23}, {0x001fffdc, 21}, {0x003fffd8, 22}, {0x007fffe5, 23},
+    {0x003fffd9, 22}, {0x007fffe6, 23}, {0x007fffe7, 23}, {0x00ffffef, 24},
+    {0x003fffda, 22}, {0x001fffdd, 21}, {0x000fffe9, 20}, {0x003fffdb, 22},
+    {0x003fffdc, 22}, {0x007fffe8, 23}, {0x007fffe9, 23}, {0x001fffde, 21},
+    {0x007fffea, 23}, {0x003fffdd, 22}, {0x003fffde, 22}, {0x00fffff0, 24},
+    {0x001fffdf, 21}, {0x003fffdf, 22}, {0x007fffeb, 23}, {0x007fffec, 23},
+    {0x001fffe0, 21}, {0x001fffe1, 21}, {0x003fffe0, 22}, {0x001fffe2, 21},
+    {0x007fffed, 23}, {0x003fffe1, 22}, {0x007fffee, 23}, {0x007fffef, 23},
+    {0x000fffea, 20}, {0x003fffe2, 22}, {0x003fffe3, 22}, {0x003fffe4, 22},
+    {0x007ffff0, 23}, {0x003fffe5, 22}, {0x003fffe6, 22}, {0x007ffff1, 23},
+    {0x03ffffe0, 26}, {0x03ffffe1, 26}, {0x000fffeb, 20}, {0x0007fff1, 19},
+    {0x003fffe7, 22}, {0x007ffff2, 23}, {0x003fffe8, 22}, {0x01ffffec, 25},
+    {0x03ffffe2, 26}, {0x03ffffe3, 26}, {0x03ffffe4, 26}, {0x07ffffde, 27},
+    {0x07ffffdf, 27}, {0x03ffffe5, 26}, {0x00fffff1, 24}, {0x01ffffed, 25},
+    {0x0007fff2, 19}, {0x001fffe3, 21}, {0x03ffffe6, 26}, {0x07ffffe0, 27},
+    {0x07ffffe1, 27}, {0x03ffffe7, 26}, {0x07ffffe2, 27}, {0x00fffff2, 24},
+    {0x001fffe4, 21}, {0x001fffe5, 21}, {0x03ffffe8, 26}, {0x03ffffe9, 26},
+    {0x0ffffffd, 28}, {0x07ffffe3, 27}, {0x07ffffe4, 27}, {0x07ffffe5, 27},
+    {0x000fffec, 20}, {0x00fffff3, 24}, {0x000fffed, 20}, {0x001fffe6, 21},
+    {0x003fffe9, 22}, {0x001fffe7, 21}, {0x001fffe8, 21}, {0x007ffff3, 23},
+    {0x003fffea, 22}, {0x003fffeb, 22}, {0x01ffffee, 25}, {0x01ffffef, 25},
+    {0x00fffff4, 24}, {0x00fffff5, 24}, {0x03ffffea, 26}, {0x007ffff4, 23},
+    {0x03ffffeb, 26}, {0x07ffffe6, 27}, {0x03ffffec, 26}, {0x03ffffed, 26},
+    {0x07ffffe7, 27}, {0x07ffffe8, 27}, {0x07ffffe9, 27}, {0x07ffffea, 27},
+    {0x07ffffeb, 27}, {0x0ffffffe, 28}, {0x07ffffec, 27}, {0x07ffffed, 27},
+    {0x07ffffee, 27}, {0x07ffffef, 27}, {0x07fffff0, 27}, {0x03ffffee, 26}
+];
+
+private static immutable HuffCode[16][256] HuffDecodeCodes = [
+	/* 0 */
+	[
+// 		 next  emit  sym  ending   next  emit  sym  ending
+		{0x04, 0x00, 0x00, 0x00}, {0x05, 0x00, 0x00, 0x00},
+		{0x07, 0x00, 0x00, 0x00}, {0x08, 0x00, 0x00, 0x00},
+		{0x0b, 0x00, 0x00, 0x00}, {0x0c, 0x00, 0x00, 0x00},
+		{0x10, 0x00, 0x00, 0x00}, {0x13, 0x00, 0x00, 0x00},
+		{0x19, 0x00, 0x00, 0x00}, {0x1c, 0x00, 0x00, 0x00},
+		{0x20, 0x00, 0x00, 0x00}, {0x23, 0x00, 0x00, 0x00},
+		{0x2a, 0x00, 0x00, 0x00}, {0x31, 0x00, 0x00, 0x00},
+		{0x39, 0x00, 0x00, 0x00}, {0x40, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0x30, 0x01}, {0x00, 0x01, 0x31, 0x01},
+		{0x00, 0x01, 0x32, 0x01}, {0x00, 0x01, 0x61, 0x01},
+		{0x00, 0x01, 0x63, 0x01}, {0x00, 0x01, 0x65, 0x01},
+		{0x00, 0x01, 0x69, 0x01}, {0x00, 0x01, 0x6f, 0x01},
+		{0x00, 0x01, 0x73, 0x01}, {0x00, 0x01, 0x74, 0x01},
+		{0x0d, 0x00, 0x00, 0x00}, {0x0e, 0x00, 0x00, 0x00},
+		{0x11, 0x00, 0x00, 0x00}, {0x12, 0x00, 0x00, 0x00},
+		{0x14, 0x00, 0x00, 0x00}, {0x15, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x01, 0x01, 0x30, 0x00}, {0x16, 0x01, 0x30, 0x01},
+		{0x01, 0x01, 0x31, 0x00}, {0x16, 0x01, 0x31, 0x01},
+		{0x01, 0x01, 0x32, 0x00}, {0x16, 0x01, 0x32, 0x01},
+		{0x01, 0x01, 0x61, 0x00}, {0x16, 0x01, 0x61, 0x01},
+		{0x01, 0x01, 0x63, 0x00}, {0x16, 0x01, 0x63, 0x01},
+		{0x01, 0x01, 0x65, 0x00}, {0x16, 0x01, 0x65, 0x01},
+		{0x01, 0x01, 0x69, 0x00}, {0x16, 0x01, 0x69, 0x01},
+		{0x01, 0x01, 0x6f, 0x00}, {0x16, 0x01, 0x6f, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x30, 0x00}, {0x09, 0x01, 0x30, 0x00},
+		{0x17, 0x01, 0x30, 0x00}, {0x28, 0x01, 0x30, 0x01},
+		{0x02, 0x01, 0x31, 0x00}, {0x09, 0x01, 0x31, 0x00},
+		{0x17, 0x01, 0x31, 0x00}, {0x28, 0x01, 0x31, 0x01},
+		{0x02, 0x01, 0x32, 0x00}, {0x09, 0x01, 0x32, 0x00},
+		{0x17, 0x01, 0x32, 0x00}, {0x28, 0x01, 0x32, 0x01},
+		{0x02, 0x01, 0x61, 0x00}, {0x09, 0x01, 0x61, 0x00},
+		{0x17, 0x01, 0x61, 0x00}, {0x28, 0x01, 0x61, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x30, 0x00}, {0x06, 0x01, 0x30, 0x00},
+		{0x0a, 0x01, 0x30, 0x00}, {0x0f, 0x01, 0x30, 0x00},
+		{0x18, 0x01, 0x30, 0x00}, {0x1f, 0x01, 0x30, 0x00},
+		{0x29, 0x01, 0x30, 0x00}, {0x38, 0x01, 0x30, 0x01},
+		{0x03, 0x01, 0x31, 0x00}, {0x06, 0x01, 0x31, 0x00},
+		{0x0a, 0x01, 0x31, 0x00}, {0x0f, 0x01, 0x31, 0x00},
+		{0x18, 0x01, 0x31, 0x00}, {0x1f, 0x01, 0x31, 0x00},
+		{0x29, 0x01, 0x31, 0x00}, {0x38, 0x01, 0x31, 0x01}
+	],
+	/* 5 */
+	[
+		{0x03, 0x01, 0x32, 0x00}, {0x06, 0x01, 0x32, 0x00},
+		{0x0a, 0x01, 0x32, 0x00}, {0x0f, 0x01, 0x32, 0x00},
+		{0x18, 0x01, 0x32, 0x00}, {0x1f, 0x01, 0x32, 0x00},
+		{0x29, 0x01, 0x32, 0x00}, {0x38, 0x01, 0x32, 0x01},
+		{0x03, 0x01, 0x61, 0x00}, {0x06, 0x01, 0x61, 0x00},
+		{0x0a, 0x01, 0x61, 0x00}, {0x0f, 0x01, 0x61, 0x00},
+		{0x18, 0x01, 0x61, 0x00}, {0x1f, 0x01, 0x61, 0x00},
+		{0x29, 0x01, 0x61, 0x00}, {0x38, 0x01, 0x61, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x63, 0x00}, {0x09, 0x01, 0x63, 0x00},
+		{0x17, 0x01, 0x63, 0x00}, {0x28, 0x01, 0x63, 0x01},
+		{0x02, 0x01, 0x65, 0x00}, {0x09, 0x01, 0x65, 0x00},
+		{0x17, 0x01, 0x65, 0x00}, {0x28, 0x01, 0x65, 0x01},
+		{0x02, 0x01, 0x69, 0x00}, {0x09, 0x01, 0x69, 0x00},
+		{0x17, 0x01, 0x69, 0x00}, {0x28, 0x01, 0x69, 0x01},
+		{0x02, 0x01, 0x6f, 0x00}, {0x09, 0x01, 0x6f, 0x00},
+		{0x17, 0x01, 0x6f, 0x00}, {0x28, 0x01, 0x6f, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x63, 0x00}, {0x06, 0x01, 0x63, 0x00},
+		{0x0a, 0x01, 0x63, 0x00}, {0x0f, 0x01, 0x63, 0x00},
+		{0x18, 0x01, 0x63, 0x00}, {0x1f, 0x01, 0x63, 0x00},
+		{0x29, 0x01, 0x63, 0x00}, {0x38, 0x01, 0x63, 0x01},
+		{0x03, 0x01, 0x65, 0x00}, {0x06, 0x01, 0x65, 0x00},
+		{0x0a, 0x01, 0x65, 0x00}, {0x0f, 0x01, 0x65, 0x00},
+		{0x18, 0x01, 0x65, 0x00}, {0x1f, 0x01, 0x65, 0x00},
+		{0x29, 0x01, 0x65, 0x00}, {0x38, 0x01, 0x65, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x69, 0x00}, {0x06, 0x01, 0x69, 0x00},
+		{0x0a, 0x01, 0x69, 0x00}, {0x0f, 0x01, 0x69, 0x00},
+		{0x18, 0x01, 0x69, 0x00}, {0x1f, 0x01, 0x69, 0x00},
+		{0x29, 0x01, 0x69, 0x00}, {0x38, 0x01, 0x69, 0x01},
+		{0x03, 0x01, 0x6f, 0x00}, {0x06, 0x01, 0x6f, 0x00},
+		{0x0a, 0x01, 0x6f, 0x00}, {0x0f, 0x01, 0x6f, 0x00},
+		{0x18, 0x01, 0x6f, 0x00}, {0x1f, 0x01, 0x6f, 0x00},
+		{0x29, 0x01, 0x6f, 0x00}, {0x38, 0x01, 0x6f, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x73, 0x00}, {0x16, 0x01, 0x73, 0x01},
+		{0x01, 0x01, 0x74, 0x00}, {0x16, 0x01, 0x74, 0x01},
+		{0x00, 0x01, 0x20, 0x01}, {0x00, 0x01, 0x25, 0x01},
+		{0x00, 0x01, 0x2d, 0x01}, {0x00, 0x01, 0x2e, 0x01},
+		{0x00, 0x01, 0x2f, 0x01}, {0x00, 0x01, 0x33, 0x01},
+		{0x00, 0x01, 0x34, 0x01}, {0x00, 0x01, 0x35, 0x01},
+		{0x00, 0x01, 0x36, 0x01}, {0x00, 0x01, 0x37, 0x01},
+		{0x00, 0x01, 0x38, 0x01}, {0x00, 0x01, 0x39, 0x01}
+	],
+	/* 10 */
+	[
+		{0x02, 0x01, 0x73, 0x00}, {0x09, 0x01, 0x73, 0x00},
+		{0x17, 0x01, 0x73, 0x00}, {0x28, 0x01, 0x73, 0x01},
+		{0x02, 0x01, 0x74, 0x00}, {0x09, 0x01, 0x74, 0x00},
+		{0x17, 0x01, 0x74, 0x00}, {0x28, 0x01, 0x74, 0x01},
+		{0x01, 0x01, 0x20, 0x00}, {0x16, 0x01, 0x20, 0x01},
+		{0x01, 0x01, 0x25, 0x00}, {0x16, 0x01, 0x25, 0x01},
+		{0x01, 0x01, 0x2d, 0x00}, {0x16, 0x01, 0x2d, 0x01},
+		{0x01, 0x01, 0x2e, 0x00}, {0x16, 0x01, 0x2e, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x73, 0x00}, {0x06, 0x01, 0x73, 0x00},
+		{0x0a, 0x01, 0x73, 0x00}, {0x0f, 0x01, 0x73, 0x00},
+		{0x18, 0x01, 0x73, 0x00}, {0x1f, 0x01, 0x73, 0x00},
+		{0x29, 0x01, 0x73, 0x00}, {0x38, 0x01, 0x73, 0x01},
+		{0x03, 0x01, 0x74, 0x00}, {0x06, 0x01, 0x74, 0x00},
+		{0x0a, 0x01, 0x74, 0x00}, {0x0f, 0x01, 0x74, 0x00},
+		{0x18, 0x01, 0x74, 0x00}, {0x1f, 0x01, 0x74, 0x00},
+		{0x29, 0x01, 0x74, 0x00}, {0x38, 0x01, 0x74, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x20, 0x00}, {0x09, 0x01, 0x20, 0x00},
+		{0x17, 0x01, 0x20, 0x00}, {0x28, 0x01, 0x20, 0x01},
+		{0x02, 0x01, 0x25, 0x00}, {0x09, 0x01, 0x25, 0x00},
+		{0x17, 0x01, 0x25, 0x00}, {0x28, 0x01, 0x25, 0x01},
+		{0x02, 0x01, 0x2d, 0x00}, {0x09, 0x01, 0x2d, 0x00},
+		{0x17, 0x01, 0x2d, 0x00}, {0x28, 0x01, 0x2d, 0x01},
+		{0x02, 0x01, 0x2e, 0x00}, {0x09, 0x01, 0x2e, 0x00},
+		{0x17, 0x01, 0x2e, 0x00}, {0x28, 0x01, 0x2e, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x20, 0x00}, {0x06, 0x01, 0x20, 0x00},
+		{0x0a, 0x01, 0x20, 0x00}, {0x0f, 0x01, 0x20, 0x00},
+		{0x18, 0x01, 0x20, 0x00}, {0x1f, 0x01, 0x20, 0x00},
+		{0x29, 0x01, 0x20, 0x00}, {0x38, 0x01, 0x20, 0x01},
+		{0x03, 0x01, 0x25, 0x00}, {0x06, 0x01, 0x25, 0x00},
+		{0x0a, 0x01, 0x25, 0x00}, {0x0f, 0x01, 0x25, 0x00},
+		{0x18, 0x01, 0x25, 0x00}, {0x1f, 0x01, 0x25, 0x00},
+		{0x29, 0x01, 0x25, 0x00}, {0x38, 0x01, 0x25, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x2d, 0x00}, {0x06, 0x01, 0x2d, 0x00},
+		{0x0a, 0x01, 0x2d, 0x00}, {0x0f, 0x01, 0x2d, 0x00},
+		{0x18, 0x01, 0x2d, 0x00}, {0x1f, 0x01, 0x2d, 0x00},
+		{0x29, 0x01, 0x2d, 0x00}, {0x38, 0x01, 0x2d, 0x01},
+		{0x03, 0x01, 0x2e, 0x00}, {0x06, 0x01, 0x2e, 0x00},
+		{0x0a, 0x01, 0x2e, 0x00}, {0x0f, 0x01, 0x2e, 0x00},
+		{0x18, 0x01, 0x2e, 0x00}, {0x1f, 0x01, 0x2e, 0x00},
+		{0x29, 0x01, 0x2e, 0x00}, {0x38, 0x01, 0x2e, 0x01}
+	],
+	/* 15 */
+	[
+		{0x01, 0x01, 0x2f, 0x00}, {0x16, 0x01, 0x2f, 0x01},
+		{0x01, 0x01, 0x33, 0x00}, {0x16, 0x01, 0x33, 0x01},
+		{0x01, 0x01, 0x34, 0x00}, {0x16, 0x01, 0x34, 0x01},
+		{0x01, 0x01, 0x35, 0x00}, {0x16, 0x01, 0x35, 0x01},
+		{0x01, 0x01, 0x36, 0x00}, {0x16, 0x01, 0x36, 0x01},
+		{0x01, 0x01, 0x37, 0x00}, {0x16, 0x01, 0x37, 0x01},
+		{0x01, 0x01, 0x38, 0x00}, {0x16, 0x01, 0x38, 0x01},
+		{0x01, 0x01, 0x39, 0x00}, {0x16, 0x01, 0x39, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x2f, 0x00}, {0x09, 0x01, 0x2f, 0x00},
+		{0x17, 0x01, 0x2f, 0x00}, {0x28, 0x01, 0x2f, 0x01},
+		{0x02, 0x01, 0x33, 0x00}, {0x09, 0x01, 0x33, 0x00},
+		{0x17, 0x01, 0x33, 0x00}, {0x28, 0x01, 0x33, 0x01},
+		{0x02, 0x01, 0x34, 0x00}, {0x09, 0x01, 0x34, 0x00},
+		{0x17, 0x01, 0x34, 0x00}, {0x28, 0x01, 0x34, 0x01},
+		{0x02, 0x01, 0x35, 0x00}, {0x09, 0x01, 0x35, 0x00},
+		{0x17, 0x01, 0x35, 0x00}, {0x28, 0x01, 0x35, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x2f, 0x00}, {0x06, 0x01, 0x2f, 0x00},
+		{0x0a, 0x01, 0x2f, 0x00}, {0x0f, 0x01, 0x2f, 0x00},
+		{0x18, 0x01, 0x2f, 0x00}, {0x1f, 0x01, 0x2f, 0x00},
+		{0x29, 0x01, 0x2f, 0x00}, {0x38, 0x01, 0x2f, 0x01},
+		{0x03, 0x01, 0x33, 0x00}, {0x06, 0x01, 0x33, 0x00},
+		{0x0a, 0x01, 0x33, 0x00}, {0x0f, 0x01, 0x33, 0x00},
+		{0x18, 0x01, 0x33, 0x00}, {0x1f, 0x01, 0x33, 0x00},
+		{0x29, 0x01, 0x33, 0x00}, {0x38, 0x01, 0x33, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x34, 0x00}, {0x06, 0x01, 0x34, 0x00},
+		{0x0a, 0x01, 0x34, 0x00}, {0x0f, 0x01, 0x34, 0x00},
+		{0x18, 0x01, 0x34, 0x00}, {0x1f, 0x01, 0x34, 0x00},
+		{0x29, 0x01, 0x34, 0x00}, {0x38, 0x01, 0x34, 0x01},
+		{0x03, 0x01, 0x35, 0x00}, {0x06, 0x01, 0x35, 0x00},
+		{0x0a, 0x01, 0x35, 0x00}, {0x0f, 0x01, 0x35, 0x00},
+		{0x18, 0x01, 0x35, 0x00}, {0x1f, 0x01, 0x35, 0x00},
+		{0x29, 0x01, 0x35, 0x00}, {0x38, 0x01, 0x35, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x36, 0x00}, {0x09, 0x01, 0x36, 0x00},
+		{0x17, 0x01, 0x36, 0x00}, {0x28, 0x01, 0x36, 0x01},
+		{0x02, 0x01, 0x37, 0x00}, {0x09, 0x01, 0x37, 0x00},
+		{0x17, 0x01, 0x37, 0x00}, {0x28, 0x01, 0x37, 0x01},
+		{0x02, 0x01, 0x38, 0x00}, {0x09, 0x01, 0x38, 0x00},
+		{0x17, 0x01, 0x38, 0x00}, {0x28, 0x01, 0x38, 0x01},
+		{0x02, 0x01, 0x39, 0x00}, {0x09, 0x01, 0x39, 0x00},
+		{0x17, 0x01, 0x39, 0x00}, {0x28, 0x01, 0x39, 0x01}
+	],
+	/* 20 */
+	[
+		{0x03, 0x01, 0x36, 0x00}, {0x06, 0x01, 0x36, 0x00},
+		{0x0a, 0x01, 0x36, 0x00}, {0x0f, 0x01, 0x36, 0x00},
+		{0x18, 0x01, 0x36, 0x00}, {0x1f, 0x01, 0x36, 0x00},
+		{0x29, 0x01, 0x36, 0x00}, {0x38, 0x01, 0x36, 0x01},
+		{0x03, 0x01, 0x37, 0x00}, {0x06, 0x01, 0x37, 0x00},
+		{0x0a, 0x01, 0x37, 0x00}, {0x0f, 0x01, 0x37, 0x00},
+		{0x18, 0x01, 0x37, 0x00}, {0x1f, 0x01, 0x37, 0x00},
+		{0x29, 0x01, 0x37, 0x00}, {0x38, 0x01, 0x37, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x38, 0x00}, {0x06, 0x01, 0x38, 0x00},
+		{0x0a, 0x01, 0x38, 0x00}, {0x0f, 0x01, 0x38, 0x00},
+		{0x18, 0x01, 0x38, 0x00}, {0x1f, 0x01, 0x38, 0x00},
+		{0x29, 0x01, 0x38, 0x00}, {0x38, 0x01, 0x38, 0x01},
+		{0x03, 0x01, 0x39, 0x00}, {0x06, 0x01, 0x39, 0x00},
+		{0x0a, 0x01, 0x39, 0x00}, {0x0f, 0x01, 0x39, 0x00},
+		{0x18, 0x01, 0x39, 0x00}, {0x1f, 0x01, 0x39, 0x00},
+		{0x29, 0x01, 0x39, 0x00}, {0x38, 0x01, 0x39, 0x01}
+	],
+	[
+		{0x1a, 0x00, 0x00, 0x00}, {0x1b, 0x00, 0x00, 0x00},
+		{0x1d, 0x00, 0x00, 0x00}, {0x1e, 0x00, 0x00, 0x00},
+		{0x21, 0x00, 0x00, 0x00}, {0x22, 0x00, 0x00, 0x00},
+		{0x24, 0x00, 0x00, 0x00}, {0x25, 0x00, 0x00, 0x00},
+		{0x2b, 0x00, 0x00, 0x00}, {0x2e, 0x00, 0x00, 0x00},
+		{0x32, 0x00, 0x00, 0x00}, {0x35, 0x00, 0x00, 0x00},
+		{0x3a, 0x00, 0x00, 0x00}, {0x3d, 0x00, 0x00, 0x00},
+		{0x41, 0x00, 0x00, 0x00}, {0x44, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0x3d, 0x01}, {0x00, 0x01, 0x41, 0x01},
+		{0x00, 0x01, 0x5f, 0x01}, {0x00, 0x01, 0x62, 0x01},
+		{0x00, 0x01, 0x64, 0x01}, {0x00, 0x01, 0x66, 0x01},
+		{0x00, 0x01, 0x67, 0x01}, {0x00, 0x01, 0x68, 0x01},
+		{0x00, 0x01, 0x6c, 0x01}, {0x00, 0x01, 0x6d, 0x01},
+		{0x00, 0x01, 0x6e, 0x01}, {0x00, 0x01, 0x70, 0x01},
+		{0x00, 0x01, 0x72, 0x01}, {0x00, 0x01, 0x75, 0x01},
+		{0x26, 0x00, 0x00, 0x00}, {0x27, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x01, 0x01, 0x3d, 0x00}, {0x16, 0x01, 0x3d, 0x01},
+		{0x01, 0x01, 0x41, 0x00}, {0x16, 0x01, 0x41, 0x01},
+		{0x01, 0x01, 0x5f, 0x00}, {0x16, 0x01, 0x5f, 0x01},
+		{0x01, 0x01, 0x62, 0x00}, {0x16, 0x01, 0x62, 0x01},
+		{0x01, 0x01, 0x64, 0x00}, {0x16, 0x01, 0x64, 0x01},
+		{0x01, 0x01, 0x66, 0x00}, {0x16, 0x01, 0x66, 0x01},
+		{0x01, 0x01, 0x67, 0x00}, {0x16, 0x01, 0x67, 0x01},
+		{0x01, 0x01, 0x68, 0x00}, {0x16, 0x01, 0x68, 0x01}
+	],
+	/* 25 */
+	[
+		{0x02, 0x01, 0x3d, 0x00}, {0x09, 0x01, 0x3d, 0x00},
+		{0x17, 0x01, 0x3d, 0x00}, {0x28, 0x01, 0x3d, 0x01},
+		{0x02, 0x01, 0x41, 0x00}, {0x09, 0x01, 0x41, 0x00},
+		{0x17, 0x01, 0x41, 0x00}, {0x28, 0x01, 0x41, 0x01},
+		{0x02, 0x01, 0x5f, 0x00}, {0x09, 0x01, 0x5f, 0x00},
+		{0x17, 0x01, 0x5f, 0x00}, {0x28, 0x01, 0x5f, 0x01},
+		{0x02, 0x01, 0x62, 0x00}, {0x09, 0x01, 0x62, 0x00},
+		{0x17, 0x01, 0x62, 0x00}, {0x28, 0x01, 0x62, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x3d, 0x00}, {0x06, 0x01, 0x3d, 0x00},
+		{0x0a, 0x01, 0x3d, 0x00}, {0x0f, 0x01, 0x3d, 0x00},
+		{0x18, 0x01, 0x3d, 0x00}, {0x1f, 0x01, 0x3d, 0x00},
+		{0x29, 0x01, 0x3d, 0x00}, {0x38, 0x01, 0x3d, 0x01},
+		{0x03, 0x01, 0x41, 0x00}, {0x06, 0x01, 0x41, 0x00},
+		{0x0a, 0x01, 0x41, 0x00}, {0x0f, 0x01, 0x41, 0x00},
+		{0x18, 0x01, 0x41, 0x00}, {0x1f, 0x01, 0x41, 0x00},
+		{0x29, 0x01, 0x41, 0x00}, {0x38, 0x01, 0x41, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x5f, 0x00}, {0x06, 0x01, 0x5f, 0x00},
+		{0x0a, 0x01, 0x5f, 0x00}, {0x0f, 0x01, 0x5f, 0x00},
+		{0x18, 0x01, 0x5f, 0x00}, {0x1f, 0x01, 0x5f, 0x00},
+		{0x29, 0x01, 0x5f, 0x00}, {0x38, 0x01, 0x5f, 0x01},
+		{0x03, 0x01, 0x62, 0x00}, {0x06, 0x01, 0x62, 0x00},
+		{0x0a, 0x01, 0x62, 0x00}, {0x0f, 0x01, 0x62, 0x00},
+		{0x18, 0x01, 0x62, 0x00}, {0x1f, 0x01, 0x62, 0x00},
+		{0x29, 0x01, 0x62, 0x00}, {0x38, 0x01, 0x62, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x64, 0x00}, {0x09, 0x01, 0x64, 0x00},
+		{0x17, 0x01, 0x64, 0x00}, {0x28, 0x01, 0x64, 0x01},
+		{0x02, 0x01, 0x66, 0x00}, {0x09, 0x01, 0x66, 0x00},
+		{0x17, 0x01, 0x66, 0x00}, {0x28, 0x01, 0x66, 0x01},
+		{0x02, 0x01, 0x67, 0x00}, {0x09, 0x01, 0x67, 0x00},
+		{0x17, 0x01, 0x67, 0x00}, {0x28, 0x01, 0x67, 0x01},
+		{0x02, 0x01, 0x68, 0x00}, {0x09, 0x01, 0x68, 0x00},
+		{0x17, 0x01, 0x68, 0x00}, {0x28, 0x01, 0x68, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x64, 0x00}, {0x06, 0x01, 0x64, 0x00},
+		{0x0a, 0x01, 0x64, 0x00}, {0x0f, 0x01, 0x64, 0x00},
+		{0x18, 0x01, 0x64, 0x00}, {0x1f, 0x01, 0x64, 0x00},
+		{0x29, 0x01, 0x64, 0x00}, {0x38, 0x01, 0x64, 0x01},
+		{0x03, 0x01, 0x66, 0x00}, {0x06, 0x01, 0x66, 0x00},
+		{0x0a, 0x01, 0x66, 0x00}, {0x0f, 0x01, 0x66, 0x00},
+		{0x18, 0x01, 0x66, 0x00}, {0x1f, 0x01, 0x66, 0x00},
+		{0x29, 0x01, 0x66, 0x00}, {0x38, 0x01, 0x66, 0x01}
+	],
+	/* 30 */
+	[
+		{0x03, 0x01, 0x67, 0x00}, {0x06, 0x01, 0x67, 0x00},
+		{0x0a, 0x01, 0x67, 0x00}, {0x0f, 0x01, 0x67, 0x00},
+		{0x18, 0x01, 0x67, 0x00}, {0x1f, 0x01, 0x67, 0x00},
+		{0x29, 0x01, 0x67, 0x00}, {0x38, 0x01, 0x67, 0x01},
+		{0x03, 0x01, 0x68, 0x00}, {0x06, 0x01, 0x68, 0x00},
+		{0x0a, 0x01, 0x68, 0x00}, {0x0f, 0x01, 0x68, 0x00},
+		{0x18, 0x01, 0x68, 0x00}, {0x1f, 0x01, 0x68, 0x00},
+		{0x29, 0x01, 0x68, 0x00}, {0x38, 0x01, 0x68, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x6c, 0x00}, {0x16, 0x01, 0x6c, 0x01},
+		{0x01, 0x01, 0x6d, 0x00}, {0x16, 0x01, 0x6d, 0x01},
+		{0x01, 0x01, 0x6e, 0x00}, {0x16, 0x01, 0x6e, 0x01},
+		{0x01, 0x01, 0x70, 0x00}, {0x16, 0x01, 0x70, 0x01},
+		{0x01, 0x01, 0x72, 0x00}, {0x16, 0x01, 0x72, 0x01},
+		{0x01, 0x01, 0x75, 0x00}, {0x16, 0x01, 0x75, 0x01},
+		{0x00, 0x01, 0x3a, 0x01}, {0x00, 0x01, 0x42, 0x01},
+		{0x00, 0x01, 0x43, 0x01}, {0x00, 0x01, 0x44, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x6c, 0x00}, {0x09, 0x01, 0x6c, 0x00},
+		{0x17, 0x01, 0x6c, 0x00}, {0x28, 0x01, 0x6c, 0x01},
+		{0x02, 0x01, 0x6d, 0x00}, {0x09, 0x01, 0x6d, 0x00},
+		{0x17, 0x01, 0x6d, 0x00}, {0x28, 0x01, 0x6d, 0x01},
+		{0x02, 0x01, 0x6e, 0x00}, {0x09, 0x01, 0x6e, 0x00},
+		{0x17, 0x01, 0x6e, 0x00}, {0x28, 0x01, 0x6e, 0x01},
+		{0x02, 0x01, 0x70, 0x00}, {0x09, 0x01, 0x70, 0x00},
+		{0x17, 0x01, 0x70, 0x00}, {0x28, 0x01, 0x70, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x6c, 0x00}, {0x06, 0x01, 0x6c, 0x00},
+		{0x0a, 0x01, 0x6c, 0x00}, {0x0f, 0x01, 0x6c, 0x00},
+		{0x18, 0x01, 0x6c, 0x00}, {0x1f, 0x01, 0x6c, 0x00},
+		{0x29, 0x01, 0x6c, 0x00}, {0x38, 0x01, 0x6c, 0x01},
+		{0x03, 0x01, 0x6d, 0x00}, {0x06, 0x01, 0x6d, 0x00},
+		{0x0a, 0x01, 0x6d, 0x00}, {0x0f, 0x01, 0x6d, 0x00},
+		{0x18, 0x01, 0x6d, 0x00}, {0x1f, 0x01, 0x6d, 0x00},
+		{0x29, 0x01, 0x6d, 0x00}, {0x38, 0x01, 0x6d, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x6e, 0x00}, {0x06, 0x01, 0x6e, 0x00},
+		{0x0a, 0x01, 0x6e, 0x00}, {0x0f, 0x01, 0x6e, 0x00},
+		{0x18, 0x01, 0x6e, 0x00}, {0x1f, 0x01, 0x6e, 0x00},
+		{0x29, 0x01, 0x6e, 0x00}, {0x38, 0x01, 0x6e, 0x01},
+		{0x03, 0x01, 0x70, 0x00}, {0x06, 0x01, 0x70, 0x00},
+		{0x0a, 0x01, 0x70, 0x00}, {0x0f, 0x01, 0x70, 0x00},
+		{0x18, 0x01, 0x70, 0x00}, {0x1f, 0x01, 0x70, 0x00},
+		{0x29, 0x01, 0x70, 0x00}, {0x38, 0x01, 0x70, 0x01}
+	],
+	/* 35 */
+	[
+		{0x02, 0x01, 0x72, 0x00}, {0x09, 0x01, 0x72, 0x00},
+		{0x17, 0x01, 0x72, 0x00}, {0x28, 0x01, 0x72, 0x01},
+		{0x02, 0x01, 0x75, 0x00}, {0x09, 0x01, 0x75, 0x00},
+		{0x17, 0x01, 0x75, 0x00}, {0x28, 0x01, 0x75, 0x01},
+		{0x01, 0x01, 0x3a, 0x00}, {0x16, 0x01, 0x3a, 0x01},
+		{0x01, 0x01, 0x42, 0x00}, {0x16, 0x01, 0x42, 0x01},
+		{0x01, 0x01, 0x43, 0x00}, {0x16, 0x01, 0x43, 0x01},
+		{0x01, 0x01, 0x44, 0x00}, {0x16, 0x01, 0x44, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x72, 0x00}, {0x06, 0x01, 0x72, 0x00},
+		{0x0a, 0x01, 0x72, 0x00}, {0x0f, 0x01, 0x72, 0x00},
+		{0x18, 0x01, 0x72, 0x00}, {0x1f, 0x01, 0x72, 0x00},
+		{0x29, 0x01, 0x72, 0x00}, {0x38, 0x01, 0x72, 0x01},
+		{0x03, 0x01, 0x75, 0x00}, {0x06, 0x01, 0x75, 0x00},
+		{0x0a, 0x01, 0x75, 0x00}, {0x0f, 0x01, 0x75, 0x00},
+		{0x18, 0x01, 0x75, 0x00}, {0x1f, 0x01, 0x75, 0x00},
+		{0x29, 0x01, 0x75, 0x00}, {0x38, 0x01, 0x75, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x3a, 0x00}, {0x09, 0x01, 0x3a, 0x00},
+		{0x17, 0x01, 0x3a, 0x00}, {0x28, 0x01, 0x3a, 0x01},
+		{0x02, 0x01, 0x42, 0x00}, {0x09, 0x01, 0x42, 0x00},
+		{0x17, 0x01, 0x42, 0x00}, {0x28, 0x01, 0x42, 0x01},
+		{0x02, 0x01, 0x43, 0x00}, {0x09, 0x01, 0x43, 0x00},
+		{0x17, 0x01, 0x43, 0x00}, {0x28, 0x01, 0x43, 0x01},
+		{0x02, 0x01, 0x44, 0x00}, {0x09, 0x01, 0x44, 0x00},
+		{0x17, 0x01, 0x44, 0x00}, {0x28, 0x01, 0x44, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x3a, 0x00}, {0x06, 0x01, 0x3a, 0x00},
+		{0x0a, 0x01, 0x3a, 0x00}, {0x0f, 0x01, 0x3a, 0x00},
+		{0x18, 0x01, 0x3a, 0x00}, {0x1f, 0x01, 0x3a, 0x00},
+		{0x29, 0x01, 0x3a, 0x00}, {0x38, 0x01, 0x3a, 0x01},
+		{0x03, 0x01, 0x42, 0x00}, {0x06, 0x01, 0x42, 0x00},
+		{0x0a, 0x01, 0x42, 0x00}, {0x0f, 0x01, 0x42, 0x00},
+		{0x18, 0x01, 0x42, 0x00}, {0x1f, 0x01, 0x42, 0x00},
+		{0x29, 0x01, 0x42, 0x00}, {0x38, 0x01, 0x42, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x43, 0x00}, {0x06, 0x01, 0x43, 0x00},
+		{0x0a, 0x01, 0x43, 0x00}, {0x0f, 0x01, 0x43, 0x00},
+		{0x18, 0x01, 0x43, 0x00}, {0x1f, 0x01, 0x43, 0x00},
+		{0x29, 0x01, 0x43, 0x00}, {0x38, 0x01, 0x43, 0x01},
+		{0x03, 0x01, 0x44, 0x00}, {0x06, 0x01, 0x44, 0x00},
+		{0x0a, 0x01, 0x44, 0x00}, {0x0f, 0x01, 0x44, 0x00},
+		{0x18, 0x01, 0x44, 0x00}, {0x1f, 0x01, 0x44, 0x00},
+		{0x29, 0x01, 0x44, 0x00}, {0x38, 0x01, 0x44, 0x01}
+	],
+	/* 40 */
+	[
+		{0x2c, 0x00, 0x00, 0x00}, {0x2d, 0x00, 0x00, 0x00},
+		{0x2f, 0x00, 0x00, 0x00}, {0x30, 0x00, 0x00, 0x00},
+		{0x33, 0x00, 0x00, 0x00}, {0x34, 0x00, 0x00, 0x00},
+		{0x36, 0x00, 0x00, 0x00}, {0x37, 0x00, 0x00, 0x00},
+		{0x3b, 0x00, 0x00, 0x00}, {0x3c, 0x00, 0x00, 0x00},
+		{0x3e, 0x00, 0x00, 0x00}, {0x3f, 0x00, 0x00, 0x00},
+		{0x42, 0x00, 0x00, 0x00}, {0x43, 0x00, 0x00, 0x00},
+		{0x45, 0x00, 0x00, 0x00}, {0x48, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0x45, 0x01}, {0x00, 0x01, 0x46, 0x01},
+		{0x00, 0x01, 0x47, 0x01}, {0x00, 0x01, 0x48, 0x01},
+		{0x00, 0x01, 0x49, 0x01}, {0x00, 0x01, 0x4a, 0x01},
+		{0x00, 0x01, 0x4b, 0x01}, {0x00, 0x01, 0x4c, 0x01},
+		{0x00, 0x01, 0x4d, 0x01}, {0x00, 0x01, 0x4e, 0x01},
+		{0x00, 0x01, 0x4f, 0x01}, {0x00, 0x01, 0x50, 0x01},
+		{0x00, 0x01, 0x51, 0x01}, {0x00, 0x01, 0x52, 0x01},
+		{0x00, 0x01, 0x53, 0x01}, {0x00, 0x01, 0x54, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x45, 0x00}, {0x16, 0x01, 0x45, 0x01},
+		{0x01, 0x01, 0x46, 0x00}, {0x16, 0x01, 0x46, 0x01},
+		{0x01, 0x01, 0x47, 0x00}, {0x16, 0x01, 0x47, 0x01},
+		{0x01, 0x01, 0x48, 0x00}, {0x16, 0x01, 0x48, 0x01},
+		{0x01, 0x01, 0x49, 0x00}, {0x16, 0x01, 0x49, 0x01},
+		{0x01, 0x01, 0x4a, 0x00}, {0x16, 0x01, 0x4a, 0x01},
+		{0x01, 0x01, 0x4b, 0x00}, {0x16, 0x01, 0x4b, 0x01},
+		{0x01, 0x01, 0x4c, 0x00}, {0x16, 0x01, 0x4c, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x45, 0x00}, {0x09, 0x01, 0x45, 0x00},
+		{0x17, 0x01, 0x45, 0x00}, {0x28, 0x01, 0x45, 0x01},
+		{0x02, 0x01, 0x46, 0x00}, {0x09, 0x01, 0x46, 0x00},
+		{0x17, 0x01, 0x46, 0x00}, {0x28, 0x01, 0x46, 0x01},
+		{0x02, 0x01, 0x47, 0x00}, {0x09, 0x01, 0x47, 0x00},
+		{0x17, 0x01, 0x47, 0x00}, {0x28, 0x01, 0x47, 0x01},
+		{0x02, 0x01, 0x48, 0x00}, {0x09, 0x01, 0x48, 0x00},
+		{0x17, 0x01, 0x48, 0x00}, {0x28, 0x01, 0x48, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x45, 0x00}, {0x06, 0x01, 0x45, 0x00},
+		{0x0a, 0x01, 0x45, 0x00}, {0x0f, 0x01, 0x45, 0x00},
+		{0x18, 0x01, 0x45, 0x00}, {0x1f, 0x01, 0x45, 0x00},
+		{0x29, 0x01, 0x45, 0x00}, {0x38, 0x01, 0x45, 0x01},
+		{0x03, 0x01, 0x46, 0x00}, {0x06, 0x01, 0x46, 0x00},
+		{0x0a, 0x01, 0x46, 0x00}, {0x0f, 0x01, 0x46, 0x00},
+		{0x18, 0x01, 0x46, 0x00}, {0x1f, 0x01, 0x46, 0x00},
+		{0x29, 0x01, 0x46, 0x00}, {0x38, 0x01, 0x46, 0x01}
+	],
+	/* 45 */
+	[
+		{0x03, 0x01, 0x47, 0x00}, {0x06, 0x01, 0x47, 0x00},
+		{0x0a, 0x01, 0x47, 0x00}, {0x0f, 0x01, 0x47, 0x00},
+		{0x18, 0x01, 0x47, 0x00}, {0x1f, 0x01, 0x47, 0x00},
+		{0x29, 0x01, 0x47, 0x00}, {0x38, 0x01, 0x47, 0x01},
+		{0x03, 0x01, 0x48, 0x00}, {0x06, 0x01, 0x48, 0x00},
+		{0x0a, 0x01, 0x48, 0x00}, {0x0f, 0x01, 0x48, 0x00},
+		{0x18, 0x01, 0x48, 0x00}, {0x1f, 0x01, 0x48, 0x00},
+		{0x29, 0x01, 0x48, 0x00}, {0x38, 0x01, 0x48, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x49, 0x00}, {0x09, 0x01, 0x49, 0x00},
+		{0x17, 0x01, 0x49, 0x00}, {0x28, 0x01, 0x49, 0x01},
+		{0x02, 0x01, 0x4a, 0x00}, {0x09, 0x01, 0x4a, 0x00},
+		{0x17, 0x01, 0x4a, 0x00}, {0x28, 0x01, 0x4a, 0x01},
+		{0x02, 0x01, 0x4b, 0x00}, {0x09, 0x01, 0x4b, 0x00},
+		{0x17, 0x01, 0x4b, 0x00}, {0x28, 0x01, 0x4b, 0x01},
+		{0x02, 0x01, 0x4c, 0x00}, {0x09, 0x01, 0x4c, 0x00},
+		{0x17, 0x01, 0x4c, 0x00}, {0x28, 0x01, 0x4c, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x49, 0x00}, {0x06, 0x01, 0x49, 0x00},
+		{0x0a, 0x01, 0x49, 0x00}, {0x0f, 0x01, 0x49, 0x00},
+		{0x18, 0x01, 0x49, 0x00}, {0x1f, 0x01, 0x49, 0x00},
+		{0x29, 0x01, 0x49, 0x00}, {0x38, 0x01, 0x49, 0x01},
+		{0x03, 0x01, 0x4a, 0x00}, {0x06, 0x01, 0x4a, 0x00},
+		{0x0a, 0x01, 0x4a, 0x00}, {0x0f, 0x01, 0x4a, 0x00},
+		{0x18, 0x01, 0x4a, 0x00}, {0x1f, 0x01, 0x4a, 0x00},
+		{0x29, 0x01, 0x4a, 0x00}, {0x38, 0x01, 0x4a, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x4b, 0x00}, {0x06, 0x01, 0x4b, 0x00},
+		{0x0a, 0x01, 0x4b, 0x00}, {0x0f, 0x01, 0x4b, 0x00},
+		{0x18, 0x01, 0x4b, 0x00}, {0x1f, 0x01, 0x4b, 0x00},
+		{0x29, 0x01, 0x4b, 0x00}, {0x38, 0x01, 0x4b, 0x01},
+		{0x03, 0x01, 0x4c, 0x00}, {0x06, 0x01, 0x4c, 0x00},
+		{0x0a, 0x01, 0x4c, 0x00}, {0x0f, 0x01, 0x4c, 0x00},
+		{0x18, 0x01, 0x4c, 0x00}, {0x1f, 0x01, 0x4c, 0x00},
+		{0x29, 0x01, 0x4c, 0x00}, {0x38, 0x01, 0x4c, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x4d, 0x00}, {0x16, 0x01, 0x4d, 0x01},
+		{0x01, 0x01, 0x4e, 0x00}, {0x16, 0x01, 0x4e, 0x01},
+		{0x01, 0x01, 0x4f, 0x00}, {0x16, 0x01, 0x4f, 0x01},
+		{0x01, 0x01, 0x50, 0x00}, {0x16, 0x01, 0x50, 0x01},
+		{0x01, 0x01, 0x51, 0x00}, {0x16, 0x01, 0x51, 0x01},
+		{0x01, 0x01, 0x52, 0x00}, {0x16, 0x01, 0x52, 0x01},
+		{0x01, 0x01, 0x53, 0x00}, {0x16, 0x01, 0x53, 0x01},
+		{0x01, 0x01, 0x54, 0x00}, {0x16, 0x01, 0x54, 0x01}
+	],
+	/* 50 */
+	[
+		{0x02, 0x01, 0x4d, 0x00}, {0x09, 0x01, 0x4d, 0x00},
+		{0x17, 0x01, 0x4d, 0x00}, {0x28, 0x01, 0x4d, 0x01},
+		{0x02, 0x01, 0x4e, 0x00}, {0x09, 0x01, 0x4e, 0x00},
+		{0x17, 0x01, 0x4e, 0x00}, {0x28, 0x01, 0x4e, 0x01},
+		{0x02, 0x01, 0x4f, 0x00}, {0x09, 0x01, 0x4f, 0x00},
+		{0x17, 0x01, 0x4f, 0x00}, {0x28, 0x01, 0x4f, 0x01},
+		{0x02, 0x01, 0x50, 0x00}, {0x09, 0x01, 0x50, 0x00},
+		{0x17, 0x01, 0x50, 0x00}, {0x28, 0x01, 0x50, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x4d, 0x00}, {0x06, 0x01, 0x4d, 0x00},
+		{0x0a, 0x01, 0x4d, 0x00}, {0x0f, 0x01, 0x4d, 0x00},
+		{0x18, 0x01, 0x4d, 0x00}, {0x1f, 0x01, 0x4d, 0x00},
+		{0x29, 0x01, 0x4d, 0x00}, {0x38, 0x01, 0x4d, 0x01},
+		{0x03, 0x01, 0x4e, 0x00}, {0x06, 0x01, 0x4e, 0x00},
+		{0x0a, 0x01, 0x4e, 0x00}, {0x0f, 0x01, 0x4e, 0x00},
+		{0x18, 0x01, 0x4e, 0x00}, {0x1f, 0x01, 0x4e, 0x00},
+		{0x29, 0x01, 0x4e, 0x00}, {0x38, 0x01, 0x4e, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x4f, 0x00}, {0x06, 0x01, 0x4f, 0x00},
+		{0x0a, 0x01, 0x4f, 0x00}, {0x0f, 0x01, 0x4f, 0x00},
+		{0x18, 0x01, 0x4f, 0x00}, {0x1f, 0x01, 0x4f, 0x00},
+		{0x29, 0x01, 0x4f, 0x00}, {0x38, 0x01, 0x4f, 0x01},
+		{0x03, 0x01, 0x50, 0x00}, {0x06, 0x01, 0x50, 0x00},
+		{0x0a, 0x01, 0x50, 0x00}, {0x0f, 0x01, 0x50, 0x00},
+		{0x18, 0x01, 0x50, 0x00}, {0x1f, 0x01, 0x50, 0x00},
+		{0x29, 0x01, 0x50, 0x00}, {0x38, 0x01, 0x50, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x51, 0x00}, {0x09, 0x01, 0x51, 0x00},
+		{0x17, 0x01, 0x51, 0x00}, {0x28, 0x01, 0x51, 0x01},
+		{0x02, 0x01, 0x52, 0x00}, {0x09, 0x01, 0x52, 0x00},
+		{0x17, 0x01, 0x52, 0x00}, {0x28, 0x01, 0x52, 0x01},
+		{0x02, 0x01, 0x53, 0x00}, {0x09, 0x01, 0x53, 0x00},
+		{0x17, 0x01, 0x53, 0x00}, {0x28, 0x01, 0x53, 0x01},
+		{0x02, 0x01, 0x54, 0x00}, {0x09, 0x01, 0x54, 0x00},
+		{0x17, 0x01, 0x54, 0x00}, {0x28, 0x01, 0x54, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x51, 0x00}, {0x06, 0x01, 0x51, 0x00},
+		{0x0a, 0x01, 0x51, 0x00}, {0x0f, 0x01, 0x51, 0x00},
+		{0x18, 0x01, 0x51, 0x00}, {0x1f, 0x01, 0x51, 0x00},
+		{0x29, 0x01, 0x51, 0x00}, {0x38, 0x01, 0x51, 0x01},
+		{0x03, 0x01, 0x52, 0x00}, {0x06, 0x01, 0x52, 0x00},
+		{0x0a, 0x01, 0x52, 0x00}, {0x0f, 0x01, 0x52, 0x00},
+		{0x18, 0x01, 0x52, 0x00}, {0x1f, 0x01, 0x52, 0x00},
+		{0x29, 0x01, 0x52, 0x00}, {0x38, 0x01, 0x52, 0x01}
+	],
+	/* 55 */
+	[
+		{0x03, 0x01, 0x53, 0x00}, {0x06, 0x01, 0x53, 0x00},
+		{0x0a, 0x01, 0x53, 0x00}, {0x0f, 0x01, 0x53, 0x00},
+		{0x18, 0x01, 0x53, 0x00}, {0x1f, 0x01, 0x53, 0x00},
+		{0x29, 0x01, 0x53, 0x00}, {0x38, 0x01, 0x53, 0x01},
+		{0x03, 0x01, 0x54, 0x00}, {0x06, 0x01, 0x54, 0x00},
+		{0x0a, 0x01, 0x54, 0x00}, {0x0f, 0x01, 0x54, 0x00},
+		{0x18, 0x01, 0x54, 0x00}, {0x1f, 0x01, 0x54, 0x00},
+		{0x29, 0x01, 0x54, 0x00}, {0x38, 0x01, 0x54, 0x01}
+	],
+	[
+		{0x00, 0x01, 0x55, 0x01}, {0x00, 0x01, 0x56, 0x01},
+		{0x00, 0x01, 0x57, 0x01}, {0x00, 0x01, 0x59, 0x01},
+		{0x00, 0x01, 0x6a, 0x01}, {0x00, 0x01, 0x6b, 0x01},
+		{0x00, 0x01, 0x71, 0x01}, {0x00, 0x01, 0x76, 0x01},
+		{0x00, 0x01, 0x77, 0x01}, {0x00, 0x01, 0x78, 0x01},
+		{0x00, 0x01, 0x79, 0x01}, {0x00, 0x01, 0x7a, 0x01},
+		{0x46, 0x00, 0x00, 0x00}, {0x47, 0x00, 0x00, 0x00},
+		{0x49, 0x00, 0x00, 0x00}, {0x4a, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x55, 0x00}, {0x16, 0x01, 0x55, 0x01},
+		{0x01, 0x01, 0x56, 0x00}, {0x16, 0x01, 0x56, 0x01},
+		{0x01, 0x01, 0x57, 0x00}, {0x16, 0x01, 0x57, 0x01},
+		{0x01, 0x01, 0x59, 0x00}, {0x16, 0x01, 0x59, 0x01},
+		{0x01, 0x01, 0x6a, 0x00}, {0x16, 0x01, 0x6a, 0x01},
+		{0x01, 0x01, 0x6b, 0x00}, {0x16, 0x01, 0x6b, 0x01},
+		{0x01, 0x01, 0x71, 0x00}, {0x16, 0x01, 0x71, 0x01},
+		{0x01, 0x01, 0x76, 0x00}, {0x16, 0x01, 0x76, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x55, 0x00}, {0x09, 0x01, 0x55, 0x00},
+		{0x17, 0x01, 0x55, 0x00}, {0x28, 0x01, 0x55, 0x01},
+		{0x02, 0x01, 0x56, 0x00}, {0x09, 0x01, 0x56, 0x00},
+		{0x17, 0x01, 0x56, 0x00}, {0x28, 0x01, 0x56, 0x01},
+		{0x02, 0x01, 0x57, 0x00}, {0x09, 0x01, 0x57, 0x00},
+		{0x17, 0x01, 0x57, 0x00}, {0x28, 0x01, 0x57, 0x01},
+		{0x02, 0x01, 0x59, 0x00}, {0x09, 0x01, 0x59, 0x00},
+		{0x17, 0x01, 0x59, 0x00}, {0x28, 0x01, 0x59, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x55, 0x00}, {0x06, 0x01, 0x55, 0x00},
+		{0x0a, 0x01, 0x55, 0x00}, {0x0f, 0x01, 0x55, 0x00},
+		{0x18, 0x01, 0x55, 0x00}, {0x1f, 0x01, 0x55, 0x00},
+		{0x29, 0x01, 0x55, 0x00}, {0x38, 0x01, 0x55, 0x01},
+		{0x03, 0x01, 0x56, 0x00}, {0x06, 0x01, 0x56, 0x00},
+		{0x0a, 0x01, 0x56, 0x00}, {0x0f, 0x01, 0x56, 0x00},
+		{0x18, 0x01, 0x56, 0x00}, {0x1f, 0x01, 0x56, 0x00},
+		{0x29, 0x01, 0x56, 0x00}, {0x38, 0x01, 0x56, 0x01}
+	],
+	/* 60 */
+	[
+		{0x03, 0x01, 0x57, 0x00}, {0x06, 0x01, 0x57, 0x00},
+		{0x0a, 0x01, 0x57, 0x00}, {0x0f, 0x01, 0x57, 0x00},
+		{0x18, 0x01, 0x57, 0x00}, {0x1f, 0x01, 0x57, 0x00},
+		{0x29, 0x01, 0x57, 0x00}, {0x38, 0x01, 0x57, 0x01},
+		{0x03, 0x01, 0x59, 0x00}, {0x06, 0x01, 0x59, 0x00},
+		{0x0a, 0x01, 0x59, 0x00}, {0x0f, 0x01, 0x59, 0x00},
+		{0x18, 0x01, 0x59, 0x00}, {0x1f, 0x01, 0x59, 0x00},
+		{0x29, 0x01, 0x59, 0x00}, {0x38, 0x01, 0x59, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x6a, 0x00}, {0x09, 0x01, 0x6a, 0x00},
+		{0x17, 0x01, 0x6a, 0x00}, {0x28, 0x01, 0x6a, 0x01},
+		{0x02, 0x01, 0x6b, 0x00}, {0x09, 0x01, 0x6b, 0x00},
+		{0x17, 0x01, 0x6b, 0x00}, {0x28, 0x01, 0x6b, 0x01},
+		{0x02, 0x01, 0x71, 0x00}, {0x09, 0x01, 0x71, 0x00},
+		{0x17, 0x01, 0x71, 0x00}, {0x28, 0x01, 0x71, 0x01},
+		{0x02, 0x01, 0x76, 0x00}, {0x09, 0x01, 0x76, 0x00},
+		{0x17, 0x01, 0x76, 0x00}, {0x28, 0x01, 0x76, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x6a, 0x00}, {0x06, 0x01, 0x6a, 0x00},
+		{0x0a, 0x01, 0x6a, 0x00}, {0x0f, 0x01, 0x6a, 0x00},
+		{0x18, 0x01, 0x6a, 0x00}, {0x1f, 0x01, 0x6a, 0x00},
+		{0x29, 0x01, 0x6a, 0x00}, {0x38, 0x01, 0x6a, 0x01},
+		{0x03, 0x01, 0x6b, 0x00}, {0x06, 0x01, 0x6b, 0x00},
+		{0x0a, 0x01, 0x6b, 0x00}, {0x0f, 0x01, 0x6b, 0x00},
+		{0x18, 0x01, 0x6b, 0x00}, {0x1f, 0x01, 0x6b, 0x00},
+		{0x29, 0x01, 0x6b, 0x00}, {0x38, 0x01, 0x6b, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x71, 0x00}, {0x06, 0x01, 0x71, 0x00},
+		{0x0a, 0x01, 0x71, 0x00}, {0x0f, 0x01, 0x71, 0x00},
+		{0x18, 0x01, 0x71, 0x00}, {0x1f, 0x01, 0x71, 0x00},
+		{0x29, 0x01, 0x71, 0x00}, {0x38, 0x01, 0x71, 0x01},
+		{0x03, 0x01, 0x76, 0x00}, {0x06, 0x01, 0x76, 0x00},
+		{0x0a, 0x01, 0x76, 0x00}, {0x0f, 0x01, 0x76, 0x00},
+		{0x18, 0x01, 0x76, 0x00}, {0x1f, 0x01, 0x76, 0x00},
+		{0x29, 0x01, 0x76, 0x00}, {0x38, 0x01, 0x76, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x77, 0x00}, {0x16, 0x01, 0x77, 0x01},
+		{0x01, 0x01, 0x78, 0x00}, {0x16, 0x01, 0x78, 0x01},
+		{0x01, 0x01, 0x79, 0x00}, {0x16, 0x01, 0x79, 0x01},
+		{0x01, 0x01, 0x7a, 0x00}, {0x16, 0x01, 0x7a, 0x01},
+		{0x00, 0x01, 0x26, 0x01}, {0x00, 0x01, 0x2a, 0x01},
+		{0x00, 0x01, 0x2c, 0x01}, {0x00, 0x01, 0x3b, 0x01},
+		{0x00, 0x01, 0x58, 0x01}, {0x00, 0x01, 0x5a, 0x01},
+		{0x4b, 0x00, 0x00, 0x00}, {0x4e, 0x00, 0x00, 0x01}
+	],
+	/* 65 */
+	[
+		{0x02, 0x01, 0x77, 0x00}, {0x09, 0x01, 0x77, 0x00},
+		{0x17, 0x01, 0x77, 0x00}, {0x28, 0x01, 0x77, 0x01},
+		{0x02, 0x01, 0x78, 0x00}, {0x09, 0x01, 0x78, 0x00},
+		{0x17, 0x01, 0x78, 0x00}, {0x28, 0x01, 0x78, 0x01},
+		{0x02, 0x01, 0x79, 0x00}, {0x09, 0x01, 0x79, 0x00},
+		{0x17, 0x01, 0x79, 0x00}, {0x28, 0x01, 0x79, 0x01},
+		{0x02, 0x01, 0x7a, 0x00}, {0x09, 0x01, 0x7a, 0x00},
+		{0x17, 0x01, 0x7a, 0x00}, {0x28, 0x01, 0x7a, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x77, 0x00}, {0x06, 0x01, 0x77, 0x00},
+		{0x0a, 0x01, 0x77, 0x00}, {0x0f, 0x01, 0x77, 0x00},
+		{0x18, 0x01, 0x77, 0x00}, {0x1f, 0x01, 0x77, 0x00},
+		{0x29, 0x01, 0x77, 0x00}, {0x38, 0x01, 0x77, 0x01},
+		{0x03, 0x01, 0x78, 0x00}, {0x06, 0x01, 0x78, 0x00},
+		{0x0a, 0x01, 0x78, 0x00}, {0x0f, 0x01, 0x78, 0x00},
+		{0x18, 0x01, 0x78, 0x00}, {0x1f, 0x01, 0x78, 0x00},
+		{0x29, 0x01, 0x78, 0x00}, {0x38, 0x01, 0x78, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x79, 0x00}, {0x06, 0x01, 0x79, 0x00},
+		{0x0a, 0x01, 0x79, 0x00}, {0x0f, 0x01, 0x79, 0x00},
+		{0x18, 0x01, 0x79, 0x00}, {0x1f, 0x01, 0x79, 0x00},
+		{0x29, 0x01, 0x79, 0x00}, {0x38, 0x01, 0x79, 0x01},
+		{0x03, 0x01, 0x7a, 0x00}, {0x06, 0x01, 0x7a, 0x00},
+		{0x0a, 0x01, 0x7a, 0x00}, {0x0f, 0x01, 0x7a, 0x00},
+		{0x18, 0x01, 0x7a, 0x00}, {0x1f, 0x01, 0x7a, 0x00},
+		{0x29, 0x01, 0x7a, 0x00}, {0x38, 0x01, 0x7a, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x26, 0x00}, {0x16, 0x01, 0x26, 0x01},
+		{0x01, 0x01, 0x2a, 0x00}, {0x16, 0x01, 0x2a, 0x01},
+		{0x01, 0x01, 0x2c, 0x00}, {0x16, 0x01, 0x2c, 0x01},
+		{0x01, 0x01, 0x3b, 0x00}, {0x16, 0x01, 0x3b, 0x01},
+		{0x01, 0x01, 0x58, 0x00}, {0x16, 0x01, 0x58, 0x01},
+		{0x01, 0x01, 0x5a, 0x00}, {0x16, 0x01, 0x5a, 0x01},
+		{0x4c, 0x00, 0x00, 0x00}, {0x4d, 0x00, 0x00, 0x00},
+		{0x4f, 0x00, 0x00, 0x00}, {0x51, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x26, 0x00}, {0x09, 0x01, 0x26, 0x00},
+		{0x17, 0x01, 0x26, 0x00}, {0x28, 0x01, 0x26, 0x01},
+		{0x02, 0x01, 0x2a, 0x00}, {0x09, 0x01, 0x2a, 0x00},
+		{0x17, 0x01, 0x2a, 0x00}, {0x28, 0x01, 0x2a, 0x01},
+		{0x02, 0x01, 0x2c, 0x00}, {0x09, 0x01, 0x2c, 0x00},
+		{0x17, 0x01, 0x2c, 0x00}, {0x28, 0x01, 0x2c, 0x01},
+		{0x02, 0x01, 0x3b, 0x00}, {0x09, 0x01, 0x3b, 0x00},
+		{0x17, 0x01, 0x3b, 0x00}, {0x28, 0x01, 0x3b, 0x01}
+	],
+	/* 70 */
+	[
+		{0x03, 0x01, 0x26, 0x00}, {0x06, 0x01, 0x26, 0x00},
+		{0x0a, 0x01, 0x26, 0x00}, {0x0f, 0x01, 0x26, 0x00},
+		{0x18, 0x01, 0x26, 0x00}, {0x1f, 0x01, 0x26, 0x00},
+		{0x29, 0x01, 0x26, 0x00}, {0x38, 0x01, 0x26, 0x01},
+		{0x03, 0x01, 0x2a, 0x00}, {0x06, 0x01, 0x2a, 0x00},
+		{0x0a, 0x01, 0x2a, 0x00}, {0x0f, 0x01, 0x2a, 0x00},
+		{0x18, 0x01, 0x2a, 0x00}, {0x1f, 0x01, 0x2a, 0x00},
+		{0x29, 0x01, 0x2a, 0x00}, {0x38, 0x01, 0x2a, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x2c, 0x00}, {0x06, 0x01, 0x2c, 0x00},
+		{0x0a, 0x01, 0x2c, 0x00}, {0x0f, 0x01, 0x2c, 0x00},
+		{0x18, 0x01, 0x2c, 0x00}, {0x1f, 0x01, 0x2c, 0x00},
+		{0x29, 0x01, 0x2c, 0x00}, {0x38, 0x01, 0x2c, 0x01},
+		{0x03, 0x01, 0x3b, 0x00}, {0x06, 0x01, 0x3b, 0x00},
+		{0x0a, 0x01, 0x3b, 0x00}, {0x0f, 0x01, 0x3b, 0x00},
+		{0x18, 0x01, 0x3b, 0x00}, {0x1f, 0x01, 0x3b, 0x00},
+		{0x29, 0x01, 0x3b, 0x00}, {0x38, 0x01, 0x3b, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x58, 0x00}, {0x09, 0x01, 0x58, 0x00},
+		{0x17, 0x01, 0x58, 0x00}, {0x28, 0x01, 0x58, 0x01},
+		{0x02, 0x01, 0x5a, 0x00}, {0x09, 0x01, 0x5a, 0x00},
+		{0x17, 0x01, 0x5a, 0x00}, {0x28, 0x01, 0x5a, 0x01},
+		{0x00, 0x01, 0x21, 0x01}, {0x00, 0x01, 0x22, 0x01},
+		{0x00, 0x01, 0x28, 0x01}, {0x00, 0x01, 0x29, 0x01},
+		{0x00, 0x01, 0x3f, 0x01}, {0x50, 0x00, 0x00, 0x00},
+		{0x52, 0x00, 0x00, 0x00}, {0x54, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x58, 0x00}, {0x06, 0x01, 0x58, 0x00},
+		{0x0a, 0x01, 0x58, 0x00}, {0x0f, 0x01, 0x58, 0x00},
+		{0x18, 0x01, 0x58, 0x00}, {0x1f, 0x01, 0x58, 0x00},
+		{0x29, 0x01, 0x58, 0x00}, {0x38, 0x01, 0x58, 0x01},
+		{0x03, 0x01, 0x5a, 0x00}, {0x06, 0x01, 0x5a, 0x00},
+		{0x0a, 0x01, 0x5a, 0x00}, {0x0f, 0x01, 0x5a, 0x00},
+		{0x18, 0x01, 0x5a, 0x00}, {0x1f, 0x01, 0x5a, 0x00},
+		{0x29, 0x01, 0x5a, 0x00}, {0x38, 0x01, 0x5a, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x21, 0x00}, {0x16, 0x01, 0x21, 0x01},
+		{0x01, 0x01, 0x22, 0x00}, {0x16, 0x01, 0x22, 0x01},
+		{0x01, 0x01, 0x28, 0x00}, {0x16, 0x01, 0x28, 0x01},
+		{0x01, 0x01, 0x29, 0x00}, {0x16, 0x01, 0x29, 0x01},
+		{0x01, 0x01, 0x3f, 0x00}, {0x16, 0x01, 0x3f, 0x01},
+		{0x00, 0x01, 0x27, 0x01}, {0x00, 0x01, 0x2b, 0x01},
+		{0x00, 0x01, 0x7c, 0x01}, {0x53, 0x00, 0x00, 0x00},
+		{0x55, 0x00, 0x00, 0x00}, {0x58, 0x00, 0x00, 0x01}
+	],
+	/* 75 */
+	[
+		{0x02, 0x01, 0x21, 0x00}, {0x09, 0x01, 0x21, 0x00},
+		{0x17, 0x01, 0x21, 0x00}, {0x28, 0x01, 0x21, 0x01},
+		{0x02, 0x01, 0x22, 0x00}, {0x09, 0x01, 0x22, 0x00},
+		{0x17, 0x01, 0x22, 0x00}, {0x28, 0x01, 0x22, 0x01},
+		{0x02, 0x01, 0x28, 0x00}, {0x09, 0x01, 0x28, 0x00},
+		{0x17, 0x01, 0x28, 0x00}, {0x28, 0x01, 0x28, 0x01},
+		{0x02, 0x01, 0x29, 0x00}, {0x09, 0x01, 0x29, 0x00},
+		{0x17, 0x01, 0x29, 0x00}, {0x28, 0x01, 0x29, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x21, 0x00}, {0x06, 0x01, 0x21, 0x00},
+		{0x0a, 0x01, 0x21, 0x00}, {0x0f, 0x01, 0x21, 0x00},
+		{0x18, 0x01, 0x21, 0x00}, {0x1f, 0x01, 0x21, 0x00},
+		{0x29, 0x01, 0x21, 0x00}, {0x38, 0x01, 0x21, 0x01},
+		{0x03, 0x01, 0x22, 0x00}, {0x06, 0x01, 0x22, 0x00},
+		{0x0a, 0x01, 0x22, 0x00}, {0x0f, 0x01, 0x22, 0x00},
+		{0x18, 0x01, 0x22, 0x00}, {0x1f, 0x01, 0x22, 0x00},
+		{0x29, 0x01, 0x22, 0x00}, {0x38, 0x01, 0x22, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x28, 0x00}, {0x06, 0x01, 0x28, 0x00},
+		{0x0a, 0x01, 0x28, 0x00}, {0x0f, 0x01, 0x28, 0x00},
+		{0x18, 0x01, 0x28, 0x00}, {0x1f, 0x01, 0x28, 0x00},
+		{0x29, 0x01, 0x28, 0x00}, {0x38, 0x01, 0x28, 0x01},
+		{0x03, 0x01, 0x29, 0x00}, {0x06, 0x01, 0x29, 0x00},
+		{0x0a, 0x01, 0x29, 0x00}, {0x0f, 0x01, 0x29, 0x00},
+		{0x18, 0x01, 0x29, 0x00}, {0x1f, 0x01, 0x29, 0x00},
+		{0x29, 0x01, 0x29, 0x00}, {0x38, 0x01, 0x29, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x3f, 0x00}, {0x09, 0x01, 0x3f, 0x00},
+		{0x17, 0x01, 0x3f, 0x00}, {0x28, 0x01, 0x3f, 0x01},
+		{0x01, 0x01, 0x27, 0x00}, {0x16, 0x01, 0x27, 0x01},
+		{0x01, 0x01, 0x2b, 0x00}, {0x16, 0x01, 0x2b, 0x01},
+		{0x01, 0x01, 0x7c, 0x00}, {0x16, 0x01, 0x7c, 0x01},
+		{0x00, 0x01, 0x23, 0x01}, {0x00, 0x01, 0x3e, 0x01},
+		{0x56, 0x00, 0x00, 0x00}, {0x57, 0x00, 0x00, 0x00},
+		{0x59, 0x00, 0x00, 0x00}, {0x5a, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x3f, 0x00}, {0x06, 0x01, 0x3f, 0x00},
+		{0x0a, 0x01, 0x3f, 0x00}, {0x0f, 0x01, 0x3f, 0x00},
+		{0x18, 0x01, 0x3f, 0x00}, {0x1f, 0x01, 0x3f, 0x00},
+		{0x29, 0x01, 0x3f, 0x00}, {0x38, 0x01, 0x3f, 0x01},
+		{0x02, 0x01, 0x27, 0x00}, {0x09, 0x01, 0x27, 0x00},
+		{0x17, 0x01, 0x27, 0x00}, {0x28, 0x01, 0x27, 0x01},
+		{0x02, 0x01, 0x2b, 0x00}, {0x09, 0x01, 0x2b, 0x00},
+		{0x17, 0x01, 0x2b, 0x00}, {0x28, 0x01, 0x2b, 0x01}
+	],
+	/* 80 */
+	[
+		{0x03, 0x01, 0x27, 0x00}, {0x06, 0x01, 0x27, 0x00},
+		{0x0a, 0x01, 0x27, 0x00}, {0x0f, 0x01, 0x27, 0x00},
+		{0x18, 0x01, 0x27, 0x00}, {0x1f, 0x01, 0x27, 0x00},
+		{0x29, 0x01, 0x27, 0x00}, {0x38, 0x01, 0x27, 0x01},
+		{0x03, 0x01, 0x2b, 0x00}, {0x06, 0x01, 0x2b, 0x00},
+		{0x0a, 0x01, 0x2b, 0x00}, {0x0f, 0x01, 0x2b, 0x00},
+		{0x18, 0x01, 0x2b, 0x00}, {0x1f, 0x01, 0x2b, 0x00},
+		{0x29, 0x01, 0x2b, 0x00}, {0x38, 0x01, 0x2b, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x7c, 0x00}, {0x09, 0x01, 0x7c, 0x00},
+		{0x17, 0x01, 0x7c, 0x00}, {0x28, 0x01, 0x7c, 0x01},
+		{0x01, 0x01, 0x23, 0x00}, {0x16, 0x01, 0x23, 0x01},
+		{0x01, 0x01, 0x3e, 0x00}, {0x16, 0x01, 0x3e, 0x01},
+		{0x00, 0x01, 0x00, 0x01}, {0x00, 0x01, 0x24, 0x01},
+		{0x00, 0x01, 0x40, 0x01}, {0x00, 0x01, 0x5b, 0x01},
+		{0x00, 0x01, 0x5d, 0x01}, {0x00, 0x01, 0x7e, 0x01},
+		{0x5b, 0x00, 0x00, 0x00}, {0x5c, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x7c, 0x00}, {0x06, 0x01, 0x7c, 0x00},
+		{0x0a, 0x01, 0x7c, 0x00}, {0x0f, 0x01, 0x7c, 0x00},
+		{0x18, 0x01, 0x7c, 0x00}, {0x1f, 0x01, 0x7c, 0x00},
+		{0x29, 0x01, 0x7c, 0x00}, {0x38, 0x01, 0x7c, 0x01},
+		{0x02, 0x01, 0x23, 0x00}, {0x09, 0x01, 0x23, 0x00},
+		{0x17, 0x01, 0x23, 0x00}, {0x28, 0x01, 0x23, 0x01},
+		{0x02, 0x01, 0x3e, 0x00}, {0x09, 0x01, 0x3e, 0x00},
+		{0x17, 0x01, 0x3e, 0x00}, {0x28, 0x01, 0x3e, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x23, 0x00}, {0x06, 0x01, 0x23, 0x00},
+		{0x0a, 0x01, 0x23, 0x00}, {0x0f, 0x01, 0x23, 0x00},
+		{0x18, 0x01, 0x23, 0x00}, {0x1f, 0x01, 0x23, 0x00},
+		{0x29, 0x01, 0x23, 0x00}, {0x38, 0x01, 0x23, 0x01},
+		{0x03, 0x01, 0x3e, 0x00}, {0x06, 0x01, 0x3e, 0x00},
+		{0x0a, 0x01, 0x3e, 0x00}, {0x0f, 0x01, 0x3e, 0x00},
+		{0x18, 0x01, 0x3e, 0x00}, {0x1f, 0x01, 0x3e, 0x00},
+		{0x29, 0x01, 0x3e, 0x00}, {0x38, 0x01, 0x3e, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x00, 0x00}, {0x16, 0x01, 0x00, 0x01},
+		{0x01, 0x01, 0x24, 0x00}, {0x16, 0x01, 0x24, 0x01},
+		{0x01, 0x01, 0x40, 0x00}, {0x16, 0x01, 0x40, 0x01},
+		{0x01, 0x01, 0x5b, 0x00}, {0x16, 0x01, 0x5b, 0x01},
+		{0x01, 0x01, 0x5d, 0x00}, {0x16, 0x01, 0x5d, 0x01},
+		{0x01, 0x01, 0x7e, 0x00}, {0x16, 0x01, 0x7e, 0x01},
+		{0x00, 0x01, 0x5e, 0x01}, {0x00, 0x01, 0x7d, 0x01},
+		{0x5d, 0x00, 0x00, 0x00}, {0x5e, 0x00, 0x00, 0x01}
+	],
+	/* 85 */
+	[
+		{0x02, 0x01, 0x00, 0x00}, {0x09, 0x01, 0x00, 0x00},
+		{0x17, 0x01, 0x00, 0x00}, {0x28, 0x01, 0x00, 0x01},
+		{0x02, 0x01, 0x24, 0x00}, {0x09, 0x01, 0x24, 0x00},
+		{0x17, 0x01, 0x24, 0x00}, {0x28, 0x01, 0x24, 0x01},
+		{0x02, 0x01, 0x40, 0x00}, {0x09, 0x01, 0x40, 0x00},
+		{0x17, 0x01, 0x40, 0x00}, {0x28, 0x01, 0x40, 0x01},
+		{0x02, 0x01, 0x5b, 0x00}, {0x09, 0x01, 0x5b, 0x00},
+		{0x17, 0x01, 0x5b, 0x00}, {0x28, 0x01, 0x5b, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x00, 0x00}, {0x06, 0x01, 0x00, 0x00},
+		{0x0a, 0x01, 0x00, 0x00}, {0x0f, 0x01, 0x00, 0x00},
+		{0x18, 0x01, 0x00, 0x00}, {0x1f, 0x01, 0x00, 0x00},
+		{0x29, 0x01, 0x00, 0x00}, {0x38, 0x01, 0x00, 0x01},
+		{0x03, 0x01, 0x24, 0x00}, {0x06, 0x01, 0x24, 0x00},
+		{0x0a, 0x01, 0x24, 0x00}, {0x0f, 0x01, 0x24, 0x00},
+		{0x18, 0x01, 0x24, 0x00}, {0x1f, 0x01, 0x24, 0x00},
+		{0x29, 0x01, 0x24, 0x00}, {0x38, 0x01, 0x24, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x40, 0x00}, {0x06, 0x01, 0x40, 0x00},
+		{0x0a, 0x01, 0x40, 0x00}, {0x0f, 0x01, 0x40, 0x00},
+		{0x18, 0x01, 0x40, 0x00}, {0x1f, 0x01, 0x40, 0x00},
+		{0x29, 0x01, 0x40, 0x00}, {0x38, 0x01, 0x40, 0x01},
+		{0x03, 0x01, 0x5b, 0x00}, {0x06, 0x01, 0x5b, 0x00},
+		{0x0a, 0x01, 0x5b, 0x00}, {0x0f, 0x01, 0x5b, 0x00},
+		{0x18, 0x01, 0x5b, 0x00}, {0x1f, 0x01, 0x5b, 0x00},
+		{0x29, 0x01, 0x5b, 0x00}, {0x38, 0x01, 0x5b, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x5d, 0x00}, {0x09, 0x01, 0x5d, 0x00},
+		{0x17, 0x01, 0x5d, 0x00}, {0x28, 0x01, 0x5d, 0x01},
+		{0x02, 0x01, 0x7e, 0x00}, {0x09, 0x01, 0x7e, 0x00},
+		{0x17, 0x01, 0x7e, 0x00}, {0x28, 0x01, 0x7e, 0x01},
+		{0x01, 0x01, 0x5e, 0x00}, {0x16, 0x01, 0x5e, 0x01},
+		{0x01, 0x01, 0x7d, 0x00}, {0x16, 0x01, 0x7d, 0x01},
+		{0x00, 0x01, 0x3c, 0x01}, {0x00, 0x01, 0x60, 0x01},
+		{0x00, 0x01, 0x7b, 0x01}, {0x5f, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x5d, 0x00}, {0x06, 0x01, 0x5d, 0x00},
+		{0x0a, 0x01, 0x5d, 0x00}, {0x0f, 0x01, 0x5d, 0x00},
+		{0x18, 0x01, 0x5d, 0x00}, {0x1f, 0x01, 0x5d, 0x00},
+		{0x29, 0x01, 0x5d, 0x00}, {0x38, 0x01, 0x5d, 0x01},
+		{0x03, 0x01, 0x7e, 0x00}, {0x06, 0x01, 0x7e, 0x00},
+		{0x0a, 0x01, 0x7e, 0x00}, {0x0f, 0x01, 0x7e, 0x00},
+		{0x18, 0x01, 0x7e, 0x00}, {0x1f, 0x01, 0x7e, 0x00},
+		{0x29, 0x01, 0x7e, 0x00}, {0x38, 0x01, 0x7e, 0x01}
+	],
+	/* 90 */
+	[
+		{0x02, 0x01, 0x5e, 0x00}, {0x09, 0x01, 0x5e, 0x00},
+		{0x17, 0x01, 0x5e, 0x00}, {0x28, 0x01, 0x5e, 0x01},
+		{0x02, 0x01, 0x7d, 0x00}, {0x09, 0x01, 0x7d, 0x00},
+		{0x17, 0x01, 0x7d, 0x00}, {0x28, 0x01, 0x7d, 0x01},
+		{0x01, 0x01, 0x3c, 0x00}, {0x16, 0x01, 0x3c, 0x01},
+		{0x01, 0x01, 0x60, 0x00}, {0x16, 0x01, 0x60, 0x01},
+		{0x01, 0x01, 0x7b, 0x00}, {0x16, 0x01, 0x7b, 0x01},
+		{0x60, 0x00, 0x00, 0x00}, {0x6e, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x5e, 0x00}, {0x06, 0x01, 0x5e, 0x00},
+		{0x0a, 0x01, 0x5e, 0x00}, {0x0f, 0x01, 0x5e, 0x00},
+		{0x18, 0x01, 0x5e, 0x00}, {0x1f, 0x01, 0x5e, 0x00},
+		{0x29, 0x01, 0x5e, 0x00}, {0x38, 0x01, 0x5e, 0x01},
+		{0x03, 0x01, 0x7d, 0x00}, {0x06, 0x01, 0x7d, 0x00},
+		{0x0a, 0x01, 0x7d, 0x00}, {0x0f, 0x01, 0x7d, 0x00},
+		{0x18, 0x01, 0x7d, 0x00}, {0x1f, 0x01, 0x7d, 0x00},
+		{0x29, 0x01, 0x7d, 0x00}, {0x38, 0x01, 0x7d, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x3c, 0x00}, {0x09, 0x01, 0x3c, 0x00},
+		{0x17, 0x01, 0x3c, 0x00}, {0x28, 0x01, 0x3c, 0x01},
+		{0x02, 0x01, 0x60, 0x00}, {0x09, 0x01, 0x60, 0x00},
+		{0x17, 0x01, 0x60, 0x00}, {0x28, 0x01, 0x60, 0x01},
+		{0x02, 0x01, 0x7b, 0x00}, {0x09, 0x01, 0x7b, 0x00},
+		{0x17, 0x01, 0x7b, 0x00}, {0x28, 0x01, 0x7b, 0x01},
+		{0x61, 0x00, 0x00, 0x00}, {0x65, 0x00, 0x00, 0x00},
+		{0x6f, 0x00, 0x00, 0x00}, {0x85, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x3c, 0x00}, {0x06, 0x01, 0x3c, 0x00},
+		{0x0a, 0x01, 0x3c, 0x00}, {0x0f, 0x01, 0x3c, 0x00},
+		{0x18, 0x01, 0x3c, 0x00}, {0x1f, 0x01, 0x3c, 0x00},
+		{0x29, 0x01, 0x3c, 0x00}, {0x38, 0x01, 0x3c, 0x01},
+		{0x03, 0x01, 0x60, 0x00}, {0x06, 0x01, 0x60, 0x00},
+		{0x0a, 0x01, 0x60, 0x00}, {0x0f, 0x01, 0x60, 0x00},
+		{0x18, 0x01, 0x60, 0x00}, {0x1f, 0x01, 0x60, 0x00},
+		{0x29, 0x01, 0x60, 0x00}, {0x38, 0x01, 0x60, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x7b, 0x00}, {0x06, 0x01, 0x7b, 0x00},
+		{0x0a, 0x01, 0x7b, 0x00}, {0x0f, 0x01, 0x7b, 0x00},
+		{0x18, 0x01, 0x7b, 0x00}, {0x1f, 0x01, 0x7b, 0x00},
+		{0x29, 0x01, 0x7b, 0x00}, {0x38, 0x01, 0x7b, 0x01},
+		{0x62, 0x00, 0x00, 0x00}, {0x63, 0x00, 0x00, 0x00},
+		{0x66, 0x00, 0x00, 0x00}, {0x69, 0x00, 0x00, 0x00},
+		{0x70, 0x00, 0x00, 0x00}, {0x77, 0x00, 0x00, 0x00},
+		{0x86, 0x00, 0x00, 0x00}, {0x99, 0x00, 0x00, 0x01}
+	],
+	/* 95 */
+	[
+		{0x00, 0x01, 0x5c, 0x01}, {0x00, 0x01, 0xc3, 0x01},
+		{0x00, 0x01, 0xd0, 0x01}, {0x64, 0x00, 0x00, 0x00},
+		{0x67, 0x00, 0x00, 0x00}, {0x68, 0x00, 0x00, 0x00},
+		{0x6a, 0x00, 0x00, 0x00}, {0x6b, 0x00, 0x00, 0x00},
+		{0x71, 0x00, 0x00, 0x00}, {0x74, 0x00, 0x00, 0x00},
+		{0x78, 0x00, 0x00, 0x00}, {0x7e, 0x00, 0x00, 0x00},
+		{0x87, 0x00, 0x00, 0x00}, {0x8e, 0x00, 0x00, 0x00},
+		{0x9a, 0x00, 0x00, 0x00}, {0xa9, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x5c, 0x00}, {0x16, 0x01, 0x5c, 0x01},
+		{0x01, 0x01, 0xc3, 0x00}, {0x16, 0x01, 0xc3, 0x01},
+		{0x01, 0x01, 0xd0, 0x00}, {0x16, 0x01, 0xd0, 0x01},
+		{0x00, 0x01, 0x80, 0x01}, {0x00, 0x01, 0x82, 0x01},
+		{0x00, 0x01, 0x83, 0x01}, {0x00, 0x01, 0xa2, 0x01},
+		{0x00, 0x01, 0xb8, 0x01}, {0x00, 0x01, 0xc2, 0x01},
+		{0x00, 0x01, 0xe0, 0x01}, {0x00, 0x01, 0xe2, 0x01},
+		{0x6c, 0x00, 0x00, 0x00}, {0x6d, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x02, 0x01, 0x5c, 0x00}, {0x09, 0x01, 0x5c, 0x00},
+		{0x17, 0x01, 0x5c, 0x00}, {0x28, 0x01, 0x5c, 0x01},
+		{0x02, 0x01, 0xc3, 0x00}, {0x09, 0x01, 0xc3, 0x00},
+		{0x17, 0x01, 0xc3, 0x00}, {0x28, 0x01, 0xc3, 0x01},
+		{0x02, 0x01, 0xd0, 0x00}, {0x09, 0x01, 0xd0, 0x00},
+		{0x17, 0x01, 0xd0, 0x00}, {0x28, 0x01, 0xd0, 0x01},
+		{0x01, 0x01, 0x80, 0x00}, {0x16, 0x01, 0x80, 0x01},
+		{0x01, 0x01, 0x82, 0x00}, {0x16, 0x01, 0x82, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x5c, 0x00}, {0x06, 0x01, 0x5c, 0x00},
+		{0x0a, 0x01, 0x5c, 0x00}, {0x0f, 0x01, 0x5c, 0x00},
+		{0x18, 0x01, 0x5c, 0x00}, {0x1f, 0x01, 0x5c, 0x00},
+		{0x29, 0x01, 0x5c, 0x00}, {0x38, 0x01, 0x5c, 0x01},
+		{0x03, 0x01, 0xc3, 0x00}, {0x06, 0x01, 0xc3, 0x00},
+		{0x0a, 0x01, 0xc3, 0x00}, {0x0f, 0x01, 0xc3, 0x00},
+		{0x18, 0x01, 0xc3, 0x00}, {0x1f, 0x01, 0xc3, 0x00},
+		{0x29, 0x01, 0xc3, 0x00}, {0x38, 0x01, 0xc3, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xd0, 0x00}, {0x06, 0x01, 0xd0, 0x00},
+		{0x0a, 0x01, 0xd0, 0x00}, {0x0f, 0x01, 0xd0, 0x00},
+		{0x18, 0x01, 0xd0, 0x00}, {0x1f, 0x01, 0xd0, 0x00},
+		{0x29, 0x01, 0xd0, 0x00}, {0x38, 0x01, 0xd0, 0x01},
+		{0x02, 0x01, 0x80, 0x00}, {0x09, 0x01, 0x80, 0x00},
+		{0x17, 0x01, 0x80, 0x00}, {0x28, 0x01, 0x80, 0x01},
+		{0x02, 0x01, 0x82, 0x00}, {0x09, 0x01, 0x82, 0x00},
+		{0x17, 0x01, 0x82, 0x00}, {0x28, 0x01, 0x82, 0x01}
+	],
+	/* 100 */
+	[
+		{0x03, 0x01, 0x80, 0x00}, {0x06, 0x01, 0x80, 0x00},
+		{0x0a, 0x01, 0x80, 0x00}, {0x0f, 0x01, 0x80, 0x00},
+		{0x18, 0x01, 0x80, 0x00}, {0x1f, 0x01, 0x80, 0x00},
+		{0x29, 0x01, 0x80, 0x00}, {0x38, 0x01, 0x80, 0x01},
+		{0x03, 0x01, 0x82, 0x00}, {0x06, 0x01, 0x82, 0x00},
+		{0x0a, 0x01, 0x82, 0x00}, {0x0f, 0x01, 0x82, 0x00},
+		{0x18, 0x01, 0x82, 0x00}, {0x1f, 0x01, 0x82, 0x00},
+		{0x29, 0x01, 0x82, 0x00}, {0x38, 0x01, 0x82, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x83, 0x00}, {0x16, 0x01, 0x83, 0x01},
+		{0x01, 0x01, 0xa2, 0x00}, {0x16, 0x01, 0xa2, 0x01},
+		{0x01, 0x01, 0xb8, 0x00}, {0x16, 0x01, 0xb8, 0x01},
+		{0x01, 0x01, 0xc2, 0x00}, {0x16, 0x01, 0xc2, 0x01},
+		{0x01, 0x01, 0xe0, 0x00}, {0x16, 0x01, 0xe0, 0x01},
+		{0x01, 0x01, 0xe2, 0x00}, {0x16, 0x01, 0xe2, 0x01},
+		{0x00, 0x01, 0x99, 0x01}, {0x00, 0x01, 0xa1, 0x01},
+		{0x00, 0x01, 0xa7, 0x01}, {0x00, 0x01, 0xac, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x83, 0x00}, {0x09, 0x01, 0x83, 0x00},
+		{0x17, 0x01, 0x83, 0x00}, {0x28, 0x01, 0x83, 0x01},
+		{0x02, 0x01, 0xa2, 0x00}, {0x09, 0x01, 0xa2, 0x00},
+		{0x17, 0x01, 0xa2, 0x00}, {0x28, 0x01, 0xa2, 0x01},
+		{0x02, 0x01, 0xb8, 0x00}, {0x09, 0x01, 0xb8, 0x00},
+		{0x17, 0x01, 0xb8, 0x00}, {0x28, 0x01, 0xb8, 0x01},
+		{0x02, 0x01, 0xc2, 0x00}, {0x09, 0x01, 0xc2, 0x00},
+		{0x17, 0x01, 0xc2, 0x00}, {0x28, 0x01, 0xc2, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x83, 0x00}, {0x06, 0x01, 0x83, 0x00},
+		{0x0a, 0x01, 0x83, 0x00}, {0x0f, 0x01, 0x83, 0x00},
+		{0x18, 0x01, 0x83, 0x00}, {0x1f, 0x01, 0x83, 0x00},
+		{0x29, 0x01, 0x83, 0x00}, {0x38, 0x01, 0x83, 0x01},
+		{0x03, 0x01, 0xa2, 0x00}, {0x06, 0x01, 0xa2, 0x00},
+		{0x0a, 0x01, 0xa2, 0x00}, {0x0f, 0x01, 0xa2, 0x00},
+		{0x18, 0x01, 0xa2, 0x00}, {0x1f, 0x01, 0xa2, 0x00},
+		{0x29, 0x01, 0xa2, 0x00}, {0x38, 0x01, 0xa2, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xb8, 0x00}, {0x06, 0x01, 0xb8, 0x00},
+		{0x0a, 0x01, 0xb8, 0x00}, {0x0f, 0x01, 0xb8, 0x00},
+		{0x18, 0x01, 0xb8, 0x00}, {0x1f, 0x01, 0xb8, 0x00},
+		{0x29, 0x01, 0xb8, 0x00}, {0x38, 0x01, 0xb8, 0x01},
+		{0x03, 0x01, 0xc2, 0x00}, {0x06, 0x01, 0xc2, 0x00},
+		{0x0a, 0x01, 0xc2, 0x00}, {0x0f, 0x01, 0xc2, 0x00},
+		{0x18, 0x01, 0xc2, 0x00}, {0x1f, 0x01, 0xc2, 0x00},
+		{0x29, 0x01, 0xc2, 0x00}, {0x38, 0x01, 0xc2, 0x01}
+	],
+	/* 105 */
+	[
+		{0x02, 0x01, 0xe0, 0x00}, {0x09, 0x01, 0xe0, 0x00},
+		{0x17, 0x01, 0xe0, 0x00}, {0x28, 0x01, 0xe0, 0x01},
+		{0x02, 0x01, 0xe2, 0x00}, {0x09, 0x01, 0xe2, 0x00},
+		{0x17, 0x01, 0xe2, 0x00}, {0x28, 0x01, 0xe2, 0x01},
+		{0x01, 0x01, 0x99, 0x00}, {0x16, 0x01, 0x99, 0x01},
+		{0x01, 0x01, 0xa1, 0x00}, {0x16, 0x01, 0xa1, 0x01},
+		{0x01, 0x01, 0xa7, 0x00}, {0x16, 0x01, 0xa7, 0x01},
+		{0x01, 0x01, 0xac, 0x00}, {0x16, 0x01, 0xac, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xe0, 0x00}, {0x06, 0x01, 0xe0, 0x00},
+		{0x0a, 0x01, 0xe0, 0x00}, {0x0f, 0x01, 0xe0, 0x00},
+		{0x18, 0x01, 0xe0, 0x00}, {0x1f, 0x01, 0xe0, 0x00},
+		{0x29, 0x01, 0xe0, 0x00}, {0x38, 0x01, 0xe0, 0x01},
+		{0x03, 0x01, 0xe2, 0x00}, {0x06, 0x01, 0xe2, 0x00},
+		{0x0a, 0x01, 0xe2, 0x00}, {0x0f, 0x01, 0xe2, 0x00},
+		{0x18, 0x01, 0xe2, 0x00}, {0x1f, 0x01, 0xe2, 0x00},
+		{0x29, 0x01, 0xe2, 0x00}, {0x38, 0x01, 0xe2, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x99, 0x00}, {0x09, 0x01, 0x99, 0x00},
+		{0x17, 0x01, 0x99, 0x00}, {0x28, 0x01, 0x99, 0x01},
+		{0x02, 0x01, 0xa1, 0x00}, {0x09, 0x01, 0xa1, 0x00},
+		{0x17, 0x01, 0xa1, 0x00}, {0x28, 0x01, 0xa1, 0x01},
+		{0x02, 0x01, 0xa7, 0x00}, {0x09, 0x01, 0xa7, 0x00},
+		{0x17, 0x01, 0xa7, 0x00}, {0x28, 0x01, 0xa7, 0x01},
+		{0x02, 0x01, 0xac, 0x00}, {0x09, 0x01, 0xac, 0x00},
+		{0x17, 0x01, 0xac, 0x00}, {0x28, 0x01, 0xac, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x99, 0x00}, {0x06, 0x01, 0x99, 0x00},
+		{0x0a, 0x01, 0x99, 0x00}, {0x0f, 0x01, 0x99, 0x00},
+		{0x18, 0x01, 0x99, 0x00}, {0x1f, 0x01, 0x99, 0x00},
+		{0x29, 0x01, 0x99, 0x00}, {0x38, 0x01, 0x99, 0x01},
+		{0x03, 0x01, 0xa1, 0x00}, {0x06, 0x01, 0xa1, 0x00},
+		{0x0a, 0x01, 0xa1, 0x00}, {0x0f, 0x01, 0xa1, 0x00},
+		{0x18, 0x01, 0xa1, 0x00}, {0x1f, 0x01, 0xa1, 0x00},
+		{0x29, 0x01, 0xa1, 0x00}, {0x38, 0x01, 0xa1, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xa7, 0x00}, {0x06, 0x01, 0xa7, 0x00},
+		{0x0a, 0x01, 0xa7, 0x00}, {0x0f, 0x01, 0xa7, 0x00},
+		{0x18, 0x01, 0xa7, 0x00}, {0x1f, 0x01, 0xa7, 0x00},
+		{0x29, 0x01, 0xa7, 0x00}, {0x38, 0x01, 0xa7, 0x01},
+		{0x03, 0x01, 0xac, 0x00}, {0x06, 0x01, 0xac, 0x00},
+		{0x0a, 0x01, 0xac, 0x00}, {0x0f, 0x01, 0xac, 0x00},
+		{0x18, 0x01, 0xac, 0x00}, {0x1f, 0x01, 0xac, 0x00},
+		{0x29, 0x01, 0xac, 0x00}, {0x38, 0x01, 0xac, 0x01}
+	],
+	/* 110 */
+	[
+		{0x72, 0x00, 0x00, 0x00}, {0x73, 0x00, 0x00, 0x00},
+		{0x75, 0x00, 0x00, 0x00}, {0x76, 0x00, 0x00, 0x00},
+		{0x79, 0x00, 0x00, 0x00}, {0x7b, 0x00, 0x00, 0x00},
+		{0x7f, 0x00, 0x00, 0x00}, {0x82, 0x00, 0x00, 0x00},
+		{0x88, 0x00, 0x00, 0x00}, {0x8b, 0x00, 0x00, 0x00},
+		{0x8f, 0x00, 0x00, 0x00}, {0x92, 0x00, 0x00, 0x00},
+		{0x9b, 0x00, 0x00, 0x00}, {0xa2, 0x00, 0x00, 0x00},
+		{0xaa, 0x00, 0x00, 0x00}, {0xb4, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0xb0, 0x01}, {0x00, 0x01, 0xb1, 0x01},
+		{0x00, 0x01, 0xb3, 0x01}, {0x00, 0x01, 0xd1, 0x01},
+		{0x00, 0x01, 0xd8, 0x01}, {0x00, 0x01, 0xd9, 0x01},
+		{0x00, 0x01, 0xe3, 0x01}, {0x00, 0x01, 0xe5, 0x01},
+		{0x00, 0x01, 0xe6, 0x01}, {0x7a, 0x00, 0x00, 0x00},
+		{0x7c, 0x00, 0x00, 0x00}, {0x7d, 0x00, 0x00, 0x00},
+		{0x80, 0x00, 0x00, 0x00}, {0x81, 0x00, 0x00, 0x00},
+		{0x83, 0x00, 0x00, 0x00}, {0x84, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x01, 0x01, 0xb0, 0x00}, {0x16, 0x01, 0xb0, 0x01},
+		{0x01, 0x01, 0xb1, 0x00}, {0x16, 0x01, 0xb1, 0x01},
+		{0x01, 0x01, 0xb3, 0x00}, {0x16, 0x01, 0xb3, 0x01},
+		{0x01, 0x01, 0xd1, 0x00}, {0x16, 0x01, 0xd1, 0x01},
+		{0x01, 0x01, 0xd8, 0x00}, {0x16, 0x01, 0xd8, 0x01},
+		{0x01, 0x01, 0xd9, 0x00}, {0x16, 0x01, 0xd9, 0x01},
+		{0x01, 0x01, 0xe3, 0x00}, {0x16, 0x01, 0xe3, 0x01},
+		{0x01, 0x01, 0xe5, 0x00}, {0x16, 0x01, 0xe5, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xb0, 0x00}, {0x09, 0x01, 0xb0, 0x00},
+		{0x17, 0x01, 0xb0, 0x00}, {0x28, 0x01, 0xb0, 0x01},
+		{0x02, 0x01, 0xb1, 0x00}, {0x09, 0x01, 0xb1, 0x00},
+		{0x17, 0x01, 0xb1, 0x00}, {0x28, 0x01, 0xb1, 0x01},
+		{0x02, 0x01, 0xb3, 0x00}, {0x09, 0x01, 0xb3, 0x00},
+		{0x17, 0x01, 0xb3, 0x00}, {0x28, 0x01, 0xb3, 0x01},
+		{0x02, 0x01, 0xd1, 0x00}, {0x09, 0x01, 0xd1, 0x00},
+		{0x17, 0x01, 0xd1, 0x00}, {0x28, 0x01, 0xd1, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xb0, 0x00}, {0x06, 0x01, 0xb0, 0x00},
+		{0x0a, 0x01, 0xb0, 0x00}, {0x0f, 0x01, 0xb0, 0x00},
+		{0x18, 0x01, 0xb0, 0x00}, {0x1f, 0x01, 0xb0, 0x00},
+		{0x29, 0x01, 0xb0, 0x00}, {0x38, 0x01, 0xb0, 0x01},
+		{0x03, 0x01, 0xb1, 0x00}, {0x06, 0x01, 0xb1, 0x00},
+		{0x0a, 0x01, 0xb1, 0x00}, {0x0f, 0x01, 0xb1, 0x00},
+		{0x18, 0x01, 0xb1, 0x00}, {0x1f, 0x01, 0xb1, 0x00},
+		{0x29, 0x01, 0xb1, 0x00}, {0x38, 0x01, 0xb1, 0x01}
+	],
+	/* 115 */
+	[
+		{0x03, 0x01, 0xb3, 0x00}, {0x06, 0x01, 0xb3, 0x00},
+		{0x0a, 0x01, 0xb3, 0x00}, {0x0f, 0x01, 0xb3, 0x00},
+		{0x18, 0x01, 0xb3, 0x00}, {0x1f, 0x01, 0xb3, 0x00},
+		{0x29, 0x01, 0xb3, 0x00}, {0x38, 0x01, 0xb3, 0x01},
+		{0x03, 0x01, 0xd1, 0x00}, {0x06, 0x01, 0xd1, 0x00},
+		{0x0a, 0x01, 0xd1, 0x00}, {0x0f, 0x01, 0xd1, 0x00},
+		{0x18, 0x01, 0xd1, 0x00}, {0x1f, 0x01, 0xd1, 0x00},
+		{0x29, 0x01, 0xd1, 0x00}, {0x38, 0x01, 0xd1, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xd8, 0x00}, {0x09, 0x01, 0xd8, 0x00},
+		{0x17, 0x01, 0xd8, 0x00}, {0x28, 0x01, 0xd8, 0x01},
+		{0x02, 0x01, 0xd9, 0x00}, {0x09, 0x01, 0xd9, 0x00},
+		{0x17, 0x01, 0xd9, 0x00}, {0x28, 0x01, 0xd9, 0x01},
+		{0x02, 0x01, 0xe3, 0x00}, {0x09, 0x01, 0xe3, 0x00},
+		{0x17, 0x01, 0xe3, 0x00}, {0x28, 0x01, 0xe3, 0x01},
+		{0x02, 0x01, 0xe5, 0x00}, {0x09, 0x01, 0xe5, 0x00},
+		{0x17, 0x01, 0xe5, 0x00}, {0x28, 0x01, 0xe5, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xd8, 0x00}, {0x06, 0x01, 0xd8, 0x00},
+		{0x0a, 0x01, 0xd8, 0x00}, {0x0f, 0x01, 0xd8, 0x00},
+		{0x18, 0x01, 0xd8, 0x00}, {0x1f, 0x01, 0xd8, 0x00},
+		{0x29, 0x01, 0xd8, 0x00}, {0x38, 0x01, 0xd8, 0x01},
+		{0x03, 0x01, 0xd9, 0x00}, {0x06, 0x01, 0xd9, 0x00},
+		{0x0a, 0x01, 0xd9, 0x00}, {0x0f, 0x01, 0xd9, 0x00},
+		{0x18, 0x01, 0xd9, 0x00}, {0x1f, 0x01, 0xd9, 0x00},
+		{0x29, 0x01, 0xd9, 0x00}, {0x38, 0x01, 0xd9, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xe3, 0x00}, {0x06, 0x01, 0xe3, 0x00},
+		{0x0a, 0x01, 0xe3, 0x00}, {0x0f, 0x01, 0xe3, 0x00},
+		{0x18, 0x01, 0xe3, 0x00}, {0x1f, 0x01, 0xe3, 0x00},
+		{0x29, 0x01, 0xe3, 0x00}, {0x38, 0x01, 0xe3, 0x01},
+		{0x03, 0x01, 0xe5, 0x00}, {0x06, 0x01, 0xe5, 0x00},
+		{0x0a, 0x01, 0xe5, 0x00}, {0x0f, 0x01, 0xe5, 0x00},
+		{0x18, 0x01, 0xe5, 0x00}, {0x1f, 0x01, 0xe5, 0x00},
+		{0x29, 0x01, 0xe5, 0x00}, {0x38, 0x01, 0xe5, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xe6, 0x00}, {0x16, 0x01, 0xe6, 0x01},
+		{0x00, 0x01, 0x81, 0x01}, {0x00, 0x01, 0x84, 0x01},
+		{0x00, 0x01, 0x85, 0x01}, {0x00, 0x01, 0x86, 0x01},
+		{0x00, 0x01, 0x88, 0x01}, {0x00, 0x01, 0x92, 0x01},
+		{0x00, 0x01, 0x9a, 0x01}, {0x00, 0x01, 0x9c, 0x01},
+		{0x00, 0x01, 0xa0, 0x01}, {0x00, 0x01, 0xa3, 0x01},
+		{0x00, 0x01, 0xa4, 0x01}, {0x00, 0x01, 0xa9, 0x01},
+		{0x00, 0x01, 0xaa, 0x01}, {0x00, 0x01, 0xad, 0x01}
+	],
+	/* 120 */
+	[
+		{0x02, 0x01, 0xe6, 0x00}, {0x09, 0x01, 0xe6, 0x00},
+		{0x17, 0x01, 0xe6, 0x00}, {0x28, 0x01, 0xe6, 0x01},
+		{0x01, 0x01, 0x81, 0x00}, {0x16, 0x01, 0x81, 0x01},
+		{0x01, 0x01, 0x84, 0x00}, {0x16, 0x01, 0x84, 0x01},
+		{0x01, 0x01, 0x85, 0x00}, {0x16, 0x01, 0x85, 0x01},
+		{0x01, 0x01, 0x86, 0x00}, {0x16, 0x01, 0x86, 0x01},
+		{0x01, 0x01, 0x88, 0x00}, {0x16, 0x01, 0x88, 0x01},
+		{0x01, 0x01, 0x92, 0x00}, {0x16, 0x01, 0x92, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xe6, 0x00}, {0x06, 0x01, 0xe6, 0x00},
+		{0x0a, 0x01, 0xe6, 0x00}, {0x0f, 0x01, 0xe6, 0x00},
+		{0x18, 0x01, 0xe6, 0x00}, {0x1f, 0x01, 0xe6, 0x00},
+		{0x29, 0x01, 0xe6, 0x00}, {0x38, 0x01, 0xe6, 0x01},
+		{0x02, 0x01, 0x81, 0x00}, {0x09, 0x01, 0x81, 0x00},
+		{0x17, 0x01, 0x81, 0x00}, {0x28, 0x01, 0x81, 0x01},
+		{0x02, 0x01, 0x84, 0x00}, {0x09, 0x01, 0x84, 0x00},
+		{0x17, 0x01, 0x84, 0x00}, {0x28, 0x01, 0x84, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x81, 0x00}, {0x06, 0x01, 0x81, 0x00},
+		{0x0a, 0x01, 0x81, 0x00}, {0x0f, 0x01, 0x81, 0x00},
+		{0x18, 0x01, 0x81, 0x00}, {0x1f, 0x01, 0x81, 0x00},
+		{0x29, 0x01, 0x81, 0x00}, {0x38, 0x01, 0x81, 0x01},
+		{0x03, 0x01, 0x84, 0x00}, {0x06, 0x01, 0x84, 0x00},
+		{0x0a, 0x01, 0x84, 0x00}, {0x0f, 0x01, 0x84, 0x00},
+		{0x18, 0x01, 0x84, 0x00}, {0x1f, 0x01, 0x84, 0x00},
+		{0x29, 0x01, 0x84, 0x00}, {0x38, 0x01, 0x84, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x85, 0x00}, {0x09, 0x01, 0x85, 0x00},
+		{0x17, 0x01, 0x85, 0x00}, {0x28, 0x01, 0x85, 0x01},
+		{0x02, 0x01, 0x86, 0x00}, {0x09, 0x01, 0x86, 0x00},
+		{0x17, 0x01, 0x86, 0x00}, {0x28, 0x01, 0x86, 0x01},
+		{0x02, 0x01, 0x88, 0x00}, {0x09, 0x01, 0x88, 0x00},
+		{0x17, 0x01, 0x88, 0x00}, {0x28, 0x01, 0x88, 0x01},
+		{0x02, 0x01, 0x92, 0x00}, {0x09, 0x01, 0x92, 0x00},
+		{0x17, 0x01, 0x92, 0x00}, {0x28, 0x01, 0x92, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x85, 0x00}, {0x06, 0x01, 0x85, 0x00},
+		{0x0a, 0x01, 0x85, 0x00}, {0x0f, 0x01, 0x85, 0x00},
+		{0x18, 0x01, 0x85, 0x00}, {0x1f, 0x01, 0x85, 0x00},
+		{0x29, 0x01, 0x85, 0x00}, {0x38, 0x01, 0x85, 0x01},
+		{0x03, 0x01, 0x86, 0x00}, {0x06, 0x01, 0x86, 0x00},
+		{0x0a, 0x01, 0x86, 0x00}, {0x0f, 0x01, 0x86, 0x00},
+		{0x18, 0x01, 0x86, 0x00}, {0x1f, 0x01, 0x86, 0x00},
+		{0x29, 0x01, 0x86, 0x00}, {0x38, 0x01, 0x86, 0x01}
+	],
+	/* 125 */
+	[
+		{0x03, 0x01, 0x88, 0x00}, {0x06, 0x01, 0x88, 0x00},
+		{0x0a, 0x01, 0x88, 0x00}, {0x0f, 0x01, 0x88, 0x00},
+		{0x18, 0x01, 0x88, 0x00}, {0x1f, 0x01, 0x88, 0x00},
+		{0x29, 0x01, 0x88, 0x00}, {0x38, 0x01, 0x88, 0x01},
+		{0x03, 0x01, 0x92, 0x00}, {0x06, 0x01, 0x92, 0x00},
+		{0x0a, 0x01, 0x92, 0x00}, {0x0f, 0x01, 0x92, 0x00},
+		{0x18, 0x01, 0x92, 0x00}, {0x1f, 0x01, 0x92, 0x00},
+		{0x29, 0x01, 0x92, 0x00}, {0x38, 0x01, 0x92, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x9a, 0x00}, {0x16, 0x01, 0x9a, 0x01},
+		{0x01, 0x01, 0x9c, 0x00}, {0x16, 0x01, 0x9c, 0x01},
+		{0x01, 0x01, 0xa0, 0x00}, {0x16, 0x01, 0xa0, 0x01},
+		{0x01, 0x01, 0xa3, 0x00}, {0x16, 0x01, 0xa3, 0x01},
+		{0x01, 0x01, 0xa4, 0x00}, {0x16, 0x01, 0xa4, 0x01},
+		{0x01, 0x01, 0xa9, 0x00}, {0x16, 0x01, 0xa9, 0x01},
+		{0x01, 0x01, 0xaa, 0x00}, {0x16, 0x01, 0xaa, 0x01},
+		{0x01, 0x01, 0xad, 0x00}, {0x16, 0x01, 0xad, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x9a, 0x00}, {0x09, 0x01, 0x9a, 0x00},
+		{0x17, 0x01, 0x9a, 0x00}, {0x28, 0x01, 0x9a, 0x01},
+		{0x02, 0x01, 0x9c, 0x00}, {0x09, 0x01, 0x9c, 0x00},
+		{0x17, 0x01, 0x9c, 0x00}, {0x28, 0x01, 0x9c, 0x01},
+		{0x02, 0x01, 0xa0, 0x00}, {0x09, 0x01, 0xa0, 0x00},
+		{0x17, 0x01, 0xa0, 0x00}, {0x28, 0x01, 0xa0, 0x01},
+		{0x02, 0x01, 0xa3, 0x00}, {0x09, 0x01, 0xa3, 0x00},
+		{0x17, 0x01, 0xa3, 0x00}, {0x28, 0x01, 0xa3, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x9a, 0x00}, {0x06, 0x01, 0x9a, 0x00},
+		{0x0a, 0x01, 0x9a, 0x00}, {0x0f, 0x01, 0x9a, 0x00},
+		{0x18, 0x01, 0x9a, 0x00}, {0x1f, 0x01, 0x9a, 0x00},
+		{0x29, 0x01, 0x9a, 0x00}, {0x38, 0x01, 0x9a, 0x01},
+		{0x03, 0x01, 0x9c, 0x00}, {0x06, 0x01, 0x9c, 0x00},
+		{0x0a, 0x01, 0x9c, 0x00}, {0x0f, 0x01, 0x9c, 0x00},
+		{0x18, 0x01, 0x9c, 0x00}, {0x1f, 0x01, 0x9c, 0x00},
+		{0x29, 0x01, 0x9c, 0x00}, {0x38, 0x01, 0x9c, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xa0, 0x00}, {0x06, 0x01, 0xa0, 0x00},
+		{0x0a, 0x01, 0xa0, 0x00}, {0x0f, 0x01, 0xa0, 0x00},
+		{0x18, 0x01, 0xa0, 0x00}, {0x1f, 0x01, 0xa0, 0x00},
+		{0x29, 0x01, 0xa0, 0x00}, {0x38, 0x01, 0xa0, 0x01},
+		{0x03, 0x01, 0xa3, 0x00}, {0x06, 0x01, 0xa3, 0x00},
+		{0x0a, 0x01, 0xa3, 0x00}, {0x0f, 0x01, 0xa3, 0x00},
+		{0x18, 0x01, 0xa3, 0x00}, {0x1f, 0x01, 0xa3, 0x00},
+		{0x29, 0x01, 0xa3, 0x00}, {0x38, 0x01, 0xa3, 0x01}
+	],
+	/* 130 */
+	[
+		{0x02, 0x01, 0xa4, 0x00}, {0x09, 0x01, 0xa4, 0x00},
+		{0x17, 0x01, 0xa4, 0x00}, {0x28, 0x01, 0xa4, 0x01},
+		{0x02, 0x01, 0xa9, 0x00}, {0x09, 0x01, 0xa9, 0x00},
+		{0x17, 0x01, 0xa9, 0x00}, {0x28, 0x01, 0xa9, 0x01},
+		{0x02, 0x01, 0xaa, 0x00}, {0x09, 0x01, 0xaa, 0x00},
+		{0x17, 0x01, 0xaa, 0x00}, {0x28, 0x01, 0xaa, 0x01},
+		{0x02, 0x01, 0xad, 0x00}, {0x09, 0x01, 0xad, 0x00},
+		{0x17, 0x01, 0xad, 0x00}, {0x28, 0x01, 0xad, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xa4, 0x00}, {0x06, 0x01, 0xa4, 0x00},
+		{0x0a, 0x01, 0xa4, 0x00}, {0x0f, 0x01, 0xa4, 0x00},
+		{0x18, 0x01, 0xa4, 0x00}, {0x1f, 0x01, 0xa4, 0x00},
+		{0x29, 0x01, 0xa4, 0x00}, {0x38, 0x01, 0xa4, 0x01},
+		{0x03, 0x01, 0xa9, 0x00}, {0x06, 0x01, 0xa9, 0x00},
+		{0x0a, 0x01, 0xa9, 0x00}, {0x0f, 0x01, 0xa9, 0x00},
+		{0x18, 0x01, 0xa9, 0x00}, {0x1f, 0x01, 0xa9, 0x00},
+		{0x29, 0x01, 0xa9, 0x00}, {0x38, 0x01, 0xa9, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xaa, 0x00}, {0x06, 0x01, 0xaa, 0x00},
+		{0x0a, 0x01, 0xaa, 0x00}, {0x0f, 0x01, 0xaa, 0x00},
+		{0x18, 0x01, 0xaa, 0x00}, {0x1f, 0x01, 0xaa, 0x00},
+		{0x29, 0x01, 0xaa, 0x00}, {0x38, 0x01, 0xaa, 0x01},
+		{0x03, 0x01, 0xad, 0x00}, {0x06, 0x01, 0xad, 0x00},
+		{0x0a, 0x01, 0xad, 0x00}, {0x0f, 0x01, 0xad, 0x00},
+		{0x18, 0x01, 0xad, 0x00}, {0x1f, 0x01, 0xad, 0x00},
+		{0x29, 0x01, 0xad, 0x00}, {0x38, 0x01, 0xad, 0x01}
+	],
+	[
+		{0x89, 0x00, 0x00, 0x00}, {0x8a, 0x00, 0x00, 0x00},
+		{0x8c, 0x00, 0x00, 0x00}, {0x8d, 0x00, 0x00, 0x00},
+		{0x90, 0x00, 0x00, 0x00}, {0x91, 0x00, 0x00, 0x00},
+		{0x93, 0x00, 0x00, 0x00}, {0x96, 0x00, 0x00, 0x00},
+		{0x9c, 0x00, 0x00, 0x00}, {0x9f, 0x00, 0x00, 0x00},
+		{0xa3, 0x00, 0x00, 0x00}, {0xa6, 0x00, 0x00, 0x00},
+		{0xab, 0x00, 0x00, 0x00}, {0xae, 0x00, 0x00, 0x00},
+		{0xb5, 0x00, 0x00, 0x00}, {0xbe, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0xb2, 0x01}, {0x00, 0x01, 0xb5, 0x01},
+		{0x00, 0x01, 0xb9, 0x01}, {0x00, 0x01, 0xba, 0x01},
+		{0x00, 0x01, 0xbb, 0x01}, {0x00, 0x01, 0xbd, 0x01},
+		{0x00, 0x01, 0xbe, 0x01}, {0x00, 0x01, 0xc4, 0x01},
+		{0x00, 0x01, 0xc6, 0x01}, {0x00, 0x01, 0xe4, 0x01},
+		{0x00, 0x01, 0xe8, 0x01}, {0x00, 0x01, 0xe9, 0x01},
+		{0x94, 0x00, 0x00, 0x00}, {0x95, 0x00, 0x00, 0x00},
+		{0x97, 0x00, 0x00, 0x00}, {0x98, 0x00, 0x00, 0x00}
+	],
+	/* 135 */
+	[
+		{0x01, 0x01, 0xb2, 0x00}, {0x16, 0x01, 0xb2, 0x01},
+		{0x01, 0x01, 0xb5, 0x00}, {0x16, 0x01, 0xb5, 0x01},
+		{0x01, 0x01, 0xb9, 0x00}, {0x16, 0x01, 0xb9, 0x01},
+		{0x01, 0x01, 0xba, 0x00}, {0x16, 0x01, 0xba, 0x01},
+		{0x01, 0x01, 0xbb, 0x00}, {0x16, 0x01, 0xbb, 0x01},
+		{0x01, 0x01, 0xbd, 0x00}, {0x16, 0x01, 0xbd, 0x01},
+		{0x01, 0x01, 0xbe, 0x00}, {0x16, 0x01, 0xbe, 0x01},
+		{0x01, 0x01, 0xc4, 0x00}, {0x16, 0x01, 0xc4, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xb2, 0x00}, {0x09, 0x01, 0xb2, 0x00},
+		{0x17, 0x01, 0xb2, 0x00}, {0x28, 0x01, 0xb2, 0x01},
+		{0x02, 0x01, 0xb5, 0x00}, {0x09, 0x01, 0xb5, 0x00},
+		{0x17, 0x01, 0xb5, 0x00}, {0x28, 0x01, 0xb5, 0x01},
+		{0x02, 0x01, 0xb9, 0x00}, {0x09, 0x01, 0xb9, 0x00},
+		{0x17, 0x01, 0xb9, 0x00}, {0x28, 0x01, 0xb9, 0x01},
+		{0x02, 0x01, 0xba, 0x00}, {0x09, 0x01, 0xba, 0x00},
+		{0x17, 0x01, 0xba, 0x00}, {0x28, 0x01, 0xba, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xb2, 0x00}, {0x06, 0x01, 0xb2, 0x00},
+		{0x0a, 0x01, 0xb2, 0x00}, {0x0f, 0x01, 0xb2, 0x00},
+		{0x18, 0x01, 0xb2, 0x00}, {0x1f, 0x01, 0xb2, 0x00},
+		{0x29, 0x01, 0xb2, 0x00}, {0x38, 0x01, 0xb2, 0x01},
+		{0x03, 0x01, 0xb5, 0x00}, {0x06, 0x01, 0xb5, 0x00},
+		{0x0a, 0x01, 0xb5, 0x00}, {0x0f, 0x01, 0xb5, 0x00},
+		{0x18, 0x01, 0xb5, 0x00}, {0x1f, 0x01, 0xb5, 0x00},
+		{0x29, 0x01, 0xb5, 0x00}, {0x38, 0x01, 0xb5, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xb9, 0x00}, {0x06, 0x01, 0xb9, 0x00},
+		{0x0a, 0x01, 0xb9, 0x00}, {0x0f, 0x01, 0xb9, 0x00},
+		{0x18, 0x01, 0xb9, 0x00}, {0x1f, 0x01, 0xb9, 0x00},
+		{0x29, 0x01, 0xb9, 0x00}, {0x38, 0x01, 0xb9, 0x01},
+		{0x03, 0x01, 0xba, 0x00}, {0x06, 0x01, 0xba, 0x00},
+		{0x0a, 0x01, 0xba, 0x00}, {0x0f, 0x01, 0xba, 0x00},
+		{0x18, 0x01, 0xba, 0x00}, {0x1f, 0x01, 0xba, 0x00},
+		{0x29, 0x01, 0xba, 0x00}, {0x38, 0x01, 0xba, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xbb, 0x00}, {0x09, 0x01, 0xbb, 0x00},
+		{0x17, 0x01, 0xbb, 0x00}, {0x28, 0x01, 0xbb, 0x01},
+		{0x02, 0x01, 0xbd, 0x00}, {0x09, 0x01, 0xbd, 0x00},
+		{0x17, 0x01, 0xbd, 0x00}, {0x28, 0x01, 0xbd, 0x01},
+		{0x02, 0x01, 0xbe, 0x00}, {0x09, 0x01, 0xbe, 0x00},
+		{0x17, 0x01, 0xbe, 0x00}, {0x28, 0x01, 0xbe, 0x01},
+		{0x02, 0x01, 0xc4, 0x00}, {0x09, 0x01, 0xc4, 0x00},
+		{0x17, 0x01, 0xc4, 0x00}, {0x28, 0x01, 0xc4, 0x01}
+	],
+	/* 140 */
+	[
+		{0x03, 0x01, 0xbb, 0x00}, {0x06, 0x01, 0xbb, 0x00},
+		{0x0a, 0x01, 0xbb, 0x00}, {0x0f, 0x01, 0xbb, 0x00},
+		{0x18, 0x01, 0xbb, 0x00}, {0x1f, 0x01, 0xbb, 0x00},
+		{0x29, 0x01, 0xbb, 0x00}, {0x38, 0x01, 0xbb, 0x01},
+		{0x03, 0x01, 0xbd, 0x00}, {0x06, 0x01, 0xbd, 0x00},
+		{0x0a, 0x01, 0xbd, 0x00}, {0x0f, 0x01, 0xbd, 0x00},
+		{0x18, 0x01, 0xbd, 0x00}, {0x1f, 0x01, 0xbd, 0x00},
+		{0x29, 0x01, 0xbd, 0x00}, {0x38, 0x01, 0xbd, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xbe, 0x00}, {0x06, 0x01, 0xbe, 0x00},
+		{0x0a, 0x01, 0xbe, 0x00}, {0x0f, 0x01, 0xbe, 0x00},
+		{0x18, 0x01, 0xbe, 0x00}, {0x1f, 0x01, 0xbe, 0x00},
+		{0x29, 0x01, 0xbe, 0x00}, {0x38, 0x01, 0xbe, 0x01},
+		{0x03, 0x01, 0xc4, 0x00}, {0x06, 0x01, 0xc4, 0x00},
+		{0x0a, 0x01, 0xc4, 0x00}, {0x0f, 0x01, 0xc4, 0x00},
+		{0x18, 0x01, 0xc4, 0x00}, {0x1f, 0x01, 0xc4, 0x00},
+		{0x29, 0x01, 0xc4, 0x00}, {0x38, 0x01, 0xc4, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xc6, 0x00}, {0x16, 0x01, 0xc6, 0x01},
+		{0x01, 0x01, 0xe4, 0x00}, {0x16, 0x01, 0xe4, 0x01},
+		{0x01, 0x01, 0xe8, 0x00}, {0x16, 0x01, 0xe8, 0x01},
+		{0x01, 0x01, 0xe9, 0x00}, {0x16, 0x01, 0xe9, 0x01},
+		{0x00, 0x01, 0x01, 0x01}, {0x00, 0x01, 0x87, 0x01},
+		{0x00, 0x01, 0x89, 0x01}, {0x00, 0x01, 0x8a, 0x01},
+		{0x00, 0x01, 0x8b, 0x01}, {0x00, 0x01, 0x8c, 0x01},
+		{0x00, 0x01, 0x8d, 0x01}, {0x00, 0x01, 0x8f, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xc6, 0x00}, {0x09, 0x01, 0xc6, 0x00},
+		{0x17, 0x01, 0xc6, 0x00}, {0x28, 0x01, 0xc6, 0x01},
+		{0x02, 0x01, 0xe4, 0x00}, {0x09, 0x01, 0xe4, 0x00},
+		{0x17, 0x01, 0xe4, 0x00}, {0x28, 0x01, 0xe4, 0x01},
+		{0x02, 0x01, 0xe8, 0x00}, {0x09, 0x01, 0xe8, 0x00},
+		{0x17, 0x01, 0xe8, 0x00}, {0x28, 0x01, 0xe8, 0x01},
+		{0x02, 0x01, 0xe9, 0x00}, {0x09, 0x01, 0xe9, 0x00},
+		{0x17, 0x01, 0xe9, 0x00}, {0x28, 0x01, 0xe9, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xc6, 0x00}, {0x06, 0x01, 0xc6, 0x00},
+		{0x0a, 0x01, 0xc6, 0x00}, {0x0f, 0x01, 0xc6, 0x00},
+		{0x18, 0x01, 0xc6, 0x00}, {0x1f, 0x01, 0xc6, 0x00},
+		{0x29, 0x01, 0xc6, 0x00}, {0x38, 0x01, 0xc6, 0x01},
+		{0x03, 0x01, 0xe4, 0x00}, {0x06, 0x01, 0xe4, 0x00},
+		{0x0a, 0x01, 0xe4, 0x00}, {0x0f, 0x01, 0xe4, 0x00},
+		{0x18, 0x01, 0xe4, 0x00}, {0x1f, 0x01, 0xe4, 0x00},
+		{0x29, 0x01, 0xe4, 0x00}, {0x38, 0x01, 0xe4, 0x01}
+	],
+	/* 145 */
+	[
+		{0x03, 0x01, 0xe8, 0x00}, {0x06, 0x01, 0xe8, 0x00},
+		{0x0a, 0x01, 0xe8, 0x00}, {0x0f, 0x01, 0xe8, 0x00},
+		{0x18, 0x01, 0xe8, 0x00}, {0x1f, 0x01, 0xe8, 0x00},
+		{0x29, 0x01, 0xe8, 0x00}, {0x38, 0x01, 0xe8, 0x01},
+		{0x03, 0x01, 0xe9, 0x00}, {0x06, 0x01, 0xe9, 0x00},
+		{0x0a, 0x01, 0xe9, 0x00}, {0x0f, 0x01, 0xe9, 0x00},
+		{0x18, 0x01, 0xe9, 0x00}, {0x1f, 0x01, 0xe9, 0x00},
+		{0x29, 0x01, 0xe9, 0x00}, {0x38, 0x01, 0xe9, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x01, 0x00}, {0x16, 0x01, 0x01, 0x01},
+		{0x01, 0x01, 0x87, 0x00}, {0x16, 0x01, 0x87, 0x01},
+		{0x01, 0x01, 0x89, 0x00}, {0x16, 0x01, 0x89, 0x01},
+		{0x01, 0x01, 0x8a, 0x00}, {0x16, 0x01, 0x8a, 0x01},
+		{0x01, 0x01, 0x8b, 0x00}, {0x16, 0x01, 0x8b, 0x01},
+		{0x01, 0x01, 0x8c, 0x00}, {0x16, 0x01, 0x8c, 0x01},
+		{0x01, 0x01, 0x8d, 0x00}, {0x16, 0x01, 0x8d, 0x01},
+		{0x01, 0x01, 0x8f, 0x00}, {0x16, 0x01, 0x8f, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x01, 0x00}, {0x09, 0x01, 0x01, 0x00},
+		{0x17, 0x01, 0x01, 0x00}, {0x28, 0x01, 0x01, 0x01},
+		{0x02, 0x01, 0x87, 0x00}, {0x09, 0x01, 0x87, 0x00},
+		{0x17, 0x01, 0x87, 0x00}, {0x28, 0x01, 0x87, 0x01},
+		{0x02, 0x01, 0x89, 0x00}, {0x09, 0x01, 0x89, 0x00},
+		{0x17, 0x01, 0x89, 0x00}, {0x28, 0x01, 0x89, 0x01},
+		{0x02, 0x01, 0x8a, 0x00}, {0x09, 0x01, 0x8a, 0x00},
+		{0x17, 0x01, 0x8a, 0x00}, {0x28, 0x01, 0x8a, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x01, 0x00}, {0x06, 0x01, 0x01, 0x00},
+		{0x0a, 0x01, 0x01, 0x00}, {0x0f, 0x01, 0x01, 0x00},
+		{0x18, 0x01, 0x01, 0x00}, {0x1f, 0x01, 0x01, 0x00},
+		{0x29, 0x01, 0x01, 0x00}, {0x38, 0x01, 0x01, 0x01},
+		{0x03, 0x01, 0x87, 0x00}, {0x06, 0x01, 0x87, 0x00},
+		{0x0a, 0x01, 0x87, 0x00}, {0x0f, 0x01, 0x87, 0x00},
+		{0x18, 0x01, 0x87, 0x00}, {0x1f, 0x01, 0x87, 0x00},
+		{0x29, 0x01, 0x87, 0x00}, {0x38, 0x01, 0x87, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x89, 0x00}, {0x06, 0x01, 0x89, 0x00},
+		{0x0a, 0x01, 0x89, 0x00}, {0x0f, 0x01, 0x89, 0x00},
+		{0x18, 0x01, 0x89, 0x00}, {0x1f, 0x01, 0x89, 0x00},
+		{0x29, 0x01, 0x89, 0x00}, {0x38, 0x01, 0x89, 0x01},
+		{0x03, 0x01, 0x8a, 0x00}, {0x06, 0x01, 0x8a, 0x00},
+		{0x0a, 0x01, 0x8a, 0x00}, {0x0f, 0x01, 0x8a, 0x00},
+		{0x18, 0x01, 0x8a, 0x00}, {0x1f, 0x01, 0x8a, 0x00},
+		{0x29, 0x01, 0x8a, 0x00}, {0x38, 0x01, 0x8a, 0x01}
+	],
+	/* 150 */
+	[
+		{0x02, 0x01, 0x8b, 0x00}, {0x09, 0x01, 0x8b, 0x00},
+		{0x17, 0x01, 0x8b, 0x00}, {0x28, 0x01, 0x8b, 0x01},
+		{0x02, 0x01, 0x8c, 0x00}, {0x09, 0x01, 0x8c, 0x00},
+		{0x17, 0x01, 0x8c, 0x00}, {0x28, 0x01, 0x8c, 0x01},
+		{0x02, 0x01, 0x8d, 0x00}, {0x09, 0x01, 0x8d, 0x00},
+		{0x17, 0x01, 0x8d, 0x00}, {0x28, 0x01, 0x8d, 0x01},
+		{0x02, 0x01, 0x8f, 0x00}, {0x09, 0x01, 0x8f, 0x00},
+		{0x17, 0x01, 0x8f, 0x00}, {0x28, 0x01, 0x8f, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x8b, 0x00}, {0x06, 0x01, 0x8b, 0x00},
+		{0x0a, 0x01, 0x8b, 0x00}, {0x0f, 0x01, 0x8b, 0x00},
+		{0x18, 0x01, 0x8b, 0x00}, {0x1f, 0x01, 0x8b, 0x00},
+		{0x29, 0x01, 0x8b, 0x00}, {0x38, 0x01, 0x8b, 0x01},
+		{0x03, 0x01, 0x8c, 0x00}, {0x06, 0x01, 0x8c, 0x00},
+		{0x0a, 0x01, 0x8c, 0x00}, {0x0f, 0x01, 0x8c, 0x00},
+		{0x18, 0x01, 0x8c, 0x00}, {0x1f, 0x01, 0x8c, 0x00},
+		{0x29, 0x01, 0x8c, 0x00}, {0x38, 0x01, 0x8c, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x8d, 0x00}, {0x06, 0x01, 0x8d, 0x00},
+		{0x0a, 0x01, 0x8d, 0x00}, {0x0f, 0x01, 0x8d, 0x00},
+		{0x18, 0x01, 0x8d, 0x00}, {0x1f, 0x01, 0x8d, 0x00},
+		{0x29, 0x01, 0x8d, 0x00}, {0x38, 0x01, 0x8d, 0x01},
+		{0x03, 0x01, 0x8f, 0x00}, {0x06, 0x01, 0x8f, 0x00},
+		{0x0a, 0x01, 0x8f, 0x00}, {0x0f, 0x01, 0x8f, 0x00},
+		{0x18, 0x01, 0x8f, 0x00}, {0x1f, 0x01, 0x8f, 0x00},
+		{0x29, 0x01, 0x8f, 0x00}, {0x38, 0x01, 0x8f, 0x01}
+	],
+	[
+		{0x9d, 0x00, 0x00, 0x00}, {0x9e, 0x00, 0x00, 0x00},
+		{0xa0, 0x00, 0x00, 0x00}, {0xa1, 0x00, 0x00, 0x00},
+		{0xa4, 0x00, 0x00, 0x00}, {0xa5, 0x00, 0x00, 0x00},
+		{0xa7, 0x00, 0x00, 0x00}, {0xa8, 0x00, 0x00, 0x00},
+		{0xac, 0x00, 0x00, 0x00}, {0xad, 0x00, 0x00, 0x00},
+		{0xaf, 0x00, 0x00, 0x00}, {0xb1, 0x00, 0x00, 0x00},
+		{0xb6, 0x00, 0x00, 0x00}, {0xb9, 0x00, 0x00, 0x00},
+		{0xbf, 0x00, 0x00, 0x00}, {0xcf, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0x93, 0x01}, {0x00, 0x01, 0x95, 0x01},
+		{0x00, 0x01, 0x96, 0x01}, {0x00, 0x01, 0x97, 0x01},
+		{0x00, 0x01, 0x98, 0x01}, {0x00, 0x01, 0x9b, 0x01},
+		{0x00, 0x01, 0x9d, 0x01}, {0x00, 0x01, 0x9e, 0x01},
+		{0x00, 0x01, 0xa5, 0x01}, {0x00, 0x01, 0xa6, 0x01},
+		{0x00, 0x01, 0xa8, 0x01}, {0x00, 0x01, 0xae, 0x01},
+		{0x00, 0x01, 0xaf, 0x01}, {0x00, 0x01, 0xb4, 0x01},
+		{0x00, 0x01, 0xb6, 0x01}, {0x00, 0x01, 0xb7, 0x01}
+	],
+	/* 155 */
+	[
+		{0x01, 0x01, 0x93, 0x00}, {0x16, 0x01, 0x93, 0x01},
+		{0x01, 0x01, 0x95, 0x00}, {0x16, 0x01, 0x95, 0x01},
+		{0x01, 0x01, 0x96, 0x00}, {0x16, 0x01, 0x96, 0x01},
+		{0x01, 0x01, 0x97, 0x00}, {0x16, 0x01, 0x97, 0x01},
+		{0x01, 0x01, 0x98, 0x00}, {0x16, 0x01, 0x98, 0x01},
+		{0x01, 0x01, 0x9b, 0x00}, {0x16, 0x01, 0x9b, 0x01},
+		{0x01, 0x01, 0x9d, 0x00}, {0x16, 0x01, 0x9d, 0x01},
+		{0x01, 0x01, 0x9e, 0x00}, {0x16, 0x01, 0x9e, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x93, 0x00}, {0x09, 0x01, 0x93, 0x00},
+		{0x17, 0x01, 0x93, 0x00}, {0x28, 0x01, 0x93, 0x01},
+		{0x02, 0x01, 0x95, 0x00}, {0x09, 0x01, 0x95, 0x00},
+		{0x17, 0x01, 0x95, 0x00}, {0x28, 0x01, 0x95, 0x01},
+		{0x02, 0x01, 0x96, 0x00}, {0x09, 0x01, 0x96, 0x00},
+		{0x17, 0x01, 0x96, 0x00}, {0x28, 0x01, 0x96, 0x01},
+		{0x02, 0x01, 0x97, 0x00}, {0x09, 0x01, 0x97, 0x00},
+		{0x17, 0x01, 0x97, 0x00}, {0x28, 0x01, 0x97, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x93, 0x00}, {0x06, 0x01, 0x93, 0x00},
+		{0x0a, 0x01, 0x93, 0x00}, {0x0f, 0x01, 0x93, 0x00},
+		{0x18, 0x01, 0x93, 0x00}, {0x1f, 0x01, 0x93, 0x00},
+		{0x29, 0x01, 0x93, 0x00}, {0x38, 0x01, 0x93, 0x01},
+		{0x03, 0x01, 0x95, 0x00}, {0x06, 0x01, 0x95, 0x00},
+		{0x0a, 0x01, 0x95, 0x00}, {0x0f, 0x01, 0x95, 0x00},
+		{0x18, 0x01, 0x95, 0x00}, {0x1f, 0x01, 0x95, 0x00},
+		{0x29, 0x01, 0x95, 0x00}, {0x38, 0x01, 0x95, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x96, 0x00}, {0x06, 0x01, 0x96, 0x00},
+		{0x0a, 0x01, 0x96, 0x00}, {0x0f, 0x01, 0x96, 0x00},
+		{0x18, 0x01, 0x96, 0x00}, {0x1f, 0x01, 0x96, 0x00},
+		{0x29, 0x01, 0x96, 0x00}, {0x38, 0x01, 0x96, 0x01},
+		{0x03, 0x01, 0x97, 0x00}, {0x06, 0x01, 0x97, 0x00},
+		{0x0a, 0x01, 0x97, 0x00}, {0x0f, 0x01, 0x97, 0x00},
+		{0x18, 0x01, 0x97, 0x00}, {0x1f, 0x01, 0x97, 0x00},
+		{0x29, 0x01, 0x97, 0x00}, {0x38, 0x01, 0x97, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x98, 0x00}, {0x09, 0x01, 0x98, 0x00},
+		{0x17, 0x01, 0x98, 0x00}, {0x28, 0x01, 0x98, 0x01},
+		{0x02, 0x01, 0x9b, 0x00}, {0x09, 0x01, 0x9b, 0x00},
+		{0x17, 0x01, 0x9b, 0x00}, {0x28, 0x01, 0x9b, 0x01},
+		{0x02, 0x01, 0x9d, 0x00}, {0x09, 0x01, 0x9d, 0x00},
+		{0x17, 0x01, 0x9d, 0x00}, {0x28, 0x01, 0x9d, 0x01},
+		{0x02, 0x01, 0x9e, 0x00}, {0x09, 0x01, 0x9e, 0x00},
+		{0x17, 0x01, 0x9e, 0x00}, {0x28, 0x01, 0x9e, 0x01}
+	],
+	/* 160 */
+	[
+		{0x03, 0x01, 0x98, 0x00}, {0x06, 0x01, 0x98, 0x00},
+		{0x0a, 0x01, 0x98, 0x00}, {0x0f, 0x01, 0x98, 0x00},
+		{0x18, 0x01, 0x98, 0x00}, {0x1f, 0x01, 0x98, 0x00},
+		{0x29, 0x01, 0x98, 0x00}, {0x38, 0x01, 0x98, 0x01},
+		{0x03, 0x01, 0x9b, 0x00}, {0x06, 0x01, 0x9b, 0x00},
+		{0x0a, 0x01, 0x9b, 0x00}, {0x0f, 0x01, 0x9b, 0x00},
+		{0x18, 0x01, 0x9b, 0x00}, {0x1f, 0x01, 0x9b, 0x00},
+		{0x29, 0x01, 0x9b, 0x00}, {0x38, 0x01, 0x9b, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x9d, 0x00}, {0x06, 0x01, 0x9d, 0x00},
+		{0x0a, 0x01, 0x9d, 0x00}, {0x0f, 0x01, 0x9d, 0x00},
+		{0x18, 0x01, 0x9d, 0x00}, {0x1f, 0x01, 0x9d, 0x00},
+		{0x29, 0x01, 0x9d, 0x00}, {0x38, 0x01, 0x9d, 0x01},
+		{0x03, 0x01, 0x9e, 0x00}, {0x06, 0x01, 0x9e, 0x00},
+		{0x0a, 0x01, 0x9e, 0x00}, {0x0f, 0x01, 0x9e, 0x00},
+		{0x18, 0x01, 0x9e, 0x00}, {0x1f, 0x01, 0x9e, 0x00},
+		{0x29, 0x01, 0x9e, 0x00}, {0x38, 0x01, 0x9e, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xa5, 0x00}, {0x16, 0x01, 0xa5, 0x01},
+		{0x01, 0x01, 0xa6, 0x00}, {0x16, 0x01, 0xa6, 0x01},
+		{0x01, 0x01, 0xa8, 0x00}, {0x16, 0x01, 0xa8, 0x01},
+		{0x01, 0x01, 0xae, 0x00}, {0x16, 0x01, 0xae, 0x01},
+		{0x01, 0x01, 0xaf, 0x00}, {0x16, 0x01, 0xaf, 0x01},
+		{0x01, 0x01, 0xb4, 0x00}, {0x16, 0x01, 0xb4, 0x01},
+		{0x01, 0x01, 0xb6, 0x00}, {0x16, 0x01, 0xb6, 0x01},
+		{0x01, 0x01, 0xb7, 0x00}, {0x16, 0x01, 0xb7, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xa5, 0x00}, {0x09, 0x01, 0xa5, 0x00},
+		{0x17, 0x01, 0xa5, 0x00}, {0x28, 0x01, 0xa5, 0x01},
+		{0x02, 0x01, 0xa6, 0x00}, {0x09, 0x01, 0xa6, 0x00},
+		{0x17, 0x01, 0xa6, 0x00}, {0x28, 0x01, 0xa6, 0x01},
+		{0x02, 0x01, 0xa8, 0x00}, {0x09, 0x01, 0xa8, 0x00},
+		{0x17, 0x01, 0xa8, 0x00}, {0x28, 0x01, 0xa8, 0x01},
+		{0x02, 0x01, 0xae, 0x00}, {0x09, 0x01, 0xae, 0x00},
+		{0x17, 0x01, 0xae, 0x00}, {0x28, 0x01, 0xae, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xa5, 0x00}, {0x06, 0x01, 0xa5, 0x00},
+		{0x0a, 0x01, 0xa5, 0x00}, {0x0f, 0x01, 0xa5, 0x00},
+		{0x18, 0x01, 0xa5, 0x00}, {0x1f, 0x01, 0xa5, 0x00},
+		{0x29, 0x01, 0xa5, 0x00}, {0x38, 0x01, 0xa5, 0x01},
+		{0x03, 0x01, 0xa6, 0x00}, {0x06, 0x01, 0xa6, 0x00},
+		{0x0a, 0x01, 0xa6, 0x00}, {0x0f, 0x01, 0xa6, 0x00},
+		{0x18, 0x01, 0xa6, 0x00}, {0x1f, 0x01, 0xa6, 0x00},
+		{0x29, 0x01, 0xa6, 0x00}, {0x38, 0x01, 0xa6, 0x01}
+	],
+	/* 165 */
+	[
+		{0x03, 0x01, 0xa8, 0x00}, {0x06, 0x01, 0xa8, 0x00},
+		{0x0a, 0x01, 0xa8, 0x00}, {0x0f, 0x01, 0xa8, 0x00},
+		{0x18, 0x01, 0xa8, 0x00}, {0x1f, 0x01, 0xa8, 0x00},
+		{0x29, 0x01, 0xa8, 0x00}, {0x38, 0x01, 0xa8, 0x01},
+		{0x03, 0x01, 0xae, 0x00}, {0x06, 0x01, 0xae, 0x00},
+		{0x0a, 0x01, 0xae, 0x00}, {0x0f, 0x01, 0xae, 0x00},
+		{0x18, 0x01, 0xae, 0x00}, {0x1f, 0x01, 0xae, 0x00},
+		{0x29, 0x01, 0xae, 0x00}, {0x38, 0x01, 0xae, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xaf, 0x00}, {0x09, 0x01, 0xaf, 0x00},
+		{0x17, 0x01, 0xaf, 0x00}, {0x28, 0x01, 0xaf, 0x01},
+		{0x02, 0x01, 0xb4, 0x00}, {0x09, 0x01, 0xb4, 0x00},
+		{0x17, 0x01, 0xb4, 0x00}, {0x28, 0x01, 0xb4, 0x01},
+		{0x02, 0x01, 0xb6, 0x00}, {0x09, 0x01, 0xb6, 0x00},
+		{0x17, 0x01, 0xb6, 0x00}, {0x28, 0x01, 0xb6, 0x01},
+		{0x02, 0x01, 0xb7, 0x00}, {0x09, 0x01, 0xb7, 0x00},
+		{0x17, 0x01, 0xb7, 0x00}, {0x28, 0x01, 0xb7, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xaf, 0x00}, {0x06, 0x01, 0xaf, 0x00},
+		{0x0a, 0x01, 0xaf, 0x00}, {0x0f, 0x01, 0xaf, 0x00},
+		{0x18, 0x01, 0xaf, 0x00}, {0x1f, 0x01, 0xaf, 0x00},
+		{0x29, 0x01, 0xaf, 0x00}, {0x38, 0x01, 0xaf, 0x01},
+		{0x03, 0x01, 0xb4, 0x00}, {0x06, 0x01, 0xb4, 0x00},
+		{0x0a, 0x01, 0xb4, 0x00}, {0x0f, 0x01, 0xb4, 0x00},
+		{0x18, 0x01, 0xb4, 0x00}, {0x1f, 0x01, 0xb4, 0x00},
+		{0x29, 0x01, 0xb4, 0x00}, {0x38, 0x01, 0xb4, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xb6, 0x00}, {0x06, 0x01, 0xb6, 0x00},
+		{0x0a, 0x01, 0xb6, 0x00}, {0x0f, 0x01, 0xb6, 0x00},
+		{0x18, 0x01, 0xb6, 0x00}, {0x1f, 0x01, 0xb6, 0x00},
+		{0x29, 0x01, 0xb6, 0x00}, {0x38, 0x01, 0xb6, 0x01},
+		{0x03, 0x01, 0xb7, 0x00}, {0x06, 0x01, 0xb7, 0x00},
+		{0x0a, 0x01, 0xb7, 0x00}, {0x0f, 0x01, 0xb7, 0x00},
+		{0x18, 0x01, 0xb7, 0x00}, {0x1f, 0x01, 0xb7, 0x00},
+		{0x29, 0x01, 0xb7, 0x00}, {0x38, 0x01, 0xb7, 0x01}
+	],
+	[
+		{0x00, 0x01, 0xbc, 0x01}, {0x00, 0x01, 0xbf, 0x01},
+		{0x00, 0x01, 0xc5, 0x01}, {0x00, 0x01, 0xe7, 0x01},
+		{0x00, 0x01, 0xef, 0x01}, {0xb0, 0x00, 0x00, 0x00},
+		{0xb2, 0x00, 0x00, 0x00}, {0xb3, 0x00, 0x00, 0x00},
+		{0xb7, 0x00, 0x00, 0x00}, {0xb8, 0x00, 0x00, 0x00},
+		{0xba, 0x00, 0x00, 0x00}, {0xbb, 0x00, 0x00, 0x00},
+		{0xc0, 0x00, 0x00, 0x00}, {0xc7, 0x00, 0x00, 0x00},
+		{0xd0, 0x00, 0x00, 0x00}, {0xdf, 0x00, 0x00, 0x01}
+	],
+	/* 170 */
+	[
+		{0x01, 0x01, 0xbc, 0x00}, {0x16, 0x01, 0xbc, 0x01},
+		{0x01, 0x01, 0xbf, 0x00}, {0x16, 0x01, 0xbf, 0x01},
+		{0x01, 0x01, 0xc5, 0x00}, {0x16, 0x01, 0xc5, 0x01},
+		{0x01, 0x01, 0xe7, 0x00}, {0x16, 0x01, 0xe7, 0x01},
+		{0x01, 0x01, 0xef, 0x00}, {0x16, 0x01, 0xef, 0x01},
+		{0x00, 0x01, 0x09, 0x01}, {0x00, 0x01, 0x8e, 0x01},
+		{0x00, 0x01, 0x90, 0x01}, {0x00, 0x01, 0x91, 0x01},
+		{0x00, 0x01, 0x94, 0x01}, {0x00, 0x01, 0x9f, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xbc, 0x00}, {0x09, 0x01, 0xbc, 0x00},
+		{0x17, 0x01, 0xbc, 0x00}, {0x28, 0x01, 0xbc, 0x01},
+		{0x02, 0x01, 0xbf, 0x00}, {0x09, 0x01, 0xbf, 0x00},
+		{0x17, 0x01, 0xbf, 0x00}, {0x28, 0x01, 0xbf, 0x01},
+		{0x02, 0x01, 0xc5, 0x00}, {0x09, 0x01, 0xc5, 0x00},
+		{0x17, 0x01, 0xc5, 0x00}, {0x28, 0x01, 0xc5, 0x01},
+		{0x02, 0x01, 0xe7, 0x00}, {0x09, 0x01, 0xe7, 0x00},
+		{0x17, 0x01, 0xe7, 0x00}, {0x28, 0x01, 0xe7, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xbc, 0x00}, {0x06, 0x01, 0xbc, 0x00},
+		{0x0a, 0x01, 0xbc, 0x00}, {0x0f, 0x01, 0xbc, 0x00},
+		{0x18, 0x01, 0xbc, 0x00}, {0x1f, 0x01, 0xbc, 0x00},
+		{0x29, 0x01, 0xbc, 0x00}, {0x38, 0x01, 0xbc, 0x01},
+		{0x03, 0x01, 0xbf, 0x00}, {0x06, 0x01, 0xbf, 0x00},
+		{0x0a, 0x01, 0xbf, 0x00}, {0x0f, 0x01, 0xbf, 0x00},
+		{0x18, 0x01, 0xbf, 0x00}, {0x1f, 0x01, 0xbf, 0x00},
+		{0x29, 0x01, 0xbf, 0x00}, {0x38, 0x01, 0xbf, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xc5, 0x00}, {0x06, 0x01, 0xc5, 0x00},
+		{0x0a, 0x01, 0xc5, 0x00}, {0x0f, 0x01, 0xc5, 0x00},
+		{0x18, 0x01, 0xc5, 0x00}, {0x1f, 0x01, 0xc5, 0x00},
+		{0x29, 0x01, 0xc5, 0x00}, {0x38, 0x01, 0xc5, 0x01},
+		{0x03, 0x01, 0xe7, 0x00}, {0x06, 0x01, 0xe7, 0x00},
+		{0x0a, 0x01, 0xe7, 0x00}, {0x0f, 0x01, 0xe7, 0x00},
+		{0x18, 0x01, 0xe7, 0x00}, {0x1f, 0x01, 0xe7, 0x00},
+		{0x29, 0x01, 0xe7, 0x00}, {0x38, 0x01, 0xe7, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xef, 0x00}, {0x09, 0x01, 0xef, 0x00},
+		{0x17, 0x01, 0xef, 0x00}, {0x28, 0x01, 0xef, 0x01},
+		{0x01, 0x01, 0x09, 0x00}, {0x16, 0x01, 0x09, 0x01},
+		{0x01, 0x01, 0x8e, 0x00}, {0x16, 0x01, 0x8e, 0x01},
+		{0x01, 0x01, 0x90, 0x00}, {0x16, 0x01, 0x90, 0x01},
+		{0x01, 0x01, 0x91, 0x00}, {0x16, 0x01, 0x91, 0x01},
+		{0x01, 0x01, 0x94, 0x00}, {0x16, 0x01, 0x94, 0x01},
+		{0x01, 0x01, 0x9f, 0x00}, {0x16, 0x01, 0x9f, 0x01}
+	],
+	/* 175 */
+	[
+		{0x03, 0x01, 0xef, 0x00}, {0x06, 0x01, 0xef, 0x00},
+		{0x0a, 0x01, 0xef, 0x00}, {0x0f, 0x01, 0xef, 0x00},
+		{0x18, 0x01, 0xef, 0x00}, {0x1f, 0x01, 0xef, 0x00},
+		{0x29, 0x01, 0xef, 0x00}, {0x38, 0x01, 0xef, 0x01},
+		{0x02, 0x01, 0x09, 0x00}, {0x09, 0x01, 0x09, 0x00},
+		{0x17, 0x01, 0x09, 0x00}, {0x28, 0x01, 0x09, 0x01},
+		{0x02, 0x01, 0x8e, 0x00}, {0x09, 0x01, 0x8e, 0x00},
+		{0x17, 0x01, 0x8e, 0x00}, {0x28, 0x01, 0x8e, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x09, 0x00}, {0x06, 0x01, 0x09, 0x00},
+		{0x0a, 0x01, 0x09, 0x00}, {0x0f, 0x01, 0x09, 0x00},
+		{0x18, 0x01, 0x09, 0x00}, {0x1f, 0x01, 0x09, 0x00},
+		{0x29, 0x01, 0x09, 0x00}, {0x38, 0x01, 0x09, 0x01},
+		{0x03, 0x01, 0x8e, 0x00}, {0x06, 0x01, 0x8e, 0x00},
+		{0x0a, 0x01, 0x8e, 0x00}, {0x0f, 0x01, 0x8e, 0x00},
+		{0x18, 0x01, 0x8e, 0x00}, {0x1f, 0x01, 0x8e, 0x00},
+		{0x29, 0x01, 0x8e, 0x00}, {0x38, 0x01, 0x8e, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x90, 0x00}, {0x09, 0x01, 0x90, 0x00},
+		{0x17, 0x01, 0x90, 0x00}, {0x28, 0x01, 0x90, 0x01},
+		{0x02, 0x01, 0x91, 0x00}, {0x09, 0x01, 0x91, 0x00},
+		{0x17, 0x01, 0x91, 0x00}, {0x28, 0x01, 0x91, 0x01},
+		{0x02, 0x01, 0x94, 0x00}, {0x09, 0x01, 0x94, 0x00},
+		{0x17, 0x01, 0x94, 0x00}, {0x28, 0x01, 0x94, 0x01},
+		{0x02, 0x01, 0x9f, 0x00}, {0x09, 0x01, 0x9f, 0x00},
+		{0x17, 0x01, 0x9f, 0x00}, {0x28, 0x01, 0x9f, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x90, 0x00}, {0x06, 0x01, 0x90, 0x00},
+		{0x0a, 0x01, 0x90, 0x00}, {0x0f, 0x01, 0x90, 0x00},
+		{0x18, 0x01, 0x90, 0x00}, {0x1f, 0x01, 0x90, 0x00},
+		{0x29, 0x01, 0x90, 0x00}, {0x38, 0x01, 0x90, 0x01},
+		{0x03, 0x01, 0x91, 0x00}, {0x06, 0x01, 0x91, 0x00},
+		{0x0a, 0x01, 0x91, 0x00}, {0x0f, 0x01, 0x91, 0x00},
+		{0x18, 0x01, 0x91, 0x00}, {0x1f, 0x01, 0x91, 0x00},
+		{0x29, 0x01, 0x91, 0x00}, {0x38, 0x01, 0x91, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x94, 0x00}, {0x06, 0x01, 0x94, 0x00},
+		{0x0a, 0x01, 0x94, 0x00}, {0x0f, 0x01, 0x94, 0x00},
+		{0x18, 0x01, 0x94, 0x00}, {0x1f, 0x01, 0x94, 0x00},
+		{0x29, 0x01, 0x94, 0x00}, {0x38, 0x01, 0x94, 0x01},
+		{0x03, 0x01, 0x9f, 0x00}, {0x06, 0x01, 0x9f, 0x00},
+		{0x0a, 0x01, 0x9f, 0x00}, {0x0f, 0x01, 0x9f, 0x00},
+		{0x18, 0x01, 0x9f, 0x00}, {0x1f, 0x01, 0x9f, 0x00},
+		{0x29, 0x01, 0x9f, 0x00}, {0x38, 0x01, 0x9f, 0x01}
+	],
+	/* 180 */
+	[
+		{0x00, 0x01, 0xab, 0x01}, {0x00, 0x01, 0xce, 0x01},
+		{0x00, 0x01, 0xd7, 0x01}, {0x00, 0x01, 0xe1, 0x01},
+		{0x00, 0x01, 0xec, 0x01}, {0x00, 0x01, 0xed, 0x01},
+		{0xbc, 0x00, 0x00, 0x00}, {0xbd, 0x00, 0x00, 0x00},
+		{0xc1, 0x00, 0x00, 0x00}, {0xc4, 0x00, 0x00, 0x00},
+		{0xc8, 0x00, 0x00, 0x00}, {0xcb, 0x00, 0x00, 0x00},
+		{0xd1, 0x00, 0x00, 0x00}, {0xd8, 0x00, 0x00, 0x00},
+		{0xe0, 0x00, 0x00, 0x00}, {0xee, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xab, 0x00}, {0x16, 0x01, 0xab, 0x01},
+		{0x01, 0x01, 0xce, 0x00}, {0x16, 0x01, 0xce, 0x01},
+		{0x01, 0x01, 0xd7, 0x00}, {0x16, 0x01, 0xd7, 0x01},
+		{0x01, 0x01, 0xe1, 0x00}, {0x16, 0x01, 0xe1, 0x01},
+		{0x01, 0x01, 0xec, 0x00}, {0x16, 0x01, 0xec, 0x01},
+		{0x01, 0x01, 0xed, 0x00}, {0x16, 0x01, 0xed, 0x01},
+		{0x00, 0x01, 0xc7, 0x01}, {0x00, 0x01, 0xcf, 0x01},
+		{0x00, 0x01, 0xea, 0x01}, {0x00, 0x01, 0xeb, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xab, 0x00}, {0x09, 0x01, 0xab, 0x00},
+		{0x17, 0x01, 0xab, 0x00}, {0x28, 0x01, 0xab, 0x01},
+		{0x02, 0x01, 0xce, 0x00}, {0x09, 0x01, 0xce, 0x00},
+		{0x17, 0x01, 0xce, 0x00}, {0x28, 0x01, 0xce, 0x01},
+		{0x02, 0x01, 0xd7, 0x00}, {0x09, 0x01, 0xd7, 0x00},
+		{0x17, 0x01, 0xd7, 0x00}, {0x28, 0x01, 0xd7, 0x01},
+		{0x02, 0x01, 0xe1, 0x00}, {0x09, 0x01, 0xe1, 0x00},
+		{0x17, 0x01, 0xe1, 0x00}, {0x28, 0x01, 0xe1, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xab, 0x00}, {0x06, 0x01, 0xab, 0x00},
+		{0x0a, 0x01, 0xab, 0x00}, {0x0f, 0x01, 0xab, 0x00},
+		{0x18, 0x01, 0xab, 0x00}, {0x1f, 0x01, 0xab, 0x00},
+		{0x29, 0x01, 0xab, 0x00}, {0x38, 0x01, 0xab, 0x01},
+		{0x03, 0x01, 0xce, 0x00}, {0x06, 0x01, 0xce, 0x00},
+		{0x0a, 0x01, 0xce, 0x00}, {0x0f, 0x01, 0xce, 0x00},
+		{0x18, 0x01, 0xce, 0x00}, {0x1f, 0x01, 0xce, 0x00},
+		{0x29, 0x01, 0xce, 0x00}, {0x38, 0x01, 0xce, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xd7, 0x00}, {0x06, 0x01, 0xd7, 0x00},
+		{0x0a, 0x01, 0xd7, 0x00}, {0x0f, 0x01, 0xd7, 0x00},
+		{0x18, 0x01, 0xd7, 0x00}, {0x1f, 0x01, 0xd7, 0x00},
+		{0x29, 0x01, 0xd7, 0x00}, {0x38, 0x01, 0xd7, 0x01},
+		{0x03, 0x01, 0xe1, 0x00}, {0x06, 0x01, 0xe1, 0x00},
+		{0x0a, 0x01, 0xe1, 0x00}, {0x0f, 0x01, 0xe1, 0x00},
+		{0x18, 0x01, 0xe1, 0x00}, {0x1f, 0x01, 0xe1, 0x00},
+		{0x29, 0x01, 0xe1, 0x00}, {0x38, 0x01, 0xe1, 0x01}
+	],
+	/* 185 */
+	[
+		{0x02, 0x01, 0xec, 0x00}, {0x09, 0x01, 0xec, 0x00},
+		{0x17, 0x01, 0xec, 0x00}, {0x28, 0x01, 0xec, 0x01},
+		{0x02, 0x01, 0xed, 0x00}, {0x09, 0x01, 0xed, 0x00},
+		{0x17, 0x01, 0xed, 0x00}, {0x28, 0x01, 0xed, 0x01},
+		{0x01, 0x01, 0xc7, 0x00}, {0x16, 0x01, 0xc7, 0x01},
+		{0x01, 0x01, 0xcf, 0x00}, {0x16, 0x01, 0xcf, 0x01},
+		{0x01, 0x01, 0xea, 0x00}, {0x16, 0x01, 0xea, 0x01},
+		{0x01, 0x01, 0xeb, 0x00}, {0x16, 0x01, 0xeb, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xec, 0x00}, {0x06, 0x01, 0xec, 0x00},
+		{0x0a, 0x01, 0xec, 0x00}, {0x0f, 0x01, 0xec, 0x00},
+		{0x18, 0x01, 0xec, 0x00}, {0x1f, 0x01, 0xec, 0x00},
+		{0x29, 0x01, 0xec, 0x00}, {0x38, 0x01, 0xec, 0x01},
+		{0x03, 0x01, 0xed, 0x00}, {0x06, 0x01, 0xed, 0x00},
+		{0x0a, 0x01, 0xed, 0x00}, {0x0f, 0x01, 0xed, 0x00},
+		{0x18, 0x01, 0xed, 0x00}, {0x1f, 0x01, 0xed, 0x00},
+		{0x29, 0x01, 0xed, 0x00}, {0x38, 0x01, 0xed, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xc7, 0x00}, {0x09, 0x01, 0xc7, 0x00},
+		{0x17, 0x01, 0xc7, 0x00}, {0x28, 0x01, 0xc7, 0x01},
+		{0x02, 0x01, 0xcf, 0x00}, {0x09, 0x01, 0xcf, 0x00},
+		{0x17, 0x01, 0xcf, 0x00}, {0x28, 0x01, 0xcf, 0x01},
+		{0x02, 0x01, 0xea, 0x00}, {0x09, 0x01, 0xea, 0x00},
+		{0x17, 0x01, 0xea, 0x00}, {0x28, 0x01, 0xea, 0x01},
+		{0x02, 0x01, 0xeb, 0x00}, {0x09, 0x01, 0xeb, 0x00},
+		{0x17, 0x01, 0xeb, 0x00}, {0x28, 0x01, 0xeb, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xc7, 0x00}, {0x06, 0x01, 0xc7, 0x00},
+		{0x0a, 0x01, 0xc7, 0x00}, {0x0f, 0x01, 0xc7, 0x00},
+		{0x18, 0x01, 0xc7, 0x00}, {0x1f, 0x01, 0xc7, 0x00},
+		{0x29, 0x01, 0xc7, 0x00}, {0x38, 0x01, 0xc7, 0x01},
+		{0x03, 0x01, 0xcf, 0x00}, {0x06, 0x01, 0xcf, 0x00},
+		{0x0a, 0x01, 0xcf, 0x00}, {0x0f, 0x01, 0xcf, 0x00},
+		{0x18, 0x01, 0xcf, 0x00}, {0x1f, 0x01, 0xcf, 0x00},
+		{0x29, 0x01, 0xcf, 0x00}, {0x38, 0x01, 0xcf, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xea, 0x00}, {0x06, 0x01, 0xea, 0x00},
+		{0x0a, 0x01, 0xea, 0x00}, {0x0f, 0x01, 0xea, 0x00},
+		{0x18, 0x01, 0xea, 0x00}, {0x1f, 0x01, 0xea, 0x00},
+		{0x29, 0x01, 0xea, 0x00}, {0x38, 0x01, 0xea, 0x01},
+		{0x03, 0x01, 0xeb, 0x00}, {0x06, 0x01, 0xeb, 0x00},
+		{0x0a, 0x01, 0xeb, 0x00}, {0x0f, 0x01, 0xeb, 0x00},
+		{0x18, 0x01, 0xeb, 0x00}, {0x1f, 0x01, 0xeb, 0x00},
+		{0x29, 0x01, 0xeb, 0x00}, {0x38, 0x01, 0xeb, 0x01}
+	],
+	/* 190 */
+	[
+		{0xc2, 0x00, 0x00, 0x00}, {0xc3, 0x00, 0x00, 0x00},
+		{0xc5, 0x00, 0x00, 0x00}, {0xc6, 0x00, 0x00, 0x00},
+		{0xc9, 0x00, 0x00, 0x00}, {0xca, 0x00, 0x00, 0x00},
+		{0xcc, 0x00, 0x00, 0x00}, {0xcd, 0x00, 0x00, 0x00},
+		{0xd2, 0x00, 0x00, 0x00}, {0xd5, 0x00, 0x00, 0x00},
+		{0xd9, 0x00, 0x00, 0x00}, {0xdc, 0x00, 0x00, 0x00},
+		{0xe1, 0x00, 0x00, 0x00}, {0xe7, 0x00, 0x00, 0x00},
+		{0xef, 0x00, 0x00, 0x00}, {0xf6, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0xc0, 0x01}, {0x00, 0x01, 0xc1, 0x01},
+		{0x00, 0x01, 0xc8, 0x01}, {0x00, 0x01, 0xc9, 0x01},
+		{0x00, 0x01, 0xca, 0x01}, {0x00, 0x01, 0xcd, 0x01},
+		{0x00, 0x01, 0xd2, 0x01}, {0x00, 0x01, 0xd5, 0x01},
+		{0x00, 0x01, 0xda, 0x01}, {0x00, 0x01, 0xdb, 0x01},
+		{0x00, 0x01, 0xee, 0x01}, {0x00, 0x01, 0xf0, 0x01},
+		{0x00, 0x01, 0xf2, 0x01}, {0x00, 0x01, 0xf3, 0x01},
+		{0x00, 0x01, 0xff, 0x01}, {0xce, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x01, 0x01, 0xc0, 0x00}, {0x16, 0x01, 0xc0, 0x01},
+		{0x01, 0x01, 0xc1, 0x00}, {0x16, 0x01, 0xc1, 0x01},
+		{0x01, 0x01, 0xc8, 0x00}, {0x16, 0x01, 0xc8, 0x01},
+		{0x01, 0x01, 0xc9, 0x00}, {0x16, 0x01, 0xc9, 0x01},
+		{0x01, 0x01, 0xca, 0x00}, {0x16, 0x01, 0xca, 0x01},
+		{0x01, 0x01, 0xcd, 0x00}, {0x16, 0x01, 0xcd, 0x01},
+		{0x01, 0x01, 0xd2, 0x00}, {0x16, 0x01, 0xd2, 0x01},
+		{0x01, 0x01, 0xd5, 0x00}, {0x16, 0x01, 0xd5, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xc0, 0x00}, {0x09, 0x01, 0xc0, 0x00},
+		{0x17, 0x01, 0xc0, 0x00}, {0x28, 0x01, 0xc0, 0x01},
+		{0x02, 0x01, 0xc1, 0x00}, {0x09, 0x01, 0xc1, 0x00},
+		{0x17, 0x01, 0xc1, 0x00}, {0x28, 0x01, 0xc1, 0x01},
+		{0x02, 0x01, 0xc8, 0x00}, {0x09, 0x01, 0xc8, 0x00},
+		{0x17, 0x01, 0xc8, 0x00}, {0x28, 0x01, 0xc8, 0x01},
+		{0x02, 0x01, 0xc9, 0x00}, {0x09, 0x01, 0xc9, 0x00},
+		{0x17, 0x01, 0xc9, 0x00}, {0x28, 0x01, 0xc9, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xc0, 0x00}, {0x06, 0x01, 0xc0, 0x00},
+		{0x0a, 0x01, 0xc0, 0x00}, {0x0f, 0x01, 0xc0, 0x00},
+		{0x18, 0x01, 0xc0, 0x00}, {0x1f, 0x01, 0xc0, 0x00},
+		{0x29, 0x01, 0xc0, 0x00}, {0x38, 0x01, 0xc0, 0x01},
+		{0x03, 0x01, 0xc1, 0x00}, {0x06, 0x01, 0xc1, 0x00},
+		{0x0a, 0x01, 0xc1, 0x00}, {0x0f, 0x01, 0xc1, 0x00},
+		{0x18, 0x01, 0xc1, 0x00}, {0x1f, 0x01, 0xc1, 0x00},
+		{0x29, 0x01, 0xc1, 0x00}, {0x38, 0x01, 0xc1, 0x01}
+	],
+	/* 195 */
+	[
+		{0x03, 0x01, 0xc8, 0x00}, {0x06, 0x01, 0xc8, 0x00},
+		{0x0a, 0x01, 0xc8, 0x00}, {0x0f, 0x01, 0xc8, 0x00},
+		{0x18, 0x01, 0xc8, 0x00}, {0x1f, 0x01, 0xc8, 0x00},
+		{0x29, 0x01, 0xc8, 0x00}, {0x38, 0x01, 0xc8, 0x01},
+		{0x03, 0x01, 0xc9, 0x00}, {0x06, 0x01, 0xc9, 0x00},
+		{0x0a, 0x01, 0xc9, 0x00}, {0x0f, 0x01, 0xc9, 0x00},
+		{0x18, 0x01, 0xc9, 0x00}, {0x1f, 0x01, 0xc9, 0x00},
+		{0x29, 0x01, 0xc9, 0x00}, {0x38, 0x01, 0xc9, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xca, 0x00}, {0x09, 0x01, 0xca, 0x00},
+		{0x17, 0x01, 0xca, 0x00}, {0x28, 0x01, 0xca, 0x01},
+		{0x02, 0x01, 0xcd, 0x00}, {0x09, 0x01, 0xcd, 0x00},
+		{0x17, 0x01, 0xcd, 0x00}, {0x28, 0x01, 0xcd, 0x01},
+		{0x02, 0x01, 0xd2, 0x00}, {0x09, 0x01, 0xd2, 0x00},
+		{0x17, 0x01, 0xd2, 0x00}, {0x28, 0x01, 0xd2, 0x01},
+		{0x02, 0x01, 0xd5, 0x00}, {0x09, 0x01, 0xd5, 0x00},
+		{0x17, 0x01, 0xd5, 0x00}, {0x28, 0x01, 0xd5, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xca, 0x00}, {0x06, 0x01, 0xca, 0x00},
+		{0x0a, 0x01, 0xca, 0x00}, {0x0f, 0x01, 0xca, 0x00},
+		{0x18, 0x01, 0xca, 0x00}, {0x1f, 0x01, 0xca, 0x00},
+		{0x29, 0x01, 0xca, 0x00}, {0x38, 0x01, 0xca, 0x01},
+		{0x03, 0x01, 0xcd, 0x00}, {0x06, 0x01, 0xcd, 0x00},
+		{0x0a, 0x01, 0xcd, 0x00}, {0x0f, 0x01, 0xcd, 0x00},
+		{0x18, 0x01, 0xcd, 0x00}, {0x1f, 0x01, 0xcd, 0x00},
+		{0x29, 0x01, 0xcd, 0x00}, {0x38, 0x01, 0xcd, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xd2, 0x00}, {0x06, 0x01, 0xd2, 0x00},
+		{0x0a, 0x01, 0xd2, 0x00}, {0x0f, 0x01, 0xd2, 0x00},
+		{0x18, 0x01, 0xd2, 0x00}, {0x1f, 0x01, 0xd2, 0x00},
+		{0x29, 0x01, 0xd2, 0x00}, {0x38, 0x01, 0xd2, 0x01},
+		{0x03, 0x01, 0xd5, 0x00}, {0x06, 0x01, 0xd5, 0x00},
+		{0x0a, 0x01, 0xd5, 0x00}, {0x0f, 0x01, 0xd5, 0x00},
+		{0x18, 0x01, 0xd5, 0x00}, {0x1f, 0x01, 0xd5, 0x00},
+		{0x29, 0x01, 0xd5, 0x00}, {0x38, 0x01, 0xd5, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xda, 0x00}, {0x16, 0x01, 0xda, 0x01},
+		{0x01, 0x01, 0xdb, 0x00}, {0x16, 0x01, 0xdb, 0x01},
+		{0x01, 0x01, 0xee, 0x00}, {0x16, 0x01, 0xee, 0x01},
+		{0x01, 0x01, 0xf0, 0x00}, {0x16, 0x01, 0xf0, 0x01},
+		{0x01, 0x01, 0xf2, 0x00}, {0x16, 0x01, 0xf2, 0x01},
+		{0x01, 0x01, 0xf3, 0x00}, {0x16, 0x01, 0xf3, 0x01},
+		{0x01, 0x01, 0xff, 0x00}, {0x16, 0x01, 0xff, 0x01},
+		{0x00, 0x01, 0xcb, 0x01}, {0x00, 0x01, 0xcc, 0x01}
+	],
+	/* 200 */
+	[
+		{0x02, 0x01, 0xda, 0x00}, {0x09, 0x01, 0xda, 0x00},
+		{0x17, 0x01, 0xda, 0x00}, {0x28, 0x01, 0xda, 0x01},
+		{0x02, 0x01, 0xdb, 0x00}, {0x09, 0x01, 0xdb, 0x00},
+		{0x17, 0x01, 0xdb, 0x00}, {0x28, 0x01, 0xdb, 0x01},
+		{0x02, 0x01, 0xee, 0x00}, {0x09, 0x01, 0xee, 0x00},
+		{0x17, 0x01, 0xee, 0x00}, {0x28, 0x01, 0xee, 0x01},
+		{0x02, 0x01, 0xf0, 0x00}, {0x09, 0x01, 0xf0, 0x00},
+		{0x17, 0x01, 0xf0, 0x00}, {0x28, 0x01, 0xf0, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xda, 0x00}, {0x06, 0x01, 0xda, 0x00},
+		{0x0a, 0x01, 0xda, 0x00}, {0x0f, 0x01, 0xda, 0x00},
+		{0x18, 0x01, 0xda, 0x00}, {0x1f, 0x01, 0xda, 0x00},
+		{0x29, 0x01, 0xda, 0x00}, {0x38, 0x01, 0xda, 0x01},
+		{0x03, 0x01, 0xdb, 0x00}, {0x06, 0x01, 0xdb, 0x00},
+		{0x0a, 0x01, 0xdb, 0x00}, {0x0f, 0x01, 0xdb, 0x00},
+		{0x18, 0x01, 0xdb, 0x00}, {0x1f, 0x01, 0xdb, 0x00},
+		{0x29, 0x01, 0xdb, 0x00}, {0x38, 0x01, 0xdb, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xee, 0x00}, {0x06, 0x01, 0xee, 0x00},
+		{0x0a, 0x01, 0xee, 0x00}, {0x0f, 0x01, 0xee, 0x00},
+		{0x18, 0x01, 0xee, 0x00}, {0x1f, 0x01, 0xee, 0x00},
+		{0x29, 0x01, 0xee, 0x00}, {0x38, 0x01, 0xee, 0x01},
+		{0x03, 0x01, 0xf0, 0x00}, {0x06, 0x01, 0xf0, 0x00},
+		{0x0a, 0x01, 0xf0, 0x00}, {0x0f, 0x01, 0xf0, 0x00},
+		{0x18, 0x01, 0xf0, 0x00}, {0x1f, 0x01, 0xf0, 0x00},
+		{0x29, 0x01, 0xf0, 0x00}, {0x38, 0x01, 0xf0, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xf2, 0x00}, {0x09, 0x01, 0xf2, 0x00},
+		{0x17, 0x01, 0xf2, 0x00}, {0x28, 0x01, 0xf2, 0x01},
+		{0x02, 0x01, 0xf3, 0x00}, {0x09, 0x01, 0xf3, 0x00},
+		{0x17, 0x01, 0xf3, 0x00}, {0x28, 0x01, 0xf3, 0x01},
+		{0x02, 0x01, 0xff, 0x00}, {0x09, 0x01, 0xff, 0x00},
+		{0x17, 0x01, 0xff, 0x00}, {0x28, 0x01, 0xff, 0x01},
+		{0x01, 0x01, 0xcb, 0x00}, {0x16, 0x01, 0xcb, 0x01},
+		{0x01, 0x01, 0xcc, 0x00}, {0x16, 0x01, 0xcc, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xf2, 0x00}, {0x06, 0x01, 0xf2, 0x00},
+		{0x0a, 0x01, 0xf2, 0x00}, {0x0f, 0x01, 0xf2, 0x00},
+		{0x18, 0x01, 0xf2, 0x00}, {0x1f, 0x01, 0xf2, 0x00},
+		{0x29, 0x01, 0xf2, 0x00}, {0x38, 0x01, 0xf2, 0x01},
+		{0x03, 0x01, 0xf3, 0x00}, {0x06, 0x01, 0xf3, 0x00},
+		{0x0a, 0x01, 0xf3, 0x00}, {0x0f, 0x01, 0xf3, 0x00},
+		{0x18, 0x01, 0xf3, 0x00}, {0x1f, 0x01, 0xf3, 0x00},
+		{0x29, 0x01, 0xf3, 0x00}, {0x38, 0x01, 0xf3, 0x01}
+	],
+	/* 205 */
+	[
+		{0x03, 0x01, 0xff, 0x00}, {0x06, 0x01, 0xff, 0x00},
+		{0x0a, 0x01, 0xff, 0x00}, {0x0f, 0x01, 0xff, 0x00},
+		{0x18, 0x01, 0xff, 0x00}, {0x1f, 0x01, 0xff, 0x00},
+		{0x29, 0x01, 0xff, 0x00}, {0x38, 0x01, 0xff, 0x01},
+		{0x02, 0x01, 0xcb, 0x00}, {0x09, 0x01, 0xcb, 0x00},
+		{0x17, 0x01, 0xcb, 0x00}, {0x28, 0x01, 0xcb, 0x01},
+		{0x02, 0x01, 0xcc, 0x00}, {0x09, 0x01, 0xcc, 0x00},
+		{0x17, 0x01, 0xcc, 0x00}, {0x28, 0x01, 0xcc, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xcb, 0x00}, {0x06, 0x01, 0xcb, 0x00},
+		{0x0a, 0x01, 0xcb, 0x00}, {0x0f, 0x01, 0xcb, 0x00},
+		{0x18, 0x01, 0xcb, 0x00}, {0x1f, 0x01, 0xcb, 0x00},
+		{0x29, 0x01, 0xcb, 0x00}, {0x38, 0x01, 0xcb, 0x01},
+		{0x03, 0x01, 0xcc, 0x00}, {0x06, 0x01, 0xcc, 0x00},
+		{0x0a, 0x01, 0xcc, 0x00}, {0x0f, 0x01, 0xcc, 0x00},
+		{0x18, 0x01, 0xcc, 0x00}, {0x1f, 0x01, 0xcc, 0x00},
+		{0x29, 0x01, 0xcc, 0x00}, {0x38, 0x01, 0xcc, 0x01}
+	],
+	[
+		{0xd3, 0x00, 0x00, 0x00}, {0xd4, 0x00, 0x00, 0x00},
+		{0xd6, 0x00, 0x00, 0x00}, {0xd7, 0x00, 0x00, 0x00},
+		{0xda, 0x00, 0x00, 0x00}, {0xdb, 0x00, 0x00, 0x00},
+		{0xdd, 0x00, 0x00, 0x00}, {0xde, 0x00, 0x00, 0x00},
+		{0xe2, 0x00, 0x00, 0x00}, {0xe4, 0x00, 0x00, 0x00},
+		{0xe8, 0x00, 0x00, 0x00}, {0xeb, 0x00, 0x00, 0x00},
+		{0xf0, 0x00, 0x00, 0x00}, {0xf3, 0x00, 0x00, 0x00},
+		{0xf7, 0x00, 0x00, 0x00}, {0xfa, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x00, 0x01, 0xd3, 0x01}, {0x00, 0x01, 0xd4, 0x01},
+		{0x00, 0x01, 0xd6, 0x01}, {0x00, 0x01, 0xdd, 0x01},
+		{0x00, 0x01, 0xde, 0x01}, {0x00, 0x01, 0xdf, 0x01},
+		{0x00, 0x01, 0xf1, 0x01}, {0x00, 0x01, 0xf4, 0x01},
+		{0x00, 0x01, 0xf5, 0x01}, {0x00, 0x01, 0xf6, 0x01},
+		{0x00, 0x01, 0xf7, 0x01}, {0x00, 0x01, 0xf8, 0x01},
+		{0x00, 0x01, 0xfa, 0x01}, {0x00, 0x01, 0xfb, 0x01},
+		{0x00, 0x01, 0xfc, 0x01}, {0x00, 0x01, 0xfd, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xd3, 0x00}, {0x16, 0x01, 0xd3, 0x01},
+		{0x01, 0x01, 0xd4, 0x00}, {0x16, 0x01, 0xd4, 0x01},
+		{0x01, 0x01, 0xd6, 0x00}, {0x16, 0x01, 0xd6, 0x01},
+		{0x01, 0x01, 0xdd, 0x00}, {0x16, 0x01, 0xdd, 0x01},
+		{0x01, 0x01, 0xde, 0x00}, {0x16, 0x01, 0xde, 0x01},
+		{0x01, 0x01, 0xdf, 0x00}, {0x16, 0x01, 0xdf, 0x01},
+		{0x01, 0x01, 0xf1, 0x00}, {0x16, 0x01, 0xf1, 0x01},
+		{0x01, 0x01, 0xf4, 0x00}, {0x16, 0x01, 0xf4, 0x01}
+	],
+	/* 210 */
+	[
+		{0x02, 0x01, 0xd3, 0x00}, {0x09, 0x01, 0xd3, 0x00},
+		{0x17, 0x01, 0xd3, 0x00}, {0x28, 0x01, 0xd3, 0x01},
+		{0x02, 0x01, 0xd4, 0x00}, {0x09, 0x01, 0xd4, 0x00},
+		{0x17, 0x01, 0xd4, 0x00}, {0x28, 0x01, 0xd4, 0x01},
+		{0x02, 0x01, 0xd6, 0x00}, {0x09, 0x01, 0xd6, 0x00},
+		{0x17, 0x01, 0xd6, 0x00}, {0x28, 0x01, 0xd6, 0x01},
+		{0x02, 0x01, 0xdd, 0x00}, {0x09, 0x01, 0xdd, 0x00},
+		{0x17, 0x01, 0xdd, 0x00}, {0x28, 0x01, 0xdd, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xd3, 0x00}, {0x06, 0x01, 0xd3, 0x00},
+		{0x0a, 0x01, 0xd3, 0x00}, {0x0f, 0x01, 0xd3, 0x00},
+		{0x18, 0x01, 0xd3, 0x00}, {0x1f, 0x01, 0xd3, 0x00},
+		{0x29, 0x01, 0xd3, 0x00}, {0x38, 0x01, 0xd3, 0x01},
+		{0x03, 0x01, 0xd4, 0x00}, {0x06, 0x01, 0xd4, 0x00},
+		{0x0a, 0x01, 0xd4, 0x00}, {0x0f, 0x01, 0xd4, 0x00},
+		{0x18, 0x01, 0xd4, 0x00}, {0x1f, 0x01, 0xd4, 0x00},
+		{0x29, 0x01, 0xd4, 0x00}, {0x38, 0x01, 0xd4, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xd6, 0x00}, {0x06, 0x01, 0xd6, 0x00},
+		{0x0a, 0x01, 0xd6, 0x00}, {0x0f, 0x01, 0xd6, 0x00},
+		{0x18, 0x01, 0xd6, 0x00}, {0x1f, 0x01, 0xd6, 0x00},
+		{0x29, 0x01, 0xd6, 0x00}, {0x38, 0x01, 0xd6, 0x01},
+		{0x03, 0x01, 0xdd, 0x00}, {0x06, 0x01, 0xdd, 0x00},
+		{0x0a, 0x01, 0xdd, 0x00}, {0x0f, 0x01, 0xdd, 0x00},
+		{0x18, 0x01, 0xdd, 0x00}, {0x1f, 0x01, 0xdd, 0x00},
+		{0x29, 0x01, 0xdd, 0x00}, {0x38, 0x01, 0xdd, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xde, 0x00}, {0x09, 0x01, 0xde, 0x00},
+		{0x17, 0x01, 0xde, 0x00}, {0x28, 0x01, 0xde, 0x01},
+		{0x02, 0x01, 0xdf, 0x00}, {0x09, 0x01, 0xdf, 0x00},
+		{0x17, 0x01, 0xdf, 0x00}, {0x28, 0x01, 0xdf, 0x01},
+		{0x02, 0x01, 0xf1, 0x00}, {0x09, 0x01, 0xf1, 0x00},
+		{0x17, 0x01, 0xf1, 0x00}, {0x28, 0x01, 0xf1, 0x01},
+		{0x02, 0x01, 0xf4, 0x00}, {0x09, 0x01, 0xf4, 0x00},
+		{0x17, 0x01, 0xf4, 0x00}, {0x28, 0x01, 0xf4, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xde, 0x00}, {0x06, 0x01, 0xde, 0x00},
+		{0x0a, 0x01, 0xde, 0x00}, {0x0f, 0x01, 0xde, 0x00},
+		{0x18, 0x01, 0xde, 0x00}, {0x1f, 0x01, 0xde, 0x00},
+		{0x29, 0x01, 0xde, 0x00}, {0x38, 0x01, 0xde, 0x01},
+		{0x03, 0x01, 0xdf, 0x00}, {0x06, 0x01, 0xdf, 0x00},
+		{0x0a, 0x01, 0xdf, 0x00}, {0x0f, 0x01, 0xdf, 0x00},
+		{0x18, 0x01, 0xdf, 0x00}, {0x1f, 0x01, 0xdf, 0x00},
+		{0x29, 0x01, 0xdf, 0x00}, {0x38, 0x01, 0xdf, 0x01}
+	],
+	/* 215 */
+	[
+		{0x03, 0x01, 0xf1, 0x00}, {0x06, 0x01, 0xf1, 0x00},
+		{0x0a, 0x01, 0xf1, 0x00}, {0x0f, 0x01, 0xf1, 0x00},
+		{0x18, 0x01, 0xf1, 0x00}, {0x1f, 0x01, 0xf1, 0x00},
+		{0x29, 0x01, 0xf1, 0x00}, {0x38, 0x01, 0xf1, 0x01},
+		{0x03, 0x01, 0xf4, 0x00}, {0x06, 0x01, 0xf4, 0x00},
+		{0x0a, 0x01, 0xf4, 0x00}, {0x0f, 0x01, 0xf4, 0x00},
+		{0x18, 0x01, 0xf4, 0x00}, {0x1f, 0x01, 0xf4, 0x00},
+		{0x29, 0x01, 0xf4, 0x00}, {0x38, 0x01, 0xf4, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xf5, 0x00}, {0x16, 0x01, 0xf5, 0x01},
+		{0x01, 0x01, 0xf6, 0x00}, {0x16, 0x01, 0xf6, 0x01},
+		{0x01, 0x01, 0xf7, 0x00}, {0x16, 0x01, 0xf7, 0x01},
+		{0x01, 0x01, 0xf8, 0x00}, {0x16, 0x01, 0xf8, 0x01},
+		{0x01, 0x01, 0xfa, 0x00}, {0x16, 0x01, 0xfa, 0x01},
+		{0x01, 0x01, 0xfb, 0x00}, {0x16, 0x01, 0xfb, 0x01},
+		{0x01, 0x01, 0xfc, 0x00}, {0x16, 0x01, 0xfc, 0x01},
+		{0x01, 0x01, 0xfd, 0x00}, {0x16, 0x01, 0xfd, 0x01}
+	],
+	[
+		{0x02, 0x01, 0xf5, 0x00}, {0x09, 0x01, 0xf5, 0x00},
+		{0x17, 0x01, 0xf5, 0x00}, {0x28, 0x01, 0xf5, 0x01},
+		{0x02, 0x01, 0xf6, 0x00}, {0x09, 0x01, 0xf6, 0x00},
+		{0x17, 0x01, 0xf6, 0x00}, {0x28, 0x01, 0xf6, 0x01},
+		{0x02, 0x01, 0xf7, 0x00}, {0x09, 0x01, 0xf7, 0x00},
+		{0x17, 0x01, 0xf7, 0x00}, {0x28, 0x01, 0xf7, 0x01},
+		{0x02, 0x01, 0xf8, 0x00}, {0x09, 0x01, 0xf8, 0x00},
+		{0x17, 0x01, 0xf8, 0x00}, {0x28, 0x01, 0xf8, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xf5, 0x00}, {0x06, 0x01, 0xf5, 0x00},
+		{0x0a, 0x01, 0xf5, 0x00}, {0x0f, 0x01, 0xf5, 0x00},
+		{0x18, 0x01, 0xf5, 0x00}, {0x1f, 0x01, 0xf5, 0x00},
+		{0x29, 0x01, 0xf5, 0x00}, {0x38, 0x01, 0xf5, 0x01},
+		{0x03, 0x01, 0xf6, 0x00}, {0x06, 0x01, 0xf6, 0x00},
+		{0x0a, 0x01, 0xf6, 0x00}, {0x0f, 0x01, 0xf6, 0x00},
+		{0x18, 0x01, 0xf6, 0x00}, {0x1f, 0x01, 0xf6, 0x00},
+		{0x29, 0x01, 0xf6, 0x00}, {0x38, 0x01, 0xf6, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xf7, 0x00}, {0x06, 0x01, 0xf7, 0x00},
+		{0x0a, 0x01, 0xf7, 0x00}, {0x0f, 0x01, 0xf7, 0x00},
+		{0x18, 0x01, 0xf7, 0x00}, {0x1f, 0x01, 0xf7, 0x00},
+		{0x29, 0x01, 0xf7, 0x00}, {0x38, 0x01, 0xf7, 0x01},
+		{0x03, 0x01, 0xf8, 0x00}, {0x06, 0x01, 0xf8, 0x00},
+		{0x0a, 0x01, 0xf8, 0x00}, {0x0f, 0x01, 0xf8, 0x00},
+		{0x18, 0x01, 0xf8, 0x00}, {0x1f, 0x01, 0xf8, 0x00},
+		{0x29, 0x01, 0xf8, 0x00}, {0x38, 0x01, 0xf8, 0x01}
+	],
+	/* 220 */
+	[
+		{0x02, 0x01, 0xfa, 0x00}, {0x09, 0x01, 0xfa, 0x00},
+		{0x17, 0x01, 0xfa, 0x00}, {0x28, 0x01, 0xfa, 0x01},
+		{0x02, 0x01, 0xfb, 0x00}, {0x09, 0x01, 0xfb, 0x00},
+		{0x17, 0x01, 0xfb, 0x00}, {0x28, 0x01, 0xfb, 0x01},
+		{0x02, 0x01, 0xfc, 0x00}, {0x09, 0x01, 0xfc, 0x00},
+		{0x17, 0x01, 0xfc, 0x00}, {0x28, 0x01, 0xfc, 0x01},
+		{0x02, 0x01, 0xfd, 0x00}, {0x09, 0x01, 0xfd, 0x00},
+		{0x17, 0x01, 0xfd, 0x00}, {0x28, 0x01, 0xfd, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xfa, 0x00}, {0x06, 0x01, 0xfa, 0x00},
+		{0x0a, 0x01, 0xfa, 0x00}, {0x0f, 0x01, 0xfa, 0x00},
+		{0x18, 0x01, 0xfa, 0x00}, {0x1f, 0x01, 0xfa, 0x00},
+		{0x29, 0x01, 0xfa, 0x00}, {0x38, 0x01, 0xfa, 0x01},
+		{0x03, 0x01, 0xfb, 0x00}, {0x06, 0x01, 0xfb, 0x00},
+		{0x0a, 0x01, 0xfb, 0x00}, {0x0f, 0x01, 0xfb, 0x00},
+		{0x18, 0x01, 0xfb, 0x00}, {0x1f, 0x01, 0xfb, 0x00},
+		{0x29, 0x01, 0xfb, 0x00}, {0x38, 0x01, 0xfb, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xfc, 0x00}, {0x06, 0x01, 0xfc, 0x00},
+		{0x0a, 0x01, 0xfc, 0x00}, {0x0f, 0x01, 0xfc, 0x00},
+		{0x18, 0x01, 0xfc, 0x00}, {0x1f, 0x01, 0xfc, 0x00},
+		{0x29, 0x01, 0xfc, 0x00}, {0x38, 0x01, 0xfc, 0x01},
+		{0x03, 0x01, 0xfd, 0x00}, {0x06, 0x01, 0xfd, 0x00},
+		{0x0a, 0x01, 0xfd, 0x00}, {0x0f, 0x01, 0xfd, 0x00},
+		{0x18, 0x01, 0xfd, 0x00}, {0x1f, 0x01, 0xfd, 0x00},
+		{0x29, 0x01, 0xfd, 0x00}, {0x38, 0x01, 0xfd, 0x01}
+	],
+	[
+		{0x00, 0x01, 0xfe, 0x01}, {0xe3, 0x00, 0x00, 0x00},
+		{0xe5, 0x00, 0x00, 0x00}, {0xe6, 0x00, 0x00, 0x00},
+		{0xe9, 0x00, 0x00, 0x00}, {0xea, 0x00, 0x00, 0x00},
+		{0xec, 0x00, 0x00, 0x00}, {0xed, 0x00, 0x00, 0x00},
+		{0xf1, 0x00, 0x00, 0x00}, {0xf2, 0x00, 0x00, 0x00},
+		{0xf4, 0x00, 0x00, 0x00}, {0xf5, 0x00, 0x00, 0x00},
+		{0xf8, 0x00, 0x00, 0x00}, {0xf9, 0x00, 0x00, 0x00},
+		{0xfb, 0x00, 0x00, 0x00}, {0xfc, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x01, 0x01, 0xfe, 0x00}, {0x16, 0x01, 0xfe, 0x01},
+		{0x00, 0x01, 0x02, 0x01}, {0x00, 0x01, 0x03, 0x01},
+		{0x00, 0x01, 0x04, 0x01}, {0x00, 0x01, 0x05, 0x01},
+		{0x00, 0x01, 0x06, 0x01}, {0x00, 0x01, 0x07, 0x01},
+		{0x00, 0x01, 0x08, 0x01}, {0x00, 0x01, 0x0b, 0x01},
+		{0x00, 0x01, 0x0c, 0x01}, {0x00, 0x01, 0x0e, 0x01},
+		{0x00, 0x01, 0x0f, 0x01}, {0x00, 0x01, 0x10, 0x01},
+		{0x00, 0x01, 0x11, 0x01}, {0x00, 0x01, 0x12, 0x01}
+	],
+	/* 225 */
+	[
+		{0x02, 0x01, 0xfe, 0x00}, {0x09, 0x01, 0xfe, 0x00},
+		{0x17, 0x01, 0xfe, 0x00}, {0x28, 0x01, 0xfe, 0x01},
+		{0x01, 0x01, 0x02, 0x00}, {0x16, 0x01, 0x02, 0x01},
+		{0x01, 0x01, 0x03, 0x00}, {0x16, 0x01, 0x03, 0x01},
+		{0x01, 0x01, 0x04, 0x00}, {0x16, 0x01, 0x04, 0x01},
+		{0x01, 0x01, 0x05, 0x00}, {0x16, 0x01, 0x05, 0x01},
+		{0x01, 0x01, 0x06, 0x00}, {0x16, 0x01, 0x06, 0x01},
+		{0x01, 0x01, 0x07, 0x00}, {0x16, 0x01, 0x07, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xfe, 0x00}, {0x06, 0x01, 0xfe, 0x00},
+		{0x0a, 0x01, 0xfe, 0x00}, {0x0f, 0x01, 0xfe, 0x00},
+		{0x18, 0x01, 0xfe, 0x00}, {0x1f, 0x01, 0xfe, 0x00},
+		{0x29, 0x01, 0xfe, 0x00}, {0x38, 0x01, 0xfe, 0x01},
+		{0x02, 0x01, 0x02, 0x00}, {0x09, 0x01, 0x02, 0x00},
+		{0x17, 0x01, 0x02, 0x00}, {0x28, 0x01, 0x02, 0x01},
+		{0x02, 0x01, 0x03, 0x00}, {0x09, 0x01, 0x03, 0x00},
+		{0x17, 0x01, 0x03, 0x00}, {0x28, 0x01, 0x03, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x02, 0x00}, {0x06, 0x01, 0x02, 0x00},
+		{0x0a, 0x01, 0x02, 0x00}, {0x0f, 0x01, 0x02, 0x00},
+		{0x18, 0x01, 0x02, 0x00}, {0x1f, 0x01, 0x02, 0x00},
+		{0x29, 0x01, 0x02, 0x00}, {0x38, 0x01, 0x02, 0x01},
+		{0x03, 0x01, 0x03, 0x00}, {0x06, 0x01, 0x03, 0x00},
+		{0x0a, 0x01, 0x03, 0x00}, {0x0f, 0x01, 0x03, 0x00},
+		{0x18, 0x01, 0x03, 0x00}, {0x1f, 0x01, 0x03, 0x00},
+		{0x29, 0x01, 0x03, 0x00}, {0x38, 0x01, 0x03, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x04, 0x00}, {0x09, 0x01, 0x04, 0x00},
+		{0x17, 0x01, 0x04, 0x00}, {0x28, 0x01, 0x04, 0x01},
+		{0x02, 0x01, 0x05, 0x00}, {0x09, 0x01, 0x05, 0x00},
+		{0x17, 0x01, 0x05, 0x00}, {0x28, 0x01, 0x05, 0x01},
+		{0x02, 0x01, 0x06, 0x00}, {0x09, 0x01, 0x06, 0x00},
+		{0x17, 0x01, 0x06, 0x00}, {0x28, 0x01, 0x06, 0x01},
+		{0x02, 0x01, 0x07, 0x00}, {0x09, 0x01, 0x07, 0x00},
+		{0x17, 0x01, 0x07, 0x00}, {0x28, 0x01, 0x07, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x04, 0x00}, {0x06, 0x01, 0x04, 0x00},
+		{0x0a, 0x01, 0x04, 0x00}, {0x0f, 0x01, 0x04, 0x00},
+		{0x18, 0x01, 0x04, 0x00}, {0x1f, 0x01, 0x04, 0x00},
+		{0x29, 0x01, 0x04, 0x00}, {0x38, 0x01, 0x04, 0x01},
+		{0x03, 0x01, 0x05, 0x00}, {0x06, 0x01, 0x05, 0x00},
+		{0x0a, 0x01, 0x05, 0x00}, {0x0f, 0x01, 0x05, 0x00},
+		{0x18, 0x01, 0x05, 0x00}, {0x1f, 0x01, 0x05, 0x00},
+		{0x29, 0x01, 0x05, 0x00}, {0x38, 0x01, 0x05, 0x01}
+	],
+	/* 230 */
+	[
+		{0x03, 0x01, 0x06, 0x00}, {0x06, 0x01, 0x06, 0x00},
+		{0x0a, 0x01, 0x06, 0x00}, {0x0f, 0x01, 0x06, 0x00},
+		{0x18, 0x01, 0x06, 0x00}, {0x1f, 0x01, 0x06, 0x00},
+		{0x29, 0x01, 0x06, 0x00}, {0x38, 0x01, 0x06, 0x01},
+		{0x03, 0x01, 0x07, 0x00}, {0x06, 0x01, 0x07, 0x00},
+		{0x0a, 0x01, 0x07, 0x00}, {0x0f, 0x01, 0x07, 0x00},
+		{0x18, 0x01, 0x07, 0x00}, {0x1f, 0x01, 0x07, 0x00},
+		{0x29, 0x01, 0x07, 0x00}, {0x38, 0x01, 0x07, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x08, 0x00}, {0x16, 0x01, 0x08, 0x01},
+		{0x01, 0x01, 0x0b, 0x00}, {0x16, 0x01, 0x0b, 0x01},
+		{0x01, 0x01, 0x0c, 0x00}, {0x16, 0x01, 0x0c, 0x01},
+		{0x01, 0x01, 0x0e, 0x00}, {0x16, 0x01, 0x0e, 0x01},
+		{0x01, 0x01, 0x0f, 0x00}, {0x16, 0x01, 0x0f, 0x01},
+		{0x01, 0x01, 0x10, 0x00}, {0x16, 0x01, 0x10, 0x01},
+		{0x01, 0x01, 0x11, 0x00}, {0x16, 0x01, 0x11, 0x01},
+		{0x01, 0x01, 0x12, 0x00}, {0x16, 0x01, 0x12, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x08, 0x00}, {0x09, 0x01, 0x08, 0x00},
+		{0x17, 0x01, 0x08, 0x00}, {0x28, 0x01, 0x08, 0x01},
+		{0x02, 0x01, 0x0b, 0x00}, {0x09, 0x01, 0x0b, 0x00},
+		{0x17, 0x01, 0x0b, 0x00}, {0x28, 0x01, 0x0b, 0x01},
+		{0x02, 0x01, 0x0c, 0x00}, {0x09, 0x01, 0x0c, 0x00},
+		{0x17, 0x01, 0x0c, 0x00}, {0x28, 0x01, 0x0c, 0x01},
+		{0x02, 0x01, 0x0e, 0x00}, {0x09, 0x01, 0x0e, 0x00},
+		{0x17, 0x01, 0x0e, 0x00}, {0x28, 0x01, 0x0e, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x08, 0x00}, {0x06, 0x01, 0x08, 0x00},
+		{0x0a, 0x01, 0x08, 0x00}, {0x0f, 0x01, 0x08, 0x00},
+		{0x18, 0x01, 0x08, 0x00}, {0x1f, 0x01, 0x08, 0x00},
+		{0x29, 0x01, 0x08, 0x00}, {0x38, 0x01, 0x08, 0x01},
+		{0x03, 0x01, 0x0b, 0x00}, {0x06, 0x01, 0x0b, 0x00},
+		{0x0a, 0x01, 0x0b, 0x00}, {0x0f, 0x01, 0x0b, 0x00},
+		{0x18, 0x01, 0x0b, 0x00}, {0x1f, 0x01, 0x0b, 0x00},
+		{0x29, 0x01, 0x0b, 0x00}, {0x38, 0x01, 0x0b, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x0c, 0x00}, {0x06, 0x01, 0x0c, 0x00},
+		{0x0a, 0x01, 0x0c, 0x00}, {0x0f, 0x01, 0x0c, 0x00},
+		{0x18, 0x01, 0x0c, 0x00}, {0x1f, 0x01, 0x0c, 0x00},
+		{0x29, 0x01, 0x0c, 0x00}, {0x38, 0x01, 0x0c, 0x01},
+		{0x03, 0x01, 0x0e, 0x00}, {0x06, 0x01, 0x0e, 0x00},
+		{0x0a, 0x01, 0x0e, 0x00}, {0x0f, 0x01, 0x0e, 0x00},
+		{0x18, 0x01, 0x0e, 0x00}, {0x1f, 0x01, 0x0e, 0x00},
+		{0x29, 0x01, 0x0e, 0x00}, {0x38, 0x01, 0x0e, 0x01}
+	],
+	/* 235 */
+	[
+		{0x02, 0x01, 0x0f, 0x00}, {0x09, 0x01, 0x0f, 0x00},
+		{0x17, 0x01, 0x0f, 0x00}, {0x28, 0x01, 0x0f, 0x01},
+		{0x02, 0x01, 0x10, 0x00}, {0x09, 0x01, 0x10, 0x00},
+		{0x17, 0x01, 0x10, 0x00}, {0x28, 0x01, 0x10, 0x01},
+		{0x02, 0x01, 0x11, 0x00}, {0x09, 0x01, 0x11, 0x00},
+		{0x17, 0x01, 0x11, 0x00}, {0x28, 0x01, 0x11, 0x01},
+		{0x02, 0x01, 0x12, 0x00}, {0x09, 0x01, 0x12, 0x00},
+		{0x17, 0x01, 0x12, 0x00}, {0x28, 0x01, 0x12, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x0f, 0x00}, {0x06, 0x01, 0x0f, 0x00},
+		{0x0a, 0x01, 0x0f, 0x00}, {0x0f, 0x01, 0x0f, 0x00},
+		{0x18, 0x01, 0x0f, 0x00}, {0x1f, 0x01, 0x0f, 0x00},
+		{0x29, 0x01, 0x0f, 0x00}, {0x38, 0x01, 0x0f, 0x01},
+		{0x03, 0x01, 0x10, 0x00}, {0x06, 0x01, 0x10, 0x00},
+		{0x0a, 0x01, 0x10, 0x00}, {0x0f, 0x01, 0x10, 0x00},
+		{0x18, 0x01, 0x10, 0x00}, {0x1f, 0x01, 0x10, 0x00},
+		{0x29, 0x01, 0x10, 0x00}, {0x38, 0x01, 0x10, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x11, 0x00}, {0x06, 0x01, 0x11, 0x00},
+		{0x0a, 0x01, 0x11, 0x00}, {0x0f, 0x01, 0x11, 0x00},
+		{0x18, 0x01, 0x11, 0x00}, {0x1f, 0x01, 0x11, 0x00},
+		{0x29, 0x01, 0x11, 0x00}, {0x38, 0x01, 0x11, 0x01},
+		{0x03, 0x01, 0x12, 0x00}, {0x06, 0x01, 0x12, 0x00},
+		{0x0a, 0x01, 0x12, 0x00}, {0x0f, 0x01, 0x12, 0x00},
+		{0x18, 0x01, 0x12, 0x00}, {0x1f, 0x01, 0x12, 0x00},
+		{0x29, 0x01, 0x12, 0x00}, {0x38, 0x01, 0x12, 0x01}
+	],
+	[
+		{0x00, 0x01, 0x13, 0x01}, {0x00, 0x01, 0x14, 0x01},
+		{0x00, 0x01, 0x15, 0x01}, {0x00, 0x01, 0x17, 0x01},
+		{0x00, 0x01, 0x18, 0x01}, {0x00, 0x01, 0x19, 0x01},
+		{0x00, 0x01, 0x1a, 0x01}, {0x00, 0x01, 0x1b, 0x01},
+		{0x00, 0x01, 0x1c, 0x01}, {0x00, 0x01, 0x1d, 0x01},
+		{0x00, 0x01, 0x1e, 0x01}, {0x00, 0x01, 0x1f, 0x01},
+		{0x00, 0x01, 0x7f, 0x01}, {0x00, 0x01, 0xdc, 0x01},
+		{0x00, 0x01, 0xf9, 0x01}, {0xfd, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x13, 0x00}, {0x16, 0x01, 0x13, 0x01},
+		{0x01, 0x01, 0x14, 0x00}, {0x16, 0x01, 0x14, 0x01},
+		{0x01, 0x01, 0x15, 0x00}, {0x16, 0x01, 0x15, 0x01},
+		{0x01, 0x01, 0x17, 0x00}, {0x16, 0x01, 0x17, 0x01},
+		{0x01, 0x01, 0x18, 0x00}, {0x16, 0x01, 0x18, 0x01},
+		{0x01, 0x01, 0x19, 0x00}, {0x16, 0x01, 0x19, 0x01},
+		{0x01, 0x01, 0x1a, 0x00}, {0x16, 0x01, 0x1a, 0x01},
+		{0x01, 0x01, 0x1b, 0x00}, {0x16, 0x01, 0x1b, 0x01}
+	],
+	/* 240 */
+	[
+		{0x02, 0x01, 0x13, 0x00}, {0x09, 0x01, 0x13, 0x00},
+		{0x17, 0x01, 0x13, 0x00}, {0x28, 0x01, 0x13, 0x01},
+		{0x02, 0x01, 0x14, 0x00}, {0x09, 0x01, 0x14, 0x00},
+		{0x17, 0x01, 0x14, 0x00}, {0x28, 0x01, 0x14, 0x01},
+		{0x02, 0x01, 0x15, 0x00}, {0x09, 0x01, 0x15, 0x00},
+		{0x17, 0x01, 0x15, 0x00}, {0x28, 0x01, 0x15, 0x01},
+		{0x02, 0x01, 0x17, 0x00}, {0x09, 0x01, 0x17, 0x00},
+		{0x17, 0x01, 0x17, 0x00}, {0x28, 0x01, 0x17, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x13, 0x00}, {0x06, 0x01, 0x13, 0x00},
+		{0x0a, 0x01, 0x13, 0x00}, {0x0f, 0x01, 0x13, 0x00},
+		{0x18, 0x01, 0x13, 0x00}, {0x1f, 0x01, 0x13, 0x00},
+		{0x29, 0x01, 0x13, 0x00}, {0x38, 0x01, 0x13, 0x01},
+		{0x03, 0x01, 0x14, 0x00}, {0x06, 0x01, 0x14, 0x00},
+		{0x0a, 0x01, 0x14, 0x00}, {0x0f, 0x01, 0x14, 0x00},
+		{0x18, 0x01, 0x14, 0x00}, {0x1f, 0x01, 0x14, 0x00},
+		{0x29, 0x01, 0x14, 0x00}, {0x38, 0x01, 0x14, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x15, 0x00}, {0x06, 0x01, 0x15, 0x00},
+		{0x0a, 0x01, 0x15, 0x00}, {0x0f, 0x01, 0x15, 0x00},
+		{0x18, 0x01, 0x15, 0x00}, {0x1f, 0x01, 0x15, 0x00},
+		{0x29, 0x01, 0x15, 0x00}, {0x38, 0x01, 0x15, 0x01},
+		{0x03, 0x01, 0x17, 0x00}, {0x06, 0x01, 0x17, 0x00},
+		{0x0a, 0x01, 0x17, 0x00}, {0x0f, 0x01, 0x17, 0x00},
+		{0x18, 0x01, 0x17, 0x00}, {0x1f, 0x01, 0x17, 0x00},
+		{0x29, 0x01, 0x17, 0x00}, {0x38, 0x01, 0x17, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x18, 0x00}, {0x09, 0x01, 0x18, 0x00},
+		{0x17, 0x01, 0x18, 0x00}, {0x28, 0x01, 0x18, 0x01},
+		{0x02, 0x01, 0x19, 0x00}, {0x09, 0x01, 0x19, 0x00},
+		{0x17, 0x01, 0x19, 0x00}, {0x28, 0x01, 0x19, 0x01},
+		{0x02, 0x01, 0x1a, 0x00}, {0x09, 0x01, 0x1a, 0x00},
+		{0x17, 0x01, 0x1a, 0x00}, {0x28, 0x01, 0x1a, 0x01},
+		{0x02, 0x01, 0x1b, 0x00}, {0x09, 0x01, 0x1b, 0x00},
+		{0x17, 0x01, 0x1b, 0x00}, {0x28, 0x01, 0x1b, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x18, 0x00}, {0x06, 0x01, 0x18, 0x00},
+		{0x0a, 0x01, 0x18, 0x00}, {0x0f, 0x01, 0x18, 0x00},
+		{0x18, 0x01, 0x18, 0x00}, {0x1f, 0x01, 0x18, 0x00},
+		{0x29, 0x01, 0x18, 0x00}, {0x38, 0x01, 0x18, 0x01},
+		{0x03, 0x01, 0x19, 0x00}, {0x06, 0x01, 0x19, 0x00},
+		{0x0a, 0x01, 0x19, 0x00}, {0x0f, 0x01, 0x19, 0x00},
+		{0x18, 0x01, 0x19, 0x00}, {0x1f, 0x01, 0x19, 0x00},
+		{0x29, 0x01, 0x19, 0x00}, {0x38, 0x01, 0x19, 0x01}
+	],
+	/* 245 */
+	[
+		{0x03, 0x01, 0x1a, 0x00}, {0x06, 0x01, 0x1a, 0x00},
+		{0x0a, 0x01, 0x1a, 0x00}, {0x0f, 0x01, 0x1a, 0x00},
+		{0x18, 0x01, 0x1a, 0x00}, {0x1f, 0x01, 0x1a, 0x00},
+		{0x29, 0x01, 0x1a, 0x00}, {0x38, 0x01, 0x1a, 0x01},
+		{0x03, 0x01, 0x1b, 0x00}, {0x06, 0x01, 0x1b, 0x00},
+		{0x0a, 0x01, 0x1b, 0x00}, {0x0f, 0x01, 0x1b, 0x00},
+		{0x18, 0x01, 0x1b, 0x00}, {0x1f, 0x01, 0x1b, 0x00},
+		{0x29, 0x01, 0x1b, 0x00}, {0x38, 0x01, 0x1b, 0x01}
+	],
+	[
+		{0x01, 0x01, 0x1c, 0x00}, {0x16, 0x01, 0x1c, 0x01},
+		{0x01, 0x01, 0x1d, 0x00}, {0x16, 0x01, 0x1d, 0x01},
+		{0x01, 0x01, 0x1e, 0x00}, {0x16, 0x01, 0x1e, 0x01},
+		{0x01, 0x01, 0x1f, 0x00}, {0x16, 0x01, 0x1f, 0x01},
+		{0x01, 0x01, 0x7f, 0x00}, {0x16, 0x01, 0x7f, 0x01},
+		{0x01, 0x01, 0xdc, 0x00}, {0x16, 0x01, 0xdc, 0x01},
+		{0x01, 0x01, 0xf9, 0x00}, {0x16, 0x01, 0xf9, 0x01},
+		{0xfe, 0x00, 0x00, 0x00}, {0xff, 0x00, 0x00, 0x01}
+	],
+	[
+		{0x02, 0x01, 0x1c, 0x00}, {0x09, 0x01, 0x1c, 0x00},
+		{0x17, 0x01, 0x1c, 0x00}, {0x28, 0x01, 0x1c, 0x01},
+		{0x02, 0x01, 0x1d, 0x00}, {0x09, 0x01, 0x1d, 0x00},
+		{0x17, 0x01, 0x1d, 0x00}, {0x28, 0x01, 0x1d, 0x01},
+		{0x02, 0x01, 0x1e, 0x00}, {0x09, 0x01, 0x1e, 0x00},
+		{0x17, 0x01, 0x1e, 0x00}, {0x28, 0x01, 0x1e, 0x01},
+		{0x02, 0x01, 0x1f, 0x00}, {0x09, 0x01, 0x1f, 0x00},
+		{0x17, 0x01, 0x1f, 0x00}, {0x28, 0x01, 0x1f, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x1c, 0x00}, {0x06, 0x01, 0x1c, 0x00},
+		{0x0a, 0x01, 0x1c, 0x00}, {0x0f, 0x01, 0x1c, 0x00},
+		{0x18, 0x01, 0x1c, 0x00}, {0x1f, 0x01, 0x1c, 0x00},
+		{0x29, 0x01, 0x1c, 0x00}, {0x38, 0x01, 0x1c, 0x01},
+		{0x03, 0x01, 0x1d, 0x00}, {0x06, 0x01, 0x1d, 0x00},
+		{0x0a, 0x01, 0x1d, 0x00}, {0x0f, 0x01, 0x1d, 0x00},
+		{0x18, 0x01, 0x1d, 0x00}, {0x1f, 0x01, 0x1d, 0x00},
+		{0x29, 0x01, 0x1d, 0x00}, {0x38, 0x01, 0x1d, 0x01}
+	],
+	[
+		{0x03, 0x01, 0x1e, 0x00}, {0x06, 0x01, 0x1e, 0x00},
+		{0x0a, 0x01, 0x1e, 0x00}, {0x0f, 0x01, 0x1e, 0x00},
+		{0x18, 0x01, 0x1e, 0x00}, {0x1f, 0x01, 0x1e, 0x00},
+		{0x29, 0x01, 0x1e, 0x00}, {0x38, 0x01, 0x1e, 0x01},
+		{0x03, 0x01, 0x1f, 0x00}, {0x06, 0x01, 0x1f, 0x00},
+		{0x0a, 0x01, 0x1f, 0x00}, {0x0f, 0x01, 0x1f, 0x00},
+		{0x18, 0x01, 0x1f, 0x00}, {0x1f, 0x01, 0x1f, 0x00},
+		{0x29, 0x01, 0x1f, 0x00}, {0x38, 0x01, 0x1f, 0x01}
+	],
+	/* 250 */
+	[
+		{0x02, 0x01, 0x7f, 0x00}, {0x09, 0x01, 0x7f, 0x00},
+		{0x17, 0x01, 0x7f, 0x00}, {0x28, 0x01, 0x7f, 0x01},
+		{0x02, 0x01, 0xdc, 0x00}, {0x09, 0x01, 0xdc, 0x00},
+		{0x17, 0x01, 0xdc, 0x00}, {0x28, 0x01, 0xdc, 0x01},
+		{0x02, 0x01, 0xf9, 0x00}, {0x09, 0x01, 0xf9, 0x00},
+		{0x17, 0x01, 0xf9, 0x00}, {0x28, 0x01, 0xf9, 0x01},
+		{0x00, 0x01, 0x0a, 0x01}, {0x00, 0x01, 0x0d, 0x01},
+		{0x00, 0x01, 0x16, 0x01}, {0xfa, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x03, 0x01, 0x7f, 0x00}, {0x06, 0x01, 0x7f, 0x00},
+		{0x0a, 0x01, 0x7f, 0x00}, {0x0f, 0x01, 0x7f, 0x00},
+		{0x18, 0x01, 0x7f, 0x00}, {0x1f, 0x01, 0x7f, 0x00},
+		{0x29, 0x01, 0x7f, 0x00}, {0x38, 0x01, 0x7f, 0x01},
+		{0x03, 0x01, 0xdc, 0x00}, {0x06, 0x01, 0xdc, 0x00},
+		{0x0a, 0x01, 0xdc, 0x00}, {0x0f, 0x01, 0xdc, 0x00},
+		{0x18, 0x01, 0xdc, 0x00}, {0x1f, 0x01, 0xdc, 0x00},
+		{0x29, 0x01, 0xdc, 0x00}, {0x38, 0x01, 0xdc, 0x01}
+	],
+	[
+		{0x03, 0x01, 0xf9, 0x00}, {0x06, 0x01, 0xf9, 0x00},
+		{0x0a, 0x01, 0xf9, 0x00}, {0x0f, 0x01, 0xf9, 0x00},
+		{0x18, 0x01, 0xf9, 0x00}, {0x1f, 0x01, 0xf9, 0x00},
+		{0x29, 0x01, 0xf9, 0x00}, {0x38, 0x01, 0xf9, 0x01},
+		{0x01, 0x01, 0x0a, 0x00}, {0x16, 0x01, 0x0a, 0x01},
+		{0x01, 0x01, 0x0d, 0x00}, {0x16, 0x01, 0x0d, 0x01},
+		{0x01, 0x01, 0x16, 0x00}, {0x16, 0x01, 0x16, 0x01},
+		{0xfc, 0x00, 0x00, 0x00}, {0xfc, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x02, 0x01, 0x0a, 0x00}, {0x09, 0x01, 0x0a, 0x00},
+		{0x17, 0x01, 0x0a, 0x00}, {0x28, 0x01, 0x0a, 0x01},
+		{0x02, 0x01, 0x0d, 0x00}, {0x09, 0x01, 0x0d, 0x00},
+		{0x17, 0x01, 0x0d, 0x00}, {0x28, 0x01, 0x0d, 0x01},
+		{0x02, 0x01, 0x16, 0x00}, {0x09, 0x01, 0x16, 0x00},
+		{0x17, 0x01, 0x16, 0x00}, {0x28, 0x01, 0x16, 0x01},
+		{0xfd, 0x00, 0x00, 0x00}, {0xfd, 0x00, 0x00, 0x00},
+		{0xfd, 0x00, 0x00, 0x00}, {0xfd, 0x00, 0x00, 0x00}
+	],
+	[
+		{0x03, 0x01, 0x0a, 0x00}, {0x06, 0x01, 0x0a, 0x00},
+		{0x0a, 0x01, 0x0a, 0x00}, {0x0f, 0x01, 0x0a, 0x00},
+		{0x18, 0x01, 0x0a, 0x00}, {0x1f, 0x01, 0x0a, 0x00},
+		{0x29, 0x01, 0x0a, 0x00}, {0x38, 0x01, 0x0a, 0x01},
+		{0x03, 0x01, 0x0d, 0x00}, {0x06, 0x01, 0x0d, 0x00},
+		{0x0a, 0x01, 0x0d, 0x00}, {0x0f, 0x01, 0x0d, 0x00},
+		{0x18, 0x01, 0x0d, 0x00}, {0x1f, 0x01, 0x0d, 0x00},
+		{0x29, 0x01, 0x0d, 0x00}, {0x38, 0x01, 0x0d, 0x01}
+	],
+	/* 255 */
+	[
+		{0x03, 0x01, 0x16, 0x00}, {0x06, 0x01, 0x16, 0x00},
+		{0x0a, 0x01, 0x16, 0x00}, {0x0f, 0x01, 0x16, 0x00},
+		{0x18, 0x01, 0x16, 0x00}, {0x1f, 0x01, 0x16, 0x00},
+		{0x29, 0x01, 0x16, 0x00}, {0x38, 0x01, 0x16, 0x01},
+		{0xff, 0x00, 0x00, 0x00}, {0xff, 0x00, 0x00, 0x00},
+		{0xff, 0x00, 0x00, 0x00}, {0xff, 0x00, 0x00, 0x00},
+		{0xff, 0x00, 0x00, 0x00}, {0xff, 0x00, 0x00, 0x00},
+		{0xff, 0x00, 0x00, 0x00}, {0xff, 0x00, 0x00, 0x00}
+	]
+];

+ 411 - 0
vtest/source/vibe/http/internal/http2/hpack/tables.d

@@ -0,0 +1,411 @@
+//module vibe.http.internal.hpack.tables;
+module vibe.http.internal.http2.hpack.tables;
+
+import vibe.http.internal.http2.hpack.exception;
+
+import vibe.http.status;
+import vibe.http.common;
+import vibe.core.log;
+import vibe.core.sync;
+import vibe.container.ringbuffer : RingBuffer;
+
+import std.variant;
+import std.traits;
+import std.meta;
+import std.range;
+import std.algorithm.iteration;
+import std.math : log10;
+import taggedalgebraic;
+
+
+alias HTTP2SettingValue = uint;
+
+// 4096 octets
+enum DEFAULT_DYNAMIC_TABLE_SIZE = 4096;
+
+/*
+	2.3.  Indexing Tables
+	HPACK uses two tables for associating header fields to indexes.  The
+	static table (see Section 2.3.1) is predefined and contains common
+	header fields (most of them with an empty value).  The dynamic table
+	(see Section 2.3.2) is dynamic and can be used by the encoder to
+	index header fields repeated in the encoded header lists.
+	These two tables are combined into a single address space for
+	defining index values (see Section 2.3.3).
+ 2.3.1.  Static Table
+	The static table consists of a predefined static list of header
+	fields.  Its entries are defined in Appendix A.
+ 2.3.2.  Dynamic Table
+	The dynamic table consists of a list of header fields maintained in
+	first-in, first-out order.  The first and newest entry in a dynamic
+	table is at the lowest index, and the oldest entry of a dynamic tabl
+	is at the highest index.
+	The dynamic table is initially empty.  Entries are added as each
+	header block is decompressed.
+	The dynamic table is initially empty.  Entries are added as each
+	header block is decompressed.
+	The dynamic table can contain duplicate entries (i.e., entries with
+	the same name and same value).  Therefore, duplicate entries MUST NOT
+	be treated as an error by a decoder.
+	The encoder decides how to update the dynamic table and as such can
+	control how much memory is used by the dynamic table.  To limit the
+	memory requirements of the decoder, the dynamic table size is
+	strictly bounded (see Section 4.2).
+	The decoder updates the dynamic table during the processing of a list
+	of header field representations (see Section 3.2).
+*/
+
+// wraps a header field = name:value
+struct HTTP2HeaderTableField {
+	private union HeaderValue {
+		string str;
+		string[] strarr;
+		HTTPStatus status;
+		HTTPMethod method;
+	}
+
+	string name;
+	TaggedAlgebraic!HeaderValue value;
+	bool index = true;
+	bool neverIndex = false;
+
+	// initializers
+	static foreach(t; __traits(allMembers, HeaderValue)) {
+		mixin("this(string n, " ~
+				typeof(__traits(getMember, HeaderValue, t)).stringof ~
+				" v) @safe { name = n; value = v; }");
+	}
+
+	this(R)(R t) @safe
+		if(is(ElementType!R : string))
+	{
+		assert(t.length == 2, "Invalid range for HTTP2HeaderTableField initializer");
+		this(t[0], t[1]);
+	}
+}
+
+// fixed as per HPACK RFC
+immutable size_t STATIC_TABLE_SIZE = 61;
+
+/** static table to index most common headers
+  * fixed size, fixed order of entries (read only)
+  * cannot be updated while decoding a header block
+  */
+static immutable HTTP2HeaderTableField[STATIC_TABLE_SIZE+1] StaticTable;
+
+shared static this() {
+	StaticTable = [
+		HTTP2HeaderTableField("",""), // 0 index is not allowed
+		HTTP2HeaderTableField(":authority", ""),
+		HTTP2HeaderTableField(":method", HTTPMethod.GET),
+		HTTP2HeaderTableField(":method", HTTPMethod.POST),
+		HTTP2HeaderTableField(":path", "/"),
+		HTTP2HeaderTableField(":path", "/index.html"),
+		HTTP2HeaderTableField(":scheme", "http"),
+		HTTP2HeaderTableField(":scheme", "https"),
+		HTTP2HeaderTableField(":status", HTTPStatus.ok), 					// 200
+		HTTP2HeaderTableField(":status", HTTPStatus.noContent), 				// 204
+		HTTP2HeaderTableField(":status", HTTPStatus.partialContent), 		// 206
+		HTTP2HeaderTableField(":status", HTTPStatus.notModified), 			// 304
+		HTTP2HeaderTableField(":status", HTTPStatus.badRequest), 			// 400
+		HTTP2HeaderTableField(":status", HTTPStatus.notFound), 				// 404
+		HTTP2HeaderTableField(":status", HTTPStatus.internalServerError), 	// 500
+		HTTP2HeaderTableField("accept-charset", ""),
+		HTTP2HeaderTableField("accept-encoding", ["gzip", "deflate"]),
+		HTTP2HeaderTableField("accept-language", ""),
+		HTTP2HeaderTableField("accept-ranges", ""),
+		HTTP2HeaderTableField("accept", ""),
+		HTTP2HeaderTableField("access-control-allow-origin", ""),
+		HTTP2HeaderTableField("age", ""),
+		HTTP2HeaderTableField("allow", ""),
+		HTTP2HeaderTableField("authorization", ""),
+		HTTP2HeaderTableField("cache-control", ""),
+		HTTP2HeaderTableField("content-disposition", ""),
+		HTTP2HeaderTableField("content-encoding", ""),
+		HTTP2HeaderTableField("content-language", ""),
+		HTTP2HeaderTableField("content-length", ""),
+		HTTP2HeaderTableField("content-location", ""),
+		HTTP2HeaderTableField("content-range", ""),
+		HTTP2HeaderTableField("content-type", ""),
+		HTTP2HeaderTableField("cookie", ""),
+		HTTP2HeaderTableField("date", ""),
+		HTTP2HeaderTableField("etag", ""),
+		HTTP2HeaderTableField("expect", ""),
+		HTTP2HeaderTableField("expires", ""),
+		HTTP2HeaderTableField("from", ""),
+		HTTP2HeaderTableField("host", ""),
+		HTTP2HeaderTableField("if-match", ""),
+		HTTP2HeaderTableField("if-modified-since", ""),
+		HTTP2HeaderTableField("if-none-match", ""),
+		HTTP2HeaderTableField("if-range", ""),
+		HTTP2HeaderTableField("if-unmodified-since", ""),
+		HTTP2HeaderTableField("last-modified", ""),
+		HTTP2HeaderTableField("link", ""),
+		HTTP2HeaderTableField("location", ""),
+		HTTP2HeaderTableField("max-forwards", ""),
+		HTTP2HeaderTableField("proxy-authenticate", ""),
+		HTTP2HeaderTableField("proxy-authorization", ""),
+		HTTP2HeaderTableField("range", ""),
+		HTTP2HeaderTableField("referer", ""),
+		HTTP2HeaderTableField("refresh", ""),
+		HTTP2HeaderTableField("retry-after", ""),
+		HTTP2HeaderTableField("server", ""),
+		HTTP2HeaderTableField("set-cookie", ""),
+		HTTP2HeaderTableField("strict-transport-security", ""),
+		HTTP2HeaderTableField("transfer-encoding", ""),
+		HTTP2HeaderTableField("user-agent", ""),
+		HTTP2HeaderTableField("vary", ""),
+		HTTP2HeaderTableField("via", ""),
+		HTTP2HeaderTableField("www-authenticate", "")
+	];
+}
+
+private ref immutable(HTTP2HeaderTableField) getStaticTableEntry(size_t key) @safe @nogc
+{
+    assert(key > 0 && key < StaticTable.length, "Invalid static table index");
+    return StaticTable[key];
+}
+
+// compute size of an entry as per RFC
+HTTP2SettingValue computeEntrySize(HTTP2HeaderTableField f) @safe
+{
+	alias k = HTTP2HeaderTableField.value.Kind;
+	HTTP2SettingValue ret = cast(HTTP2SettingValue)f.name.length + 32;
+
+	final switch (f.value.kind) {
+		case k.str: ret += f.value.get!string.length; break;
+		case k.strarr: ret += f.value.get!(string[]).map!(s => s.length).sum(); break;
+		case k.status: ret += cast(size_t)log10(cast(double)f.value.get!HTTPStatus) + 1; break;
+		case k.method: ret += httpMethodString(f.value.get!HTTPMethod).length; break;
+	}
+	return ret;
+}
+
+private struct DynamicTable {
+	private {
+		// default table is 4096 octs. / n. octets of an empty HTTP2HeaderTableField struct (32)
+		RingBuffer!(HTTP2HeaderTableField, DEFAULT_DYNAMIC_TABLE_SIZE/HTTP2HeaderTableField.sizeof, false) m_table;
+
+		// extra table is a circular buffer, initially empty, used when
+		// maxsize > DEFAULT_DYNAMIC_TABLE_SIZE
+		RingBuffer!HTTP2HeaderTableField m_extraTable;
+
+		// as defined in SETTINGS_HEADER_TABLE_SIZE
+		HTTP2SettingValue m_maxsize;
+
+		// current size
+		size_t m_size = 0;
+
+		// last index (table index starts from 1)
+		size_t m_index = 0;
+
+		// extra table index (starts from 0)
+		size_t m_extraIndex = 0;
+	}
+
+	this(HTTP2SettingValue ms) @trusted nothrow
+	{
+		m_maxsize = ms;
+
+		if(ms > DEFAULT_DYNAMIC_TABLE_SIZE) {
+			m_extraTable.capacity = (ms - DEFAULT_DYNAMIC_TABLE_SIZE)/HTTP2HeaderTableField.sizeof;
+		}
+	}
+
+	@property void dispose() nothrow { m_extraTable.dispose(); }
+
+	// number of elements inside dynamic table
+	@property size_t size() @safe @nogc { return m_size; }
+
+	@property size_t index() @safe @nogc { return m_index; }
+
+	HTTP2HeaderTableField opIndex(size_t idx) @safe @nogc
+	{
+		size_t totIndex = m_index + m_extraIndex;
+		assert(idx > 0 && idx <= totIndex, "Invalid table index");
+		if(idx > m_index && idx < totIndex) return m_extraTable[idx-m_index];
+		else return m_table[idx-1];
+	}
+
+	// insert at the head
+	void insert(HTTP2HeaderTableField header) @safe
+	{
+		auto nsize = computeEntrySize(header);
+		// ensure that the new entry does not exceed table capacity
+		while(m_size + nsize > m_maxsize) {
+			//logDebug("Maximum header table size exceeded"); // requires gc
+			remove();
+		}
+
+		// insert
+		if(m_size + nsize > DEFAULT_DYNAMIC_TABLE_SIZE) {
+			m_extraTable.put(header);
+			m_extraIndex++;
+		} else {
+			m_table.put(header);
+			m_index++;
+		}
+
+		m_size += nsize;
+	}
+
+	// evict an entry
+	void remove() @safe
+	{
+		enforceHPACK(!m_table.empty, "Cannot remove element from empty table");
+
+		if(m_extraIndex > 0) {
+			m_size -= computeEntrySize(m_extraTable.back);
+			m_extraTable.removeFront();
+			m_extraIndex--;
+		} else {
+			m_size -= computeEntrySize(m_table.back);
+			m_table.removeFront();
+			m_index--;
+		}
+	}
+
+	/** new size should be lower than the max set one
+	  * after size is successfully changed, an ACK has to be sent
+	  * multiple changes between two header fields are possible
+	  * if multiple changes occour, only the smallest maximum size
+	  * requested has to be acknowledged
+	*/
+	void updateSize(HTTP2SettingValue sz) @safe @nogc
+	{
+		m_maxsize = sz;
+	}
+}
+
+unittest {
+	// static table
+	auto a = getStaticTableEntry(1);
+	static assert(is(typeof(a) == immutable(HTTP2HeaderTableField)));
+	assert(a.name == ":authority");
+	assert(getStaticTableEntry(2).name == ":method" && getStaticTableEntry(2).value == HTTPMethod.GET);
+
+	DynamicTable dt = DynamicTable(DEFAULT_DYNAMIC_TABLE_SIZE+2048);
+	assert(dt.size == 0);
+	assert(dt.index == 0);
+
+	// dynamic table
+	import std.algorithm.comparison : equal;
+
+	auto h = HTTP2HeaderTableField("test", "testval");
+	dt.insert(h);
+	assert(dt.size > 0);
+	assert(dt.index == 1);
+	assert(dt[dt.index].name == "test");
+
+	dt.remove();
+	assert(dt.size == 0);
+	assert(dt.index == 0);
+}
+
+/** provides an unified address space through operator overloading
+  * this is the only interface that will be used for the two tables
+  */
+struct IndexingTable {
+	private {
+		DynamicTable m_dynamic;
+		RecursiveTaskMutex m_lock;
+	}
+
+	// requires the maximum size for the dynamic table
+	this(HTTP2SettingValue ms) @trusted nothrow
+	{
+		m_dynamic = DynamicTable(ms);
+		try m_lock = new RecursiveTaskMutex;
+		catch (Exception e) assert(false, e.msg);
+	}
+
+	~this()
+	nothrow {
+		m_dynamic.dispose();
+	}
+
+	@property size_t size() @safe @nogc { return STATIC_TABLE_SIZE + m_dynamic.index + 1; }
+
+	@property bool empty() @safe @nogc { return m_dynamic.size == 0; }
+
+	@property HTTP2HeaderTableField front() @safe { return this[0]; }
+
+	@property void popFront() @safe
+	{
+		assert(!empty, "Cannot call popFront on an empty dynamic table");
+		m_lock.performLocked!({
+			m_dynamic.remove();
+		});
+	}
+
+	// element retrieval
+	HTTP2HeaderTableField opIndex(size_t idx) @safe
+	{
+		enforceHPACK(idx > 0 && idx < size(), "Invalid HPACK table index");
+
+		if (idx < STATIC_TABLE_SIZE+1) return getStaticTableEntry(idx);
+		else return m_dynamic[m_dynamic.index - (idx - STATIC_TABLE_SIZE) + 1];
+	}
+
+	// dollar == size
+	// +1 to mantain consistency with the dollar operator
+	size_t opDollar() @safe @nogc
+	{
+		return size();
+	}
+
+	// assignment can only be done on the dynamic table
+	void insert(HTTP2HeaderTableField hf) @safe
+	{
+		m_lock.performLocked!({
+			m_dynamic.insert(hf);
+		});
+	}
+
+	// update max dynamic table size
+	void updateSize(HTTP2SettingValue sz) @safe
+	{
+		m_lock.performLocked!({
+			m_dynamic.updateSize(sz);
+		});
+	}
+}
+
+unittest {
+	// indexing table
+	IndexingTable table = IndexingTable(DEFAULT_DYNAMIC_TABLE_SIZE);
+	assert(table[2].name == ":method" && table[2].value == HTTPMethod.GET);
+
+	// assignment
+	auto h = HTTP2HeaderTableField("test", "testval");
+	table.insert(h);
+	assert(table.size == STATIC_TABLE_SIZE + 2);
+	assert(table[STATIC_TABLE_SIZE+1].name == "test");
+
+	auto h2 = HTTP2HeaderTableField("test2", "testval2");
+	table.insert(h2);
+	assert(table.size == STATIC_TABLE_SIZE + 3);
+	assert(table[STATIC_TABLE_SIZE+1].name == "test2");
+
+	// dollar
+	auto h3 = HTTP2HeaderTableField("test3", "testval3");
+	table.insert(h3);
+	assert(table.size == STATIC_TABLE_SIZE + 4);
+	assert(table[$-1].name == "test");
+	assert(table[$-2].name == "test2");
+	assert(table[STATIC_TABLE_SIZE+1].name == "test3");
+
+	// test removal on full table
+
+	HTTP2SettingValue hts = computeEntrySize(h); // only one header
+	IndexingTable t2 = IndexingTable(hts);
+	t2.insert(h);
+	t2.insert(h);
+	assert(t2.size == STATIC_TABLE_SIZE + 2);
+	assert(t2[STATIC_TABLE_SIZE + 1].name == "test");
+	assert(t2[$ - 1].name == "test");
+
+	auto h4 = HTTP2HeaderTableField("","");
+	hts = computeEntrySize(h4); // entry size of an empty field is 32 octets
+	assert(hts == 32);
+}

+ 14 - 0
vtest/source/vibe/http/internal/http2/hpack/util.d

@@ -0,0 +1,14 @@
+module vibe.http.internal.http2.hpack.util;
+
+import std.range;
+
+// decode ubyte as integer representation according to prefix
+size_t toInteger(ubyte bbuf, uint prefix) @safe @nogc
+{
+	assert(prefix < 8, "Prefix must be at most an octet long");
+
+	bbuf = bbuf & ((1 << (8 - prefix)) - 1);
+	assert(bbuf >= 0, "Invalid decoded integer");
+
+	return bbuf;
+}

+ 276 - 0
vtest/source/vibe/http/internal/http2/multiplexing.d

@@ -0,0 +1,276 @@
+module vibe.http.internal.http2.multiplexing;
+
+import vibe.container.hashmap;
+import vibe.container.internal.utilallocator;
+import vibe.core.sync;
+import vibe.core.log;
+import vibe.core.net;
+import vibe.core.core : yield;
+
+import std.exception;
+import std.container : RedBlackTree;
+
+
+/** Stream multiplexing in HTTP/2
+  * References: https://tools.ietf.org/html/rfc7540#section-5
+  *
+  * The purposes of stream registration into a multiplexer are the following:
+  * 1. Check correctness of HTTP/2 frames received, following the rules defined
+  *    in the HTTP/2 RFC (https://tools.ietf.org/html/rfc7540)
+  * 2. Implement stream prioritization / dependency:
+  *    https://tools.ietf.org/html/rfc7540#section-5.3
+  * 3. Hold data structures which are supposed to mantain the state of a connection,
+  *	   since HTTP/2 opens only 1 tcp connection on which multiple frames can be sent.
+*/
+
+
+/* ======================================================= */
+/* ================ STREAM MANAGEMENT =================== */
+/* ======================================================= */
+
+/// register a stream on a MUX
+auto registerStream(Mux)(ref Mux multiplexer, const uint sid) @trusted
+{
+	return multiplexer.register(sid);
+}
+
+/// close a stream on a MUX
+auto closeStream(Mux)(ref Mux multiplexer, const uint sid) @trusted
+{
+	return multiplexer.close(sid);
+}
+
+/// check if stream is OPEN (meaning, currently registered and active)
+auto isOpenStream(Mux)(ref Mux multiplexer, const uint sid) @trusted
+{
+	return multiplexer.isOpen(sid);
+}
+
+/// connection preface (SETTINGS) can be received only ONCE
+auto isConnectionPreface(Mux)(ref Mux multiplexer) @trusted
+{
+	return multiplexer.isConnPreface();
+}
+
+/* ======================================================= */
+/* ================= FLOW CONTROL ======================== */
+/* ======================================================= */
+
+/** Per-connection window
+  * Valid for EVERY stream in MUX[idx]
+  */
+auto connectionWindow(Mux)(ref Mux multiplexer) @trusted
+{
+	return multiplexer.connWindow;
+}
+
+/// Update the connection window value
+auto updateConnectionWindow(Mux)(ref Mux multiplexer, const ulong newWin) @trusted
+{
+	return multiplexer.updateConnWindow(newWin);
+}
+
+/** Per-stream window
+  * Valid for stream `sid` in MUX[idx]
+  */
+auto streamConnectionWindow(Mux)(ref Mux multiplexer, const uint sid) @trusted
+{
+	return multiplexer.streamConnWindow(sid);
+}
+
+/// Update the stream connection window value
+auto updateStreamConnectionWindow(Mux)(ref Mux multiplexer, const uint sid, const ulong newWin) @trusted
+{
+	return multiplexer.updateStreamConnWindow(sid, newWin);
+}
+
+/** A TaskCondition is used to synchronize DATA frame sending
+  * this enforces flow control on every outgoing DATA frame
+  * So that the client-established connection/stream window
+  * is not exceeded.
+  * Each connection (MUX) has its own condition.
+  */
+void waitCondition(Mux)(ref Mux multiplexer, const uint sid) @trusted
+{
+	multiplexer.wait(sid);
+}
+
+/// signal the waiting task(s) that a change
+/// in the connection window has occourred
+void notifyCondition(Mux)(ref Mux multiplexer) @trusted
+{
+	multiplexer.notify();
+}
+
+/// check if waiting tasks are enqueued for this connection
+uint checkCondition(Mux)(ref Mux multiplexer, const uint sid) @trusted
+{
+	return multiplexer.checkCond(sid);
+}
+
+/// signal that the DATA dispatch is over
+/// task is no longer enqueued
+void doneCondition(Mux)(ref Mux multiplexer, const uint sid) @trusted
+{
+	multiplexer.endWait(sid);
+}
+
+/** Underlying multiplexer data structure
+  * Uses a TaskMutex to perform sensitive operations
+  * since multiple streams might be operating on the same
+  * connection (MUX)
+  */
+struct HTTP2Multiplexer {
+	/// used to register open streams, which must be unique
+	private alias H2Queue = RedBlackTree!uint;
+
+	private {
+		IAllocator m_alloc;
+		H2Queue m_open;		// set of open streams
+		uint m_closed;		// index of the last closed stream
+		uint m_last;		// index of last open stream
+		uint m_max;			// maximum number of streams open at the same time
+		uint m_countOpen;   // current number of open streams (in m_open)
+		TaskMutex m_lock;
+		TaskCondition m_cond;
+		uint[uint] m_waiting;
+		ulong m_wsize;
+		ulong[uint] m_streamWSize;
+		bool m_connPreface = true;
+	}
+
+	@disable this();
+
+	this(Alloc)(Alloc alloc, const uint max, const ulong wsize, const uint tsize=4096) @trusted
+	nothrow {
+		m_alloc = alloc;
+		try {
+			m_lock = alloc.make!TaskMutex();
+			m_cond = alloc.make!TaskCondition(m_lock);
+			m_open = alloc.make!H2Queue();
+		} catch (Exception e) assert(false, e.msg);
+		m_last = 0;
+		m_max = max;
+		m_wsize = wsize;
+	}
+
+	/** The methods from here downwards
+	  * are not supposed to be used directly,
+	  * but through the documented wrappers above.
+	  */
+	@property void wait(const uint sid) @trusted
+	{
+		synchronized(m_lock) {
+			if(!(sid in m_waiting)) m_waiting[sid] = 0;
+			else m_waiting[sid]++;
+			m_cond.wait();
+		}
+	}
+
+	@property void endWait(const uint sid) @trusted
+	{
+		synchronized(m_lock) {
+			if(!(sid in m_waiting)) m_waiting[sid] = 0;
+			else m_waiting[sid]--;
+		}
+	}
+
+	@property void notify() @trusted
+	{
+		m_cond.notify();
+	}
+
+	@property uint checkCond(const uint sid) @safe
+	{
+		if(!(sid in m_waiting)) return 0;
+		return m_waiting[sid] > 0 && isOpen(sid);
+	}
+
+	@property ulong connWindow() @safe
+	{
+		return m_wsize;
+	}
+
+	@property ulong streamConnWindow(const uint sid) @safe
+	{
+		if(!(sid in m_streamWSize)) return 0;
+
+		return m_streamWSize[sid];
+	}
+
+	@property bool isConnPreface() @safe
+	{
+		// can only be true once per connection
+		auto b = m_connPreface;
+
+		m_lock.performLocked!({
+			m_connPreface = false;
+		});
+
+		return b;
+	}
+
+	// register a new open stream
+	bool register(const uint sid) @safe
+	{
+		if(sid == 0) return true; 					// success, but sid=0 is not registered
+		if(m_countOpen + 1 > m_max) return false; 	// PROTOCOL_ERROR: too many open streams
+		if(sid <= m_last && sid != 0) return false; // Stream ID must be greater than previously
+													// registered ones
+		m_lock.performLocked!({
+			m_countOpen++;
+			m_open.insert(sid);
+			m_last = sid;
+			m_streamWSize[sid] = m_wsize;
+		});
+		return true;
+	}
+
+	// close an open stream
+	bool close(const uint sid) @safe
+	{
+		if(!(sid in m_open)) return false; //Cannot close a stream which is not open
+		if(sid in m_waiting && m_waiting[sid]) return false; //Cannot close a stream which is blocked
+
+		m_lock.performLocked!({
+			m_countOpen--;
+			m_open.removeKey(sid);
+			m_streamWSize.remove(sid);
+		});
+		return true;
+	}
+
+	// open streams are present in m_open
+	bool isOpen(const uint sid) @safe
+	{
+		return sid in m_open;
+	}
+
+	bool updateConnWindow(const ulong newWin) @safe
+	{
+		if(newWin > ulong.max || newWin < 0) return false;
+		logDebug("MUX: CONTROL FLOW WINDOW: from %d to %d bytes", m_wsize, newWin);
+
+		m_lock.performLocked!({
+			m_wsize = newWin;
+		});
+
+		return true;
+	}
+
+	bool updateStreamConnWindow(const uint sid, const ulong newWin) @safe
+	{
+		if(newWin > ulong.max || newWin < 0) return false;
+		if(sid == 0) return true;
+
+		logDebug("MUX: CONTROL FLOW WINDOW: stream %d from %d to %d bytes",
+				sid, (sid in m_streamWSize) ? m_streamWSize[sid] : m_wsize, newWin);
+
+		m_lock.performLocked!({
+				m_streamWSize[sid] = newWin;
+		});
+
+		return true;
+	}
+
+}

+ 971 - 0
vtest/source/vibe/http/internal/http2/server.d

@@ -0,0 +1,971 @@
+module vibe.http.internal.http2.server;
+
+import vibe.http.internal.http2.error;
+import vibe.http.internal.http2.multiplexing;
+import vibe.http.internal.http2.frame;
+import vibe.http.internal.http2.settings;
+import vibe.http.internal.http2.exchange;
+import vibe.http.internal.http2.hpack.tables;
+import vibe.http.internal.http2.hpack.hpack;
+import vibe.http.internal.http2.hpack.exception;
+import vibe.http.server;
+
+import vibe.container.internal.utilallocator;
+import vibe.core.log;
+import vibe.core.net;
+import vibe.core.core;
+import vibe.core.stream;
+import vibe.stream.tls;
+import vibe.internal.array;
+import vibe.internal.freelistref;
+import vibe.internal.interfaceproxy;
+
+import std.range;
+import std.base64;
+import std.traits;
+import std.bitmanip; // read from ubyte (decoding)
+import std.typecons;
+import std.conv : to;
+import std.exception : enforce;
+import std.algorithm : canFind; // alpn callback
+import std.algorithm.iteration;
+import std.variant : Algebraic;
+
+/*
+   3.2.  Starting HTTP/2 for "http" URIs
+
+   A client that makes a request for an "http" URI without prior
+   knowledge about support for HTTP/2 on the next hop uses the HTTP
+   Upgrade mechanism (Section 6.7 of [RFC7230]).  The client does so by
+   making an HTTP/1.1 request that includes an Upgrade header field with
+   the "h2c" token.  Such an HTTP/1.1 request MUST include exactly one
+   HTTP2-Settings (Section 3.2.1) header field.
+
+   For example:
+
+	 GET / HTTP/1.1
+	 Host: server.example.com
+	 Connection: Upgrade, HTTP2-Settings
+	 Upgrade: h2c
+	 HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
+
+   Requests that contain a payload body MUST be sent in their entirety
+   before the client can send HTTP/2 frames.  This means that a large
+   request can block the use of the connection until it is completely
+   sent.
+
+   If concurrency of an initial request with subsequent requests is
+   important, an OPTIONS request can be used to perform the upgrade to
+   HTTP/2, at the cost of an additional round trip.
+
+
+   A server that does not support HTTP/2 can respond to the request as
+   though the Upgrade header field were absent:
+
+	 HTTP/1.1 200 OK
+	 Content-Length: 243
+	 Content-Type: text/html
+
+	 ...
+
+   A server MUST ignore an "h2" token in an Upgrade header field.
+   Presence of a token with "h2" implies HTTP/2 over TLS, which is
+   instead negotiated as described in Section 3.3.
+
+   A server that supports HTTP/2 accepts the upgrade with a 101
+   (Switching Protocols) response.  After the empty line that terminates
+   the 101 response, the server can begin sending HTTP/2 frames.  These
+   frames MUST include a response to the request that initiated the
+   upgrade.
+
+   For example:
+
+	 HTTP/1.1 101 Switching Protocols
+	 Connection: Upgrade
+	 Upgrade: h2c
+
+	 [ HTTP/2 connection ...
+
+   The first HTTP/2 frame sent by the server MUST be a server connection
+   preface (Section 3.5) consisting of a SETTINGS frame (Section 6.5).
+   Upon receiving the 101 response, the client MUST send a connection
+   preface (Section 3.5), which includes a SETTINGS frame.
+
+   The HTTP/1.1 request that is sent prior to upgrade is assigned a
+   stream identifier of 1 (see Section 5.1.1) with default priority
+   values (Section 5.3.5).  Stream 1 is implicitly "half-closed" from
+   the client toward the server (see Section 5.1), since the request is
+   completed as an HTTP/1.1 request.  After commencing the HTTP/2
+   connection, stream 1 is used for the response.
+
+3.2.1.  HTTP2-Settings Header Field
+
+   A request that upgrades from HTTP/1.1 to HTTP/2 MUST include exactly
+   one "HTTP2-Settings" header field.  The HTTP2-Settings header field
+   is a connection-specific header field that includes parameters that
+   govern the HTTP/2 connection, provided in anticipation of the server
+   accepting the request to upgrade.
+
+	 HTTP2-Settings	= token68
+
+   A server MUST NOT upgrade the connection to HTTP/2 if this header
+   field is not present or if more than one is present.  A server MUST
+   NOT send this header field.
+
+   The content of the HTTP2-Settings header field is the payload of a
+   SETTINGS frame (Section 6.5), encoded as a base64url string (that is,
+   the URL- and filename-safe Base64 encoding described in Section 5 of
+   [RFC4648], with any trailing '=' characters omitted).  The ABNF
+   [RFC5234] production for "token68" is defined in Section 2.1 of
+   [RFC7235].
+
+   Since the upgrade is only intended to apply to the immediate
+   connection, a client sending the HTTP2-Settings header field MUST
+   also send "HTTP2-Settings" as a connection option in the Connection
+   header field to prevent it from being forwarded (see Section 6.1 of
+   [RFC7230]).
+
+   A server decodes and interprets these values as it would any other
+   SETTINGS frame.  Explicit acknowledgement of these settings
+   (Section 6.5.3) is not necessary, since a 101 response serves as
+   implicit acknowledgement.  Providing these values in the upgrade
+   request gives a client an opportunity to provide parameters prior to
+   receiving any frames from the server.
+
+*/
+
+/**
+  * an ALPN callback which can be used to detect the "h2" protocol
+  * must be set before initializing the server with 'listenHTTP'
+  * if the protocol is not set, it replies with HTTP/1.1
+  */
+TLSALPNCallback http2Callback = (string[] choices) {
+	if (choices.canFind("h2")) return "h2";
+	else return "http/1.1";
+};
+
+private alias TLSStreamType = ReturnType!(createTLSStreamFL!(InterfaceProxy!Stream));
+
+/* ==================================================== */
+/* 				CONNECTION INITIALIZATION				*/
+/* ==================================================== */
+
+/** h2c protocol switching ONLY: Check if SETTINGS payload is valid by trying to decode it
+  * if !valid, close connection and refuse to upgrade (RFC)
+  * if valid, send SWITCHING_PROTOCOL response and start an HTTP/2 connection handler
+  */
+bool startHTTP2Connection(ConnectionStream, H)(ConnectionStream connection, string h2settings,
+		HTTP2ServerContext context, HTTPServerResponse switchRes, H headers, string st,
+		IAllocator alloc, ubyte[] resBody) @safe
+	if (isConnectionStream!ConnectionStream)
+{
+	// init settings
+	HTTP2Settings settings;
+	logTrace("Starting HTTP/2 connection");
+
+	// try decoding settings
+	if (settings.decode!Base64URL(h2settings)) {
+
+		context.settings = settings;
+
+		// initialize IndexingTable (HPACK)
+		() @trusted {
+
+			if(!context.hasTable) context.table = FreeListRef!IndexingTable(context.settings.headerTableSize);
+
+			// save response converted to HTTP/2
+			context.resFrame = alloc.makeArray!ubyte(buildHeaderFrame!(StartLine.RESPONSE)
+						(st, headers, context, alloc));
+			context.resFrame ~= resBody;
+
+		} ();
+
+		// send response
+		switchRes.switchToHTTP2(&handleHTTP2Connection!ConnectionStream, context);
+		return true;
+
+	} else {
+		// reply with a 400 (bad request) header
+		switchRes.sendBadRequest();
+		connection.close;
+		return false;
+	}
+}
+
+/** client AND server should send a connection preface
+  * server should receive a connection preface from the client + SETTINGS Frame
+  * server connection preface consists of a SETTINGS Frame
+  */
+void handleHTTP2Connection(ConnectionStream)(ConnectionStream stream,
+		TCPConnection connection, HTTP2ServerContext context, bool priorKnowledge=false) @safe
+	if (isConnectionStream!ConnectionStream || is(ConnectionStream : TLSStreamType))
+{
+	logTrace("HTTP/2 Connection Handler");
+
+	// read the connection preface
+	if(!priorKnowledge) {
+		ubyte[24] h2connPreface;
+		stream.read(h2connPreface);
+
+		if(h2connPreface != "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") {
+			logDebug("Ignoring invalid HTTP/2 client connection preface");
+			return;
+		}
+		logTrace("Received client http2 connection preface");
+	}
+
+	// initialize Frame handler
+	handleHTTP2FrameChain(stream, connection, context);
+}
+
+/* ==================================================== */
+/* 					FRAME HANDLING						*/
+/* ==================================================== */
+
+/// async frame handler: in charge of closing the connection if no data flows
+private void handleHTTP2FrameChain(ConnectionStream)(ConnectionStream stream, TCPConnection
+		connection, HTTP2ServerContext context) @safe nothrow
+	if (isConnectionStream!ConnectionStream || is(ConnectionStream : TLSStream))
+{
+	logTrace("HTTP/2 Frame Chain Handler");
+
+	static struct CB {
+		ConnectionStream stream;
+		TCPConnection connection;
+		HTTP2ServerContext context;
+
+		void opCall(bool st)
+		{
+			if (!st) connection.close;
+			else runTask(&handleHTTP2FrameChain, stream, connection, context);
+		}
+	}
+
+	while(true) {
+		try {
+			CB cb = {stream, connection, context};
+			auto st = connection.waitForDataAsync(cb);
+
+			final switch(st) {
+				case WaitForDataAsyncStatus.waiting:
+					logTrace("need to wait for more data asynchronously");
+					return;
+
+				case WaitForDataAsyncStatus.noMoreData:
+					logTrace("connection closed by remote side");
+					stream.finalize();
+					connection.close();
+					return;
+
+				case WaitForDataAsyncStatus.dataAvailable:
+					// start the frame handler
+					bool close = handleHTTP2Frame(stream, connection, context);
+
+					// determine if this connection needs to be closed
+					if(close) {
+						logTrace("Closing connection.");
+						stream.finalize();
+						connection.close();
+						return;
+					}
+			}
+		} catch (Exception e) {
+			logException(e, "Failed to handle HTTP/2 frame chain");
+			connection.close();
+			return;
+		}
+	}
+}
+
+/// initializes an allocator and handles stream closing
+private bool handleHTTP2Frame(ConnectionStream)(ConnectionStream stream, TCPConnection
+		connection, HTTP2ServerContext context) @trusted
+	if (isConnectionStream!ConnectionStream || is(ConnectionStream : TLSStream))
+{
+	import vibe.container.internal.utilallocator: RegionListAllocator;
+	logTrace("HTTP/2 Frame Handler");
+
+	bool close = false;
+
+	() @trusted {
+		version (VibeManualMemoryManagement)
+			scope alloc = new RegionListAllocator!(shared(Mallocator), false)
+			(1024, Mallocator.instance);
+		else
+			scope alloc = new RegionListAllocator!(shared(GCAllocator), true)
+				(1024, GCAllocator.instance);
+
+		if(!context.hasMultiplexer) context.multiplexer = FreeListRef!HTTP2Multiplexer(
+				alloc,
+				context.settings.maxConcurrentStreams,
+				context.settings.initialWindowSize,
+				context.settings.headerTableSize);
+
+		if(!context.hasTable) context.table = FreeListRef!IndexingTable(context.settings.headerTableSize);
+
+		// create a HTTP/2 Stream
+		auto h2stream = HTTP2ConnectionStream!ConnectionStream(stream, 0, alloc);
+
+
+		close = handleFrameAlloc(h2stream, connection, context, alloc);
+
+		// if stream has to be closed
+		if(h2stream.state == HTTP2StreamState.CLOSED) {
+			try {
+				closeStream(context.multiplexer, h2stream.streamId);
+			} catch(Exception e) {
+				logWarn("Unable to close stream: " ~ e.msg);
+				close = true;
+			}
+		}
+	} ();
+
+	return close;
+}
+
+/// used (mixin) to check the validity of the received Frame w.r.to its stream ID
+private const string checkvalid = "enforceHTTP2(valid, \"Invalid stream ID\", HTTP2Error.STREAM_CLOSED);";
+
+/** Receives an HTTP2ConnectionStream, and handles the data received by decoding frames
+  * Currently supports simple requests / responses
+  * Stream Lifecycle is treated according to RFC 7540, Section 5.1
+*/
+private bool handleFrameAlloc(ConnectionStream)(ref ConnectionStream stream, TCPConnection connection,
+		HTTP2ServerContext context, IAllocator alloc) @trusted
+{
+	logTrace("HTTP/2 Frame Handler (Alloc)");
+
+	uint len = 0;
+
+	// payload buffer
+	auto rawBuf = AllocAppender!(ubyte[])(alloc);
+	auto payload = AllocAppender!(ubyte[])(alloc);
+
+	// Frame properties
+	bool endStream = false;
+	bool endHeaders = false;
+	bool isAck = false;
+	bool close = false;
+	scope HTTP2FrameStreamDependency sdep;
+
+	// frame struct
+	scope HTTP2FrameHeader header;
+
+/* ==================================================== */
+/* 				read received header 					*/
+/* ==================================================== */
+	if(stream.canRead) {
+		try {
+			len = stream.readHeader(rawBuf);
+		} catch (UncaughtException e) {
+			// failed reading from stream, do not close the connection
+			stream.state = HTTP2StreamState.CLOSED;
+			return false;
+		}
+	} else {
+		// failed reading from stream, do not close the connection
+		//stream.state = HTTP2StreamState.CLOSED;
+		return false;
+	}
+
+
+	// adjust buffer sizes
+	rawBuf.reserve(len);
+	payload.reserve(len);
+
+/* ==================================================== */
+/* 				read received payload 					*/
+/* ==================================================== */
+	if(len) stream.readPayload(rawBuf, len);
+
+
+/* ==================================================== */
+/* 				parse received Frame 					*/
+/* ==================================================== */
+	try {
+		header = payload.unpackHTTP2Frame(rawBuf.data, endStream, endHeaders, isAck, sdep);
+	} catch (HTTP2Exception e) {
+		if (stream.state != HTTP2StreamState.IDLE || stream.streamId == 0) {
+			ubyte[GOAWAYFrameLength] f;
+			f.buildGOAWAYFrame(stream.streamId, e.code);
+			stream.write(f);
+			stream.state = HTTP2StreamState.CLOSED;
+			logWarn("%s: %s", "Sent GOAWAY Frame", e.message);
+			return true;
+		} else {
+			logDebug("Ignoring unsupported extension header.");
+			return false;
+		}
+	} catch (Exception e) {
+		logWarn(e.msg);
+	}
+
+/* ==================================================== */
+/*    register stream on MUX and determine Frame type	*/
+/* ==================================================== */
+	try {
+		auto valid = registerStream(context.multiplexer, header.streamId);
+
+		logDebug("Received: "~to!string(header.type)~" on streamID "~to!string(header.streamId));
+
+		enforceHTTP2(header.streamId % 2 != 0 || header.streamId == 0, "Clients cannot register even streams", HTTP2Error.PROTOCOL_ERROR);
+
+		if(stream.needsContinuation) enforceHTTP2(header.type == HTTP2FrameType.CONTINUATION,
+				"Expected continuation frame", HTTP2Error.PROTOCOL_ERROR);
+
+		stream.streamId = header.streamId;
+
+		final switch(header.type) {
+/* ==================================================== */
+/* 					DATA Frame (TODO)					*/
+/* ==================================================== */
+			case HTTP2FrameType.DATA:
+				mixin(checkvalid);
+
+				if(endStream) {
+					if(stream.state == HTTP2StreamState.HALF_CLOSED_LOCAL) {
+						stream.state = HTTP2StreamState.CLOSED;
+						closeStream(context.multiplexer, stream.streamId);
+
+					} else if(stream.state == HTTP2StreamState.OPEN) {
+						stream.state = HTTP2StreamState.HALF_CLOSED_REMOTE;
+
+					} else if(stream.state == HTTP2StreamState.IDLE) {
+						enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
+
+					} else {
+						enforceHTTP2(false, "Stream closed", HTTP2Error.STREAM_CLOSED);
+					}
+				}
+				break;
+
+/* ==================================================== */
+/* 					HEADERS Frame 						*/
+/* ==================================================== */
+			case HTTP2FrameType.HEADERS:
+				mixin(checkvalid);
+				enforceHTTP2(stream.streamId > 0, "Invalid stream ID", HTTP2Error.PROTOCOL_ERROR);
+
+				stream.state = HTTP2StreamState.OPEN;
+				if(sdep.isSet) {
+					// update stream dependency with data in `sdep`
+				}
+
+				// save the header block for processing
+				stream.putHeaderBlock(payload.data);
+
+				if(endStream) {
+					stream.state = HTTP2StreamState.HALF_CLOSED_REMOTE;
+				}
+
+				// parse headers in payload
+				if(endHeaders) {
+					logDebug("Received full HEADERS block");
+					handleHTTP2HeadersFrame(stream, connection, context, alloc);
+
+				} else {
+					// wait for the next CONTINUATION frame until end_headers flag is set
+					// END_STREAM flag does not count in this case
+					logDebug("Incomplete HEADERS block, waiting for CONTINUATION frame.");
+					close = handleFrameAlloc(stream, connection, context, alloc);
+				}
+
+				break;
+
+/* ==================================================== */
+/* 					PRIORITY Frame (TODO) 				*/
+/* ==================================================== */
+			case HTTP2FrameType.PRIORITY:
+				// do not check validity since PRIORITY frames can be received on CLOSED
+				// streams
+
+				enforceHTTP2(stream.streamId > 0, "Invalid stream ID", HTTP2Error.PROTOCOL_ERROR);
+				// update stream dependency with data in `sdep`
+				break;
+
+/* ==================================================== */
+/* 					RST_STREAM Frame					*/
+/* ==================================================== */
+			case HTTP2FrameType.RST_STREAM:
+				enforceHTTP2(stream.state != HTTP2StreamState.IDLE || !valid,
+						"Invalid state", HTTP2Error.PROTOCOL_ERROR);
+
+				// reset stream in `closed` state
+				if(stream.state != HTTP2StreamState.CLOSED) {
+					closeStream(context.multiplexer, stream.streamId);
+				}
+
+				logDebug("RST_STREAM: Stream %d closed,  error: %s",
+						stream.streamId, cast(HTTP2Error)fromBytes(payload.data,4));
+				break;
+
+/* ==================================================== */
+/* 					SETTINGS Frame 						*/
+/* ==================================================== */
+			case HTTP2FrameType.SETTINGS:
+				if(!isAck) {
+					handleHTTP2SettingsFrame(stream, connection, payload.data, header, context);
+				} else {
+					enforceHTTP2(payload.data.length == 0,
+							"Invalid SETTINGS ACK (payload not empty)", HTTP2Error.FRAME_SIZE_ERROR);
+					logDebug("Received SETTINGS ACK");
+				}
+				break;
+
+/* ==================================================== */
+/* 			      PUSH_PROMISE Frame 					*/
+/* ==================================================== */
+			case HTTP2FrameType.PUSH_PROMISE:
+				enforceHTTP2(false,
+						"Client should not send PUSH_PROMISE Frames.", HTTP2Error.PROTOCOL_ERROR);
+				break;
+
+/* ==================================================== */
+/* 				      PING Frame 						*/
+/* ==================================================== */
+			case HTTP2FrameType.PING:
+				if(!isAck) {
+					// acknowledge ping with PING ACK Frame
+					FixedAppender!(ubyte[], 17) buf;
+					buf.createHTTP2FrameHeader(len, header.type, 0x1, header.streamId);
+
+					// write PING Frame header
+					stream.write(buf.data);
+					// write PING Frame payload (equal to the received one)
+					stream.write(payload.data);
+					logDebug("Sent PING ACK response");
+				}
+				break;
+
+/* ==================================================== */
+/* 				     GOAWAY Frame 						*/
+/* ==================================================== */
+			case HTTP2FrameType.GOAWAY:
+				logDebug("Received GOAWAY Frame. Closing connection");
+
+				stream.state = HTTP2StreamState.CLOSED;
+				closeStream(context.multiplexer, stream.streamId);
+				close = true;
+
+				break;
+
+/* ==================================================== */
+/* 				     WINDOW_UPDATE Frame 				*/
+/* ==================================================== */
+			case HTTP2FrameType.WINDOW_UPDATE:
+				// can be received on closed streams (in case of pending data)
+				enforceHTTP2(stream.state != HTTP2StreamState.IDLE || !valid ||
+						stream.streamId == 0, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
+
+				auto inc = fromBytes(payload.data, 4);
+				uint maxinc = 1 << 31;
+				enforceHTTP2(inc > 0, "Invalid WINDOW_UPDATE increment", HTTP2Error.PROTOCOL_ERROR);
+
+				// connection-based control window must be updated
+				auto cw = connectionWindow(context.multiplexer);
+				enforceHTTP2(cw + inc < maxinc, "Reached maximum WINDOW size",
+						HTTP2Error.FLOW_CONTROL_ERROR);
+				updateConnectionWindow(context.multiplexer, cw + inc);
+
+				// per-stream control window must be updated (together with cw)
+				if(stream.streamId > 0) {
+					auto scw = streamConnectionWindow(context.multiplexer, stream.streamId);
+					enforceHTTP2(scw + inc < maxinc, "Reached maximum WINDOW size",
+							HTTP2Error.FLOW_CONTROL_ERROR);
+
+					updateStreamConnectionWindow(context.multiplexer, stream.streamId, scw + inc);
+				}
+
+				// notify waiting DATA tasks if needed
+				if(checkCondition(context.multiplexer, stream.streamId)) {
+					logDebug("Notifying stopped tasks");
+					notifyCondition(context.multiplexer);
+					yield();
+				}
+				break;
+
+/* ==================================================== */
+/* 				     CONTINUATION Frame
+/* ==================================================== */
+			case HTTP2FrameType.CONTINUATION:
+				// must be received immediately after a HEADERS Frame or a
+				// CONTINUATION Frame
+				enforceHTTP2(stream.state != HTTP2StreamState.IDLE, "Invalid state",
+						HTTP2Error.PROTOCOL_ERROR);
+
+				// add the received block to buffer
+				stream.putHeaderBlock(payload.data);
+
+				// process header block fragment in payload
+				if(endHeaders) {
+					logDebug("Received full HEADERS block");
+					handleHTTP2HeadersFrame(stream, connection, context, alloc);
+				} else {
+					logDebug("Incomplete HEADERS block, waiting for CONTINUATION frame.");
+					handleFrameAlloc(stream, connection, context, alloc);
+				}
+				break;
+		}
+
+/* ==================================================== */
+/* 				  `h2c`: First Response
+/* ==================================================== */
+		static if(!is(ConnectionStream : TLSStream)) {
+
+			if (context.resFrame) {
+				auto l = context.resFrame.takeExactly(3).fromBytes(3) + 9;
+
+				if(l < context.settings.maxFrameSize)
+				{
+					auto isEndStream = (context.resFrame.length > l) ? 0x0 : 0x1;
+
+					context.resFrame[4] += 0x4 + isEndStream;
+
+					try {
+						stream.write(context.resFrame[0..l]);
+					} catch (Exception e) {
+						logWarn("Unable to write HEADERS Frame to stream");
+					}
+
+				} else {
+					// TODO CONTINUATION frames
+					assert(false);
+				}
+
+
+				auto resBody = context.resFrame[l..$];
+				alloc.dispose(context.resFrame);
+
+				// send DATA (body) if present
+				// since the first response is part of HTTP/2 initialization,
+				// this task is NOT executed asynchronously (for now) TODO
+				if(resBody.length > 0) {
+
+					auto dataFrame = AllocAppender!(ubyte[])(alloc);
+
+					// create DATA Frame with END_STREAM (0x1) flag
+					if(resBody.length > uint.max) assert(false, "TODO");
+
+					// create DATA frame header
+					dataFrame.createHTTP2FrameHeader(cast(uint)resBody.length, HTTP2FrameType.DATA, 0x1, 1);
+
+					// append the DATA body
+					dataFrame.put(resBody);
+
+					// try writing data
+					try {
+						stream.write(dataFrame.data);
+					} catch(Exception e) {
+						logWarn("Unable to write DATA Frame to stream.");
+					}
+
+					logTrace("Sent DATA frame on streamID %s", stream.streamId);
+
+				}
+
+			}
+		}
+
+		closeStream(context.multiplexer, stream.streamId);
+
+
+	} catch(HTTP2Exception e) {
+		ubyte[GOAWAYFrameLength] f;
+		f.buildGOAWAYFrame(stream.streamId, e.code);
+		stream.write(f);
+		logWarn("%s: %s", "Sent GOAWAY Frame", e.message);
+		stream.state = HTTP2StreamState.CLOSED;
+		return true;
+	} catch(HPACKException e) {
+		ubyte[GOAWAYFrameLength] f;
+		f.buildGOAWAYFrame(stream.streamId, HTTP2Error.COMPRESSION_ERROR);
+		stream.write(f);
+		stream.state = HTTP2StreamState.CLOSED;
+		logWarn("%s: %s", "Sent GOAWAY Frame", e.message);
+		return true;
+
+	} catch(Exception e) {
+		logWarn(e.msg);
+	}
+
+	return close;
+}
+/// process an HEADERS frame
+void handleHTTP2HeadersFrame(Stream)(ref Stream stream, TCPConnection connection,
+		HTTP2ServerContext context,  IAllocator alloc)
+{
+	// AllocAppender cannot be used here (TODO discuss)
+	auto hdec = appender!(HTTP2HeaderTableField[])();
+
+	// decode headers
+	decodeHPACK(cast(immutable(ubyte)[])stream.headerBlock, hdec, context.table, alloc, context.settings.headerTableSize);
+
+	// insert data in table
+	hdec.data.each!((h) { if(h.index) context.table.insert(h); });
+
+	// write a response (HEADERS + DATA according to request method)
+	handleHTTP2Request(stream, connection, context, hdec.data, context.table, alloc);
+
+	// clean the header block buffer
+	stream.resetHeaderBlock();
+}
+
+/// handle SETTINGS frame exchange
+void handleHTTP2SettingsFrame(Stream)(ref Stream stream, TCPConnection connection, ubyte[] data, HTTP2FrameHeader header, HTTP2ServerContext context) @safe
+{
+	// parse settings payload
+	context.settings.unpackSettings(data);
+
+	// update the connection window and notify waiting workers
+	if(stream.streamId == 0) updateConnectionWindow(context.multiplexer, context.settings.initialWindowSize);
+	updateStreamConnectionWindow(context.multiplexer, stream.streamId, context.settings.initialWindowSize);
+
+	// notify waiting threads if needed
+	if(checkCondition(context.multiplexer, stream.streamId)) {
+		logTrace("Notifying stopped tasks");
+		notifyCondition(context.multiplexer);
+		//yield();
+	}
+
+	// acknowledge settings with SETTINGS ACK Frame
+	FixedAppender!(ubyte[], 9) ackReply;
+	ackReply.createHTTP2FrameHeader(0, header.type, 0x1, header.streamId);
+
+	// new connection: must send a SETTINGS Frame as preface
+	if(isConnectionPreface(context.multiplexer)) sendHTTP2SettingsFrame(stream, context);
+
+	// write SETTINGS ACK
+	stream.write(ackReply.data);
+	logDebug("Sent SETTINGS ACK");
+}
+
+/// send a SETTINGS Frame
+void sendHTTP2SettingsFrame(Stream)(ref Stream stream, HTTP2ServerContext context) @safe
+{
+	FixedAppender!(ubyte[], HTTP2HeaderLength+36) settingDst;
+
+	settingDst.createHTTP2FrameHeader(36, HTTP2FrameType.SETTINGS, 0x0, 0);
+	settingDst.serializeSettings(context.settings);
+	stream.write(settingDst.data);
+
+	logDebug("Sent SETTINGS Frame");
+}
+
+enum HTTP2StreamState {
+	IDLE,
+	RESERVED_LOCAL,
+	RESERVED_REMOTE,
+	OPEN,
+	HALF_CLOSED_LOCAL,
+	HALF_CLOSED_REMOTE,
+	CLOSED
+}
+
+/** Represent a HTTP/2 Stream
+  * The underlying connection can be TCPConnection or TLSStream
+  * TODO: stream dependency, proper handling of stream IDs
+  * approach: mantain a union of IDs so that only correct streams are initialized
+*/
+struct HTTP2ConnectionStream(CS)
+{
+	static assert(isConnectionStream!CS || is(CS : TLSStream) || isOutputStream!Stream);
+
+	private {
+		enum Parse { HEADER, PAYLOAD };
+		CS m_conn;
+		uint m_streamId;
+		Parse toParse = Parse.HEADER;
+		HTTP2StreamState m_state;
+		AllocAppender!(ubyte[]) m_headerBlock;
+
+		// Stream dependency TODO
+		HTTP2FrameStreamDependency m_dependency;
+	}
+
+	// embed underlying connection
+	alias m_conn this;
+
+	this(CS)(ref CS conn, uint sid, IAllocator alloc) @safe
+	{
+		m_conn = conn;
+		m_streamId = sid;
+		m_state = HTTP2StreamState.IDLE;
+		m_headerBlock = AllocAppender!(ubyte[])(alloc);
+	}
+
+	this(CS)(ref CS conn, IAllocator alloc) @safe
+	{
+		this(conn, 0, alloc);
+	}
+
+	@property CS connection() @safe { return m_conn; }
+
+	@property HTTP2StreamState state() @safe @nogc { return m_state; }
+
+	@property bool canRead() @safe @nogc
+	{
+		return (m_state == HTTP2StreamState.OPEN || m_state == HTTP2StreamState.IDLE);
+	}
+
+	/// set state according to Stream lifecycle (RFC 7540 section 5.1)
+	@property void state(HTTP2StreamState st) @safe
+	{
+		switch(st) {
+			// allowed: IDLE -> OPEN
+			//          OPEN -> OPEN
+			case HTTP2StreamState.OPEN:
+				if(m_state == HTTP2StreamState.IDLE ||
+						m_state == HTTP2StreamState.OPEN)
+					m_state = st;
+				else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
+				break;
+
+			// allowed: OPEN -> HCLOCAL
+			// 			RESERVED_REMOTE -> HCLOCAL
+			// 			HCLOCAL -> HCLOCAL
+			case HTTP2StreamState.HALF_CLOSED_LOCAL:
+				if(m_state == HTTP2StreamState.OPEN ||
+						m_state == HTTP2StreamState.RESERVED_REMOTE ||
+						m_state == HTTP2StreamState.HALF_CLOSED_LOCAL)
+					m_state = st;
+				else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
+				break;
+
+			// allowed: OPEN -> HCREMOTE
+			// 			RESERVED_LOCAL -> HCREMOTE
+			// 			HCREMOTE -> HCREMOTE
+			case HTTP2StreamState.HALF_CLOSED_REMOTE:
+				if(m_state == HTTP2StreamState.OPEN ||
+						m_state == HTTP2StreamState.RESERVED_LOCAL ||
+						m_state == HTTP2StreamState.HALF_CLOSED_REMOTE)
+					m_state = st;
+				else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
+				break;
+
+			// allowed: all transitions to CLOSED
+			//	(RST_STREAM, GOAWAY permit this)
+			case HTTP2StreamState.CLOSED:
+				m_state = st;
+				break;
+
+			// specific to PUSH_PROMISE Frames
+			case HTTP2StreamState.RESERVED_LOCAL:
+			case HTTP2StreamState.RESERVED_REMOTE:
+				if(m_state == HTTP2StreamState.IDLE) m_state = st;
+				else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
+				break;
+
+			default:
+				enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
+ 		}
+		logTrace("Stream: %d state: %s", m_streamId, st);
+	}
+
+	@property uint streamId() @safe @nogc { return m_streamId; }
+
+	@property void streamId(uint sid) @safe @nogc { m_streamId = sid; }
+
+	@property HTTP2FrameStreamDependency dependency() @safe @nogc { return m_dependency; }
+
+	@property ubyte[] headerBlock() @safe
+	{
+		assert(!m_headerBlock.data.empty, "No data in header block buffer");
+		return m_headerBlock.data;
+	}
+
+	@property bool needsContinuation() @safe
+	{
+		return !m_headerBlock.data.empty;
+	}
+
+	/// reads from stream a frame header
+	uint readHeader(R)(ref R dst) @safe
+	{
+		assert(toParse == Parse.HEADER);
+
+		ubyte[HTTP2HeaderLength] buf; // should always be 9
+
+		m_conn.read(buf);
+		dst.put(buf);
+
+		// length of payload
+		auto len = dst.data[0..3].fromBytes(3);
+		if(len > 0) toParse = Parse.PAYLOAD;
+
+		return len;
+	}
+
+	/// reads from stream a frame payload
+	void readPayload(R)(ref R dst, int len) @safe
+	{
+		assert(toParse == Parse.PAYLOAD);
+		toParse = Parse.HEADER;
+
+		ubyte[8] buf = void;
+
+		/// perform multiple reads until payload is over (@nogc compatibility)
+		while(len > 0) {
+			auto end = (len < buf.length) ? len : buf.length;
+			len -= m_conn.read(buf[0..end], IOMode.all);
+			dst.put(buf[0..end]);
+		}
+	}
+
+	/// save a HEADERS / CONTINUATION block for processing
+	void putHeaderBlock(T)(T src) @safe
+		if(isInputRange!T && is(ElementType!T : ubyte))
+	{
+		m_headerBlock.put(src);
+	}
+
+	void resetHeaderBlock() @trusted
+	{
+		m_headerBlock.reset(AppenderResetMode.freeData);
+	}
+
+	void finalize() @safe { }
+}
+
+unittest {
+	import vibe.core.core : runApplication;
+	// empty handler, just to test if protocol switching works
+	void handleReq(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	@safe {
+		if (req.path == "/")
+			res.writeBody("Hello, World! This response is sent through HTTP/2");
+	}
+
+	auto settings = new HTTPServerSettings();
+	settings.port = 8090;
+	settings.bindAddresses = ["localhost"];
+
+	listenHTTP(settings, &handleReq);
+	//runApplication();
+}
+
+unittest {
+	import vibe.core.core : runApplication;
+
+	void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	@safe {
+		if (req.path == "/")
+			res.writeBody("Hello, World! This response is sent through HTTP/2\n");
+	}
+
+
+	auto settings = new HTTPServerSettings;
+	settings.port = 8091;
+	settings.bindAddresses = ["127.0.0.1", "192.168.1.131"];
+	settings.tlsContext = createTLSContext(TLSContextKind.server);
+	settings.tlsContext.useCertificateChainFile("tests/server.crt");
+	settings.tlsContext.usePrivateKeyFile("tests/server.key");
+
+	// set alpn callback to support HTTP/2
+	// should accept the 'h2' protocol request
+	settings.tlsContext.alpnCallback(http2Callback);
+
+	// dummy, just for testing
+	listenHTTP(settings, &handleRequest);
+	//runApplication();
+}
+

+ 382 - 0
vtest/source/vibe/http/internal/http2/settings.d

@@ -0,0 +1,382 @@
+module vibe.http.internal.http2.settings;
+
+import vibe.http.internal.http2.multiplexing;
+import vibe.http.internal.http2.frame;
+import vibe.http.internal.http2.hpack.tables;
+import vibe.http.internal.http2.error;
+
+import vibe.http.server;
+import vibe.core.log;
+import vibe.core.net;
+import vibe.core.task;
+import vibe.internal.freelistref;
+
+import std.range;
+import std.base64;
+import std.traits;
+import std.bitmanip; // read from ubyte (decoding)
+import std.typecons;
+import std.conv : to;
+import std.exception : enforce;
+import std.algorithm : canFind; // alpn callback
+import std.variant : Algebraic;
+
+/*
+ *  6.5.1.  SETTINGS Format
+ *
+ *   The payload of a SETTINGS frame consists of zero or more parameters,
+ *   each consisting of an unsigned 16-bit setting identifier and an
+ *   unsigned 32-bit value.
+ *
+ *   +-------------------------------+
+ *   |	   IDentifier (16)		 |
+ *   +-------------------------------+-------------------------------+
+ *   |						Value (32)							 |
+ *   +---------------------------------------------------------------+
+ *						Figure 10: Setting Format
+ *
+ *   6.5.2.  Defined SETTINGS Parameters
+ *
+ *   The following parameters are defined:
+ *
+ *   SETTINGS_HEADER_TABLE_SIZE (0x1):  Allows the sender to inform the
+ *	 remote endpoint of the maximum size of the header compression
+ *	 table used to decode header blocks, in octets.  The encoder can
+ *	 select any size equal to or less than this value by using
+ *	 signaling specific to the header compression format inside a
+ *	 header block (see [COMPRESSION]).  The initial value is 4,096
+ *	 octets.
+ *
+ *   SETTINGS_ENABLE_PUSH (0x2):  This setting can be used to disable
+ *	  server push (Section 8.2).  An endpoint MUST NOT send a
+ *	  PUSH_PROMISE frame if it receives this parameter set to a value of
+ *	  0.  An endpoint that has both set this parameter to 0 and had it
+ *	  acknowledged MUST treat the receipt of a PUSH_PROMISE frame as a
+ *	  connection error (Section 5.4.1) of type PROTOCOL_ERROR.
+ *
+ *	  The initial value is 1, which indicates that server push is
+ *	  permitted.  Any value other than 0 or 1 MUST be treated as a
+ *	  connection error (Section 5.4.1) of type PROTOCOL_ERROR.
+ *
+ *	SETTINGS_MAX_CONCURRENT_STREAMS (0x3):  Indicates the maximum number
+ *	  of concurrent streams that the sender will allow.  This limit is
+ *	  directional: it applies to the number of streams that the sender
+ *	  permits the receiver to create.  Initially, there is no limit to
+ *	  this value.  It is recommended that this value be no smaller than
+ *	  100, so as to not unnecessarily limit parallelism.
+ *
+ *	  A value of 0 for SETTINGS_MAX_CONCURRENT_STREAMS SHOULD NOT be
+ *	  treated as special by endpoints.  A zero value does prevent the
+ *	  creation of new streams; however, this can also happen for any
+ *	  limit that is exhausted with active streams.  Servers SHOULD only
+ *	  set a zero value for short durations; if a server does not wish to
+ *	  accept requests, closing the connection is more appropriate.
+ *
+ *	SETTINGS_INITIAL_WINDOW_SIZE (0x4):  Indicates the sender's initial
+ *	   window size (in octets) for stream-level flow control.  The
+ *	   initial value is 2^16-1 (65,535) octets.
+ *
+ *	   This setting affects the window size of all streams (see
+ *	   Section 6.9.2).
+ *
+ *	   Values above the maximum flow-control window size of 2^31-1 MUST
+ *	   be treated as a connection error (Section 5.4.1) of type
+ *	   FLOW_CONTROL_ERROR.
+ *
+ *	SETTINGS_MAX_FRAME_SIZE (0x5):  Indicates the size of the largest
+ *	   frame payload that the sender is willing to receive, in octets.
+ *
+ *	   The initial value is 2^14 (16,384) octets.  The value advertised
+ *	   by an endpoint MUST be between this initial value and the maximum
+ *	   allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
+ *	   Values outside this range MUST be treated as a connection error
+ *	   (Section 5.4.1) of type PROTOCOL_ERROR.
+ *
+ *	SETTINGS_MAX_HEADER_LIST_SIZE (0x6):  This advisory setting informs a
+ *	   peer of the maximum size of header list that the sender is
+ *	   prepared to accept, in octets.  The value is based on the
+ *	   uncompressed size of header fields, including the length of the
+ *	   name and value in octets plus an overhead of 32 octets for each
+ *	   header field.
+ *
+ *	   For any given request, a lower limit than what is advertised MAY
+ *	   be enforced.  The initial value of this setting is unlimited.
+ *
+ *   An endpoint that receives a SETTINGS frame with any unknown or
+ *   unsupported identifier MUST ignore that setting.
+*/
+//version = VibeForceALPN;
+
+alias HTTP2SettingID = ushort;
+alias HTTP2SettingValue = uint;
+
+// useful for bound checking
+const HTTP2SettingID minID = 0x1;
+const HTTP2SettingID maxID = 0x6;
+
+enum  HTTP2SettingsParameter {
+	headerTableSize				 = 0x1,
+	enablePush					  = 0x2,
+	maxConcurrentStreams			= 0x3,
+	initialWindowSize			   = 0x4,
+	maxFrameSize					= 0x5,
+	maxHeaderListSize			   = 0x6
+}
+
+// UDAs
+struct HTTP2Setting {
+	HTTP2SettingID id;
+	string name;
+}
+
+// UDAs
+HTTP2Setting http2Setting(HTTP2SettingID id, string name) {
+	if (!__ctfe) assert(false, "May only be used as a UDA");
+	return HTTP2Setting(id, name);
+}
+
+struct HTTP2Settings {
+
+	// no limit specified in the RFC
+	@http2Setting(0x1, "SETTINGS_HEADER_TABLE_SIZE")
+	HTTP2SettingValue headerTableSize = DEFAULT_DYNAMIC_TABLE_SIZE;
+
+	// TODO {0,1} otherwise CONNECTION_ERROR
+	@http2Setting(0x2, "SETTINGS_ENABLE_PUSH")
+	HTTP2SettingValue enablePush = 1;
+
+	/* set to the max value (UNLIMITED)
+	 * TODO manage connection with value == 0
+	 * might be closed as soon as possible
+	 */
+	@http2Setting(0x3, "SETTINGS_MAX_CONCURRENT_STREAMS")
+	//HTTP2SettingValue maxConcurrentStreams = HTTP2SettingValue.max; // lowered
+	//to 2^16
+	HTTP2SettingValue maxConcurrentStreams = 65536;
+
+	// TODO FLOW_CONTROL_ERRROR on values > 2^31-1
+	@http2Setting(0x4, "SETTINGS_INITIAL_WINDOW_SIZE")
+	HTTP2SettingValue initialWindowSize = 65535;
+
+	// TODO PROTOCOL_ERROR on values > 2^24-1
+	@http2Setting(0x5, "SETTINGS_MAX_FRAME_SIZE")
+	HTTP2SettingValue maxFrameSize = 16384;
+
+	// set to the max value (UNLIMITED)
+	@http2Setting(0x6, "SETTINGS_MAX_HEADER_LIST_SIZE")
+	HTTP2SettingValue maxHeaderListSize = HTTP2SettingValue.max;
+
+	/**
+	 * Use Decoder to decode a string and set the corresponding settings
+	 * The decoder must follow the base64url encoding
+	 * `bool` since the handler must ignore the Upgrade request
+	 * if the settings cannot be decoded
+	 */
+	bool decode(alias Decoder)(string encodedSettings) @safe
+		if (isInstanceOf!(Base64Impl, Decoder))
+	{
+		ubyte[] uset;
+		try {
+			// the Base64URL decoder throws a Base64exception if it fails
+			uset = Decoder.decode(encodedSettings);
+			enforce!Base64Exception(uset.length % 6 == 0, "Invalid SETTINGS payload length");
+		} catch (Base64Exception e) {
+			logDiagnostic("Failed to decode SETTINGS payload: " ~ e.msg);
+			return false;
+		}
+
+		// set values
+		while(!uset.empty) m_set(uset.read!HTTP2SettingID, uset.read!HTTP2SettingValue);
+		return true;
+	}
+
+	/*
+	 * Set parameter 'id' to 'value'
+	 * private overload for decoded parameters assignment
+	 */
+	void set(HTTP2SettingID id)(HTTP2SettingValue value) @safe
+		if(id <= maxID && id >= minID)
+	{
+		m_set(id,value);
+	}
+
+	private void m_set(HTTP2SettingID id, HTTP2SettingValue value) @safe
+	{
+		// must use labeled break w. static foreach
+		assign: switch(id) {
+			default: logWarn("Unsupported SETTINGS code:" ~ to!string(id)); return;
+			static foreach(c; __traits(allMembers, HTTP2SettingsParameter)) {
+				case __traits(getMember, HTTP2SettingsParameter, c):
+					__traits(getMember, this, c) = value;
+					break assign;
+			}
+		}
+
+	}
+
+}
+
+void serializeSettings(R)(ref R dst, HTTP2Settings settings) @safe @nogc
+{
+	static foreach(s; __traits(allMembers, HTTP2Settings)) {
+		static if(is(typeof(__traits(getMember, HTTP2Settings, s)) == HTTP2SettingValue)) {
+			mixin("dst.putBytes!2((getUDAs!(settings."~s~",HTTP2Setting)[0]).id);");
+			mixin("dst.putBytes!4(settings."~s~");");
+		}
+	}
+}
+
+void unpackSettings(R)(ref HTTP2Settings settings, R src) @safe
+{
+	while(!src.empty) {
+		auto id = src.takeExactly(2).fromBytes(2);
+		src.popFrontN(2);
+
+		// invalid IDs: ignore setting
+		if(!(id >= minID && id <= maxID)) {
+			src.popFrontN(4);
+			continue;
+		}
+
+		static foreach(s; __traits(allMembers, HTTP2Settings)) {
+			static if(is(typeof(__traits(getMember, HTTP2Settings, s)) == HTTP2SettingValue)) {
+				mixin("if(id == ((getUDAs!(settings."~s~",HTTP2Setting)[0]).id)) {
+							settings."~s~" = src.takeExactly(4).fromBytes(4);
+							src.popFrontN(4);
+						}");
+
+			}
+		}
+	}
+
+	enforceHTTP2(settings.enablePush == 0 || settings.enablePush == 1,
+			"Invalid value for ENABLE_PUSH setting.", HTTP2Error.PROTOCOL_ERROR);
+
+	enforceHTTP2(settings.initialWindowSize < (1 << 31),
+			"Invalid value for INITIAL_WINDOW_SIZE setting.", HTTP2Error.FLOW_CONTROL_ERROR);
+
+	enforceHTTP2(settings.maxFrameSize >= (1 << 14) && settings.maxFrameSize < (1 << 24),
+			"Invalid value for MAX_FRAME_SIZE setting.", HTTP2Error.FLOW_CONTROL_ERROR);
+}
+
+unittest {
+
+	HTTP2Settings settings;
+
+	// retrieve a value
+	assert(settings.headerTableSize == 4096);
+
+	//set a SETTINGS value using the enum table
+	settings.set!(HTTP2SettingsParameter.headerTableSize)(2048);
+	assert(settings.headerTableSize == 2048);
+
+	//set a SETTINGS value using the code directly
+	settings.set!0x4(1024);
+	assert(settings.initialWindowSize == 1024);
+
+	// SHOULD NOT COMPILE
+	//settings.set!0x7(1);
+
+	// get a HTTP2Setting struct containing the code and the parameter name
+	import std.traits : getUDAs;
+	assert(getUDAs!(settings.headerTableSize, HTTP2Setting)[0] == HTTP2Setting(0x1,
+				"SETTINGS_HEADER_TABLE_SIZE"));
+
+	// test decoding from base64url
+	// h2settings contains:
+	// 0x2 -> 0
+	// 0x3 -> 100
+	// 0x4 -> 1073741824
+	string h2settings = "AAMAAABkAARAAAAAAAIAAAAA";
+	assert(settings.decode!Base64URL(h2settings));
+
+	assert(settings.enablePush == 0);
+	assert(settings.maxConcurrentStreams == 100);
+	assert(settings.initialWindowSize == 1073741824);
+
+	// should throw a Base64Exception error (caught) and a logWarn
+	assert(!settings.decode!Base64URL("a|b+*-c"));
+}
+
+/** Context is initialized on each new connection
+  * it MUST remain consistent between streams of the same connection
+  * Contains HTTP2Settings negotiated during handshake
+  * TODO set of USED streams for proper HTTP2ConnectionStream initialization
+*/
+final class HTTP2ServerContext
+{
+	private {
+		HTTPServerContext m_context;
+		Nullable!HTTP2Settings m_settings;
+		uint m_sid = 0;
+		FreeListRef!IndexingTable m_table;
+		FreeListRef!HTTP2Multiplexer m_multiplexer;
+		bool m_initializedT = false;
+		bool m_initializedM = false;
+
+	}
+
+	alias m_context this;
+
+	// used to mantain the first request in case of `h2c` protocol switching
+	ubyte[] resFrame = void;
+
+	this(HTTPServerContext ctx, HTTP2Settings settings) @safe
+	{
+		m_context = ctx;
+		m_settings = settings;
+	}
+
+	this(HTTPServerContext ctx) @safe
+	{
+		m_context = ctx;
+	}
+
+	@property auto ref table() @safe { return m_table.get; }
+
+	@property bool hasTable() @safe { return m_initializedT; }
+
+	@property void table(T)(T table) @safe
+		if(is(T == typeof(m_table)))
+	{
+		assert(!m_initializedT);
+		m_table = table;
+		m_initializedT = true;
+	}
+
+	@property auto ref multiplexer() @safe { return m_multiplexer.get; }
+
+	@property bool hasMultiplexer() @safe { return m_initializedM; }
+
+	@property void multiplexer(T)(T multiplexer) @safe
+		if(is(T == typeof(m_multiplexer)))
+	{
+		assert(!m_initializedM);
+		m_multiplexer = multiplexer;
+		m_initializedM = true;
+	}
+
+	@property HTTPServerContext h1context() @safe @nogc { return m_context; }
+
+	@property uint next_sid() @safe @nogc { return m_sid; }
+
+	@property void next_sid(uint sid) @safe @nogc { m_sid = sid; }
+
+	@property ref HTTP2Settings settings() @safe @nogc
+	{
+		assert(!m_settings.isNull);
+		return m_settings.get;
+	}
+
+	@property void settings(ref HTTP2Settings settings) @safe
+	{
+		assert(m_settings.isNull);
+		m_settings = settings;
+		() @trusted {
+			if (settings.headerTableSize != 4096) {
+				table.updateSize(settings.headerTableSize);
+			}
+		} ();
+	}
+}
+

+ 38 - 0
vtest/source/vibe/http/internal/utils.d

@@ -0,0 +1,38 @@
+module vibe.http.internal.utils;
+
+import core.time : nsecs, seconds;
+import std.datetime : SysTime;
+import vibe.container.internal.appender : FixedAppender;
+import vibe.inet.message : writeRFC822DateTimeString;
+
+
+string formatRFC822DateAlloc(SysTime time)
+@safe {
+	static LAST = CacheTime(SysTime.min());
+
+	if (time > LAST.nextUpdate) {
+		auto app = new FixedAppender!(string, 32);
+		writeRFC822DateTimeString(app, time);
+		LAST.update(time);
+		LAST.cachedDate = () @trusted { return app.data; } ();
+		return () @trusted { return app.data; } ();
+	} else
+		return LAST.cachedDate;
+}
+
+private struct CacheTime
+{
+	string cachedDate;
+	SysTime nextUpdate;
+
+	this(SysTime nextUpdate) @safe @nogc pure nothrow
+	{
+		this.nextUpdate = nextUpdate;
+	}
+
+	void update(SysTime time) @safe
+	{
+		this.nextUpdate = time + 1.seconds;
+		this.nextUpdate.fracSecs = nsecs(0);
+	}
+}

+ 259 - 0
vtest/source/vibe/http/log.d

@@ -0,0 +1,259 @@
+/**
+	A HTTP 1.1/1.0 server implementation.
+
+	Copyright: © 2012-2013 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig, Jan Krüger
+*/
+module vibe.http.log;
+
+import vibe.core.file;
+import vibe.core.log;
+import vibe.core.sync : InterruptibleTaskMutex, performLocked;
+import vibe.http.server;
+import vibe.container.internal.appender : FixedAppender;
+
+import std.array;
+import std.conv;
+import std.exception;
+import std.string;
+
+
+class HTTPLogger {
+	@safe:
+
+	private {
+		string m_format;
+		const(HTTPServerSettings) m_settings;
+		InterruptibleTaskMutex m_mutex;
+		Appender!(char[]) m_lineAppender;
+	}
+
+	this(const HTTPServerSettings settings, string format)
+	{
+		m_format = format;
+		m_settings = settings;
+		m_mutex = new InterruptibleTaskMutex;
+		m_lineAppender.reserve(2048);
+	}
+
+	void close() {}
+
+	final void log(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	{
+		m_mutex.performLocked!(() @safe {
+			m_lineAppender.clear();
+			formatApacheLog(m_lineAppender, m_format, req, res, m_settings);
+			writeLine(m_lineAppender.data);
+		});
+	}
+
+	protected abstract void writeLine(const(char)[] ln);
+}
+
+
+final class HTTPConsoleLogger : HTTPLogger {
+@safe:
+
+	this(HTTPServerSettings settings, string format)
+	{
+		super(settings, format);
+	}
+
+	protected override void writeLine(const(char)[] ln)
+	{
+		logInfo("%s", ln);
+	}
+}
+
+
+final class HTTPFileLogger : HTTPLogger {
+@safe:
+
+	private {
+		FileStream m_stream;
+	}
+
+	this(HTTPServerSettings settings, string format, string filename)
+	{
+		m_stream = openFile(filename, FileMode.append);
+		super(settings, format);
+	}
+
+	override void close()
+	{
+		m_stream.close();
+		m_stream = FileStream.init;
+	}
+
+	protected override void writeLine(const(char)[] ln)
+	{
+		assert(!!m_stream);
+		m_stream.write(ln);
+		m_stream.write("\n");
+		m_stream.flush();
+	}
+}
+
+void formatApacheLog(R)(ref R ln, string format, scope HTTPServerRequest req, scope HTTPServerResponse res, in HTTPServerSettings settings)
+@safe {
+	import std.format : formattedWrite;
+	enum State {Init, Directive, Status, Key, Command}
+
+	State state = State.Init;
+	bool conditional = false;
+	bool negate = false;
+	bool match = false;
+	string statusStr;
+	string key = "";
+	while( format.length > 0 ) {
+		final switch(state) {
+			case State.Init:
+				auto idx = format.indexOf('%');
+				if( idx < 0 ) {
+					ln.put( format );
+					format = "";
+				} else {
+					ln.put( format[0 .. idx] );
+					format = format[idx+1 .. $];
+
+					state = State.Directive;
+				}
+				break;
+			case State.Directive:
+				if( format[0] == '!' ) {
+					conditional = true;
+					negate = true;
+					format = format[1 .. $];
+					state = State.Status;
+				} else if( format[0] == '%' ) {
+					ln.put("%");
+					format = format[1 .. $];
+					state = State.Init;
+				} else if( format[0] == '{' ) {
+					format = format[1 .. $];
+					state = State.Key;
+				} else if( format[0] >= '0' && format[0] <= '9' ) {
+					conditional = true;
+					state = State.Status;
+				} else {
+					state = State.Command;
+				}
+				break;
+			case State.Status:
+				if( format[0] >= '0' && format[0] <= '9' ) {
+					statusStr ~= format[0];
+					format = format[1 .. $];
+				} else if( format[0] == ',' ) {
+					statusStr = "";
+					format = format[1 .. $];
+				} else if( format[0] == '{' ) {
+					format = format[1 .. $];
+					state = State.Key;
+				} else {
+					state = State.Command;
+				}
+				if (statusStr.length == 3 && !match) {
+					auto status = parse!int(statusStr);
+					match = status == res.statusCode;
+				}
+				break;
+			case State.Key:
+				auto idx = format.indexOf('}');
+				enforce(idx > -1, "Missing '}'");
+				key = format[0 .. idx];
+				format = format[idx+1 .. $];
+				state = State.Command;
+				break;
+			case State.Command:
+				if( conditional && negate == match ) {
+					ln.put('-');
+					format = format[1 .. $];
+					state = State.Init;
+					break;
+				}
+				switch(format[0]) {
+					case 'a': //Remote IP-address
+						ln.put(req.peer);
+						break;
+					//TODO case 'A': //Local IP-address
+					//case 'B': //Size of Response in bytes, excluding headers
+					case 'b': //same as 'B' but a '-' is written if no bytes where sent
+						if (!res.bytesWritten) ln.put('-');
+						else formattedWrite(() @trusted { return &ln; } (), "%s", res.bytesWritten);
+						break;
+					case 'C': //Cookie content {cookie}
+						import std.algorithm : joiner;
+						enforce(key != "", "cookie name missing");
+						auto values = req.cookies.getAll(key);
+						if (values.length) ln.formattedWrite("%s", values.joiner(";"));
+						else ln.put("-");
+						break;
+					case 'D': //The time taken to serve the request
+						auto d = res.timeFinalized - req.timeCreated;
+						formattedWrite(() @trusted { return &ln; } (), "%s", d.total!"msecs"());
+						break;
+					//case 'e': //Environment variable {variable}
+					//case 'f': //Filename
+					case 'h': //Remote host
+						ln.put(req.peer);
+						break;
+					case 'H': //The request protocol
+						ln.put("HTTP");
+						break;
+					case 'i': //Request header {header}
+						enforce(key != "", "header name missing");
+						if (auto pv = key in req.headers) ln.put(*pv);
+						else ln.put("-");
+						break;
+					case 'm': //Request method
+						ln.put(httpMethodString(req.method));
+						break;
+					case 'o': //Response header {header}
+						enforce(key != "", "header name missing");
+						if( auto pv = key in res.headers ) ln.put(*pv);
+						else ln.put("-");
+						break;
+					case 'p': //port
+						formattedWrite(() @trusted { return &ln; } (), "%s", settings.port);
+						break;
+					//case 'P': //Process ID
+					case 'q': //query string (with prepending '?')
+						ln.put("?");
+						ln.put(req.queryString);
+						break;
+					case 'r': //First line of Request
+						ln.put(httpMethodString(req.method));
+						ln.put(' ');
+						ln.put(req.requestURL);
+						ln.put(' ');
+						ln.put(getHTTPVersionString(req.httpVersion));
+						break;
+					case 's': //Status
+						formattedWrite(() @trusted { return &ln; } (), "%s", res.statusCode);
+						break;
+					case 't': //Time the request was received {format}
+						ln.put(req.timeCreated.toSimpleString());
+						break;
+					case 'T': //Time taken to server the request in seconds
+						auto d = res.timeFinalized - req.timeCreated;
+						formattedWrite(() @trusted { return &ln; } (), "%s", d.total!"seconds");
+						break;
+					case 'u': //Remote user
+						ln.put(req.username.length ? req.username : "-");
+						break;
+					case 'U': //The URL path without query string
+						ln.put(req.requestPath.toString());
+						break;
+					case 'v': //Server name
+						ln.put(req.host.length ? req.host : "-");
+						break;
+					default:
+						throw new Exception("Unknown directive '" ~ format[0] ~ "' in log format string");
+				}
+				state = State.Init;
+				format = format[1 .. $];
+				break;
+		}
+	}
+}

+ 300 - 0
vtest/source/vibe/http/proxy.d

@@ -0,0 +1,300 @@
+/**
+	HTTP (reverse) proxy implementation
+
+	Copyright: © 2012 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.http.proxy;
+
+import vibe.core.core : runTask;
+import vibe.core.log;
+import vibe.http.client;
+import vibe.http.server;
+import vibe.inet.message;
+import vibe.stream.operations;
+import vibe.internal.interfaceproxy : InterfaceProxy;
+
+import std.conv;
+import std.exception;
+
+
+/*
+	TODO:
+		- use a client pool
+		- implement a path based reverse proxy
+*/
+
+/**
+	Transparently forwards all requests to the proxy to another host.
+
+	The configurations set in 'settings' and 'proxy_settings' determines the exact
+	behavior.
+*/
+void listenHTTPProxy(HTTPServerSettings settings, HTTPProxySettings proxy_settings)
+{
+	// disable all advanced parsing in the server
+	settings.options = HTTPServerOption.none;
+	listenHTTP(settings, proxyRequest(proxy_settings));
+}
+
+/**
+	Transparently forwards all requests to the proxy to a destination_host.
+
+	You can use the hostName field in the 'settings' to combine multiple internal HTTP servers
+	into one public web server with multiple virtual hosts.
+*/
+void listenHTTPReverseProxy(HTTPServerSettings settings, string destination_host, ushort destination_port)
+{
+	URL url;
+	url.schema = "http";
+	url.host = destination_host;
+	url.port = destination_port;
+	auto proxy_settings = new HTTPProxySettings(ProxyMode.reverse);
+	proxy_settings.destination = url;
+	listenHTTPProxy(settings, proxy_settings);
+}
+
+/**
+	Transparently forwards all requests to the proxy to the requestURL of the request.
+*/
+void listenHTTPForwardProxy(HTTPServerSettings settings) {
+	auto proxy_settings = new HTTPProxySettings(ProxyMode.forward);
+	proxy_settings.handleConnectRequests = true;
+	listenHTTPProxy(settings, proxy_settings);
+}
+
+/**
+	Returns a HTTP request handler that forwards any request to the specified or requested host/port.
+*/
+HTTPServerRequestDelegateS proxyRequest(HTTPProxySettings settings)
+{
+	static immutable string[] non_forward_headers = ["Content-Length", "Transfer-Encoding", "Content-Encoding", "Connection"];
+	static InetHeaderMap non_forward_headers_map;
+	if (non_forward_headers_map.length == 0)
+		foreach (n; non_forward_headers)
+			non_forward_headers_map[n] = "";
+
+	void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	@safe {
+		auto url = settings.destination;
+
+		if (settings.proxyMode == ProxyMode.reverse) {
+			url.localURI = req.requestURL;
+		}
+		else {
+			url = URL(req.requestURL);
+		}
+
+		//handle connect tunnels
+		if (req.method == HTTPMethod.CONNECT) {
+			if (!settings.handleConnectRequests)
+			{
+				throw new HTTPStatusException(HTTPStatus.methodNotAllowed);
+			}
+
+			// CONNECT resources are of the form server:port and not
+			// schema://server:port, so they need some adjustment
+			// TODO: use a more efficient means to parse this
+			url = URL.parse("http://"~req.requestURL);
+
+			TCPConnection ccon;
+			try ccon = connectTCP(url.getFilteredHost, url.port);
+			catch (Exception e) {
+				throw new HTTPStatusException(HTTPStatus.badGateway, "Connection to upstream server failed: "~e.msg);
+			}
+
+			res.writeVoidBody();
+			auto scon = res.connectProxy();
+			assert (scon);
+
+			runTask(() nothrow {
+				try scon.pipe(ccon);
+				catch (Exception e) {
+					logException(e, "Failed to forward proxy data from server to client");
+					try scon.close();
+					catch (Exception e) logException(e, "Failed to close server connection after error");
+					try ccon.close();
+					catch (Exception e) logException(e, "Failed to close client connection after error");
+				}
+			});
+			ccon.pipe(scon);
+			return;
+		}
+
+		//handle protocol upgrades
+		auto pUpgrade = "Upgrade" in req.headers;
+		auto pConnection = "Connection" in req.headers;
+
+
+		import std.algorithm : splitter, canFind;
+		import vibe.internal.string : icmp2;
+		bool isUpgrade = pConnection && (*pConnection).splitter(',').canFind!(a => a.icmp2("upgrade"));
+
+		void setupClientRequest(scope HTTPClientRequest creq)
+		{
+			creq.method = req.method;
+			creq.headers = req.headers.dup;
+			creq.headers["Host"] = url.getFilteredHost;
+
+			//handle protocol upgrades
+			if (!isUpgrade) {
+				creq.headers["Connection"] = "keep-alive";
+			}
+			if (settings.avoidCompressedRequests && "Accept-Encoding" in creq.headers)
+				creq.headers.remove("Accept-Encoding");
+			if (auto pfh = "X-Forwarded-Host" !in creq.headers) creq.headers["X-Forwarded-Host"] = req.headers["Host"];
+			if (auto pfp = "X-Forwarded-Proto" !in creq.headers) creq.headers["X-Forwarded-Proto"] = req.tls ? "https" : "http";
+			if (auto pff = "X-Forwarded-For" in req.headers) creq.headers["X-Forwarded-For"] = *pff ~ ", " ~ req.peer;
+			else creq.headers["X-Forwarded-For"] = req.peer;
+			req.bodyReader.pipe(creq.bodyWriter);
+		}
+
+		void handleClientResponse(scope HTTPClientResponse cres)
+		{
+			// copy the response to the original requester
+			res.statusCode = cres.statusCode;
+
+			//handle protocol upgrades
+			if (cres.statusCode == HTTPStatus.switchingProtocols && isUpgrade) {
+				res.headers = cres.headers.dup;
+
+				auto scon = res.switchProtocol("");
+				auto ccon = cres.switchProtocol("");
+
+				runTask(() nothrow {
+					try ccon.pipe(scon);
+					catch (Exception e) {
+						logException(e, "Failed to forward proxy data from client to server");
+						try scon.close();
+						catch (Exception e) logException(e, "Failed to close server connection after error");
+						try ccon.close();
+						catch (Exception e) logException(e, "Failed to close client connection after error");
+					}
+				});
+
+				scon.pipe(ccon);
+				return;
+			}
+
+			// special case for empty response bodies
+			if ("Content-Length" !in cres.headers && "Transfer-Encoding" !in cres.headers || req.method == HTTPMethod.HEAD) {
+				foreach (key, ref value; cres.headers.byKeyValue)
+					if (icmp2(key, "Connection") != 0)
+						res.headers.addField(key,value);
+				res.writeVoidBody();
+				return;
+			}
+
+			// enforce compatibility with HTTP/1.0 clients that do not support chunked encoding
+			// (Squid and some other proxies)
+			if (res.httpVersion == HTTPVersion.HTTP_1_0 && ("Transfer-Encoding" in cres.headers || "Content-Length" !in cres.headers)) {
+				// copy all headers that may pass from upstream to client
+				foreach (n, ref v; cres.headers.byKeyValue)
+					if (n !in non_forward_headers_map)
+						res.headers.addField(n,v);
+
+				if ("Transfer-Encoding" in res.headers) res.headers.remove("Transfer-Encoding");
+				auto content = cres.bodyReader.readAll(1024*1024);
+				res.headers["Content-Length"] = to!string(content.length);
+				if (res.isHeadResponse) res.writeVoidBody();
+				else res.bodyWriter.write(content);
+				return;
+			}
+
+			// to perform a verbatim copy of the client response
+			if ("Content-Length" in cres.headers) {
+				if ("Content-Encoding" in res.headers) res.headers.remove("Content-Encoding");
+				foreach (key, ref value; cres.headers.byKeyValue)
+					if (icmp2(key, "Connection") != 0)
+						res.headers.addField(key,value);
+				auto size = cres.headers["Content-Length"].to!size_t();
+				if (res.isHeadResponse) res.writeVoidBody();
+				else cres.readRawBody((scope InterfaceProxy!InputStream reader) { res.writeRawBody(reader, size); });
+				assert(res.headerWritten);
+				return;
+			}
+
+			// fall back to a generic re-encoding of the response
+			// copy all headers that may pass from upstream to client
+			foreach (n, ref v; cres.headers.byKeyValue)
+				if (n !in non_forward_headers_map)
+					res.headers.addField(n,v);
+			if (res.isHeadResponse) res.writeVoidBody();
+			else cres.bodyReader.pipe(res.bodyWriter);
+		}
+
+		try requestHTTP(url, &setupClientRequest, &handleClientResponse);
+		catch (Exception e) {
+			throw new HTTPStatusException(HTTPStatus.badGateway, "Connection to upstream server failed: "~e.msg);
+		}
+	}
+
+	return &handleRequest;
+}
+
+/**
+	Returns a HTTP request handler that forwards any request to the specified host/port.
+*/
+HTTPServerRequestDelegateS reverseProxyRequest(string destination_host, ushort destination_port)
+{
+	URL url;
+	url.schema = "http";
+	url.host = destination_host;
+	url.port = destination_port;
+	auto settings = new HTTPProxySettings(ProxyMode.reverse);
+	settings.destination = url;
+	return proxyRequest(settings);
+}
+
+/// ditto
+HTTPServerRequestDelegateS reverseProxyRequest(URL destination)
+{
+	auto settings = new HTTPProxySettings(ProxyMode.reverse);
+	settings.destination = destination;
+	return proxyRequest(settings);
+}
+
+/**
+	Returns a HTTP request handler that forwards any request to the requested host/port.
+*/
+HTTPServerRequestDelegateS forwardProxyRequest() {
+    return proxyRequest(new HTTPProxySettings(ProxyMode.forward));
+}
+
+/**
+	Enum to represent the two modes a proxy can operate as.
+*/
+enum ProxyMode {forward, reverse}
+
+/**
+	Provides advanced configuration facilities for reverse proxy servers.
+*/
+final class HTTPProxySettings {
+	/// Scheduled for deprecation - use `destination.host` instead.
+	@property string destinationHost() const { return destination.host; }
+	/// ditto
+	@property void destinationHost(string host) { destination.host = host; }
+	/// Scheduled for deprecation - use `destination.port` instead.
+	@property ushort destinationPort() const { return destination.port; }
+	/// ditto
+	@property void destinationPort(ushort port) { destination.port = port; }
+
+	/// The destination URL to forward requests to
+	URL destination = URL("http", InetPath(""));
+	/// The mode of the proxy i.e forward, reverse
+	ProxyMode proxyMode;
+	/// Avoids compressed transfers between proxy and destination hosts
+	bool avoidCompressedRequests;
+	/// Handle CONNECT requests for creating a tunnel to the destination host
+	bool handleConnectRequests;
+
+	/// Empty default constructor for backwards compatibility - will be deprecated soon.
+	deprecated("Pass an explicit `ProxyMode` argument")
+	this() { proxyMode = ProxyMode.reverse; }
+	/// Explicitly sets the proxy mode.
+	this(ProxyMode mode) { proxyMode = mode; }
+}
+/// Compatibility alias
+deprecated("Use `HTTPProxySettings(ProxyMode.reverse)` instead.")
+alias HTTPReverseProxySettings = HTTPProxySettings;

+ 1431 - 0
vtest/source/vibe/http/router.d

@@ -0,0 +1,1431 @@
+/**
+	Pattern based URL router for HTTP request.
+
+	See `URLRouter` for more details.
+
+	Copyright: © 2012-2015 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.http.router;
+
+public import vibe.http.server;
+
+import vibe.core.log;
+
+import std.digest.murmurhash : MurmurHash3;
+import std.functional;
+
+
+/**
+	Routes HTTP requests based on the request method and URL.
+
+	Routes are matched using a special URL match string that supports two forms
+	of placeholders. See the sections below for more details.
+
+	Registered routes are matched according to the same sequence as initially
+	specified using `match`, `get`, `post` etc. Matching ends as soon as a route
+	handler writes a response using `res.writeBody()` or similar means. If no
+	route matches or if no route handler writes a response, the router will
+	simply not handle the request and the HTTP server will automatically
+	generate a 404 error.
+
+	Match_patterns:
+		Match patterns are character sequences that can optionally contain
+		placeholders or raw wildcards ("*"). Raw wild cards match any character
+		sequence, while placeholders match only sequences containing no slash
+		("/") characters.
+
+		Placeholders are started using a colon (":") and are directly followed
+		by their name. The first "/" character (or the end of the match string)
+		denotes the end of the placeholder name. The part of the string that
+		matches a placeholder will be stored in the `HTTPServerRequest.params`
+		map using the placeholder name as the key.
+
+		Match strings are subject to the following rules:
+		$(UL
+			$(LI A raw wildcard ("*") may only occur at the end of the match string)
+			$(LI At least one character must be placed between any two placeholders or wildcards)
+			$(LI The maximum allowed number of placeholders in a single match string is 64)
+		)
+
+	Match_String_Examples:
+		$(UL
+			$(LI `"/foo/bar"` matches only `"/foo/bar"` itself)
+			$(LI `"/foo/*"` matches `"/foo/"`, `"/foo/bar"`, `"/foo/bar/baz"` or _any other string beginning with `"/foo/"`)
+			$(LI `"/:x/"` matches `"/foo/"`, `"/bar/"` and similar strings (and stores `"foo"`/`"bar"` in `req.params["x"]`), but not `"/foo/bar/"`)
+			$(LI Matching partial path entries with wildcards is possible: `"/foo:x"` matches `"/foo"`, `"/foobar"`, but not `"/foo/bar"`)
+			$(LI Multiple placeholders and raw wildcards can be combined: `"/:x/:y/*"`)
+		)
+*/
+final class URLRouter : HTTPServerRequestHandler {
+	@safe:
+
+	private {
+		MatchTree!Route m_routes;
+		string m_prefix;
+		bool m_computeBasePath;
+	}
+
+	this(string prefix = null)
+	{
+		m_prefix = prefix;
+	}
+
+	/** Sets a common prefix for all registered routes.
+
+		All routes will implicitly have this prefix prepended before being
+		matched against incoming requests.
+	*/
+	@property string prefix() const { return m_prefix; }
+
+	/** Controls the computation of the "routerRootDir" parameter.
+
+		This parameter is available as `req.params["routerRootDir"]` and
+		contains the relative path to the base path of the router. The base
+		path is determined by the `prefix` property.
+
+		Note that this feature currently is requires dynamic memory allocations
+		and is opt-in for this reason.
+	*/
+	@property void enableRootDir(bool enable) { m_computeBasePath = enable; }
+
+	/// Returns a single route handle to conveniently register multiple methods.
+	URLRoute route(string path)
+	in { assert(path.length, "Cannot register null or empty path!"); }
+	do { return URLRoute(this, path); }
+
+	///
+	unittest {
+		void getFoo(scope HTTPServerRequest req, scope HTTPServerResponse res) { /* ... */ }
+		void postFoo(scope HTTPServerRequest req, scope HTTPServerResponse res) { /* ... */ }
+		void deleteFoo(scope HTTPServerRequest req, scope HTTPServerResponse res) { /* ... */ }
+
+		auto r = new URLRouter;
+
+		// using 'with' statement
+		with (r.route("/foo")) {
+			get(&getFoo);
+			post(&postFoo);
+			delete_(&deleteFoo);
+		}
+
+		// using method chaining
+		r.route("/foo")
+			.get(&getFoo)
+			.post(&postFoo)
+			.delete_(&deleteFoo);
+
+		// without using route()
+		r.get("/foo", &getFoo);
+		r.post("/foo", &postFoo);
+		r.delete_("/foo", &deleteFoo);
+	}
+
+	/// Adds a new route for requests matching the specified HTTP method and pattern.
+	URLRouter match(Handler)(HTTPMethod method, string path, Handler handler)
+		if (isValidHandler!Handler)
+	{
+		import vibe.core.path : InetPath, PosixPath;
+		import vibe.textfilter.urlencode : urlDecode;
+		import std.algorithm : count, map;
+		assert(path.length, "Cannot register null or empty path!");
+		assert(count(path, ':') <= maxRouteParameters, "Too many route parameters");
+		logDebug("add route %s %s", method, path);
+		// Perform URL-encoding on the path before adding it as a route.
+		string iPath = PosixPath(path)
+			.bySegment
+			.map!(s => InetPath.Segment(urlDecode(s.name), s.separator))
+			.InetPath
+			.toString;
+		m_routes.addTerminal(iPath, Route(method, iPath, handlerDelegate(handler)));
+		return this;
+	}
+
+	/// ditto
+	URLRouter match(HTTPMethod method, string path, HTTPServerRequestDelegate handler)
+	{
+		return match!HTTPServerRequestDelegate(method, path, handler);
+	}
+
+	/// Adds a new route for GET requests matching the specified pattern.
+	URLRouter get(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.GET, url_match, handler); }
+	/// ditto
+	URLRouter get(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.GET, url_match, handler); }
+
+	/// Adds a new route for POST requests matching the specified pattern.
+	URLRouter post(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.POST, url_match, handler); }
+	/// ditto
+	URLRouter post(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.POST, url_match, handler); }
+
+	/// Adds a new route for PUT requests matching the specified pattern.
+	URLRouter put(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.PUT, url_match, handler); }
+	/// ditto
+	URLRouter put(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.PUT, url_match, handler); }
+
+	/// Adds a new route for DELETE requests matching the specified pattern.
+	URLRouter delete_(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.DELETE, url_match, handler); }
+	/// ditto
+	URLRouter delete_(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.DELETE, url_match, handler); }
+
+	/// Adds a new route for PATCH requests matching the specified pattern.
+	URLRouter patch(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.PATCH, url_match, handler); }
+	/// ditto
+	URLRouter patch(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.PATCH, url_match, handler); }
+
+	/// Adds a new route for requests matching the specified pattern, regardless of their HTTP verb.
+	URLRouter any(Handler)(string url_match, Handler handler)
+	{
+		import std.traits;
+		static HTTPMethod[] all_methods = [EnumMembers!HTTPMethod];
+		foreach(immutable method; all_methods)
+			match(method, url_match, handler);
+
+		return this;
+	}
+	/// ditto
+	URLRouter any(string url_match, HTTPServerRequestDelegate handler) { return any!HTTPServerRequestDelegate(url_match, handler); }
+
+
+	/** Rebuilds the internal matching structures to account for newly added routes.
+
+		This should be used after a lot of routes have been added to the router, to
+		force eager computation of the match structures. The alternative is to
+		let the router lazily compute the structures when the first request happens,
+		which can delay this request.
+	*/
+	void rebuild()
+	{
+		m_routes.rebuildGraph();
+	}
+
+	/// Handles a HTTP request by dispatching it to the registered route handlers.
+	void handleRequest(HTTPServerRequest req, HTTPServerResponse res)
+	{
+		import vibe.textfilter.urlencode : urlDecode;
+
+		auto method = req.method;
+
+		string calcBasePath()
+		@safe {
+			import vibe.core.path : InetPath, relativeToWeb;
+			auto p = InetPath(prefix.length ? prefix : "/");
+			p.endsWithSlash = true;
+			return p.relativeToWeb(req.requestPath).toString();
+		}
+
+		string path;
+
+		// NOTE: Instead of failing, we just ignore requests with invalid path
+		//       segments (i.e. containing path separators) here. Any request
+		//       handlers later in the queue may still choose to process them
+		//       appropriately.
+		try path = req.requestPath.toString();
+		catch (Exception e) return;
+
+		if (path.length < m_prefix.length || path[0 .. m_prefix.length] != m_prefix) return;
+		path = path[m_prefix.length .. $];
+
+		while (true) {
+			bool done = m_routes.match(path, (ridx, scope values) @safe {
+				auto r = () @trusted { return &m_routes.getTerminalData(ridx); } ();
+				if (r.method != method) return false;
+
+				logDebugV("route match: %s -> %s %s %s", req.requestPath, r.method, r.pattern, values);
+				foreach (i, v; values) {
+					req.params[m_routes.getTerminalVarNames(ridx)[i]] = urlDecode(v);
+				}
+				if (m_computeBasePath) req.params["routerRootDir"] = calcBasePath();
+				r.cb(req, res);
+				return res.headerWritten;
+			});
+			if (done) return;
+
+			if (method == HTTPMethod.HEAD) method = HTTPMethod.GET;
+			else break;
+		}
+
+		logDebug("no route match: %s %s", req.method, req.requestURL);
+	}
+
+	/// Returns all registered routes as const AA
+	const(Route)[] getAllRoutes()
+	{
+		auto routes = new Route[m_routes.terminalCount];
+		foreach (i, ref r; routes)
+			r = m_routes.getTerminalData(i);
+		return routes;
+	}
+
+	template isValidHandler(Handler) {
+		@system {
+			alias USDel = void delegate(HTTPServerRequest, HTTPServerResponse) @system;
+			alias USFun = void function(HTTPServerRequest, HTTPServerResponse) @system;
+			alias USDelS = void delegate(scope HTTPServerRequest, scope HTTPServerResponse) @system;
+			alias USFunS = void function(scope HTTPServerRequest, scope HTTPServerResponse) @system;
+		}
+
+		static if (
+				is(Handler : HTTPServerRequestDelegate) ||
+				is(Handler : HTTPServerRequestFunction) ||
+				is(Handler : HTTPServerRequestHandler) ||
+				is(Handler : HTTPServerRequestDelegateS) ||
+				is(Handler : HTTPServerRequestFunctionS) ||
+				is(Handler : HTTPServerRequestHandlerS)
+			)
+		{
+			enum isValidHandler = true;
+		} else static if (
+				is(Handler : USDel) || is(Handler : USFun) ||
+				is(Handler : USDelS) || is(Handler : USFunS)
+			)
+		{
+			enum isValidHandler = true;
+		} else {
+			enum isValidHandler = false;
+		}
+	}
+
+	static void delegate(HTTPServerRequest, HTTPServerResponse) @safe handlerDelegate(Handler)(Handler handler)
+	{
+		import std.traits : isFunctionPointer;
+		static if (isFunctionPointer!Handler) return handlerDelegate(() @trusted { return toDelegate(handler); } ());
+		else static if (is(Handler == class) || is(Handler == interface)) return &handler.handleRequest;
+		else static if (__traits(compiles, () @safe { handler(HTTPServerRequest.init, HTTPServerResponse.init); } ())) return handler;
+		else return (req, res) @trusted { handler(req, res); };
+	}
+
+	unittest {
+		static assert(isValidHandler!HTTPServerRequestFunction);
+		static assert(isValidHandler!HTTPServerRequestDelegate);
+		static assert(isValidHandler!HTTPServerRequestHandler);
+		static assert(isValidHandler!HTTPServerRequestFunctionS);
+		static assert(isValidHandler!HTTPServerRequestDelegateS);
+		static assert(isValidHandler!HTTPServerRequestHandlerS);
+		static assert(isValidHandler!(void delegate(HTTPServerRequest req, HTTPServerResponse res) @system));
+		static assert(isValidHandler!(void function(HTTPServerRequest req, HTTPServerResponse res) @system));
+		static assert(isValidHandler!(void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res) @system));
+		static assert(isValidHandler!(void function(scope HTTPServerRequest req, scope HTTPServerResponse res) @system));
+		static assert(!isValidHandler!(int delegate(HTTPServerRequest req, HTTPServerResponse res) @system));
+		static assert(!isValidHandler!(int delegate(HTTPServerRequest req, HTTPServerResponse res) @safe));
+		void test(H)(H h)
+		{
+			static assert(isValidHandler!H);
+		}
+		test((HTTPServerRequest req, HTTPServerResponse res) {});
+	}
+}
+
+///
+@safe unittest {
+	import vibe.http.fileserver;
+
+	void addGroup(HTTPServerRequest req, HTTPServerResponse res)
+	{
+		// Route variables are accessible via the params map
+		logInfo("Getting group %s for user %s.", req.params["groupname"], req.params["username"]);
+	}
+
+	void deleteUser(HTTPServerRequest req, HTTPServerResponse res)
+	{
+		// ...
+	}
+
+	void auth(HTTPServerRequest req, HTTPServerResponse res)
+	{
+		// TODO: check req.session to see if a user is logged in and
+		//       write an error page or throw an exception instead.
+	}
+
+	void setup()
+	{
+		auto router = new URLRouter;
+		// Matches all GET requests for /users/*/groups/* and places
+		// the place holders in req.params as 'username' and 'groupname'.
+		router.get("/users/:username/groups/:groupname", &addGroup);
+
+		// Matches all requests. This can be useful for authorization and
+		// similar tasks. The auth method will only write a response if the
+		// user is _not_ authorized. Otherwise, the router will fall through
+		// and continue with the following routes.
+		router.any("*", &auth);
+
+		// Matches a POST request
+		router.post("/users/:username/delete", &deleteUser);
+
+		// Matches all GET requests in /static/ such as /static/img.png or
+		// /static/styles/sty.css
+		router.get("/static/*", serveStaticFiles("public/"));
+
+		// Setup a HTTP server...
+		auto settings = new HTTPServerSettings;
+		// ...
+
+		// The router can be directly passed to the listenHTTP function as
+		// the main request handler.
+		listenHTTP(settings, router);
+	}
+}
+
+/** Using nested routers to map components to different sub paths. A component
+	could for example be an embedded blog engine.
+*/
+@safe unittest {
+	// some embedded component:
+
+	void showComponentHome(HTTPServerRequest req, HTTPServerResponse res)
+	{
+		// ...
+	}
+
+	void showComponentUser(HTTPServerRequest req, HTTPServerResponse res)
+	{
+		// ...
+	}
+
+	void registerComponent(URLRouter router)
+	{
+		router.get("/", &showComponentHome);
+		router.get("/users/:user", &showComponentUser);
+	}
+
+	// main application:
+
+	void showHome(HTTPServerRequest req, HTTPServerResponse res)
+	{
+		// ...
+	}
+
+	void setup()
+	{
+		auto c1router = new URLRouter("/component1");
+		registerComponent(c1router);
+
+		auto mainrouter = new URLRouter;
+		mainrouter.get("/", &showHome);
+		// forward all unprocessed requests to the component router
+		mainrouter.any("*", c1router);
+
+		// now the following routes will be matched:
+		// / -> showHome
+		// /component1/ -> showComponentHome
+		// /component1/users/:user -> showComponentUser
+
+		// Start the HTTP server
+		auto settings = new HTTPServerSettings;
+		// ...
+		listenHTTP(settings, mainrouter);
+	}
+}
+
+@safe unittest { // issue #1668
+	auto r = new URLRouter;
+	r.get("/", (req, res) {
+		if ("foo" in req.headers)
+			res.writeBody("bar");
+	});
+
+	r.get("/", (scope req, scope res) {
+		if ("foo" in req.headers)
+			res.writeBody("bar");
+	});
+	r.get("/", (req, res) {});
+	r.post("/", (req, res) {});
+	r.put("/", (req, res) {});
+	r.delete_("/", (req, res) {});
+	r.patch("/", (req, res) {});
+	r.any("/", (req, res) {});
+}
+
+@safe unittest { // issue #1866
+	auto r = new URLRouter;
+	r.match(HTTPMethod.HEAD, "/foo", (scope req, scope res) { res.writeVoidBody; });
+	r.match(HTTPMethod.HEAD, "/foo", (req, res) { res.writeVoidBody; });
+	r.match(HTTPMethod.HEAD, "/foo", (scope HTTPServerRequest req, scope HTTPServerResponse res) { res.writeVoidBody; });
+	r.match(HTTPMethod.HEAD, "/foo", (HTTPServerRequest req, HTTPServerResponse res) { res.writeVoidBody; });
+
+	auto r2 = new URLRouter;
+	r.match(HTTPMethod.HEAD, "/foo", r2);
+}
+
+@safe unittest {
+	import vibe.inet.url;
+
+	auto router = new URLRouter;
+	string result;
+	void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; }
+	void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; }
+	void c(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "x", "Wrong variable contents: "~req.params["test"]); result ~= "C"; }
+	void d(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "y", "Wrong variable contents: "~req.params["test"]); result ~= "D"; }
+	void e(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "z/z", "Wrong variable contents: "~req.params["test"]); result ~= "E"; }
+	void f(HTTPServerRequest req, HTTPServerResponse) { result ~= "F"; }
+	router.get("/test", &a);
+	router.post("/test", &b);
+	router.get("/a/:test", &c);
+	router.get("/a/:test/", &d);
+	router.get("/e/:test", &e);
+	router.get("/f%2F", &f);
+
+	auto res = createTestHTTPServerResponse();
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/")), res);
+	assert(result == "", "Matched for non-existent / path");
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res);
+	assert(result == "A", "Didn't match a simple GET request");
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test"), HTTPMethod.POST), res);
+	assert(result == "AB", "Didn't match a simple POST request");
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/"), HTTPMethod.GET), res);
+	assert(result == "AB", "Matched empty variable. "~result);
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/x"), HTTPMethod.GET), res);
+	assert(result == "ABC", "Didn't match a trailing 1-character var.");
+	// currently fails due to Path not accepting "//"
+	//router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a//"), HTTPMethod.GET), res);
+	//assert(result == "ABC", "Matched empty string or slash as var. "~result);
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/y/"), HTTPMethod.GET), res);
+	assert(result == "ABCD", "Didn't match 1-character infix variable.");
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/e/z%2Fz"), HTTPMethod.GET), res);
+	assert(result == "ABCDE", "URL-escaped '/' confused router.");
+        router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/f%2F"), HTTPMethod.GET), res);
+	assert(result == "ABCDEF", "Unable to match '%2F' in path.");
+}
+
+@safe unittest {
+	import vibe.inet.url;
+
+	auto router = new URLRouter("/test");
+
+	string result;
+	void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; }
+	void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; }
+	router.get("/x", &a);
+	router.get("/y", &b);
+
+	auto res = createTestHTTPServerResponse();
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res);
+	assert(result == "");
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/x")), res);
+	assert(result == "A");
+	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/y")), res);
+	assert(result == "AB");
+}
+
+@safe unittest {
+	string ensureMatch(string pattern, string local_uri, string[string] expected_params = null)
+	{
+		import vibe.inet.url : URL;
+		string ret = local_uri ~ " did not match " ~ pattern;
+		auto r = new URLRouter;
+		r.get(pattern, (req, res) {
+			ret = null;
+			foreach (k, v; expected_params) {
+				if (k !in req.params) { ret = "Parameter "~k~" was not matched."; return; }
+				if (req.params[k] != v) { ret = "Parameter "~k~" is '"~req.params[k]~"' instead of '"~v~"'."; return; }
+			}
+		});
+		auto req = createTestHTTPServerRequest(URL("http://localhost"~local_uri));
+		auto res = createTestHTTPServerResponse();
+		r.handleRequest(req, res);
+		return ret;
+	}
+
+	assert(ensureMatch("/foo bar/", "/foo%20bar/") is null);   // normalized pattern: "/foo%20bar/"
+	assert(ensureMatch("/foo%20bar/", "/foo%20bar/") is null); // normalized pattern: "/foo%20bar/"
+	assert(ensureMatch("/foo/bar/", "/foo/bar/") is null);     // normalized pattern: "/foo/bar/"
+	//assert(ensureMatch("/foo/bar/", "/foo%2fbar/") !is null);
+	//assert(ensureMatch("/foo%2fbar/", "/foo%2fbar/") is null); // normalized pattern: "/foo%2Fbar/"
+	//assert(ensureMatch("/foo%2Fbar/", "/foo%2fbar/") is null); // normalized pattern: "/foo%2Fbar/"
+	//assert(ensureMatch("/foo%2fbar/", "/foo%2Fbar/") is null);
+	//assert(ensureMatch("/foo%2fbar/", "/foo/bar/") !is null);
+	//assert(ensureMatch("/:foo/", "/foo%2Fbar/", ["foo": "foo/bar"]) is null);
+	assert(ensureMatch("/:foo/", "/foo/bar/") !is null);
+	assert(ensureMatch("/test", "/tes%74") is null);
+}
+
+unittest { // issue #2561
+	import vibe.http.server : createTestHTTPServerRequest, createTestHTTPServerResponse;
+	import vibe.inet.url : URL;
+	import vibe.stream.memory : createMemoryOutputStream;
+
+	Route[] routes = [
+		Route(HTTPMethod.PUT, "/public/devices/commandList"),
+		Route(HTTPMethod.PUT, "/public/devices/logicStateList"),
+		Route(HTTPMethod.OPTIONS, "/public/devices/commandList"),
+		Route(HTTPMethod.OPTIONS, "/public/devices/logicStateList"),
+		Route(HTTPMethod.PUT, "/public/mnemoschema"),
+		Route(HTTPMethod.PUT, "/public/static"),
+		Route(HTTPMethod.PUT, "/public/dynamic"),
+		Route(HTTPMethod.PUT, "/public/info"),
+		Route(HTTPMethod.PUT, "/public/info-network"),
+		Route(HTTPMethod.PUT, "/public/events"),
+		Route(HTTPMethod.PUT, "/public/eventList"),
+		Route(HTTPMethod.PUT, "/public/availBatteryModels"),
+		Route(HTTPMethod.OPTIONS, "/public/availBatteryModels"),
+		Route(HTTPMethod.OPTIONS, "/public/dynamic"),
+		Route(HTTPMethod.OPTIONS, "/public/eventList"),
+		Route(HTTPMethod.OPTIONS, "/public/events"),
+		Route(HTTPMethod.OPTIONS, "/public/info"),
+		Route(HTTPMethod.OPTIONS, "/public/info-network"),
+		Route(HTTPMethod.OPTIONS, "/public/mnemoschema"),
+		Route(HTTPMethod.OPTIONS, "/public/static"),
+		Route(HTTPMethod.PUT, "/settings/admin/getinfo"),
+		Route(HTTPMethod.PUT, "/settings/admin/setconf"),
+		Route(HTTPMethod.PUT, "/settings/admin/checksetaccess"),
+		Route(HTTPMethod.OPTIONS, "/settings/admin/checksetaccess"),
+		Route(HTTPMethod.OPTIONS, "/settings/admin/getinfo"),
+		Route(HTTPMethod.OPTIONS, "/settings/admin/setconf"),
+	];
+
+	auto router = new URLRouter;
+
+	foreach (r; routes)
+		router.match(r.method, r.pattern, (req, res) {
+			res.writeBody("OK");
+		});
+
+	{ // make sure unmatched routes are not handled by the router
+		auto req = createTestHTTPServerRequest(URL("http://localhost/foobar"), HTTPMethod.PUT);
+		auto res = createTestHTTPServerResponse();
+		router.handleRequest(req, res);
+		assert(!res.headerWritten);
+	}
+
+	// ensure all routes are matched
+	foreach (r; routes) {
+		auto url = URL("http://localhost"~r.pattern);
+		auto output = createMemoryOutputStream();
+		auto req = createTestHTTPServerRequest(url, r.method);
+		auto res = createTestHTTPServerResponse(output, null, TestHTTPResponseMode.bodyOnly);
+		router.handleRequest(req, res);
+		//assert(res.headerWritten);
+		assert(output.data == "OK");
+	}
+}
+
+
+/**
+	Convenience abstraction for a single `URLRouter` route.
+
+	See `URLRouter.route` for a usage example.
+*/
+struct URLRoute {
+@safe:
+
+	URLRouter router;
+	string path;
+
+	ref URLRoute get(Handler)(Handler h) { router.get(path, h); return this; }
+	ref URLRoute post(Handler)(Handler h) { router.post(path, h); return this; }
+	ref URLRoute put(Handler)(Handler h) { router.put(path, h); return this; }
+	ref URLRoute delete_(Handler)(Handler h) { router.delete_(path, h); return this; }
+	ref URLRoute patch(Handler)(Handler h) { router.patch(path, h); return this; }
+	ref URLRoute any(Handler)(Handler h) { router.any(path, h); return this; }
+	ref URLRoute match(Handler)(HTTPMethod method, Handler h) { router.match(method, path, h); return this; }
+}
+
+
+private enum maxRouteParameters = 64;
+
+private struct Route {
+	HTTPMethod method;
+	string pattern;
+	HTTPServerRequestDelegate cb;
+}
+
+private string skipPathNode(string str, ref size_t idx)
+@safe {
+	size_t start = idx;
+	while( idx < str.length && str[idx] != '/' ) idx++;
+	return str[start .. idx];
+}
+
+private string skipPathNode(ref string str)
+@safe {
+	size_t idx = 0;
+	auto ret = skipPathNode(str, idx);
+	str = str[idx .. $];
+	return ret;
+}
+
+private struct MatchTree(T) {
+@safe:
+
+	import std.algorithm : countUntil;
+	import std.array : array;
+
+	private {
+		alias NodeIndex = uint;
+		alias TerminalTagIndex = uint;
+		alias TerminalIndex = ushort;
+		alias VarIndex = ushort;
+
+		struct Node {
+			TerminalTagIndex terminalsStart; // slice into m_terminalTags
+			TerminalTagIndex terminalsEnd;
+			NodeIndex[ubyte.max+1] edges = NodeIndex.max; // character -> index into m_nodes
+		}
+		struct TerminalTag {
+			TerminalIndex index; // index into m_terminals array
+			VarIndex var = VarIndex.max; // index into Terminal.varNames/varValues or VarIndex.max
+		}
+		struct Terminal {
+			string pattern;
+			T data;
+			string[] varNames;
+			VarIndex[NodeIndex] varMap;
+		}
+		Node[] m_nodes; // all nodes as a single array
+		TerminalTag[] m_terminalTags;
+		Terminal[] m_terminals;
+
+		enum TerminalChar = 0;
+		bool m_upToDate = false;
+	}
+
+	@property size_t terminalCount() const { return m_terminals.length; }
+
+	void addTerminal(string pattern, T data)
+	{
+		assert(m_terminals.length < TerminalIndex.max, "Attempt to register too many routes.");
+		m_terminals ~= Terminal(pattern, data, null, null);
+		m_upToDate = false;
+	}
+
+	bool match(string text, scope bool delegate(size_t terminal, scope string[] vars) @safe del)
+	{
+		// lazily update the match graph
+		if (!m_upToDate) rebuildGraph();
+
+		return doMatch(text, del);
+	}
+
+	const(string)[] getTerminalVarNames(size_t terminal) const { return m_terminals[terminal].varNames; }
+	ref inout(T) getTerminalData(size_t terminal) inout { return m_terminals[terminal].data; }
+
+	void print()
+	const {
+		import std.algorithm : map;
+		import std.array : join;
+		import std.conv : to;
+		import std.range : iota;
+		import std.string : format;
+
+		logInfo("Nodes:");
+		foreach (i, n; m_nodes) {
+			logInfo("  %s %s", i, m_terminalTags[n.terminalsStart .. n.terminalsEnd]
+				.map!(t => format("T%s%s", t.index, t.var != VarIndex.max ? "("~m_terminals[t.index].varNames[t.var]~")" : "")).join(" "));
+			//logInfo("  %s %s-%s", i, n.terminalsStart, n.terminalsEnd);
+
+
+			static string mapChar(ubyte ch) {
+				if (ch == TerminalChar) return "$";
+				if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch);
+				if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch);
+				if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch);
+				if (ch == '/') return "/";
+				if (ch == '^') return "^";
+				return ch.to!string;
+			}
+
+			void printRange(uint node, ubyte from, ubyte to)
+			{
+				if (to - from <= 10) logInfo("    %s -> %s", iota(from, cast(uint)to+1).map!(ch => mapChar(cast(ubyte)ch)).join("|"), node);
+				else logInfo("    %s-%s -> %s", mapChar(from), mapChar(to), node);
+			}
+
+			auto last_to = NodeIndex.max;
+			ubyte last_ch = 0;
+			foreach (ch, e; n.edges)
+				if (e != last_to) {
+					if (last_to != NodeIndex.max)
+						printRange(last_to, last_ch, cast(ubyte)(ch-1));
+					last_ch = cast(ubyte)ch;
+					last_to = e;
+				}
+			if (last_to != NodeIndex.max)
+				printRange(last_to, last_ch, ubyte.max);
+		}
+	}
+
+	private bool doMatch(string text, scope bool delegate(size_t terminal, scope string[] vars) @safe del)
+	const {
+		string[maxRouteParameters] vars_buf;// = void;
+
+		import std.algorithm : canFind;
+
+		// first, determine the end node, if any
+		auto n = matchTerminals(text);
+		if (!n) return false;
+
+		// then, go through the terminals and match their variables
+		foreach (ref t; m_terminalTags[n.terminalsStart .. n.terminalsEnd]) {
+			auto term = &m_terminals[t.index];
+			auto vars = vars_buf[0 .. term.varNames.length];
+			matchVars(vars, term, text);
+			if (vars.canFind!(v => v.length == 0)) continue; // all variables must be non-empty to match
+			if (del(t.index, vars)) return true;
+		}
+		return false;
+	}
+
+	/// Given a hexadecimal character in [0-9a-fA-F], convert it to an integer value in [0, 15].
+	private static uint hexDigit(char ch) @safe nothrow @nogc {
+		assert((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'));
+		if (ch >= '0' && ch <= '9') return ch - '0';
+		else if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
+		else return ch - 'A' + 10;
+	}
+
+	/// Reads a single character from text, decoding any unreserved percent-encoded character, so
+	/// that it matches the format used for route matches.
+	static char nextMatchChar(string text, ref size_t i) {
+		import std.ascii : isHexDigit;
+
+		char ch = text[i];
+		// See if we have to decode an encoded unreserved character.
+		if (ch == '%' && i + 2 < text.length && isHexDigit(text[i+1]) && isHexDigit(text[i+2])) {
+			uint c = hexDigit(text[i+1]) * 16 + hexDigit(text[i+2]);
+			// Check if we have an encoded unreserved character:
+			// https://en.wikipedia.org/wiki/Percent-encoding
+			if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
+					|| c == '-' || c == '_' || c == '.' || c == '~') {
+				// Decode the character before performing route matching.
+				ch = cast(char) c;
+				i += 3;
+				return ch;
+			}
+		}
+		i += 1;
+		return ch;
+	}
+
+	private inout(Node)* matchTerminals(string text)
+	inout {
+		if (!m_nodes.length) return null;
+
+		auto n = &m_nodes[0];
+
+		// follow the path through the match graph
+
+		// Routes match according to their percent-encoded normal form, with reserved-characters
+		// percent-encoded and unreserved-charcters not percent-encoded.
+		size_t i = 0;
+		while (i < text.length) {
+			char ch = nextMatchChar(text, i);
+			auto nidx = n.edges[cast(size_t)ch];
+			if (nidx == NodeIndex.max) return null;
+			n = &m_nodes[nidx];
+		}
+
+		// finally, find a matching terminal node
+		auto nidx = n.edges[TerminalChar];
+		if (nidx == NodeIndex.max) return null;
+		n = &m_nodes[nidx];
+		return n;
+	}
+
+	private void matchVars(string[] dst, in Terminal* term, string text)
+	const {
+		NodeIndex nidx = 0;
+		VarIndex activevar = VarIndex.max;
+		size_t activevarstart;
+
+		dst[] = null;
+
+		// follow the path through the match graph
+		size_t i = 0;
+		while (i < text.length) {
+			auto var = term.varMap.get(nidx, VarIndex.max);
+
+			// detect end of variable
+			if (var != activevar && activevar != VarIndex.max) {
+				dst[activevar] = text[activevarstart .. i-1];
+				activevar = VarIndex.max;
+			}
+
+			// detect beginning of variable
+			if (var != VarIndex.max && activevar == VarIndex.max) {
+				activevar = var;
+				activevarstart = i;
+			}
+
+			char ch = nextMatchChar(text, i);
+			nidx = m_nodes[nidx].edges[cast(ubyte)ch];
+			assert(nidx != NodeIndex.max);
+		}
+
+		// terminate any active varible with the end of the input string or with the last character
+		auto var = term.varMap.get(nidx, VarIndex.max);
+		if (activevar != VarIndex.max) dst[activevar] = text[activevarstart .. (var == activevar ? $ : $-1)];
+	}
+
+	private void rebuildGraph()
+	@trusted {
+		import std.array : appender;
+		import std.conv : to;
+
+		if (m_upToDate) return;
+		m_upToDate = true;
+
+		m_nodes = null;
+		m_terminalTags = null;
+
+		if (!m_terminals.length) return;
+
+		MatchGraphBuilder builder;
+		foreach (i, ref t; m_terminals) {
+			t.varNames = builder.insert(t.pattern, i.to!TerminalIndex);
+			assert(t.varNames.length <= VarIndex.max, "Too many variables in route.");
+		}
+		//builder.print();
+		builder.disambiguate();
+
+		auto nodemap = new NodeIndex[builder.m_nodes.length];
+		nodemap[] = NodeIndex.max;
+
+		auto nodes = appender!(Node[]);
+		nodes.reserve(1024);
+		auto termtags = appender!(TerminalTag[]);
+		termtags.reserve(1024);
+
+		NodeIndex process(NodeIndex n)
+		{
+			import std.algorithm : canFind;
+
+			if (nodemap[n] != NodeIndex.max) return nodemap[n];
+			auto nmidx = cast(NodeIndex)nodes.data.length;
+			nodemap[n] = nmidx;
+			nodes.put(Node.init);
+
+			Node nn;
+			nn.terminalsStart = termtags.data.length.to!TerminalTagIndex;
+			foreach (t; builder.m_nodes[n].terminals) {
+				auto var = cast(VarIndex)t.var;
+				assert(!termtags.data[nn.terminalsStart .. $].canFind!(u => u.index == t.index && u.var == var));
+				termtags ~= TerminalTag(cast(TerminalIndex)t.index, var);
+				if (var != VarIndex.max)
+					m_terminals[t.index].varMap[nmidx] = var;
+			}
+			nn.terminalsEnd = termtags.data.length.to!TerminalTagIndex;
+			foreach (ch, targets; builder.m_nodes[n].edges)
+				foreach (to; builder.m_edgeEntries.getItems(targets))
+					nn.edges[ch] = process(to);
+
+			nodes.data[nmidx] = nn;
+
+			return nmidx;
+		}
+		assert(builder.m_edgeEntries.hasLength(builder.m_nodes[0].edges['^'], 1),
+			"Graph must be disambiguated before purging.");
+		process(builder.m_edgeEntries.getItems(builder.m_nodes[0].edges['^']).front);
+
+		m_nodes = nodes.data;
+		m_terminalTags = termtags.data;
+
+		logDebug("Match tree has %s (of %s in the builder) nodes, %s terminals", m_nodes.length, builder.m_nodes.length, m_terminals.length);
+	}
+}
+
+unittest {
+	import std.string : format;
+	MatchTree!int m;
+
+	void testMatch(string str, size_t[] terms, string[] vars)
+	{
+		size_t[] mterms;
+		string[] mvars;
+		m.match(str, (t, scope vals) {
+			mterms ~= t;
+			mvars ~= vals;
+			return false;
+		});
+		assert(mterms == terms, format("Mismatched terminals: %s (expected %s)", mterms, terms));
+		assert(mvars == vars, format("Mismatched variables; %s (expected %s)", mvars, vars));
+	}
+
+	m.addTerminal("a", 0);
+	m.addTerminal("b", 0);
+	m.addTerminal("ab", 0);
+	m.rebuildGraph();
+	assert(m.getTerminalVarNames(0) == []);
+	assert(m.getTerminalVarNames(1) == []);
+	assert(m.getTerminalVarNames(2) == []);
+	testMatch("a", [0], []);
+	testMatch("ab", [2], []);
+	testMatch("abc", [], []);
+	testMatch("b", [1], []);
+
+	m = MatchTree!int.init;
+	m.addTerminal("ab", 0);
+	m.addTerminal("a*", 0);
+	m.rebuildGraph();
+	assert(m.getTerminalVarNames(0) == []);
+	assert(m.getTerminalVarNames(1) == []);
+	testMatch("a", [1], []);
+	testMatch("ab", [0, 1], []);
+	testMatch("abc", [1], []);
+
+	m = MatchTree!int.init;
+	m.addTerminal("ab", 0);
+	m.addTerminal("a:var", 0);
+	m.rebuildGraph();
+	assert(m.getTerminalVarNames(0) == []);
+	assert(m.getTerminalVarNames(1) == ["var"], format("%s", m.getTerminalVarNames(1)));
+	testMatch("a", [], []); // vars may not be empty
+	testMatch("ab", [0, 1], ["b"]);
+	testMatch("abc", [1], ["bc"]);
+
+	m = MatchTree!int.init;
+	m.addTerminal(":var1/:var2", 0);
+	m.addTerminal("a/:var3", 0);
+	m.addTerminal(":var4/b", 0);
+	m.rebuildGraph();
+	assert(m.getTerminalVarNames(0) == ["var1", "var2"]);
+	assert(m.getTerminalVarNames(1) == ["var3"]);
+	assert(m.getTerminalVarNames(2) == ["var4"]);
+	testMatch("a", [], []);
+	testMatch("a/a", [0, 1], ["a", "a", "a"]);
+	testMatch("a/b", [0, 1, 2], ["a", "b", "b", "a"]);
+	testMatch("a/bc", [0, 1], ["a", "bc", "bc"]);
+	testMatch("ab/b", [0, 2], ["ab", "b", "ab"]);
+	testMatch("ab/bc", [0], ["ab", "bc"]);
+
+	m = MatchTree!int.init;
+	m.addTerminal(":var1/", 0);
+	m.rebuildGraph();
+	assert(m.getTerminalVarNames(0) == ["var1"]);
+	testMatch("ab/", [0], ["ab"]);
+	testMatch("ab", [], []);
+	testMatch("/ab", [], []);
+	testMatch("a/b", [], []);
+	testMatch("ab//", [], []);
+}
+
+
+private struct MatchGraphBuilder {
+@safe:
+	import std.container.array : Array;
+	import std.array : array;
+	import std.algorithm : filter;
+	import std.string : format;
+
+	alias NodeIndex = uint;
+	alias TerminalIndex = ushort;
+	alias VarIndex = ushort;
+	alias NodeSet = LinkedSetBacking!NodeIndex.Handle;
+
+	private {
+		enum TerminalChar = 0;
+		struct TerminalTag {
+			TerminalIndex index;
+			VarIndex var = VarIndex.max;
+		}
+		struct Node {
+			Array!TerminalTag terminals;
+			NodeSet[ubyte.max+1] edges;
+		}
+		Array!Node m_nodes;
+		LinkedSetBacking!NodeIndex m_edgeEntries;
+	}
+
+	@disable this(this);
+
+	string[] insert(string pattern, TerminalIndex terminal)
+	{
+		import std.algorithm : canFind;
+
+		auto full_pattern = pattern;
+		string[] vars;
+		if (!m_nodes.length) addNode();
+
+		// create start node and connect to zero node
+		auto nidx = addNode();
+		addEdge(0, nidx, '^', terminal);
+
+		while (pattern.length) {
+			auto ch = pattern[0];
+			if (ch == '*') {
+				assert(pattern.length == 1, "Asterisk is only allowed at the end of a pattern: "~full_pattern);
+				pattern = null;
+
+				foreach (v; ubyte.min .. ubyte.max+1) {
+					if (v == TerminalChar) continue;
+					addEdge(nidx, nidx, cast(ubyte)v, terminal);
+				}
+			} else if (ch == ':') {
+				pattern = pattern[1 .. $];
+				auto name = skipPathNode(pattern);
+				assert(name.length > 0, "Missing placeholder name: "~full_pattern);
+				assert(!vars.canFind(name), "Duplicate placeholder name ':"~name~"': '"~full_pattern~"'");
+				auto varidx = cast(VarIndex)vars.length;
+				vars ~= name;
+				assert(!pattern.length || (pattern[0] != '*' && pattern[0] != ':'),
+					"Cannot have two placeholders directly follow each other.");
+
+				foreach (v; ubyte.min .. ubyte.max+1) {
+					if (v == TerminalChar || v == '/') continue;
+					addEdge(nidx, nidx, cast(ubyte)v, terminal, varidx);
+				}
+			} else {
+				nidx = addEdge(nidx, ch, terminal);
+				pattern = pattern[1 .. $];
+			}
+		}
+
+		addEdge(nidx, TerminalChar, terminal);
+		return vars;
+	}
+
+	void disambiguate()
+	@trusted {
+		import std.algorithm : map, sum;
+		import std.array : appender, join;
+
+		//logInfo("Disambiguate with %s initial nodes", m_nodes.length);
+		if (!m_nodes.length) return;
+
+		import vibe.container.hashmap : HashMap;
+		HashMap!(LinkedSetHash, NodeIndex) combined_nodes;
+		Array!bool visited;
+		visited.length = m_nodes.length * 2;
+		Stack!NodeIndex node_stack;
+		node_stack.reserve(m_nodes.length);
+		node_stack.push(0);
+		while (!node_stack.empty) {
+			auto n = node_stack.pop();
+
+			while (n >= visited.length) visited.length = visited.length * 2;
+			if (visited[n]) continue;
+			//logInfo("Disambiguate %s (stack=%s)", n, node_stack.fill);
+			visited[n] = true;
+
+			foreach (ch; ubyte.min .. ubyte.max+1) {
+				auto chnodes = m_nodes[n].edges[ch];
+				LinkedSetHash chhash = m_edgeEntries.getHash(chnodes);
+
+				// handle trivial cases
+				if (m_edgeEntries.hasMaxLength(chnodes, 1))
+					continue;
+
+				// generate combined state for ambiguous edges
+				if (auto pn = () @trusted { return chhash in combined_nodes; } ()) {
+					m_nodes[n].edges[ch] = m_edgeEntries.create(*pn);
+					assert(m_edgeEntries.hasLength(m_nodes[n].edges[ch], 1));
+					continue;
+				}
+
+				// for new combinations, create a new node
+				NodeIndex ncomb = addNode();
+				combined_nodes[chhash] = ncomb;
+
+				// write all edges
+				size_t idx = 0;
+				foreach (to_ch; ubyte.min .. ubyte.max+1) {
+					auto e = &m_nodes[ncomb].edges[to_ch];
+					foreach (chn; m_edgeEntries.getItems(chnodes))
+						m_edgeEntries.insert(e, m_edgeEntries.getItems(m_nodes[chn].edges[to_ch]));
+				}
+
+				// add terminal indices
+				foreach (chn; m_edgeEntries.getItems(chnodes))
+					foreach (t; m_nodes[chn].terminals)
+						addTerminal(ncomb, t.index, t.var);
+				foreach (i; 1 .. m_nodes[ncomb].terminals.length)
+					assert(m_nodes[ncomb].terminals[0] != m_nodes[ncomb].terminals[i]);
+
+				m_nodes[n].edges[ch] = m_edgeEntries.create(ncomb);
+				assert(m_edgeEntries.hasLength(m_nodes[n].edges[ch], 1));
+			}
+
+			// process nodes recursively
+			foreach (ch; ubyte.min .. ubyte.max+1) {
+				// should only have single out-edges now
+				assert(m_edgeEntries.hasMaxLength(m_nodes[n].edges[ch], 1));
+				foreach (e; m_edgeEntries.getItems(m_nodes[n].edges[ch]))
+					node_stack.push(e);
+			}
+		}
+
+		import std.algorithm.sorting : sort;
+		foreach (ref n; m_nodes)
+			n.terminals[].sort!((a, b) => a.index < b.index)();
+
+		debug logDebug("Disambiguate done: %s nodes, %s max stack size", m_nodes.length, node_stack.maxSize);
+	}
+
+	void print()
+	const @trusted {
+		import std.algorithm : map;
+		import std.array : join;
+		import std.conv : to;
+		import std.string : format;
+
+		logInfo("Nodes:");
+		size_t i = 0;
+		foreach (n; m_nodes) {
+			string mapChar(ubyte ch) {
+				if (ch == TerminalChar) return "$";
+				if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch);
+				if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch);
+				if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch);
+				if (ch == '^') return "^";
+				if (ch == '/') return "/";
+				return format("$%s", ch);
+			}
+			logInfo("  %s: %s", i, n.terminals[].map!(t => t.var != VarIndex.max ? format("T%s(%s)", t.index, t.var) : format("T%s", t.index)).join(" "));
+			ubyte first_char;
+			LinkedSetHash list_hash;
+			NodeSet list;
+
+			void printEdges(ubyte last_char) {
+				if (!list.empty) {
+					string targets;
+					foreach (tn; m_edgeEntries.getItems(list))
+						targets ~= format(" %s", tn);
+					if (targets.length > 0)
+						logInfo("    [%s ... %s] -> %s", mapChar(first_char), mapChar(last_char), targets);
+				}
+			}
+			foreach (ch, tnodes; n.edges) {
+				auto h = m_edgeEntries.getHash(tnodes);
+				if (h != list_hash) {
+					printEdges(cast(ubyte)(ch-1));
+					list_hash = h;
+					list = tnodes;
+					first_char = cast(ubyte)ch;
+				}
+			}
+			printEdges(ubyte.max);
+			i++;
+		}
+	}
+
+	private void addEdge(NodeIndex from, NodeIndex to, ubyte ch, TerminalIndex terminal, VarIndex var = VarIndex.max)
+	@trusted {
+		m_edgeEntries.insert(&m_nodes[from].edges[ch], to);
+		addTerminal(to, terminal, var);
+	}
+
+	private NodeIndex addEdge(NodeIndex from, ubyte ch, TerminalIndex terminal, VarIndex var = VarIndex.max)
+	@trusted {
+		import std.algorithm : canFind;
+		import std.string : format;
+		if (!m_nodes[from].edges[ch].empty)
+			assert(false, format("%s is in %s", ch, m_nodes[from].edges[]));
+		auto nidx = addNode();
+		addEdge(from, nidx, ch, terminal, var);
+		return nidx;
+	}
+
+	private void addTerminal(NodeIndex node, TerminalIndex terminal, VarIndex var)
+	@trusted {
+		foreach (ref t; m_nodes[node].terminals) {
+			if (t.index == terminal) {
+				if (t.var != VarIndex.max && t.var != var)
+					assert(false, format("Ambiguous route var match!? %s vs %s", t.var, var));
+				t.var = var;
+				return;
+			}
+		}
+		m_nodes[node].terminals ~= TerminalTag(terminal, var);
+	}
+
+	private NodeIndex addNode()
+	@trusted {
+		assert(m_nodes.length <= 1_000_000_000, "More than 1B nodes in tree!?");
+		auto idx = cast(NodeIndex)m_nodes.length;
+		m_nodes ~= Node.init;
+		return idx;
+	}
+}
+
+
+/** Used to store and manipulate multiple linked lists within a single array
+	based storage.
+*/
+struct LinkedSetBacking(T) {
+	import std.container.array : Array;
+	import std.range : isInputRange;
+
+	static struct Handle {
+		uint index = uint.max;
+		@property bool empty() const { return index == uint.max; }
+	}
+
+	private {
+		static struct Entry {
+			uint next;
+			T value;
+		}
+
+		Array!Entry m_storage;
+
+		static struct Range {
+			private {
+				Array!Entry* backing;
+				uint index;
+			}
+
+			@property bool empty() const { return index == uint.max; }
+			@property ref const(T) front() const { return (*backing)[index].value; }
+
+			void popFront()
+			{
+				index = (*backing)[index].next;
+			}
+		}
+	}
+
+	@property Handle emptySet() { return Handle.init; }
+
+	Handle create(scope T[] items...)
+	{
+		Handle ret;
+		foreach (i; items)
+			ret.index = createNode(i, ret.index);
+		return ret;
+	}
+
+	void insert(Handle* h, T value)
+	{
+		/*foreach (c; getItems(*h))
+			if (value == c)
+				return;*/
+		h.index = createNode(value, h.index);
+	}
+
+	void insert(R)(Handle* h, R items)
+		if (isInputRange!R)
+	{
+		foreach (itm; items)
+			insert(h, itm);
+	}
+
+	LinkedSetHash getHash(Handle sh)
+	const {
+		// NOTE: the returned hash is order independent, to avoid bogus
+		//       mismatches when comparing lists of different order
+		LinkedSetHash ret = linkedSetHashOf(null);
+		while (sh != Handle.init) {
+			auto h = linkedSetHashOf(cast(const(ubyte)[])(&m_storage[sh.index].value)[0 .. 1]);
+			foreach (i; 0 .. ret.length) ret[i] ^= h[i];
+			sh.index = m_storage[sh.index].next;
+		}
+		return ret;
+	}
+
+	auto getItems(Handle sh) { return Range(&m_storage, sh.index); }
+	auto getItems(Handle sh) const { return Range(&(cast()this).m_storage, sh.index); }
+
+	bool hasMaxLength(Handle sh, size_t l)
+	const {
+		uint idx = sh.index;
+		do {
+			if (idx == uint.max) return true;
+			idx = m_storage[idx].next;
+		} while (l-- > 0);
+		return false;
+	}
+
+	bool hasLength(Handle sh, size_t l)
+	const {
+		uint idx = sh.index;
+		while (l-- > 0) {
+			if (idx == uint.max) return false;
+			idx = m_storage[idx].next;
+		}
+		return idx == uint.max;
+	}
+
+	private uint createNode(ref T val, uint next)
+	{
+		auto id = cast(uint)m_storage.length;
+		m_storage ~= Entry(next, val);
+		return id;
+	}
+}
+
+unittest {
+	import std.algorithm.comparison : equal;
+
+	LinkedSetBacking!int b;
+	auto s = b.emptySet;
+	assert(s.empty);
+	assert(b.getItems(s).empty);
+	s = b.create(3, 5, 7);
+	assert(b.getItems(s).equal([7, 5, 3]));
+	assert(!b.hasLength(s, 2));
+	assert(b.hasLength(s, 3));
+	assert(!b.hasLength(s, 4));
+	assert(!b.hasMaxLength(s, 2));
+	assert(b.hasMaxLength(s, 3));
+	assert(b.hasMaxLength(s, 4));
+
+	auto h = b.getHash(s);
+	assert(h != b.getHash(b.emptySet));
+	s = b.create(5, 3, 7);
+	assert(b.getHash(s) == h);
+	assert(b.getItems(s).equal([7, 3, 5]));
+
+	b.insert(&s, 11);
+	assert(b.hasLength(s, 4));
+	assert(b.getHash(s) != h);
+	assert(b.getItems(s).equal([11, 7, 3, 5]));
+}
+
+alias LinkedSetHasher = MurmurHash3!(128, 64);
+alias LinkedSetHash = ulong[16/ulong.sizeof];
+LinkedSetHash linkedSetHashOf(scope const(ubyte)[] bytes)
+{
+	LinkedSetHasher h;
+	h.start();
+	h.put(bytes);
+	return cast(LinkedSetHash)h.finish();
+}
+
+private struct Stack(E)
+{
+	import std.range : isInputRange;
+
+	private {
+		E[] m_storage;
+		size_t m_fill;
+		debug size_t m_maxFill;
+	}
+
+	@property bool empty() const { return m_fill == 0; }
+
+	@property size_t fill() const { return m_fill; }
+
+	debug @property size_t maxSize() const { return m_maxFill; }
+
+	void reserve(size_t amt)
+	{
+		auto minsz = m_fill + amt;
+		if (m_storage.length < minsz) {
+			auto newlength = 64;
+			while (newlength < minsz) newlength *= 2;
+			m_storage.length = newlength;
+		}
+	}
+
+	void push(E el)
+	{
+		reserve(1);
+		m_storage[m_fill++] = el;
+		debug if (m_fill > m_maxFill) m_maxFill = m_fill;
+	}
+
+	void push(R)(R els)
+		if (isInputRange!R)
+	{
+		reserve(els.length);
+		foreach (el; els)
+			m_storage[m_fill++] = el;
+		debug if (m_fill > m_maxFill) m_maxFill = m_fill;
+	}
+
+	E pop()
+	{
+		assert(!empty, "Stack underflow.");
+		return m_storage[--m_fill];
+	}
+}

+ 2013 - 0
vtest/source/vibe/http/server.d

@@ -0,0 +1,2013 @@
+/**
+	A HTTP 1.1/1.0 server implementation.
+
+	Copyright: © 2012-2017 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig, Jan Krüger, Ilya Shipunov
+*/
+module vibe.http.server;
+
+public import vibe.core.net;
+public import vibe.http.common;
+public import vibe.http.session;
+
+import vibe.container.internal.appender : FixedAppender;
+import vibe.container.internal.utilallocator;
+import vibe.core.file;
+import vibe.core.log;
+import vibe.data.json;
+import vibe.http.dist;
+import vibe.http.log;
+import vibe.inet.message;
+import vibe.inet.url;
+import vibe.inet.webform;
+import vibe.internal.interfaceproxy : InterfaceProxy;
+import vibe.stream.counting;
+import vibe.stream.operations;
+import vibe.stream.tls;
+import vibe.stream.wrapper : ConnectionProxyStream, createConnectionProxyStream, createConnectionProxyStreamFL;
+import vibe.textfilter.urlencode;
+import vibe.internal.freelistref;
+import vibe.internal.string : formatAlloc, icmp2;
+
+import core.atomic;
+import core.vararg;
+//import diet.traits : SafeFilterCallback, dietTraits;
+import std.algorithm : canFind, splitter;
+import std.array;
+import std.conv;
+import std.datetime;
+import std.encoding : sanitize;
+import std.exception;
+import std.format;
+import std.functional : toDelegate;
+import std.string;
+import std.traits : ReturnType;
+import std.typecons;
+import std.uri;
+
+
+version (VibeNoSSL) enum HaveNoTLS = true;
+else version (Have_botan) enum HaveNoTLS = false;
+else version (Have_openssl) enum HaveNoTLS = false;
+else enum HaveNoTLS = true;
+
+
+// rm - comment - diet-ng shit
+// https://github.com/rejectedsoftware/diet-ng/blob/v1.8.4/source/diet/traits.d#L159
+
+
+/**************************************************************************************************/
+/* Public functions                                                                               */
+/**************************************************************************************************/
+
+/**
+	Starts a HTTP server listening on the specified port.
+
+	request_handler will be called for each HTTP request that is made. The
+	res parameter of the callback then has to be filled with the response
+	data.
+
+	request_handler can be either HTTPServerRequestDelegate/HTTPServerRequestFunction
+	or a class/struct with a member function 'handleRequest' that has the same
+	signature.
+
+	Note that if the application has been started with the --disthost command line
+	switch, listenHTTP() will automatically listen on the specified VibeDist host
+	instead of locally. This allows for a seamless switch from single-host to
+	multi-host scenarios without changing the code. If you need to listen locally,
+	use listenHTTPPlain() instead.
+
+	Params:
+		settings = Customizes the HTTP servers functionality (host string or HTTPServerSettings object)
+		request_handler = This callback is invoked for each incoming request and is responsible
+			for generating the response.
+
+	Returns:
+		A handle is returned that can be used to stop listening for further HTTP
+		requests with the supplied settings. Another call to `listenHTTP` can be
+		used afterwards to start listening again.
+*/
+HTTPListener listenHTTP(Settings)(Settings _settings, HTTPServerRequestDelegate request_handler)
+@safe
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	// auto-construct HTTPServerSettings
+	static if (is(Settings == string))
+		auto settings = new HTTPServerSettings(_settings);
+	else
+		alias settings = _settings;
+
+	enforce(settings.bindAddresses.length, "Must provide at least one bind address for a HTTP server.");
+
+	// if a VibeDist host was specified on the command line, register there instead of listening
+	// directly.
+	if (s_distHost.length && !settings.disableDistHost) {
+		return listenHTTPDist(settings, request_handler, s_distHost, s_distPort);
+	} else {
+		return listenHTTPPlain(settings, request_handler);
+	}
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestFunction request_handler)
+@safe
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, () @trusted { return toDelegate(request_handler); } ());
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestHandler request_handler)
+@safe
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, &request_handler.handleRequest);
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestDelegateS request_handler)
+@safe
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, cast(HTTPServerRequestDelegate)request_handler);
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestFunctionS request_handler)
+@safe
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, () @trusted { return toDelegate(request_handler); } ());
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, HTTPServerRequestHandlerS request_handler)
+@safe
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, &request_handler.handleRequest);
+}
+
+/// Scheduled for deprecation - use a `@safe` callback instead.
+HTTPListener listenHTTP(Settings)(Settings settings, void delegate(HTTPServerRequest, HTTPServerResponse) @system request_handler)
+@system
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, (req, res) @trusted => request_handler(req, res));
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, void function(HTTPServerRequest, HTTPServerResponse) @system request_handler)
+@system
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, (req, res) @trusted => request_handler(req, res));
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, void delegate(scope HTTPServerRequest, scope HTTPServerResponse) @system request_handler)
+@system
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, (scope req, scope res) @trusted => request_handler(req, res));
+}
+/// ditto
+HTTPListener listenHTTP(Settings)(Settings settings, void function(scope HTTPServerRequest, scope HTTPServerResponse) @system request_handler)
+@system
+if (is(Settings == string) || is(Settings == HTTPServerSettings)) {
+	return listenHTTP(settings, (scope req, scope res) @trusted => request_handler(req, res));
+}
+
+unittest
+{
+	void test()
+	{
+		static void testSafeFunction(HTTPServerRequest req, HTTPServerResponse res) @safe {}
+		listenHTTP("0.0.0.0:8080", &testSafeFunction);
+		listenHTTP(":8080", new class HTTPServerRequestHandler {
+			void handleRequest(HTTPServerRequest req, HTTPServerResponse res) @safe {}
+		});
+		listenHTTP(":8080", (req, res) {});
+
+		static void testSafeFunctionS(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {}
+		listenHTTP(":8080", &testSafeFunctionS);
+		void testSafeDelegateS(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {}
+		listenHTTP(":8080", &testSafeDelegateS);
+		listenHTTP(":8080", new class HTTPServerRequestHandler {
+			void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe {}
+		});
+		listenHTTP(":8080", (scope req, scope res) {});
+	}
+}
+
+
+/** Treats an existing connection as an HTTP connection and processes incoming
+	requests.
+
+	After all requests have been processed, the connection will be closed and
+	the function returns to the caller.
+
+	Params:
+		connection = The stream to treat as an incoming HTTP client connection.
+		context = Information about the incoming listener and available
+			virtual hosts
+*/
+void handleHTTPConnection(TCPConnection connection, HTTPServerContext context)
+@safe {
+	import vibe.http.internal.http1.server : handleHTTP1Connection;
+	import vibe.http.internal.http2.server : handleHTTP2Connection;
+	import vibe.http.internal.http2.settings : HTTP2ServerContext, HTTP2Settings;
+
+	static if (HaveNoTLS) {
+		alias TLSStreamType = Stream;
+	} else {
+		alias TLSStreamType = ReturnType!(createTLSStreamFL!(InterfaceProxy!Stream));
+	}
+
+	InterfaceProxy!Stream http_stream;
+	http_stream = connection;
+
+	scope (exit) connection.close();
+
+	// check wether the client's address is banned
+	foreach (ref virtual_host; context.m_virtualHosts)
+		if ((virtual_host.settings.rejectConnectionPredicate !is null) &&
+			virtual_host.settings.rejectConnectionPredicate(connection.remoteAddress()))
+			return;
+
+	// Set NODELAY to true, to avoid delays caused by sending the response
+	// header and body in separate chunks. Note that to avoid other performance
+	// issues (caused by tiny packets), this requires using an output buffer in
+	// the event driver, which is the case at least for the legacy libevent
+	// based driver.
+	connection.tcpNoDelay = true;
+
+	TLSStreamType tls_stream;
+
+	if (!connection.waitForData(10.seconds())) {
+		logDebug("Client didn't send the initial request in a timely manner. Closing connection.");
+		return;
+	}
+
+	// If this is a HTTPS server, initiate TLS
+	if (context.tlsContext) {
+		static if (HaveNoTLS) assert(false, "No TLS support compiled in.");
+		else {
+			logDebug("Accept TLS connection: %s", context.tlsContext.kind);
+			// TODO: reverse DNS lookup for peer_name of the incoming connection for TLS client certificate verification purposes
+			tls_stream = createTLSStreamFL(http_stream, context.tlsContext, TLSStreamState.accepting, null, connection.remoteAddress);
+
+			Nullable!string proto = tls_stream.alpn;
+			if(!proto.isNull && proto == "h2" && (context.m_virtualHosts[0].settings.options & HTTPServerOption.enableHTTP2)) {
+				logTrace("Using HTTP/2 as requested per ALPN");
+				HTTP2Settings settings;
+				auto h2context = new HTTP2ServerContext(context, settings);
+				handleHTTP2Connection(tls_stream, connection, h2context);
+				return;
+			}
+
+			http_stream = tls_stream;
+		}
+	}
+
+	handleHTTP1Connection(connection, tls_stream, http_stream, context);
+
+	logTrace("Done handling connection.");
+}
+
+
+/**
+	Provides a HTTP request handler that responds with a static Diet template.
+*/
+@property HTTPServerRequestDelegateS staticTemplate(string template_file)()
+{
+	return (scope HTTPServerRequest req, scope HTTPServerResponse res){
+		res.render!(template_file, req);
+	};
+}
+
+/**
+	Provides a HTTP request handler that responds with a static redirection to the specified URL.
+
+	Params:
+		url = The URL to redirect to
+		status = Redirection status to use $(LPAREN)by default this is $(D HTTPStatus.found)$(RPAREN).
+
+	Returns:
+		Returns a $(D HTTPServerRequestDelegate) that performs the redirect
+*/
+HTTPServerRequestDelegate staticRedirect(string url, HTTPStatus status = HTTPStatus.found)
+@safe {
+	return (HTTPServerRequest req, HTTPServerResponse res){
+		res.redirect(url, status);
+	};
+}
+/// ditto
+HTTPServerRequestDelegate staticRedirect(URL url, HTTPStatus status = HTTPStatus.found)
+@safe {
+	return (HTTPServerRequest req, HTTPServerResponse res){
+		res.redirect(url, status);
+	};
+}
+
+///
+unittest {
+	import vibe.http.router;
+
+	void test()
+	{
+		auto router = new URLRouter;
+		router.get("/old_url", staticRedirect("http://example.org/new_url", HTTPStatus.movedPermanently));
+
+		listenHTTP(new HTTPServerSettings, router);
+	}
+}
+
+
+/**
+	Sets a VibeDist host to register with.
+*/
+void setVibeDistHost(string host, ushort port)
+@safe {
+	s_distHost = host;
+	s_distPort = port;
+}
+
+
+/**
+	Renders the given Diet template and makes all ALIASES available to the template.
+
+	You can call this function as a pseudo-member of `HTTPServerResponse` using
+	D's uniform function call syntax.
+
+	See_also: `diet.html.compileHTMLDietFile`
+
+	Examples:
+		---
+		string title = "Hello, World!";
+		int pageNumber = 1;
+		res.render!("mytemplate.dt", title, pageNumber);
+		---
+*/
+// comment cause no diet
+/+
+@property void render(string template_file, ALIASES...)(HTTPServerResponse res)
+{
+	res.contentType = "text/html; charset=UTF-8";
+	version (VibeUseOldDiet)
+		pragma(msg, "VibeUseOldDiet is not supported anymore. Please undefine in the package recipe.");
+	import vibe.stream.wrapper : streamOutputRange;
+	//import diet.html : compileHTMLDietFile;
+	auto output = streamOutputRange!1024(res.bodyWriter);
+	compileHTMLDietFile!(template_file, ALIASES, DefaultDietFilters)(output);
+}
+
+
+/**
+	Provides the default `css`, `javascript`, `markdown` and `htmlescape` filters
+ */
+@dietTraits
+struct DefaultDietFilters {
+	import diet.html : HTMLOutputStyle;
+	//import diet.traits : SafeFilterCallback;
+	import std.string : splitLines;
+
+	version (VibeOutputCompactHTML) enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.compact;
+	else enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty;
+
+	static string filterCss(I)(I text, size_t indent = 0)
+	{
+		auto lines = splitLines(text);
+
+		string indent_string = "\n";
+		while (indent-- > 0) indent_string ~= '\t';
+
+		string ret = indent_string~"<style><!--";
+		indent_string = indent_string ~ '\t';
+		foreach (ln; lines) ret ~= indent_string ~ ln;
+		indent_string = indent_string[0 .. $-1];
+		ret ~= indent_string ~ "--></style>";
+
+		return ret;
+	}
+
+
+	static string filterJavascript(I)(I text, size_t indent = 0)
+	{
+		auto lines = splitLines(text);
+
+		string indent_string = "\n";
+		while (indent-- > 0) indent_string ~= '\t';
+
+		string ret = indent_string~"<script>";
+		ret ~= indent_string~'\t' ~ "//<![CDATA[";
+		foreach (ln; lines) ret ~= indent_string ~ '\t' ~ ln;
+		ret ~= indent_string ~ '\t' ~ "//]]>" ~ indent_string ~ "</script>";
+
+		return ret;
+	}
+
+	static string filterMarkdown(I)(I text)
+	{
+		import vibe.textfilter.markdown : markdown = filterMarkdown;
+		// TODO: indent
+		return markdown(text);
+	}
+
+	static string filterHtmlescape(I)(I text)
+	{
+		import vibe.textfilter.html : htmlEscape;
+		// TODO: indent
+		return htmlEscape(text);
+	}
+
+	static this()
+	{
+		filters["css"] = (in input, scope output) { output(filterCss(input)); };
+		filters["javascript"] = (in input, scope output) { output(filterJavascript(input)); };
+		filters["markdown"] = (in input, scope output) { output(filterMarkdown(() @trusted { return cast(string)input; } ())); };
+		filters["htmlescape"] = (in input, scope output) { output(filterHtmlescape(input)); };
+	}
+
+	static SafeFilterCallback[string] filters;
+}
+
+
+unittest {
+	static string compile(string diet)() {
+		import std.array : appender;
+		import std.string : strip;
+		import diet.html : compileHTMLDietString;
+		auto dst = appender!string;
+		dst.compileHTMLDietString!(diet, DefaultDietFilters);
+		return strip(cast(string)(dst.data));
+	}
+
+	assert(compile!":css .test" == "<style><!--\n\t.test\n--></style>");
+	assert(compile!":javascript test();" == "<script>\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>");
+	assert(compile!":markdown **test**" == "<p><strong>test</strong>\n</p>");
+	assert(compile!":htmlescape <test>" == "&lt;test&gt;");
+	assert(compile!":css !{\".test\"}" == "<style><!--\n\t.test\n--></style>");
+	assert(compile!":javascript !{\"test();\"}" == "<script>\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>");
+	assert(compile!":markdown !{\"**test**\"}" == "<p><strong>test</strong>\n</p>");
+	assert(compile!":htmlescape !{\"<test>\"}" == "&lt;test&gt;");
+	assert(compile!":javascript\n\ttest();" == "<script>\n\t//<![CDATA[\n\ttest();\n\t//]]>\n</script>");
+}
++/
+
+/**
+	Creates a HTTPServerRequest suitable for writing unit tests.
+*/
+HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method = HTTPMethod.GET, InputStream data = null)
+@safe {
+	InetHeaderMap headers;
+	return createTestHTTPServerRequest(url, method, headers, data);
+}
+/// ditto
+HTTPServerRequest createTestHTTPServerRequest(URL url, HTTPMethod method, InetHeaderMap headers, InputStream data = null)
+@safe {
+	auto tls = url.schema == "https";
+	auto ret = new HTTPServerRequest(Clock.currTime(UTC()), url.port ? url.port : tls ? 443 : 80);
+	ret.m_settings = new HTTPServerSettings;
+	ret.requestPath = url.path;
+	ret.queryString = url.queryString;
+	ret.username = url.username;
+	ret.password = url.password;
+	ret.requestURI = url.localURI;
+	ret.method = method;
+	ret.tls = tls;
+	ret.headers = headers;
+	ret.bodyReader = data;
+	return ret;
+}
+
+/**
+	Creates a HTTPServerResponse suitable for writing unit tests.
+
+	Params:
+		data_sink = Optional output stream that captures the data that gets
+			written to the response
+		session_store = Optional session store to use when sessions are involved
+		data_mode = If set to `TestHTTPResponseMode.bodyOnly`, only the body
+			contents get written to `data_sink`. Otherwise the raw response
+			including the HTTP header is written.
+*/
+HTTPServerResponse createTestHTTPServerResponse(OutputStream data_sink = null,
+	SessionStore session_store = null,
+	TestHTTPResponseMode data_mode = TestHTTPResponseMode.plain)
+@safe {
+	import vibe.stream.wrapper : createProxyStream;
+	import vibe.http.internal.http1.server : HTTP1ServerExchange;
+
+	HTTPServerSettings settings;
+	if (session_store) {
+		settings = new HTTPServerSettings;
+		settings.sessionStore = session_store;
+	}
+
+	final class TestExchange : HTTP1ServerExchange {
+		this(StreamProxy conn, OutputStream body_writer)
+		{
+			super(conn, ConnectionStreamProxy.init);
+			m_bodyWriter = body_writer;
+		}
+	}
+
+	StreamProxy outstr;
+	if (data_sink && data_mode == TestHTTPResponseMode.plain)
+		outstr = createProxyStream(Stream.init, data_sink);
+	else outstr = createProxyStream(Stream.init, nullSink);
+
+	auto exchange = new TestExchange(outstr, data_sink && data_mode == TestHTTPResponseMode.bodyOnly ? data_sink : null);
+	auto ret = new HTTPServerResponse(exchange, settings, () @trusted { return vibeThreadAllocator(); } ());
+	return ret;
+}
+
+
+/**************************************************************************************************/
+/* Public types                                                                                   */
+/**************************************************************************************************/
+
+/// Delegate based request handler
+alias HTTPServerRequestDelegate = void delegate(HTTPServerRequest req, HTTPServerResponse res) @safe;
+/// Static function based request handler
+alias HTTPServerRequestFunction = void function(HTTPServerRequest req, HTTPServerResponse res) @safe;
+/// Interface for class based request handlers
+interface HTTPServerRequestHandler {
+	/// Handles incoming HTTP requests
+	void handleRequest(HTTPServerRequest req, HTTPServerResponse res) @safe ;
+}
+
+/// Delegate based request handler with scoped parameters
+alias HTTPServerRequestDelegateS = void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe;
+/// Static function based request handler with scoped parameters
+alias HTTPServerRequestFunctionS  = void function(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe;
+/// Interface for class based request handlers with scoped parameters
+interface HTTPServerRequestHandlerS {
+	/// Handles incoming HTTP requests
+	void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe;
+}
+
+unittest {
+	static assert(is(HTTPServerRequestDelegateS : HTTPServerRequestDelegate));
+	static assert(is(HTTPServerRequestFunctionS : HTTPServerRequestFunction));
+}
+
+/// Aggregates all information about an HTTP error status.
+final class HTTPServerErrorInfo {
+	/// The HTTP status code
+	int code;
+	/// The error message
+	string message;
+	/// Extended error message with debug information such as a stack trace
+	string debugMessage;
+	/// The error exception, if any
+	Throwable exception;
+}
+
+/// Delegate type used for user defined error page generator callbacks.
+alias HTTPServerErrorPageHandler = void delegate(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) @safe;
+
+
+enum TestHTTPResponseMode {
+	plain,
+	bodyOnly
+}
+
+
+/**
+	Specifies optional features of the HTTP server.
+
+	Disabling unneeded features can speed up the server or reduce its memory usage.
+
+	Note that the options `parseFormBody`, `parseJsonBody` and `parseMultiPartBody`
+	will also drain the `HTTPServerRequest.bodyReader` stream whenever a request
+	body with form or JSON data is encountered.
+*/
+enum HTTPServerOption {
+	none                      = 0,
+	/** Enables stack traces (`HTTPServerErrorInfo.debugMessage`).
+
+		Note that generating the stack traces are generally a costly
+		operation that should usually be avoided in production
+		environments. It can also reveal internal information about
+		the application, such as function addresses, which can
+		help an attacker to abuse possible security holes.
+	*/
+	errorStackTraces          = 1<<7,
+	/// Enable port reuse in `listenTCP()`
+	reusePort                 = 1<<8,
+	/// Enable address reuse in `listenTCP()`
+	reuseAddress              = 1<<10,
+	/// Enable *experimental* HTTP/2 support
+	enableHTTP2               = 1<<11,
+	/** The default set of options.
+
+		Includes all parsing options, as well as the `errorStackTraces`
+		option if the code is compiled in debug mode.
+	*/
+	defaults                  = () { auto ret = reuseAddress; debug ret |= errorStackTraces; return ret; } (),
+}
+
+
+/**
+	Contains all settings for configuring a basic HTTP server.
+
+	The defaults are sufficient for most normal uses.
+*/
+final class HTTPServerSettings {
+	/** The port on which the HTTP server is listening.
+
+		The default value is 80. If you are running a TLS enabled server you may want to set this
+		to 443 instead.
+
+		Using a value of `0` instructs the server to use any available port on
+		the given `bindAddresses` the actual addresses and ports can then be
+		queried with `TCPListener.bindAddresses`.
+	*/
+	ushort port = 80;
+
+	/** The interfaces on which the HTTP server is listening.
+
+		By default, the server will listen on all IPv4 and IPv6 interfaces.
+	*/
+	string[] bindAddresses = ["::", "0.0.0.0"];
+
+	/** Determines the server host name.
+
+		If multiple servers are listening on the same port, the host name will determine which one
+		gets a request.
+	*/
+	string hostName;
+
+	/** Provides a way to reject incoming connections as early as possible.
+
+		Allows to ban and unban network addresses and reduce the impact of DOS
+		attacks.
+
+		If the callback returns `true` for a specific `NetworkAddress`,
+		then all incoming requests from that address will be rejected.
+	*/
+	RejectConnectionPredicate rejectConnectionPredicate;
+
+	/** Configures optional features of the HTTP server
+
+		Disabling unneeded features can improve performance or reduce the server
+		load in case of invalid or unwanted requests (DoS). By default,
+		HTTPServerOption.defaults is used.
+	*/
+	HTTPServerOption options = HTTPServerOption.defaults;
+
+	/** Time of a request after which the connection is closed with an error; not supported yet
+
+		The default limit of 0 means that the request time is not limited.
+	*/
+	Duration maxRequestTime = 0.seconds;
+
+	/** Maximum time between two request on a keep-alive connection
+
+		The default value is 10 seconds.
+	*/
+	Duration keepAliveTimeout = 10.seconds;
+
+	/// Maximum number of transferred bytes per request after which the connection is closed with
+	/// an error
+	ulong maxRequestSize = 2097152;
+
+
+	///	Maximum number of transferred bytes for the request header. This includes the request line
+	/// the url and all headers.
+	ulong maxRequestHeaderSize = 8192;
+
+	/// Maximum number of bytes in a single line in the request header.
+	size_t maxRequestHeaderLineSize = 4096;
+
+	/// Sets a custom handler for displaying error pages for HTTP errors
+	@property HTTPServerErrorPageHandler errorPageHandler() @safe { return errorPageHandler_; }
+	/// ditto
+	@property void errorPageHandler(HTTPServerErrorPageHandler del) @safe { errorPageHandler_ = del; }
+	/// Scheduled for deprecation - use a `@safe` callback instead.
+	@property void errorPageHandler(void delegate(HTTPServerRequest, HTTPServerResponse, HTTPServerErrorInfo) @system del)
+	@system {
+		this.errorPageHandler = (req, res, err) @trusted { del(req, res, err); };
+	}
+
+	package HTTPServerErrorPageHandler errorPageHandler_ = null;
+
+	/// If set, a HTTPS server will be started instead of plain HTTP.
+	TLSContext tlsContext;
+
+	/// Session management is enabled if a session store instance is provided
+	SessionStore sessionStore;
+	string sessionIdCookie = "vibe.session_id";
+
+	/// Session options to use when initializing a new session.
+	SessionOption sessionOptions = SessionOption.httpOnly;
+
+	///
+	import vibe.core.core : vibeVersionString;
+	string serverString = "vibe.d/" ~ vibeVersionString;
+
+	/** Specifies the format used for the access log.
+
+		The log format is given using the Apache server syntax. By default NCSA combined is used.
+
+		---
+		"%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""
+		---
+	*/
+	string accessLogFormat = "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"";
+
+	/// Spefifies the name of a file to which access log messages are appended.
+	string accessLogFile = "";
+
+	/// If set, access log entries will be output to the console.
+	bool accessLogToConsole = false;
+
+	/** Specifies a custom access logger instance.
+	*/
+	HTTPLogger accessLogger;
+
+	/// Returns a duplicate of the settings object.
+	@property HTTPServerSettings dup()
+	@safe {
+		auto ret = new HTTPServerSettings;
+		foreach (mem; __traits(allMembers, HTTPServerSettings)) {
+			static if (mem == "sslContext") {}
+			else static if (mem == "bindAddresses") ret.bindAddresses = bindAddresses.dup;
+			else static if (__traits(compiles, __traits(getMember, ret, mem) = __traits(getMember, this, mem)))
+				__traits(getMember, ret, mem) = __traits(getMember, this, mem);
+		}
+		return ret;
+	}
+
+	/// Disable support for VibeDist and instead start listening immediately.
+	bool disableDistHost = false;
+
+	/** Responds to "Accept-Encoding" by using compression if possible.
+
+		Compression can also be manually enabled by setting the
+		"Content-Encoding" header of the HTTP response appropriately before
+		sending the response body.
+
+		This setting is disabled by default. Also note that there are still some
+		known issues with the GZIP compression code.
+	*/
+	bool useCompressionIfPossible = false;
+
+
+	/** Interval between WebSocket ping frames.
+
+		The default value is 60 seconds; set to Duration.zero to disable pings.
+	*/
+	Duration webSocketPingInterval = 60.seconds;
+
+	/** Constructs a new settings object with default values.
+	*/
+	this() @safe {}
+
+	/** Constructs a new settings object with a custom bind interface and/or port.
+
+		The syntax of `bind_string` is `[<IP address>][:<port>]`, where either of
+		the two parts can be left off. IPv6 addresses must be enclosed in square
+		brackets, as they would within a URL.
+
+		Throws:
+			An exception is thrown if `bind_string` is malformed.
+	*/
+	this(string bind_string)
+	@safe {
+		this();
+
+		if (bind_string.startsWith('[')) {
+			auto idx = bind_string.indexOf(']');
+			enforce(idx > 0, "Missing closing bracket for IPv6 address.");
+			bindAddresses = [bind_string[1 .. idx]];
+			bind_string = bind_string[idx+1 .. $];
+
+			enforce(bind_string.length == 0 || bind_string.startsWith(':'),
+				"Only a colon may follow the IPv6 address.");
+		}
+
+		auto idx = bind_string.indexOf(':');
+		if (idx < 0) {
+			if (bind_string.length > 0) bindAddresses = [bind_string];
+		} else {
+			if (idx > 0) bindAddresses = [bind_string[0 .. idx]];
+			port = bind_string[idx+1 .. $].to!ushort;
+		}
+	}
+
+	///
+	unittest {
+		auto s = new HTTPServerSettings(":8080");
+		assert(s.bindAddresses == ["::", "0.0.0.0"]); // default bind addresses
+		assert(s.port == 8080);
+
+		s = new HTTPServerSettings("123.123.123.123");
+		assert(s.bindAddresses == ["123.123.123.123"]);
+		assert(s.port == 80);
+
+		s = new HTTPServerSettings("[::1]:443");
+		assert(s.bindAddresses == ["::1"]);
+		assert(s.port == 443);
+	}
+}
+
+
+/// Callback type used to determine whether to reject incoming connections
+alias RejectConnectionPredicate = bool delegate (in NetworkAddress) @safe nothrow;
+
+
+/**
+	Options altering how sessions are created.
+
+	Multiple values can be or'ed together.
+
+	See_Also: HTTPServerResponse.startSession
+*/
+enum SessionOption {
+	/// No options.
+	none = 0,
+
+	/** Instructs the browser to disallow accessing the session ID from JavaScript.
+
+		See_Also: Cookie.httpOnly
+	*/
+	httpOnly = 1<<0,
+
+	/** Instructs the browser to disallow sending the session ID over
+		unencrypted connections.
+
+		By default, the type of the connection on which the session is started
+		will be used to determine if secure or noSecure is used.
+
+		See_Also: noSecure, Cookie.secure
+	*/
+	secure = 1<<1,
+
+	/** Instructs the browser to allow sending the session ID over unencrypted
+		connections.
+
+		By default, the type of the connection on which the session is started
+		will be used to determine if secure or noSecure is used.
+
+		See_Also: secure, Cookie.secure
+	*/
+	noSecure = 1<<2,
+
+	/**
+    Instructs the browser to allow sending this cookie along with cross-site requests.
+
+    By default, the protection is `strict`. This flag allows to set it to `lax`.
+    The strict value will prevent the cookie from being sent by the browser
+    to the target site in all cross-site browsing context,
+    even when following a regular link.
+	*/
+	noSameSiteStrict = 1<<3,
+}
+
+
+/**
+	Represents a HTTP request as received by the server side.
+*/
+final class HTTPServerRequest : HTTPRequest {
+	import std.variant : Variant;
+	import vibe.container.dictionarylist : DictionaryList;
+
+	package {
+		SysTime m_timeCreated;
+		HTTPServerSettings m_settings;
+		ushort m_port;
+		string m_peer;
+
+		// lazily parsed request components
+		Nullable!string m_path;
+		Nullable!CookieValueMap m_cookies;
+		Nullable!FormFields m_query;
+		Nullable!Json m_json;
+		Nullable!FormFields m_form;
+		FilePartFormFields m_files;
+	}
+
+	/// The IP address of the client
+	NetworkAddress clientAddress;
+
+	/// Determines if the request should be logged to the access log file.
+	bool noLog;
+
+	/// Determines if the request was issued over an TLS encrypted channel.
+	bool tls;
+
+	/** Information about the TLS certificate provided by the client.
+
+		Remarks: This field is only set if `tls` is true, and the peer
+		presented a client certificate.
+	*/
+	TLSCertificateInformation clientCertificate;
+
+	/** The path part of the requested URI.
+	*/
+	InetPath requestPath;
+
+	/** The user name part of the URL, if present.
+	*/
+	string username;
+
+	/** The _password part of the URL, if present.
+	*/
+	string password;
+
+	/** The _query string part of the URL.
+	*/
+	string queryString;
+
+	/** A map of general parameters for the request.
+
+		This map is supposed to be used by middleware functionality to store
+		information for later stages. For example vibe.http.router.URLRouter uses this map
+		to store the value of any named placeholders.
+	*/
+	DictionaryList!(string, true, 8) params;
+
+	/** A map of context items for the request.
+
+		This is especially useful for passing application specific data down
+		the chain of processors along with the request itself.
+
+		For example, a generic route may be defined to check user login status,
+		if the user is logged in, add a reference to user specific data to the
+		context.
+
+		This is implemented with `std.variant.Variant` to allow any type of data.
+	*/
+	DictionaryList!(Variant, true, 2) context;
+
+	/** Supplies the request body as a stream.
+
+		Note that when certain server options are set (such as
+		HTTPServerOption.parseJsonBody) and a matching request was sent,
+		the returned stream will be empty. If needed, remove those
+		options and do your own processing of the body when launching
+		the server. HTTPServerOption has a list of all options that affect
+		the request body.
+	*/
+	InputStream bodyReader;
+
+	/** The current Session object.
+
+		This field is set if HTTPServerResponse.startSession() has been called
+		on a previous response and if the client has sent back the matching
+		cookie.
+
+		Remarks: Requires the HTTPServerOption.parseCookies option.
+	*/
+	Session session;
+
+
+	this(SysTime time, ushort port)
+	@safe scope {
+		m_timeCreated = time.toUTC();
+		m_port = port;
+	}
+
+	/// The IP address of the client in string form
+	@property string peer()
+	@safe nothrow scope {
+		if (!m_peer) {
+			version (Have_vibe_core) {} else scope (failure) assert(false);
+			// store the IP address (IPv4 addresses forwarded over IPv6 are stored in IPv4 format)
+			auto peer_address_string = this.clientAddress.toString();
+			if (peer_address_string.startsWith("::ffff:") && peer_address_string[7 .. $].indexOf(':') < 0)
+				m_peer = peer_address_string[7 .. $];
+			else m_peer = peer_address_string;
+		}
+		return m_peer;
+	}
+
+	/** The _path part of the URL.
+
+		Note that this function contains the decoded version of the
+		requested path, which can yield incorrect results if the path
+		contains URL encoded path separators. Use `requestPath` instead to
+		get an encoding-aware representation.
+	*/
+	deprecated("Use .requestPath instead")
+	string path()
+	@safe scope {
+		if (m_path.isNull)
+			m_path = urlDecode(requestPath.toString);
+		return m_path.get;
+	}
+
+	/** Contains the list of cookies that are stored on the client.
+
+		Note that the a single cookie name may occur multiple times if multiple
+		cookies have that name but different paths or domains that all match
+		the request URI. By default, the first cookie will be returned, which is
+		the or one of the cookies with the closest path match.
+	*/
+	@property ref CookieValueMap cookies()
+	@safe return {
+		if (m_cookies.isNull) {
+			m_cookies = CookieValueMap.init;
+			if (auto pv = "cookie" in headers)
+				parseCookies(*pv, m_cookies.get);
+		}
+		return m_cookies.get;
+	}
+
+	/** Contains all _form fields supplied using the _query string.
+
+		The fields are stored in the same order as they are received.
+	*/
+	@property ref FormFields query()
+	@safe return {
+		if (m_query.isNull) {
+			m_query = FormFields.init;
+			parseURLEncodedForm(queryString, m_query.get);
+		}
+
+		return m_query.get;
+	}
+
+	/** Contains the parsed Json for a JSON request.
+
+		A JSON request must have the Content-Type "application/json" or "application/vnd.api+json".
+	*/
+	@property ref Json json()
+	@safe return {
+		if (m_json.isNull) {
+			auto splitter = contentType.splitter(';');
+			auto ctype = splitter.empty ? "" : splitter.front;
+
+			if (icmp2(ctype, "application/json") == 0 || icmp2(ctype, "application/vnd.api+json") == 0) {
+				auto bodyStr = bodyReader.readAllUTF8();
+				if (!bodyStr.empty) m_json = parseJson(bodyStr);
+				else m_json = Json.undefined;
+			} else {
+				m_json = Json.undefined;
+			}
+		}
+		return m_json.get;
+	}
+
+	/// Get the json body when there is no content-type header
+	unittest {
+		assert(createTestHTTPServerRequest(URL("http://localhost/")).json.type == Json.Type.undefined);
+	}
+
+	/** Contains the parsed parameters of a HTML POST _form request.
+
+		The fields are stored in the same order as they are received.
+
+		Remarks:
+			A form request must either have the Content-Type
+			"application/x-www-form-urlencoded" or "multipart/form-data".
+	*/
+	@property ref FormFields form()
+	@safe return {
+		if (m_form.isNull)
+			parseFormAndFiles();
+
+		return m_form.get;
+	}
+
+	/** Contains information about any uploaded file for a HTML _form request.
+	*/
+	@property ref FilePartFormFields files()
+	@safe return {
+		// m_form and m_files are parsed in one step
+		if (m_form.isNull) {
+			parseFormAndFiles();
+			assert(!m_form.isNull);
+		}
+
+        return m_files;
+	}
+
+	/** Time when this request started processing.
+	*/
+	@property SysTime timeCreated() const @safe scope { return m_timeCreated; }
+
+
+	/** The full URL that corresponds to this request.
+
+		The host URL includes the protocol, host and optionally the user
+		and password that was used for this request. This field is useful to
+		construct self referencing URLs.
+
+		Note that the port is currently not set, so that this only works if
+		the standard port is used.
+	*/
+	@property URL fullURL()
+	const @safe scope {
+		URL url;
+
+		auto xfh = this.headers.get("X-Forwarded-Host");
+		auto xfp = this.headers.get("X-Forwarded-Port");
+		auto xfpr = this.headers.get("X-Forwarded-Proto");
+
+		// Set URL host segment.
+		if (xfh.length) {
+			url.host = xfh;
+		} else if (!this.host.empty) {
+			url.host = this.host;
+		} else if (!m_settings.hostName.empty) {
+			url.host = m_settings.hostName;
+		} else {
+			url.host = m_settings.bindAddresses[0];
+		}
+
+		// Set URL schema segment.
+		if (xfpr.length) {
+			url.schema = xfpr;
+		} else if (this.tls) {
+			url.schema = "https";
+		} else {
+			url.schema = "http";
+		}
+
+		// Set URL port segment.
+		if (xfp.length) {
+			try {
+				url.port = xfp.to!ushort;
+			} catch (ConvException) {
+				// TODO : Consider responding with a 400/etc. error from here.
+				logWarn("X-Forwarded-Port header was not valid port (%s)", xfp);
+			}
+		} else if (!xfh) {
+			if (url.schema == "https") {
+				if (m_port != 443U) url.port = m_port;
+			} else {
+				if (m_port != 80U)  url.port = m_port;
+			}
+		}
+
+		if (url.host.startsWith('[')) { // handle IPv6 address
+			auto idx = url.host.indexOf(']');
+			if (idx >= 0 && idx+1 < url.host.length && url.host[idx+1] == ':')
+				url.host = url.host[1 .. idx];
+		} else { // handle normal host names or IPv4 address
+			auto idx = url.host.indexOf(':');
+			if (idx >= 0) url.host = url.host[0 .. idx];
+		}
+
+		url.username = this.username;
+		url.password = this.password;
+		url.localURI = this.requestURI;
+
+		return url;
+	}
+
+	/** The relative path to the root folder.
+
+		Using this function instead of absolute URLs for embedded links can be
+		useful to avoid dead link when the site is piped through a
+		reverse-proxy.
+
+		The returned string always ends with a slash.
+	*/
+	@property string rootDir()
+	const @safe scope {
+		import std.algorithm.searching : count;
+		import std.range : empty;
+		auto depth = requestPath.bySegment.count!(s => !s.name.empty);
+		if (depth > 0 && !requestPath.endsWithSlash) depth--;
+		return depth == 0 ? "./" : replicate("../", depth);
+	}
+
+	unittest {
+		assert(createTestHTTPServerRequest(URL("http://localhost/")).rootDir == "./");
+		assert(createTestHTTPServerRequest(URL("http://localhost/foo")).rootDir == "./");
+		assert(createTestHTTPServerRequest(URL("http://localhost/foo/")).rootDir == "../");
+		assert(createTestHTTPServerRequest(URL("http://localhost/foo/bar")).rootDir == "../");
+		assert(createTestHTTPServerRequest(URL("http://localhost")).rootDir == "./");
+	}
+
+	/** The settings of the server serving this request.
+	 */
+	package @property const(HTTPServerSettings) serverSettings()
+	const @safe scope {
+		return m_settings;
+	}
+
+	private void parseFormAndFiles()
+	@safe scope {
+		m_form = FormFields.init;
+		parseFormData(m_form.get, m_files, headers.get("Content-Type", ""), bodyReader, m_settings.maxRequestHeaderLineSize);
+	}
+}
+
+
+/**
+	Represents a HTTP response as sent from the server side.
+*/
+final class HTTPServerResponse : HTTPResponse {
+	alias Allocator = typeof(vibeThreadAllocator());
+
+	package {
+		HTTPServerExchange m_exchange;
+		Allocator m_requestAlloc;
+		HTTPServerSettings m_settings;
+		Session m_session;
+		bool m_tls;
+		SysTime m_timeFinalized;
+	}
+
+	static if (!is(Stream == StreamProxy)) {
+		deprecated("Use the constructor taking a HTTPServerExchange argument instead")
+		this(Stream conn, ConnectionStream raw_connection, HTTPServerSettings settings, Allocator req_alloc)
+		@safe scope {
+			this(StreamProxy(conn), InterfaceProxy!ConnectionStream(raw_connection), settings, req_alloc);
+		}
+	}
+
+	deprecated("Use the constructor taking a HTTPServerExchange argument instead")
+	this(StreamProxy conn, ConnectionStreamProxy raw_connection, HTTPServerSettings settings, Allocator req_alloc)
+	@safe scope {
+		import vibe.http.internal.http1.server : HTTP1ServerExchange;
+		this(new HTTP1ServerExchange(conn, raw_connection), settings, req_alloc);
+	}
+
+	this(HTTPServerExchange exchange, HTTPServerSettings settings, Allocator req_alloc)
+	@safe {
+		m_exchange = exchange;
+		m_settings = settings;
+		m_requestAlloc = req_alloc;
+	}
+
+	/** Sends a redirect request to the client.
+
+		Params:
+			url = The URL to redirect to
+			status = The HTTP redirect status (3xx) to send - by default this is $(D HTTPStatus.found)
+	*/
+	void redirect(string url, int status = HTTPStatus.found)
+	@safe scope {
+		// Disallow any characters that may influence the header parsing
+		enforce(!url.representation.canFind!(ch => ch < 0x20),
+			"Control character in redirection URL.");
+
+		statusCode = status;
+		headers["Location"] = url;
+		writeBody("redirecting...");
+	}
+	/// ditto
+	void redirect(URL url, int status = HTTPStatus.found)
+	@safe scope {
+		redirect(url.toString(), status);
+	}
+
+	///
+	@safe unittest {
+		import vibe.http.router;
+
+		void request_handler(HTTPServerRequest req, HTTPServerResponse res)
+		{
+			res.redirect("http://example.org/some_other_url");
+		}
+
+		void test()
+		{
+			auto router = new URLRouter;
+			router.get("/old_url", &request_handler);
+
+			listenHTTP(new HTTPServerSettings, router);
+		}
+	}
+
+scope:
+
+	/** Returns the time at which the request was finalized.
+
+		Note that this field will only be set after `finalize` has been called.
+	*/
+	@property SysTime timeFinalized() const @safe { return m_timeFinalized; }
+
+	/** Determines if the HTTP header has already been written.
+	*/
+	@property bool headerWritten() const @safe { return m_exchange.headerWritten; }
+
+	/** Determines if the response does not need a body.
+	*/
+	bool isHeadResponse() const @safe { return m_exchange.isHeadResponse; }
+
+	/** Determines if the response is sent over an encrypted connection.
+	*/
+	bool tls() const @safe { return m_tls; }
+
+	/** Writes the entire response body at once.
+
+		Params:
+			data = The data to write as the body contents
+			status = Optional response status code to set
+			content_type = Optional content type to apply to the response.
+				If no content type is given and no "Content-Type" header is
+				set in the response, this will default to
+				`"application/octet-stream"`.
+
+		See_Also: `HTTPStatusCode`
+	*/
+	void writeBody(in ubyte[] data, string content_type = null)
+	@safe {
+		if (content_type.length) headers["Content-Type"] = content_type;
+		else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream";
+		headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", data.length);
+		bodyWriter.write(data);
+	}
+	/// ditto
+	void writeBody(in ubyte[] data, int status, string content_type = null)
+	@safe {
+		statusCode = status;
+		writeBody(data, content_type);
+	}
+	/// ditto
+	void writeBody(scope InputStream data, string content_type = null)
+	@safe {
+		if (content_type.length) headers["Content-Type"] = content_type;
+		else if ("Content-Type" !in headers) headers["Content-Type"] = "application/octet-stream";
+		data.pipe(bodyWriter);
+	}
+	/// ditto
+	void writeBody(scope InputStream data, int status, string content_type = null)
+	{
+		statusCode = status;
+		writeBody(data, content_type);
+	}
+
+	/** Writes the entire response body as a single string.
+
+		Params:
+			data = The string to write as the body contents
+			status = Optional response status code to set
+			content_type = Optional content type to apply to the response.
+				If no content type is given and no "Content-Type" header is
+				set in the response, this will default to
+				`"text/plain; charset=UTF-8"`.
+
+		See_Also: `HTTPStatusCode`
+	*/
+	void writeBody(string data, string content_type = null)
+	@safe {
+		if (!content_type.length && "Content-Type" !in headers)
+			content_type = "text/plain; charset=UTF-8";
+		writeBody(cast(const(ubyte)[])data, content_type);
+	}
+	/// ditto
+	void writeBody(string data, int status, string content_type = null)
+	@safe {
+		statusCode = status;
+		writeBody(data, content_type);
+	}
+
+	/** Writes the whole response body at once, without doing any further encoding.
+
+		The caller has to make sure that the appropriate headers are set correctly
+		(i.e. Content-Type and Content-Encoding).
+
+		Note that the version taking a RandomAccessStream may perform additional
+		optimizations such as sending a file directly from the disk to the
+		network card using a DMA transfer.
+
+	*/
+	void writeRawBody(RandomAccessStream)(RandomAccessStream stream) @safe
+		if (isRandomAccessStream!RandomAccessStream)
+	{
+		m_exchange.writeBody(this, RandomAccessStreamProxy(stream));
+	}
+	/// ditto
+	void writeRawBody(InputStream)(InputStream stream, size_t num_bytes = size_t.max) @safe
+		if (isInputStream!InputStream && !isRandomAccessStream!InputStream)
+	{
+		m_exchange.writeBody(this, InputStreamProxy(stream), num_bytes == 0 ? size_t.max : num_bytes);
+	}
+	/// ditto
+	void writeRawBody(RandomAccessStream)(RandomAccessStream stream, int status) @safe
+		if (isRandomAccessStream!RandomAccessStream)
+	{
+		statusCode = status;
+		writeRawBody(stream);
+	}
+	/// ditto
+	void writeRawBody(InputStream)(InputStream stream, int status, size_t num_bytes = 0) @safe
+		if (isInputStream!InputStream && !isRandomAccessStream!InputStream)
+	{
+		statusCode = status;
+		writeRawBody(stream, num_bytes);
+	}
+
+
+	/// Writes a JSON message with the specified status
+	void writeJsonBody(T)(T data, int status, bool allow_chunked = false)
+	{
+		statusCode = status;
+		writeJsonBody(data, allow_chunked);
+	}
+	/// ditto
+	void writeJsonBody(T)(T data, int status, string content_type, bool allow_chunked = false)
+	{
+		statusCode = status;
+		writeJsonBody(data, content_type, allow_chunked);
+	}
+
+	/// ditto
+	void writeJsonBody(T)(T data, string content_type, bool allow_chunked = false)
+	{
+		headers["Content-Type"] = content_type;
+		writeJsonBody(data, allow_chunked);
+	}
+	/// ditto
+	void writeJsonBody(T)(T data, bool allow_chunked = false)
+	{
+		doWriteJsonBody!(T, false)(data, allow_chunked);
+	}
+	/// ditto
+	void writePrettyJsonBody(T)(T data, bool allow_chunked = false)
+	{
+		doWriteJsonBody!(T, true)(data, allow_chunked);
+	}
+
+	private void doWriteJsonBody(T, bool PRETTY)(T data, bool allow_chunked = false)
+	{
+		import std.traits;
+		import vibe.stream.wrapper;
+
+		static if (!is(T == Json) && is(typeof(data.data())) && isArray!(typeof(data.data()))) {
+			static assert(!is(T == Appender!(typeof(data.data()))), "Passed an Appender!T to writeJsonBody - this is most probably not doing what's indended.");
+		}
+
+		if ("Content-Type" !in headers)
+			headers["Content-Type"] = "application/json; charset=UTF-8";
+
+
+		// set an explicit content-length field if chunked encoding is not allowed
+		if (!allow_chunked) {
+			import vibe.internal.rangeutil;
+			long length = 0;
+			auto counter = RangeCounter(() @trusted { return &length; } ());
+			static if (PRETTY) serializeToPrettyJson(counter, data);
+			else serializeToJson(counter, data);
+			headers["Content-Length"] = formatAlloc(m_requestAlloc, "%d", length);
+		}
+
+		auto rng = streamOutputRange!1024(bodyWriter);
+		static if (PRETTY) serializeToPrettyJson(() @trusted { return &rng; } (), data);
+		else serializeToJson(() @trusted { return &rng; } (), data);
+	}
+
+	/**
+	 * Writes the response with no body.
+	 *
+	 * This method should be used in situations where no body is
+	 * requested, such as a HEAD request. For an empty body, just use writeBody,
+	 * as this method causes problems with some keep-alive connections.
+	 */
+	void writeVoidBody()
+	@safe {
+		m_exchange.writeVoidBody(this);
+	}
+
+	/** A stream for writing the body of the HTTP response.
+
+		Note that after 'bodyWriter' has been accessed for the first time, it
+		is not allowed to change any header or the status code of the response.
+	*/
+	@property InterfaceProxy!OutputStream bodyWriter()
+	@safe scope {
+		assert(!!m_exchange);
+		return m_exchange.bodyWriter(this);
+	}
+
+
+
+	/** Special method sending a SWITCHING_PROTOCOLS response to the client.
+
+		Notice: For the overload that returns a `ConnectionStream`, it must be
+			ensured that the returned instance doesn't outlive the request
+			handler callback.
+
+		Params:
+			protocol = The protocol set in the "Upgrade" header of the response.
+				Use an empty string to skip setting this field.
+	*/
+	ConnectionStream switchProtocol(string protocol)
+	@safe {
+		return m_exchange.switchProtocol(this, protocol);
+	}
+	/// ditto
+	void switchProtocol(string protocol, scope void delegate(scope ConnectionStream) @safe del)
+	@safe {
+		m_exchange.switchProtocol(this, protocol, del);
+	}
+
+	/** Special method for handling CONNECT proxy tunnel
+
+		Notice: For the overload that returns a `ConnectionStream`, it must be
+			ensured that the returned instance doesn't outlive the request
+			handler callback.
+	*/
+	ConnectionStream connectProxy()
+	@safe {
+		return m_exchange.connectProxy(this);
+	}
+	/// ditto
+	void connectProxy(scope void delegate(scope ConnectionStream) @safe del)
+	@safe {
+		m_exchange.connectProxy(this, del);
+	}
+
+	/** Sets the specified cookie value.
+
+		Params:
+			name = Name of the cookie
+			value = New cookie value - pass null to clear the cookie
+			path = Path (as seen by the client) of the directory tree in which the cookie is visible
+			encoding = Optional encoding (url, raw), default to URL encoding
+	*/
+	Cookie setCookie(string name, string value, string path = "/", Cookie.Encoding encoding = Cookie.Encoding.url)
+	@safe {
+		auto cookie = new Cookie();
+		cookie.path = path;
+		cookie.setValue(value, encoding);
+		if (value is null) {
+			cookie.maxAge = 0;
+			cookie.expires = "Thu, 01 Jan 1970 00:00:00 GMT";
+		}
+		cookies[name] = cookie;
+		return cookie;
+	}
+
+	/**
+		Initiates a new session.
+
+		The session is stored in the SessionStore that was specified when
+		creating the server. Depending on this, the session can be persistent
+		or temporary and specific to this server instance.
+	*/
+	Session startSession(string path = "/")
+	@safe {
+		return startSession(path, m_settings.sessionOptions);
+	}
+
+	/// ditto
+	Session startSession(string path, SessionOption options)
+	@safe {
+		assert(m_settings.sessionStore, "no session store set");
+		assert(!m_session, "Try to start a session, but already started one.");
+
+		bool secure;
+		if (options & SessionOption.secure) secure = true;
+		else if (options & SessionOption.noSecure) secure = false;
+		else secure = this.tls;
+
+		m_session = m_settings.sessionStore.create();
+		m_session.set("$sessionCookiePath", path);
+		m_session.set("$sessionCookieSecure", secure);
+		auto cookie = setCookie(m_settings.sessionIdCookie, m_session.id, path);
+		cookie.secure = secure;
+		cookie.httpOnly = (options & SessionOption.httpOnly) != 0;
+		cookie.sameSite = (options & SessionOption.noSameSiteStrict) ?
+						  Cookie.SameSite.lax : Cookie.SameSite.strict;
+		return m_session;
+	}
+
+	/**
+		Terminates the current session (if any).
+	*/
+	void terminateSession()
+	@safe {
+		if (!m_session) return;
+		auto cookie = setCookie(m_settings.sessionIdCookie, null, m_session.get!string("$sessionCookiePath"));
+		cookie.secure = m_session.get!bool("$sessionCookieSecure");
+		m_session.destroy();
+		m_session = Session.init;
+	}
+
+	@property ulong bytesWritten() @safe const { return m_exchange.bytesWritten; }
+
+	/**
+		Waits until either the connection closes, data arrives, or until the
+		given timeout is reached.
+
+		Returns:
+			$(D true) if the connection was closed and $(D false) if either the
+			timeout was reached, or if data has arrived for consumption.
+
+		See_Also: `connected`
+	*/
+	bool waitForConnectionClose(Duration timeout = Duration.max)
+	@safe {
+		return m_exchange.waitForConnectionClose(timeout);
+	}
+
+	/**
+		Determines if the underlying connection is still alive.
+
+		Returns $(D true) if the remote peer is still connected and $(D false)
+		if the remote peer closed the connection.
+
+		See_Also: `waitForConnectionClose`
+	*/
+	@property bool connected()
+	@safe const {
+		return m_exchange.connected;
+	}
+
+	/**
+		Finalizes the response. This is usually called automatically by the server.
+
+		This method can be called manually after writing the response to force
+		all network traffic associated with the current request to be finalized.
+		After the call returns, the `timeFinalized` property will be set.
+	*/
+	void finalize()
+	@safe {
+		m_exchange.finalize(this);
+	}
+}
+
+interface HTTPServerExchange {
+@safe:
+	@property bool isHeadResponse() const;
+	@property bool headerWritten() const;
+	@property ulong bytesWritten() const;
+	@property bool connected() const;
+
+	bool waitForConnectionClose(Duration timeout);
+	void writeBody(HTTPServerResponse res, RandomAccessStreamProxy streamx);
+	void writeBody(HTTPServerResponse res, InputStreamProxy stream, ulong num_bytes = ulong.max);
+	void writeVoidBody(HTTPServerResponse res);
+	OutputStreamProxy bodyWriter(HTTPServerResponse res);
+	ConnectionStream switchProtocol(HTTPServerResponse res, string protocol);
+	void switchProtocol(HTTPServerResponse res, string protocol, scope void delegate(scope ConnectionStream) @safe del);
+	ConnectionStream connectProxy(HTTPServerResponse res);
+	void connectProxy(HTTPServerResponse res, scope void delegate(scope ConnectionStream) @safe del);
+	void finalize(HTTPServerResponse res);
+}
+
+
+/**
+	Represents the request listener for a specific `listenHTTP` call.
+
+	This struct can be used to stop listening for HTTP requests at runtime.
+*/
+struct HTTPListener {
+	private {
+		size_t[] m_virtualHostIDs;
+	}
+
+	private this(size_t[] ids) @safe { m_virtualHostIDs = ids; }
+
+	@property NetworkAddress[] bindAddresses()
+	@safe {
+		NetworkAddress[] ret;
+		foreach (l; s_listeners)
+			if (l.m_virtualHosts.canFind!(v => m_virtualHostIDs.canFind(v.id))) {
+				NetworkAddress a;
+				a = resolveHost(l.bindAddress);
+				a.port = l.bindPort;
+				ret ~= a;
+			}
+		return ret;
+	}
+
+	/** Stops handling HTTP requests and closes the TCP listening port if
+		possible.
+	*/
+	void stopListening()
+	@safe {
+		import std.algorithm : countUntil;
+
+		foreach (vhid; m_virtualHostIDs) {
+			foreach (lidx, l; s_listeners) {
+				if (l.removeVirtualHost(vhid)) {
+					if (!l.hasVirtualHosts) {
+						l.m_listener.stopListening();
+						logInfo("Stopped to listen for HTTP%s requests on %s:%s", l.tlsContext ? "S": "", l.bindAddress, l.bindPort);
+						s_listeners = s_listeners[0 .. lidx] ~ s_listeners[lidx+1 .. $];
+					}
+					break;
+				}
+			}
+		}
+	}
+}
+
+
+/** Represents a single HTTP server port.
+
+	This class defines the incoming interface, port, and TLS configuration of
+	the public server port. The public server port may differ from the local
+	one if a reverse proxy of some kind is facing the public internet and
+	forwards to this HTTP server.
+
+	Multiple virtual hosts can be configured to be served from the same port.
+	Their TLS settings must be compatible and each virtual host must have a
+	unique name.
+*/
+final class HTTPServerContext {
+	package struct VirtualHost {
+		HTTPServerRequestDelegate requestHandler;
+		HTTPServerSettings settings;
+		HTTPLogger[] loggers;
+		size_t id;
+	}
+
+	package {
+		TCPListener m_listener;
+		VirtualHost[] m_virtualHosts;
+		string m_bindAddress;
+		ushort m_bindPort;
+		TLSContext m_tlsContext;
+		static size_t s_vhostIDCounter = 1;
+	}
+
+	@safe:
+
+	this(string bind_address, ushort bind_port)
+	{
+		m_bindAddress = bind_address;
+		m_bindPort = bind_port;
+	}
+
+	/** Returns the TLS context associated with the listener.
+
+		For non-HTTPS listeners, `null` will be returned. Otherwise, if only a
+		single virtual host has been added, the TLS context of that host's
+		settings is returned. For multiple virtual hosts, an SNI context is
+		returned, which forwards to the individual contexts based on the
+		requested host name.
+	*/
+	@property TLSContext tlsContext() { return m_tlsContext; }
+
+	/// The local network interface IP address associated with this listener
+	@property string bindAddress() const { return m_bindAddress; }
+
+	/// The local port associated with this listener
+	@property ushort bindPort() const { return m_bindPort; }
+
+	/// Determines if any virtual hosts have been addded
+	@property bool hasVirtualHosts() const { return m_virtualHosts.length > 0; }
+
+	/** Adds a single virtual host.
+
+		Note that the port and bind address defined in `settings` must match the
+		ones for this listener. The `settings.host` field must be unique for
+		all virtual hosts.
+
+		Returns: Returns a unique ID for the new virtual host
+	*/
+	size_t addVirtualHost(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler)
+	{
+		assert(settings.port == 0 || settings.port == m_bindPort, "Virtual host settings do not match bind port.");
+		assert(settings.bindAddresses.canFind(m_bindAddress), "Virtual host settings do not match bind address.");
+
+		VirtualHost vhost;
+		vhost.id = s_vhostIDCounter++;
+		vhost.settings = settings;
+		vhost.requestHandler = request_handler;
+
+		if (settings.accessLogger) vhost.loggers ~= settings.accessLogger;
+		if (settings.accessLogToConsole)
+			vhost.loggers ~= new HTTPConsoleLogger(settings, settings.accessLogFormat);
+		if (settings.accessLogFile.length)
+			vhost.loggers ~= new HTTPFileLogger(settings, settings.accessLogFormat, settings.accessLogFile);
+
+		if (!m_virtualHosts.length) m_tlsContext = settings.tlsContext;
+
+		enforce((m_tlsContext !is null) == (settings.tlsContext !is null),
+			"Cannot mix HTTP and HTTPS virtual hosts within the same listener.");
+
+		if (m_tlsContext) addSNIHost(settings);
+
+		m_virtualHosts ~= vhost;
+
+		if (settings.hostName.length) {
+			auto proto = settings.tlsContext ? "https" : "http";
+			auto port = settings.tlsContext && settings.port == 443 || !settings.tlsContext && settings.port == 80 ? "" : ":" ~ settings.port.to!string;
+			logInfo("Added virtual host %s://%s:%s/ (%s)", proto, settings.hostName, m_bindPort, m_bindAddress);
+		}
+
+		return vhost.id;
+	}
+
+	/// Removes a previously added virtual host using its ID.
+	bool removeVirtualHost(size_t id)
+	{
+		import std.algorithm.searching : countUntil;
+
+		auto idx = m_virtualHosts.countUntil!(c => c.id == id);
+		if (idx < 0) return false;
+
+		auto ctx = m_virtualHosts[idx];
+		m_virtualHosts = m_virtualHosts[0 .. idx] ~ m_virtualHosts[idx+1 .. $];
+		return true;
+	}
+
+	private void addSNIHost(HTTPServerSettings settings)
+	{
+		if (settings.tlsContext !is m_tlsContext && m_tlsContext.kind != TLSContextKind.serverSNI) {
+			logDebug("Create SNI TLS context for %s, port %s", bindAddress, bindPort);
+			m_tlsContext = createTLSContext(TLSContextKind.serverSNI);
+			m_tlsContext.sniCallback = &onSNI;
+		}
+
+		foreach (ctx; m_virtualHosts) {
+			/*enforce(ctx.settings.hostName != settings.hostName,
+				"A server with the host name '"~settings.hostName~"' is already "
+				"listening on "~addr~":"~to!string(settings.port)~".");*/
+		}
+	}
+
+	private TLSContext onSNI(string servername)
+	{
+		foreach (vhost; m_virtualHosts)
+			if (vhost.settings.hostName.icmp(servername) == 0) {
+				logDebug("Found context for SNI host '%s'.", servername);
+				return vhost.settings.tlsContext;
+			}
+		logDebug("No context found for SNI host '%s'.", servername);
+		return null;
+	}
+}
+
+/**************************************************************************************************/
+/* Private types                                                                                  */
+/**************************************************************************************************/
+
+package final class LimitedHTTPInputStream : LimitedInputStream {
+@safe:
+
+	this(InterfaceProxy!InputStream stream, ulong byte_limit, bool silent_limit = false) {
+		super(stream, byte_limit, silent_limit, true);
+	}
+	override void onSizeLimitReached() {
+		throw new HTTPStatusException(HTTPStatus.requestEntityTooLarge);
+	}
+}
+
+package final class TimeoutHTTPInputStream : InputStream {
+@safe:
+
+	private {
+		long m_timeref;
+		long m_timeleft;
+		InterfaceProxy!InputStream m_in;
+	}
+
+	this(InterfaceProxy!InputStream stream, Duration timeleft, SysTime reftime)
+	{
+		enforce(timeleft > 0.seconds, "Timeout required");
+		m_in = stream;
+		m_timeleft = timeleft.total!"hnsecs"();
+		m_timeref = reftime.stdTime();
+	}
+
+	@property bool empty() { enforce(m_in, "InputStream missing"); return m_in.empty(); }
+	@property ulong leastSize() { enforce(m_in, "InputStream missing"); return m_in.leastSize();  }
+	@property bool dataAvailableForRead() {  enforce(m_in, "InputStream missing"); return m_in.dataAvailableForRead; }
+	const(ubyte)[] peek() { return m_in.peek(); }
+
+	size_t read(scope ubyte[] dst, IOMode mode)
+	{
+		enforce(m_in, "InputStream missing");
+		size_t nread = 0;
+		checkTimeout();
+		// FIXME: this should use ConnectionStream.waitForData to enforce the timeout during the
+		// read operation
+		return m_in.read(dst, mode);
+	}
+
+	alias read = InputStream.read;
+
+	private void checkTimeout()
+	@safe {
+		auto curr = Clock.currStdTime();
+		auto diff = curr - m_timeref;
+		if (diff > m_timeleft) throw new HTTPStatusException(HTTPStatus.requestTimeout);
+		m_timeleft -= diff;
+		m_timeref = curr;
+	}
+}
+
+/**************************************************************************************************/
+/* Private functions                                                                              */
+/**************************************************************************************************/
+
+private {
+	import core.sync.mutex;
+
+	shared string s_distHost;
+	shared ushort s_distPort = 11000;
+
+	HTTPServerContext[] s_listeners;
+}
+
+/**
+	[private] Starts a HTTP server listening on the specified port.
+
+	This is the same as listenHTTP() except that it does not use a VibeDist host for
+	remote listening, even if specified on the command line.
+*/
+private HTTPListener listenHTTPPlain(HTTPServerSettings settings, HTTPServerRequestDelegate request_handler)
+@safe {
+	import vibe.core.core : runWorkerTaskDist;
+	import std.algorithm : canFind, find;
+
+	static TCPListener doListen(HTTPServerContext listen_info, bool reusePort, bool reuseAddress, bool is_tls)
+	@safe {
+		try {
+			TCPListenOptions options = TCPListenOptions.defaults;
+			if(reuseAddress) options |= TCPListenOptions.reuseAddress; else options &= ~TCPListenOptions.reuseAddress;
+			if(reusePort) options |= TCPListenOptions.reusePort; else options &= ~TCPListenOptions.reusePort;
+			auto ret = listenTCP(listen_info.bindPort, (TCPConnection conn) nothrow @safe {
+					try handleHTTPConnection(conn, listen_info);
+					catch (Exception e) {
+						logError("HTTP connection handler has thrown at the peer %s: %s", conn.peerAddress, e.msg);
+						debug logDebug("Full error: %s", () @trusted { return e.toString().sanitize(); } ());
+						try conn.close();
+						catch (Exception e) logError("Failed to close connection: %s", e.msg);
+					}
+				}, listen_info.bindAddress, options);
+
+			// support port 0 meaning any available port
+			if (listen_info.bindPort == 0)
+				listen_info.m_bindPort = ret.bindAddress.port;
+
+			auto proto = is_tls ? "https" : "http";
+			auto urladdr = listen_info.bindAddress;
+			if (urladdr.canFind(':')) urladdr = "["~urladdr~"]";
+			logInfo("Listening for requests on %s://%s:%s/", proto, urladdr, listen_info.bindPort);
+			return ret;
+		} catch( Exception e ) {
+			logWarn("Failed to listen on %s:%s", listen_info.bindAddress, listen_info.bindPort);
+			return TCPListener.init;
+		}
+	}
+
+	size_t[] vid;
+
+	// Check for every bind address/port, if a new listening socket needs to be created and
+	// check for conflicting servers
+	foreach (addr; settings.bindAddresses) {
+		HTTPServerContext linfo;
+
+		auto l = s_listeners.find!(l => l.bindAddress == addr && l.bindPort == settings.port);
+		if (!l.empty) linfo = l.front;
+		else {
+			auto li = new HTTPServerContext(addr, settings.port);
+			if (auto tcp_lst = doListen(li,
+					(settings.options & HTTPServerOption.reusePort) != 0,
+					(settings.options & HTTPServerOption.reuseAddress) != 0,
+					settings.tlsContext !is null)) // DMD BUG 2043
+			{
+				li.m_listener = tcp_lst;
+				s_listeners ~= li;
+				linfo = li;
+			}
+		}
+
+		if (linfo) vid ~= linfo.addVirtualHost(settings, request_handler);
+	}
+
+	enforce(vid.length > 0, "Failed to listen for incoming HTTP connections on any of the supplied interfaces.");
+
+	return HTTPListener(vid);
+}
+
+private void parseCookies(string str, ref CookieValueMap cookies)
+@safe {
+	import std.encoding : sanitize;
+	import std.array : split;
+	import std.string : strip;
+	import std.algorithm.iteration : map, filter, each;
+	import vibe.http.common : Cookie;
+	() @trusted { return str.sanitize; } ()
+		.split(";")
+		.map!(kv => kv.strip.split("="))
+		.filter!(kv => kv.length == 2) //ignore illegal cookies
+		.each!(kv => cookies.add(kv[0], kv[1], Cookie.Encoding.raw) );
+}
+
+unittest
+{
+  auto cvm = CookieValueMap();
+  parseCookies("foo=bar;; baz=zinga; öö=üü   ;   møøse=was=sacked;    onlyval1; =onlyval2; onlykey=", cvm);
+  assert(cvm["foo"] == "bar");
+  assert(cvm["baz"] == "zinga");
+  assert(cvm["öö"] == "üü");
+  assert( "møøse" ! in cvm); //illegal cookie gets ignored
+  assert( "onlyval1" ! in cvm); //illegal cookie gets ignored
+  assert(cvm["onlykey"] == "");
+  assert(cvm[""] == "onlyval2");
+  assert(cvm.length() == 5);
+  cvm = CookieValueMap();
+  parseCookies("", cvm);
+  assert(cvm.length() == 0);
+  cvm = CookieValueMap();
+  parseCookies(";;=", cvm);
+  assert(cvm.length() == 1);
+  assert(cvm[""] == "");
+}
+
+shared static this()
+{
+	version (VibeNoDefaultArgs) {}
+	else {
+		string disthost = s_distHost;
+		ushort distport = s_distPort;
+		import vibe.core.args : readOption;
+		readOption("disthost|d", () @trusted { return &disthost; } (), "Sets the name of a vibedist server to use for load balancing.");
+		readOption("distport", () @trusted { return &distport; } (), "Sets the port used for load balancing.");
+		setVibeDistHost(disthost, distport);
+	}
+}
+
+version (VibeDebugCatchAll) package alias UncaughtException = Throwable;
+else package alias UncaughtException = Exception;

+ 294 - 0
vtest/source/vibe/http/session.d

@@ -0,0 +1,294 @@
+/**
+	Cookie based session support.
+
+	Copyright: © 2012-2013 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Jan Krüger, Sönke Ludwig, Ilya Shipunov
+*/
+module vibe.http.session;
+
+import vibe.core.log;
+import vibe.crypto.cryptorand;
+
+import std.array;
+import std.base64;
+import std.traits : hasAliasing;
+import std.variant;
+
+//random number generator
+//TODO: Use Whirlpool or SHA-512 here
+private SHA1HashMixerRNG g_rng;
+
+//The "URL and Filename safe" Base64 without padding
+alias Base64URLNoPadding = Base64Impl!('-', '_', Base64.NoPadding);
+
+
+/**
+	Represents a single HTTP session.
+
+	Indexing the session object with string keys allows to store arbitrary key/value pairs.
+*/
+struct Session {
+	private {
+		SessionStore m_store;
+		string m_id;
+		SessionStorageType m_storageType;
+	}
+
+	// created by the SessionStore using SessionStore.createSessionInstance
+	private this(SessionStore store, string id = null)
+	@safe {
+		assert(id.length > 0);
+		m_store = store;
+		m_id = id;
+		m_storageType = store.storageType;
+	}
+
+	/** Checks if the session is active.
+
+		This operator enables a $(D Session) value to be used in conditionals
+		to check if they are actially valid/active.
+	*/
+	bool opCast() const @safe { return m_store !is null; }
+
+	///
+	unittest {
+		//import vibe.http.server;
+		// workaround for cyclic module ctor compiler error
+		class HTTPServerRequest { Session session; string[string] form; }
+		class HTTPServerResponse { Session startSession() { assert(false); } }
+
+		void login(scope HTTPServerRequest req, scope HTTPServerResponse res)
+		{
+			// TODO: validate username+password
+
+			// ensure that there is an active session
+			if (!req.session) req.session = res.startSession();
+
+			// update session variables
+			req.session.set("loginUser", req.form["user"]);
+		}
+	}
+
+	/// Returns the unique session id of this session.
+	@property string id() const @safe { return m_id; }
+
+	/// Queries the session for the existence of a particular key.
+	bool isKeySet(string key) @safe { return m_store.isKeySet(m_id, key); }
+
+	/** Gets a typed field from the session.
+	*/
+	const(T) get(T)(string key, lazy T def_value = T.init)
+	@trusted { // Variant, deserializeJson/deserializeBson
+		static assert(!hasAliasing!T, "Type "~T.stringof~" contains references, which is not supported for session storage.");
+		auto val = m_store.get(m_id, key, serialize(def_value));
+		return deserialize!T(val);
+	}
+
+	/** Sets a typed field to the session.
+	*/
+	void set(T)(string key, T value)
+	{
+		static assert(!hasAliasing!T, "Type "~T.stringof~" contains references, which is not supported for session storage.");
+		m_store.set(m_id, key, serialize(value));
+	}
+
+	// Removes a field from a session
+	void remove(string key) @safe { m_store.remove(m_id, key); }
+
+	/**
+		Enables foreach-iteration over all keys of the session.
+	*/
+	int opApply(scope int delegate(string key) @safe del)
+	@safe {
+		return m_store.iterateSession(m_id, del);
+	}
+	///
+	unittest {
+		//import vibe.http.server;
+		// workaround for cyclic module ctor compiler error
+		class HTTPServerRequest { Session session; }
+		class HTTPServerResponse { import vibe.core.stream; OutputStream bodyWriter() @safe { assert(false); } string contentType; }
+
+		// sends all session entries to the requesting browser
+		// assumes that all entries are strings
+		void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res)
+		{
+			res.contentType = "text/plain";
+			req.session.opApply((key) @safe {
+				res.bodyWriter.write(key ~ ": " ~ req.session.get!string(key) ~ "\n");
+				return 0;
+			});
+		}
+	}
+
+	package void destroy() @safe { m_store.destroy(m_id); }
+
+	private Variant serialize(T)(T val)
+	{
+		import vibe.data.json;
+		import vibe.data.bson;
+
+		final switch (m_storageType) with (SessionStorageType) {
+			case native: return () @trusted { return Variant(val); } ();
+			case json: return () @trusted { return Variant(serializeToJson(val)); } ();
+			case bson: return () @trusted { return Variant(serializeToBson(val)); } ();
+		}
+	}
+
+	private T deserialize(T)(ref Variant val)
+	{
+		import vibe.data.json;
+		import vibe.data.bson;
+
+		final switch (m_storageType) with (SessionStorageType) {
+			case native: return () @trusted { return val.get!T; } ();
+			case json: return () @trusted { return deserializeJson!T(val.get!Json); } ();
+			case bson: return () @trusted { return deserializeBson!T(val.get!Bson); } ();
+		}
+	}
+}
+
+
+/**
+	Interface for a basic session store.
+
+	A session store is responsible for storing the id and the associated key/value pairs of a
+	session.
+*/
+interface SessionStore {
+@safe:
+
+	/// Returns the internal type used for storing session keys.
+	@property SessionStorageType storageType() const;
+
+	/// Creates a new session.
+	Session create();
+
+	/// Opens an existing session.
+	Session open(string id);
+
+	/// Sets a name/value pair for a given session.
+	void set(string id, string name, Variant value);
+
+	/// Returns the value for a given session key.
+	Variant get(string id, string name, lazy Variant defaultVal);
+
+	/// Determines if a certain session key is set.
+	bool isKeySet(string id, string key);
+
+	/// Removes a key from a session
+	void remove(string id, string key);
+
+	/// Terminates the given session.
+	void destroy(string id);
+
+	/// Iterates all keys stored in the given session.
+	int iterateSession(string id, scope int delegate(string key) @safe del);
+
+	/// Creates a new Session object which sources its contents from this store.
+	protected final Session createSessionInstance(string id = null)
+	{
+		if (!id.length) {
+			ubyte[64] rand;
+			if (!g_rng) g_rng = new SHA1HashMixerRNG();
+			g_rng.read(rand);
+			id = () @trusted { return cast(immutable)Base64URLNoPadding.encode(rand); } ();
+		}
+		return Session(this, id);
+	}
+}
+
+enum SessionStorageType {
+	native,
+	json,
+	bson
+}
+
+
+/**
+	Session store for storing a session in local memory.
+
+	If the server is running as a single instance (no thread or process clustering), this kind of
+	session store provies the fastest and simplest way to store sessions. In any other case,
+	a persistent session store based on a database is necessary.
+*/
+final class MemorySessionStore : SessionStore {
+@safe:
+
+	private {
+		Variant[string][string] m_sessions;
+	}
+
+	@property SessionStorageType storageType()
+	const {
+		return SessionStorageType.native;
+	}
+
+	Session create()
+	{
+		auto s = createSessionInstance();
+		m_sessions[s.id] = null;
+		return s;
+	}
+
+	Session open(string id)
+	{
+		auto pv = id in m_sessions;
+		return pv ? createSessionInstance(id) : Session.init;
+	}
+
+	void set(string id, string name, Variant value)
+	@trusted { // Variant
+		m_sessions[id][name] = value;
+		foreach(k, v; m_sessions[id]) logTrace("Csession[%s][%s] = %s", id, k, v);
+	}
+
+	Variant get(string id, string name, lazy Variant defaultVal)
+	@trusted { // Variant
+		assert(id in m_sessions, "session not in store");
+		foreach(k, v; m_sessions[id]) logTrace("Dsession[%s][%s] = %s", id, k, v);
+		if (auto pv = name in m_sessions[id]) {
+			return *pv;
+		} else {
+			return defaultVal;
+		}
+	}
+
+	bool isKeySet(string id, string key)
+	{
+		return (key in m_sessions[id]) !is null;
+	}
+
+	void remove(string id, string key)
+	{
+		m_sessions[id].remove(key);
+	}
+
+	void destroy(string id)
+	{
+		m_sessions.remove(id);
+	}
+
+	int delegate(int delegate(ref string key, ref Variant value) @safe) @safe iterateSession(string id)
+	{
+		assert(id in m_sessions, "session not in store");
+		int iterator(int delegate(ref string key, ref Variant value) @safe del)
+		@safe {
+			foreach( key, ref value; m_sessions[id] )
+				if( auto ret = del(key, value) != 0 )
+					return ret;
+			return 0;
+		}
+		return &iterator;
+	}
+
+	int iterateSession(string id, scope int delegate(string key) @safe del)
+	@trusted { // hash map iteration
+		assert(id in m_sessions, "session not in store");
+		foreach (key; m_sessions[id].byKey)
+			if (auto ret = del(key))
+				return ret;
+		return 0;
+	}
+}

+ 155 - 0
vtest/source/vibe/http/status.d

@@ -0,0 +1,155 @@
+/**
+	List of all standard HTTP status codes.
+
+	Copyright: © 2012 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Jan Krüger
+*/
+module vibe.http.status;
+
+/**
+	Definitions of all standard HTTP status codes.
+*/
+enum HTTPStatus {
+	continue_                    = 100,
+	switchingProtocols           = 101,
+	ok                           = 200,
+	created                      = 201,
+	accepted                     = 202,
+	nonAuthoritativeInformation  = 203,
+	noContent                    = 204,
+	resetContent                 = 205,
+	partialContent               = 206,
+	multipleChoices              = 300,
+	movedPermanently             = 301,
+	found                        = 302,
+	seeOther                     = 303,
+	notModified                  = 304,
+	useProxy                     = 305,
+	temporaryRedirect            = 307,
+	badRequest                   = 400,
+	unauthorized                 = 401,
+	paymentRequired              = 402,
+	forbidden                    = 403,
+	notFound                     = 404,
+	methodNotAllowed             = 405,
+	notAcceptable                = 406,
+	proxyAuthenticationRequired  = 407,
+	requestTimeout               = 408,
+	conflict                     = 409,
+	gone                         = 410,
+	lengthRequired               = 411,
+	preconditionFailed           = 412,
+	requestEntityTooLarge        = 413,
+	requestURITooLarge           = 414,
+	unsupportedMediaType         = 415,
+	rangeNotSatisfiable          = 416,
+	expectationFailed            = 417,
+	tooManyRequests              = 429,
+	unavailableForLegalReasons   = 451,
+	internalServerError          = 500,
+	notImplemented               = 501,
+	badGateway                   = 502,
+	serviceUnavailable           = 503,
+	gatewayTimeout               = 504,
+	httpVersionNotSupported      = 505,
+	// WebDAV status codes
+	processing                   = 102, /// See: https://tools.ietf.org/html/rfc2518#section-10.1
+	multiStatus                  = 207,
+	unprocessableEntity          = 422,
+	locked                       = 423,
+	failedDependency             = 424,
+	insufficientStorage          = 507,
+}
+
+
+@safe nothrow @nogc pure:
+
+/**
+	Returns a standard text description of the specified HTTP status code.
+*/
+string httpStatusText(int code)
+{
+	switch(code)
+	{
+		default: break;
+		case HTTPStatus.continue_                    : return "Continue";
+		case HTTPStatus.switchingProtocols           : return "Switching Protocols";
+		case HTTPStatus.ok                           : return "OK";
+		case HTTPStatus.created                      : return "Created";
+		case HTTPStatus.accepted                     : return "Accepted";
+		case HTTPStatus.nonAuthoritativeInformation  : return "Non-Authoritative Information";
+		case HTTPStatus.noContent                    : return "No Content";
+		case HTTPStatus.resetContent                 : return "Reset Content";
+		case HTTPStatus.partialContent               : return "Partial Content";
+		case HTTPStatus.multipleChoices              : return "Multiple Choices";
+		case HTTPStatus.movedPermanently             : return "Moved Permanently";
+		case HTTPStatus.found                        : return "Found";
+		case HTTPStatus.seeOther                     : return "See Other";
+		case HTTPStatus.notModified                  : return "Not Modified";
+		case HTTPStatus.useProxy                     : return "Use Proxy";
+		case HTTPStatus.temporaryRedirect            : return "Temporary Redirect";
+		case HTTPStatus.badRequest                   : return "Bad Request";
+		case HTTPStatus.unauthorized                 : return "Unauthorized";
+		case HTTPStatus.paymentRequired              : return "Payment Required";
+		case HTTPStatus.forbidden                    : return "Forbidden";
+		case HTTPStatus.notFound                     : return "Not Found";
+		case HTTPStatus.methodNotAllowed             : return "Method Not Allowed";
+		case HTTPStatus.notAcceptable                : return "Not Acceptable";
+		case HTTPStatus.proxyAuthenticationRequired  : return "Proxy Authentication Required";
+		case HTTPStatus.requestTimeout               : return "Request Time-out";
+		case HTTPStatus.conflict                     : return "Conflict";
+		case HTTPStatus.gone                         : return "Gone";
+		case HTTPStatus.lengthRequired               : return "Length Required";
+		case HTTPStatus.preconditionFailed           : return "Precondition Failed";
+		case HTTPStatus.requestEntityTooLarge        : return "Request Entity Too Large";
+		case HTTPStatus.requestURITooLarge           : return "Request-URI Too Large";
+		case HTTPStatus.unsupportedMediaType         : return "Unsupported Media Type";
+		case HTTPStatus.rangeNotSatisfiable          : return "Requested range not satisfiable";
+		case HTTPStatus.expectationFailed            : return "Expectation Failed";
+		case HTTPStatus.unavailableForLegalReasons   : return "Unavailable For Legal Reasons";
+		case HTTPStatus.internalServerError          : return "Internal Server Error";
+		case HTTPStatus.notImplemented               : return "Not Implemented";
+		case HTTPStatus.badGateway                   : return "Bad Gateway";
+		case HTTPStatus.serviceUnavailable           : return "Service Unavailable";
+		case HTTPStatus.gatewayTimeout               : return "Gateway Time-out";
+		case HTTPStatus.httpVersionNotSupported      : return "HTTP Version not supported";
+		// WebDAV
+		case HTTPStatus.multiStatus                  : return "Multi-Status";
+		case HTTPStatus.unprocessableEntity          : return "Unprocessable Entity";
+		case HTTPStatus.locked                       : return "Locked";
+		case HTTPStatus.failedDependency             : return "Failed Dependency";
+		case HTTPStatus.insufficientStorage          : return "Insufficient Storage";
+		case HTTPStatus.processing                   : return "Processing";
+	}
+	if( code >= 600 ) return "Unknown";
+	if( code >= 500 ) return "Unknown server error";
+	if( code >= 400 ) return "Unknown error";
+	if( code >= 300 ) return "Unknown redirection";
+	if( code >= 200 ) return "Unknown success";
+	if( code >= 100 ) return "Unknown information";
+	return "Unknown";
+}
+
+/**
+	Determines if the given status code justifies closing the connection (e.g. evil big request bodies)
+*/
+bool justifiesConnectionClose(int status)
+{
+	switch(status) {
+		default: return false;
+		case HTTPStatus.requestEntityTooLarge:
+		case HTTPStatus.requestURITooLarge:
+		case HTTPStatus.requestTimeout:
+			return true;
+	}
+}
+
+/**
+	Determines if status code is generally successful (>= 200 && < 300)
+*/
+bool isSuccessCode(HTTPStatus status)
+{
+	return status >= 200 && status < 300;
+}
+

+ 1291 - 0
vtest/source/vibe/http/websockets.d

@@ -0,0 +1,1291 @@
+/**
+	Implements WebSocket support and fallbacks for older browsers.
+
+	Standards: $(LINK2 https://tools.ietf.org/html/rfc6455, RFC6455)
+	Copyright: © 2012-2014 Sönke Ludwig
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Jan Krüger
+*/
+module vibe.http.websockets;
+
+///
+@safe unittest {
+	void handleConn(scope WebSocket sock)
+	{
+		// simple echo server
+		while (sock.connected) {
+			auto msg = sock.receiveText();
+			sock.send(msg);
+		}
+	}
+
+	void startServer()
+	{
+		import vibe.http.router;
+		auto router = new URLRouter;
+		router.get("/ws", handleWebSockets(&handleConn));
+
+		// Start HTTP server using listenHTTP()...
+	}
+}
+
+import vibe.core.core;
+import vibe.core.log;
+import vibe.core.net;
+import vibe.core.sync;
+import vibe.stream.operations;
+import vibe.http.server;
+import vibe.http.client;
+import vibe.core.connectionpool;
+
+import core.time;
+import std.algorithm: equal, splitter;
+import std.array;
+import std.base64;
+import std.conv;
+import std.exception;
+import std.bitmanip;
+import std.digest.sha;
+import std.string;
+import std.functional;
+import std.uuid;
+import std.base64;
+import std.digest.sha;
+import std.uni: asLowerCase;
+import vibe.crypto.cryptorand;
+
+@safe:
+
+
+alias WebSocketHandshakeDelegate = void delegate(scope WebSocket) nothrow;
+
+
+/// Exception thrown by $(D vibe.http.websockets).
+class WebSocketException: Exception
+{
+	@safe pure nothrow:
+
+	///
+	this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
+	{
+		super(msg, file, line, next);
+	}
+
+	///
+	this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__)
+	{
+		super(msg, next, file, line);
+	}
+}
+
+/** Establishes a WebSocket connection at the specified endpoint.
+*/
+WebSocket connectWebSocketEx(URL url,
+	scope void delegate(scope HTTPClientRequest) @safe request_modifier,
+	const(HTTPClientSettings) settings = defaultSettings)
+@safe {
+	const use_tls = (url.schema == "wss" || url.schema == "https") ? true : false;
+	url.schema = use_tls ? "https" : "http";
+
+	auto rng = secureRNG();
+	auto challengeKey = generateChallengeKey(rng);
+	auto answerKey = computeAcceptKey(challengeKey);
+	auto res = requestHTTP(url, (scope req){
+		req.method = HTTPMethod.GET;
+		req.headers["Upgrade"] = "websocket";
+		req.headers["Connection"] = "Upgrade";
+		req.headers["Sec-WebSocket-Version"] = "13";
+		req.headers["Sec-WebSocket-Key"] = challengeKey;
+		request_modifier(req);
+	}, settings);
+
+	enforce(res.statusCode == HTTPStatus.switchingProtocols, "Server didn't accept the protocol upgrade request.");
+
+	auto key = "sec-websocket-accept" in res.headers;
+	enforce(key !is null, "Response is missing the Sec-WebSocket-Accept header.");
+	enforce(*key == answerKey, "Response has wrong accept key");
+	auto conn = res.switchProtocol("websocket");
+	return new WebSocket(conn, rng, res);
+}
+
+/// ditto
+void connectWebSocketEx(URL url,
+	scope void delegate(scope HTTPClientRequest) @safe request_modifier,
+	scope WebSocketHandshakeDelegate del,
+	const(HTTPClientSettings) settings = defaultSettings)
+@safe {
+	const use_tls = (url.schema == "wss" || url.schema == "https") ? true : false;
+	url.schema = use_tls ? "https" : "http";
+
+	/*scope*/auto rng = secureRNG();
+	auto challengeKey = generateChallengeKey(rng);
+	auto answerKey = computeAcceptKey(challengeKey);
+
+	requestHTTP(url,
+		(scope req) {
+			req.method = HTTPMethod.GET;
+			req.headers["Upgrade"] = "websocket";
+			req.headers["Connection"] = "Upgrade";
+			req.headers["Sec-WebSocket-Version"] = "13";
+			req.headers["Sec-WebSocket-Key"] = challengeKey;
+			request_modifier(req);
+		},
+		(scope res) {
+			enforce(res.statusCode == HTTPStatus.switchingProtocols, "Server didn't accept the protocol upgrade request.");
+			auto key = "sec-websocket-accept" in res.headers;
+			enforce(key !is null, "Response is missing the Sec-WebSocket-Accept header.");
+			enforce(*key == answerKey, "Response has wrong accept key");
+			res.switchProtocol("websocket", (scope conn) @trusted {
+				scope ws = new WebSocket(conn, rng, res);
+				del(ws);
+				if (ws.connected) ws.close();
+			});
+		},
+		settings
+	);
+}
+
+/// ditto
+WebSocket connectWebSocket(URL url, const(HTTPClientSettings) settings = defaultSettings)
+@safe {
+	return connectWebSocketEx(url, (scope req) {}, settings);
+}
+/// ditto
+void connectWebSocket(URL url, scope WebSocketHandshakeDelegate del, const(HTTPClientSettings) settings = defaultSettings)
+@safe {
+	connectWebSocketEx(url, (scope req) {}, del, settings);
+}
+/// ditto
+void connectWebSocket(URL url, scope void delegate(scope WebSocket) @system del, const(HTTPClientSettings) settings = defaultSettings)
+@system {
+	connectWebSocket(url, (scope ws) nothrow {
+		try del(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	}, settings);
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+void connectWebSocket(URL url, scope void delegate(scope WebSocket) @system nothrow del, const(HTTPClientSettings) settings = defaultSettings)
+@system {
+	connectWebSocket(url, (scope ws) @trusted => del(ws), settings);
+}
+/// Scheduled for deprecation - use a `nothrow` callback instead.
+void connectWebSocket(URL url, scope void delegate(scope WebSocket) @safe del, const(HTTPClientSettings) settings = defaultSettings)
+@safe {
+	connectWebSocket(url, (scope ws) nothrow {
+		try del(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	}, settings);
+}
+
+
+/**
+	Establishes a web socket conection and passes it to the $(D on_handshake) delegate.
+*/
+void handleWebSocket(scope WebSocketHandshakeDelegate on_handshake, scope HTTPServerRequest req, scope HTTPServerResponse res)
+@safe {
+	auto pUpgrade = "Upgrade" in req.headers;
+	auto pConnection = "Connection" in req.headers;
+	auto pKey = "Sec-WebSocket-Key" in req.headers;
+	//auto pProtocol = "Sec-WebSocket-Protocol" in req.headers;
+	auto pVersion = "Sec-WebSocket-Version" in req.headers;
+
+	auto isUpgrade = false;
+
+	if( pConnection ) {
+		auto connectionTypes = splitter(*pConnection, ",");
+		foreach( t ; connectionTypes ) {
+			if( t.strip().asLowerCase().equal("upgrade") ) {
+				isUpgrade = true;
+				break;
+			}
+		}
+	}
+
+	string req_error;
+	if (!isUpgrade) req_error = "WebSocket endpoint only accepts \"Connection: upgrade\" requests.";
+	else if (!pUpgrade || icmp(*pUpgrade, "websocket") != 0) req_error = "WebSocket endpoint requires \"Upgrade: websocket\" header.";
+	else if (!pVersion || *pVersion != "13") req_error = "Only version 13 of the WebSocket protocol is supported.";
+	else if (!pKey) req_error = "Missing \"Sec-WebSocket-Key\" header.";
+
+	if (req_error.length) {
+		logDebug("Browser sent invalid WebSocket request: %s", req_error);
+		res.statusCode = HTTPStatus.badRequest;
+		res.writeBody(req_error);
+		return;
+	}
+
+	auto accept = () @trusted { return cast(string)Base64.encode(sha1Of(*pKey ~ s_webSocketGuid)); } ();
+	res.headers["Sec-WebSocket-Accept"] = accept;
+	res.headers["Connection"] = "Upgrade";
+	ConnectionStream conn = res.switchProtocol("websocket");
+
+	// NOTE: silencing scope warning here - WebSocket references the scoped
+	//       req/res objects throughout its lifetime, which has a narrower scope
+	scope socket = () @trusted { return new WebSocket(conn, req, res); } ();
+	try {
+		on_handshake(socket);
+	} catch (Exception e) {
+		logDiagnostic("WebSocket handler failed: %s", e.msg);
+	}
+	socket.close();
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+void handleWebSocket(scope void delegate(scope WebSocket) @system nothrow on_handshake, scope HTTPServerRequest req, scope HTTPServerResponse res)
+@system {
+	handleWebSocket((scope ws) @trusted => on_handshake(ws), req, res);
+}
+/// Scheduled for deprecation - use a `nothrow` callback instead.
+void handleWebSocket(scope void delegate(scope WebSocket) @safe on_handshake, scope HTTPServerRequest req, scope HTTPServerResponse res)
+{
+	handleWebSocket((scope ws) nothrow {
+		try on_handshake(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	}, req, res);
+}
+/// ditto
+void handleWebSocket(scope void delegate(scope WebSocket) @system on_handshake, scope HTTPServerRequest req, scope HTTPServerResponse res)
+@system {
+	handleWebSocket((scope ws) nothrow {
+		try on_handshake(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	}, req, res);
+}
+
+
+/**
+	Returns a HTTP request handler that establishes web socket conections.
+*/
+HTTPServerRequestDelegateS handleWebSockets(void function(scope WebSocket) @safe nothrow on_handshake)
+@safe {
+	return handleWebSockets(() @trusted { return toDelegate(on_handshake); } ());
+}
+/// ditto
+HTTPServerRequestDelegateS handleWebSockets(WebSocketHandshakeDelegate on_handshake)
+@safe {
+	void callback(scope HTTPServerRequest req, scope HTTPServerResponse res)
+	@safe {
+		auto pUpgrade = "Upgrade" in req.headers;
+		auto pConnection = "Connection" in req.headers;
+		auto pKey = "Sec-WebSocket-Key" in req.headers;
+		//auto pProtocol = "Sec-WebSocket-Protocol" in req.headers;
+		auto pVersion = "Sec-WebSocket-Version" in req.headers;
+
+		auto isUpgrade = false;
+
+		if( pConnection ) {
+			auto connectionTypes = splitter(*pConnection, ",");
+			foreach( t ; connectionTypes ) {
+				if( t.strip().asLowerCase().equal("upgrade") ) {
+					isUpgrade = true;
+					break;
+				}
+			}
+		}
+		if( !(isUpgrade &&
+			  pUpgrade && icmp(*pUpgrade, "websocket") == 0 &&
+			  pKey &&
+			  pVersion && *pVersion == "13") )
+		{
+			logDebug("Browser sent invalid WebSocket request.");
+			res.statusCode = HTTPStatus.badRequest;
+			res.writeVoidBody();
+			return;
+		}
+
+		auto accept = () @trusted { return cast(string)Base64.encode(sha1Of(*pKey ~ s_webSocketGuid)); } ();
+		res.headers["Sec-WebSocket-Accept"] = accept;
+		res.headers["Connection"] = "Upgrade";
+		res.switchProtocol("websocket", (scope conn) {
+			// TODO: put back 'scope' once it is actually enforced by DMD
+			/*scope*/ auto socket = new WebSocket(conn, req, res);
+			try on_handshake(socket);
+			catch (Exception e) {
+				logDiagnostic("WebSocket handler failed: %s", e.msg);
+			}
+			socket.close();
+		});
+	}
+	return &callback;
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+HTTPServerRequestDelegateS handleWebSockets(void delegate(scope WebSocket) @system nothrow on_handshake)
+@system {
+	return handleWebSockets(delegate (scope ws) @trusted => on_handshake(ws));
+}
+/// Scheduled for deprecation - use a `@safe` callback instead.
+HTTPServerRequestDelegateS handleWebSockets(void function(scope WebSocket) @system nothrow on_handshake)
+@system {
+	return handleWebSockets(delegate (scope ws) @trusted => on_handshake(ws));
+}
+/// Scheduled for deprecation - use a `nothrow` callback instead.
+HTTPServerRequestDelegateS handleWebSockets(void delegate(scope WebSocket) @safe on_handshake)
+{
+	return handleWebSockets(delegate (scope ws) nothrow {
+		try on_handshake(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	});
+}
+/// ditto
+HTTPServerRequestDelegateS handleWebSockets(void function(scope WebSocket) @safe on_handshake)
+{
+	return handleWebSockets(delegate (scope ws) nothrow {
+		try on_handshake(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	});
+}
+/// ditto
+HTTPServerRequestDelegateS handleWebSockets(void delegate(scope WebSocket) @system on_handshake)
+@system {
+	return handleWebSockets(delegate (scope ws) nothrow {
+		try on_handshake(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	});
+}
+/// ditto
+HTTPServerRequestDelegateS handleWebSockets(void function(scope WebSocket) @system on_handshake)
+@system {
+	return handleWebSockets(delegate (scope ws) nothrow {
+		try on_handshake(ws);
+		catch (Exception e) logWarn("WebSocket handler failed: %s", e.msg);
+	});
+}
+
+/**
+ * Provides the reason that a websocket connection has closed.
+ *
+ * Further documentation for the WebSocket and it's codes can be found from:
+ * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
+ *
+ * ---
+ *
+ * void echoSocket(scope WebSocket sock)
+ * {
+ *   import std.datetime : seconds;
+ *
+ *   while(sock.waitForData(3.seconds))
+ *   {
+ *     string msg = sock.receiveText;
+ *     logInfo("Got a message: %s", msg);
+ *     sock.send(msg);
+ *   }
+ *
+ *   if(sock.connected)
+ *     sock.close(WebSocketCloseReason.policyViolation, "timeout");
+ * }
+ * ---
+ */
+enum WebSocketCloseReason : short
+{
+	none = 0,
+	normalClosure = 1000,
+	goingAway = 1001,
+	protocolError = 1002,
+	unsupportedData = 1003,
+	noStatusReceived = 1005,
+	abnormalClosure = 1006,
+	invalidFramePayloadData = 1007,
+	policyViolation = 1008,
+	messageTooBig = 1009,
+	internalError = 1011,
+	serviceRestart = 1012,
+	tryAgainLater = 1013,
+	badGateway = 1014,
+	tlsHandshake = 1015
+}
+
+string closeReasonString(WebSocketCloseReason reason) @nogc @safe
+{
+	import std.math : floor;
+
+	//round down to the nearest thousand to get category
+	switch(cast(short)(cast(float)reason / 1000f).floor)
+	{
+		case 0:
+			return "Reserved and Unused";
+		case 1:
+			switch(reason)
+			{
+				case 1000:
+					return "Normal Closure";
+				case 1001:
+					return "Going Away";
+				case 1002:
+					return "Protocol Error";
+				case 1003:
+					return "Unsupported Data";
+				case 1004:
+					return "RESERVED";
+				case 1005:
+					return "No Status Recvd";
+				case 1006:
+					return "Abnormal Closure";
+				case 1007:
+					return "Invalid Frame Payload Data";
+				case 1008:
+					return "Policy Violation";
+				case 1009:
+					return "Message Too Big";
+				case 1010:
+					return "Missing Extension";
+				case 1011:
+					return "Internal Error";
+				case 1012:
+					return "Service Restart";
+				case 1013:
+					return "Try Again Later";
+				case 1014:
+					return "Bad Gateway";
+				case 1015:
+					return "TLS Handshake";
+				default:
+					return "RESERVED";
+			}
+		case 2:
+			return "Reserved for extensions";
+		case 3:
+			return "Available for frameworks and libraries";
+		case 4:
+			return "Available for applications";
+		default:
+			return "UNDEFINED - Nasal Demons";
+	}
+}
+
+unittest
+{
+	assert((cast(WebSocketCloseReason)   0).closeReasonString == "Reserved and Unused");
+	assert((cast(WebSocketCloseReason)   1).closeReasonString == "Reserved and Unused");
+	assert(WebSocketCloseReason.normalClosure.closeReasonString == "Normal Closure");
+	assert(WebSocketCloseReason.abnormalClosure.closeReasonString == "Abnormal Closure");
+	assert((cast(WebSocketCloseReason)1020).closeReasonString == "RESERVED");
+	assert((cast(WebSocketCloseReason)2000).closeReasonString == "Reserved for extensions");
+	assert((cast(WebSocketCloseReason)3000).closeReasonString == "Available for frameworks and libraries");
+	assert((cast(WebSocketCloseReason)4000).closeReasonString == "Available for applications");
+	assert((cast(WebSocketCloseReason)5000).closeReasonString == "UNDEFINED - Nasal Demons");
+	assert((cast(WebSocketCloseReason)  -1).closeReasonString == "UNDEFINED - Nasal Demons");
+
+	//check the other spec cases
+	for(short i = 1000; i < 1017; i++)
+	{
+		if(i == 1004 || i > 1015)
+		{
+			assert(
+				(cast(WebSocketCloseReason)i).closeReasonString == "RESERVED",
+				"(incorrect) code %d = %s".format(i, closeReasonString(cast(WebSocketCloseReason)i))
+			);
+		}
+		else
+			assert(
+				(cast(WebSocketCloseReason)i).closeReasonString != "RESERVED",
+				"(incorrect) code %d = %s".format(i, closeReasonString(cast(WebSocketCloseReason)i))
+			);
+	}
+}
+
+
+/**
+ * Represents a single _WebSocket connection.
+ *
+ * ---
+ * int main (string[] args)
+ * {
+ *   auto taskHandle = runTask(() => connectToWS());
+ *   return runApplication(&args);
+ * }
+ *
+ * void connectToWS ()
+ * {
+ *   auto ws_url = URL("wss://websockets.example.com/websocket/auth_token");
+ *   auto ws = connectWebSocket(ws_url);
+ *   logInfo("WebSocket connected");
+ *
+ *   while (ws.waitForData())
+ *   {
+ *     auto txt = ws.receiveText;
+ *     logInfo("Received: %s", txt);
+ *   }
+ *   logFatal("Connection lost!");
+ * }
+ * ---
+ */
+final class WebSocket {
+@safe:
+
+	private {
+		ConnectionStream m_conn;
+		bool m_sentCloseFrame = false;
+		IncomingWebSocketMessage m_nextMessage = null;
+		HTTPServerRequest m_request;
+		HTTPServerResponse m_serverResponse;
+		HTTPClientResponse m_clientResponse;
+		Task m_reader;
+		Task m_ownerTask;
+		InterruptibleTaskMutex m_readMutex, m_writeMutex;
+		InterruptibleTaskCondition m_readCondition;
+		Timer m_pingTimer;
+		uint m_lastPingIndex;
+		bool m_pongReceived;
+		short m_closeCode;
+		const(char)[] m_closeReason;
+		/// The entropy generator to use
+		/// If not null, it means this is a server socket.
+		RandomNumberStream m_rng;
+	}
+
+scope:
+
+	/**
+	 * Private constructor, called from `connectWebSocket`.
+	 *
+	 * Params:
+	 *	 conn = Underlying connection string
+	 *	 request = HTTP request used to establish the connection
+	 *	 rng = Source of entropy to use.  If null, assume we're a server socket
+	 *   client_res = For client sockets, the response object (keeps the http client locked until the socket is done)
+	 */
+	private this(ConnectionStream conn, HTTPServerRequest request, HTTPServerResponse server_res, RandomNumberStream rng, HTTPClientResponse client_res)
+	{
+		m_ownerTask = Task.getThis();
+		m_conn = conn;
+		m_request = request;
+		m_clientResponse = client_res;
+		m_serverResponse = server_res;
+		assert(m_conn);
+		m_rng = rng;
+		m_writeMutex = new InterruptibleTaskMutex;
+		m_readMutex = new InterruptibleTaskMutex;
+		m_readCondition = new InterruptibleTaskCondition(m_readMutex);
+		m_readMutex.performLocked!({
+			// NOTE: Silencing scope warning here - m_reader MUST be stopped
+			//       before the end of the lifetime of the WebSocket object,
+			//       which is done in the mandatory call to close().
+			//       The same goes for m_pingTimer below.
+			m_reader = () @trusted { return runTask(&startReader); } ();
+			if (request !is null && request.serverSettings.webSocketPingInterval != Duration.zero) {
+				m_pongReceived = true;
+				m_pingTimer = () @trusted { return setTimer(request.serverSettings.webSocketPingInterval, &sendPing, true); } ();
+			}
+		});
+	}
+
+	private this(ConnectionStream conn, RandomNumberStream rng, HTTPClientResponse client_res)
+	{
+		this(conn, null, null, rng, client_res);
+	}
+
+	private this(ConnectionStream conn, HTTPServerRequest request, HTTPServerResponse res)
+	{
+		this(conn, request, res, null, null);
+	}
+
+	/**
+		Determines if the WebSocket connection is still alive and ready for sending.
+
+		Note that for determining the ready state for $(EM reading), you need
+		to use $(D waitForData) instead, because both methods can return
+		different values while a disconnect is in proress.
+
+		See_also: $(D waitForData)
+	*/
+	@property bool connected() { return m_conn && m_conn.connected && !m_sentCloseFrame; }
+
+	/**
+		Returns the close code sent by the remote end.
+
+		Note if the connection was never opened, is still alive, or was closed
+		locally this value will be 0. If no close code was given by the remote
+		end in the close frame, the value will be 1005. If the connection was
+		not closed cleanly by the remote end, this value will be 1006.
+	*/
+	@property short closeCode() { return m_closeCode; }
+
+	/**
+		Returns the close reason sent by the remote end.
+
+		Note if the connection was never opened, is still alive, or was closed
+		locally this value will be an empty string.
+	*/
+	@property const(char)[] closeReason() { return m_closeReason; }
+
+	/**
+		The HTTP request that established the web socket connection.
+	*/
+	@property inout(HTTPServerRequest) request() inout { return m_request; }
+
+	/**
+		Checks if data is readily available for read.
+	*/
+	@property bool dataAvailableForRead() { return m_conn.dataAvailableForRead || m_nextMessage !is null; }
+
+	/** Waits until either a message arrives or until the connection is closed.
+
+		This function can be used in a read loop to cleanly determine when to stop reading.
+	*/
+	bool waitForData()
+	{
+		if (m_nextMessage) return true;
+
+		m_readMutex.performLocked!({
+			while (connected && m_nextMessage is null)
+				m_readCondition.wait();
+		});
+		return m_nextMessage !is null;
+	}
+
+	/// ditto
+	bool waitForData(Duration timeout)
+	{
+		import std.datetime;
+
+		if (m_nextMessage) return true;
+
+		immutable limit_time = Clock.currTime(UTC()) + timeout;
+
+		m_readMutex.performLocked!({
+			while (connected && m_nextMessage is null && timeout > 0.seconds) {
+				m_readCondition.wait(timeout);
+				timeout = limit_time - Clock.currTime(UTC());
+			}
+		});
+		return m_nextMessage !is null;
+	}
+
+	/**
+		Sends a text message.
+
+		On the JavaScript side, the text will be available as message.data (type string).
+
+		Throws:
+			A `WebSocketException` is thrown if the connection gets closed
+			before or during the transfer of the message.
+	*/
+	void send(scope const(char)[] data)
+	{
+		send(
+			(scope message) { message.write(cast(const ubyte[])data); },
+			FrameOpcode.text);
+	}
+
+	/**
+		Sends a binary message.
+
+		On the JavaScript side, the text will be available as message.data (type Blob).
+
+		Throws:
+			A `WebSocketException` is thrown if the connection gets closed
+			before or during the transfer of the message.
+	*/
+	void send(in ubyte[] data)
+	{
+		send((scope message){ message.write(data); }, FrameOpcode.binary);
+	}
+
+	/**
+		Sends a message using an output stream.
+
+		Throws:
+			A `WebSocketException` is thrown if the connection gets closed
+			before or during the transfer of the message.
+	*/
+	void send(scope void delegate(scope OutgoingWebSocketMessage) @safe sender, FrameOpcode frameOpcode)
+	{
+		m_writeMutex.performLocked!({
+			enforce!WebSocketException(!m_sentCloseFrame, "WebSocket connection already actively closed.");
+			/*scope*/auto message = new OutgoingWebSocketMessage(m_conn, frameOpcode, m_rng);
+			scope(exit) message.finalize();
+			sender(message);
+		});
+	}
+
+	/**
+		Actively closes the connection.
+
+		Params:
+			code = Numeric code indicating a termination reason.
+			reason = Message describing why the connection was terminated.
+	*/
+	void close(short code = WebSocketCloseReason.normalClosure, scope const(char)[] reason = "")
+	{
+		import std.algorithm.comparison : min;
+		if(reason !is null && reason.length == 0)
+			reason = (cast(WebSocketCloseReason)code).closeReasonString;
+
+		//control frame payloads are limited to 125 bytes
+		version(assert)
+			assert(reason.length <= 123);
+		else
+			reason = reason[0 .. min($, 123)];
+
+		if (connected) {
+			try {
+				send((scope msg) {
+					m_sentCloseFrame = true;
+					if (code != 0) {
+						msg.write(std.bitmanip.nativeToBigEndian(code));
+						msg.write(cast(const ubyte[])reason);
+					}
+				}, FrameOpcode.close);
+			} catch (Exception e) {
+				logDiagnostic("Failed to send active web socket close frame: %s", e.msg);
+			}
+		}
+		if (m_pingTimer) m_pingTimer.stop();
+
+
+		if (Task.getThis() == m_ownerTask) {
+			m_writeMutex.performLocked!({
+				if (m_clientResponse) {
+					m_clientResponse.disconnect();
+					m_clientResponse = HTTPClientResponse.init;
+				}
+				if (m_serverResponse) {
+					m_serverResponse.finalize();
+					m_serverResponse = HTTPServerResponse.init;
+				}
+			});
+
+			m_reader.join();
+
+			() @trusted { destroy(m_conn); } ();
+			m_conn = ConnectionStream.init;
+		}
+	}
+
+	/**
+		Receives a new message and returns its contents as a newly allocated data array.
+
+		Params:
+			strict = If set, ensures the exact frame type (text/binary) is received and throws an execption otherwise.
+		Throws: WebSocketException if the connection is closed or
+			if $(D strict == true) and the frame received is not the right type
+	*/
+	ubyte[] receiveBinary(bool strict = true)
+	{
+		ubyte[] ret;
+		receive((scope message){
+			enforce!WebSocketException(!strict || message.frameOpcode == FrameOpcode.binary,
+				"Expected a binary message, got "~message.frameOpcode.to!string());
+			ret = message.readAll();
+		});
+		return ret;
+	}
+	/// ditto
+	string receiveText(bool strict = true)
+	{
+		string ret;
+		receive((scope message){
+			enforce!WebSocketException(!strict || message.frameOpcode == FrameOpcode.text,
+				"Expected a text message, got "~message.frameOpcode.to!string());
+			ret = message.readAllUTF8();
+		});
+		return ret;
+	}
+
+	/**
+		Receives a new message using an InputStream.
+		Throws: WebSocketException if the connection is closed.
+	*/
+	void receive(scope void delegate(scope IncomingWebSocketMessage) @safe receiver)
+	{
+		m_readMutex.performLocked!({
+			while (!m_nextMessage) {
+				enforce!WebSocketException(connected, "Connection closed while reading message.");
+				m_readCondition.wait();
+			}
+			receiver(m_nextMessage);
+			m_nextMessage = null;
+			m_readCondition.notifyAll();
+		});
+	}
+
+	private void startReader()
+	nothrow {
+		try m_readMutex.performLocked!({}); //Wait until initialization
+		catch (Exception e) {
+			logException(e, "WebSocket reader task failed to wait for initialization");
+			try m_conn.close();
+			catch (Exception e) logException(e, "Failed to close WebSocket connection after initialization failure");
+			m_closeCode = WebSocketCloseReason.abnormalClosure;
+			try m_readCondition.notifyAll();
+			catch (Exception e) assert(false, e.msg);
+			return;
+		}
+
+		try {
+			loop:
+			while (!m_conn.empty) {
+				assert(!m_nextMessage);
+				/*scope*/auto msg = new IncomingWebSocketMessage(m_conn, m_rng);
+
+				switch (msg.frameOpcode) {
+					default: throw new WebSocketException("unknown frame opcode");
+					case FrameOpcode.ping:
+						send((scope pong_msg) { pong_msg.write(msg.peek()); }, FrameOpcode.pong);
+						break;
+					case FrameOpcode.pong:
+						// test if pong matches previous ping
+						if (msg.peek.length != uint.sizeof || m_lastPingIndex != littleEndianToNative!uint(msg.peek()[0..uint.sizeof])) {
+							logDebugV("Received PONG that doesn't match previous ping.");
+							break;
+						}
+						logDebugV("Received matching PONG.");
+						m_pongReceived = true;
+						break;
+					case FrameOpcode.close:
+						logDebug("Got closing frame (%s)", m_sentCloseFrame);
+
+						// If no close code was passed, we default to 1005
+						this.m_closeCode = WebSocketCloseReason.noStatusReceived;
+
+						// If provided in the frame, attempt to parse the close code/reason
+						if (msg.peek().length >= short.sizeof) {
+							this.m_closeCode = bigEndianToNative!short(msg.peek()[0..short.sizeof]);
+
+							if (msg.peek().length > short.sizeof) {
+								this.m_closeReason = cast(const(char) [])msg.peek()[short.sizeof..$];
+							}
+						}
+
+						if(!m_sentCloseFrame) close();
+						logDebug("Terminating connection (%s)", m_sentCloseFrame);
+						break loop;
+					case FrameOpcode.text:
+					case FrameOpcode.binary:
+					case FrameOpcode.continuation: // FIXME: add proper support for continuation frames!
+						m_readMutex.performLocked!({
+							m_nextMessage = msg;
+							m_readCondition.notifyAll();
+							while (m_nextMessage) m_readCondition.wait();
+						});
+						break;
+				}
+			}
+		} catch (Exception e) {
+			logDiagnostic("Error while reading websocket message: %s", e.msg);
+			logDiagnostic("Closing connection.");
+		}
+
+		// If no close code was passed, e.g. this was an unclean termination
+		//  of our websocket connection, set the close code to 1006.
+		if (m_closeCode == 0) m_closeCode = WebSocketCloseReason.abnormalClosure;
+
+		try m_conn.close();
+		catch (Exception e) logException(e, "Failed to close WebSocket connection");
+		try m_readCondition.notifyAll();
+		catch (Exception e) assert(false, e.msg);
+	}
+
+	private void sendPing()
+	nothrow {
+		try {
+			if (!m_pongReceived) {
+				logDebug("Pong skipped. Closing connection.");
+				close();
+				try m_readCondition.notifyAll();
+				catch (Exception e) assert(false, e.msg);
+				return;
+			}
+			m_pongReceived = false;
+			send((scope msg) { msg.write(nativeToLittleEndian(++m_lastPingIndex)); }, FrameOpcode.ping);
+			logDebugV("Ping sent");
+		} catch (Exception e) {
+			logError("Failed to acquire write mutex for sending a WebSocket ping frame: %s", e.msg);
+		}
+	}
+}
+
+/**
+	Represents a single outgoing _WebSocket message as an OutputStream.
+*/
+final class OutgoingWebSocketMessage : OutputStream {
+@safe:
+	private {
+		RandomNumberStream m_rng;
+		Stream m_conn;
+		FrameOpcode m_frameOpcode;
+		Appender!(ubyte[]) m_buffer;
+		bool m_finalized = false;
+	}
+
+	private this(Stream conn, FrameOpcode frameOpcode, RandomNumberStream rng)
+	{
+		assert(conn !is null);
+		m_conn = conn;
+		m_frameOpcode = frameOpcode;
+		m_rng = rng;
+	}
+
+	static if (is(typeof(.OutputStream.outputStreamVersion)) && .OutputStream.outputStreamVersion > 1) {
+		override size_t write(scope const(ubyte)[] bytes_, IOMode mode) { return doWrite(bytes_, mode); }
+	} else {
+		override size_t write(in ubyte[] bytes_, IOMode mode) { return doWrite(bytes_, mode); }
+	}
+
+	alias write = OutputStream.write;
+
+	private size_t doWrite(scope const(ubyte)[] bytes, IOMode mode)
+	{
+		assert(!m_finalized);
+
+		if (!m_buffer.data.length) {
+			ubyte[Frame.maxHeaderSize] header_padding;
+			m_buffer.put(header_padding[]);
+		}
+
+		m_buffer.put(bytes);
+		return bytes.length;
+	}
+
+	void flush()
+	{
+		assert(!m_finalized);
+		if (m_buffer.data.length > 0)
+			sendFrame(false);
+	}
+
+	void finalize()
+	{
+		if (m_finalized) return;
+		m_finalized = true;
+		sendFrame(true);
+	}
+
+	private void sendFrame(bool fin)
+	{
+		if (!m_buffer.data.length)
+			write(null, IOMode.once);
+
+		assert(m_buffer.data.length >= Frame.maxHeaderSize);
+
+		Frame frame;
+		frame.fin = fin;
+		frame.opcode = m_frameOpcode;
+		frame.payload = m_buffer.data[Frame.maxHeaderSize .. $];
+		auto hsize = frame.getHeaderSize(m_rng !is null);
+		auto msg = m_buffer.data[Frame.maxHeaderSize-hsize .. $];
+		frame.writeHeader(msg[0 .. hsize], m_rng);
+		m_conn.write(msg);
+		m_conn.flush();
+		m_buffer.clear();
+	}
+
+	alias write = OutputStream.write;
+}
+
+
+/**
+	Represents a single incoming _WebSocket message as an InputStream.
+*/
+final class IncomingWebSocketMessage : InputStream {
+@safe:
+	private {
+		RandomNumberStream m_rng;
+		Stream m_conn;
+		Frame m_currentFrame;
+	}
+
+	private this(Stream conn, RandomNumberStream rng)
+	{
+		assert(conn !is null);
+		m_conn = conn;
+		m_rng = rng;
+		skipFrame(); // reads the first frame
+	}
+
+	@property bool empty() const { return m_currentFrame.payload.length == 0; }
+
+	@property ulong leastSize() const { return m_currentFrame.payload.length; }
+
+	@property bool dataAvailableForRead() { return true; }
+
+	/// The frame type for this nessage;
+	@property FrameOpcode frameOpcode() const { return m_currentFrame.opcode; }
+
+	const(ubyte)[] peek() { return m_currentFrame.payload; }
+
+	/**
+	 * Retrieve the next websocket frame of the stream and discard the current
+	 * one
+	 *
+	 * This function is helpful if one wish to process frames by frames,
+	 * or minimize memory allocation, as `peek` will only return the current
+	 * frame data, and read requires a pre-allocated buffer.
+	 *
+	 * Returns:
+	 * `false` if the current frame is the final one, `true` if a new frame
+	 * was read.
+	 */
+	bool skipFrame()
+	{
+		if (m_currentFrame.fin)
+			return false;
+
+		m_currentFrame = Frame.readFrame(m_conn);
+		return true;
+	}
+
+	size_t read(scope ubyte[] dst, IOMode mode)
+	{
+		size_t nread = 0;
+
+		while (dst.length > 0) {
+			enforce!WebSocketException(!empty , "cannot read from empty stream");
+			enforce!WebSocketException(leastSize > 0, "no data available" );
+
+			import std.algorithm : min;
+			auto sz = cast(size_t)min(leastSize, dst.length);
+			dst[0 .. sz] = m_currentFrame.payload[0 .. sz];
+			dst = dst[sz .. $];
+			m_currentFrame.payload = m_currentFrame.payload[sz .. $];
+			nread += sz;
+
+			if (leastSize == 0) {
+				if (mode == IOMode.immediate || mode == IOMode.once && nread > 0)
+					break;
+				this.skipFrame();
+			}
+		}
+
+		return nread;
+	}
+
+	alias read = InputStream.read;
+}
+
+/// Magic string defined by the RFC for challenging the server during upgrade
+private static immutable s_webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+
+/**
+ * The Opcode is 4 bits, as defined in Section 5.2
+ *
+ * Values are defined in section 11.8
+ * Currently only 6 values are defined, however the opcode is defined as
+ * taking 4 bits.
+ */
+public enum FrameOpcode : ubyte {
+	continuation = 0x0,
+	text = 0x1,
+	binary = 0x2,
+	close = 0x8,
+	ping = 0x9,
+	pong = 0xA
+}
+static assert(FrameOpcode.max < 0b1111, "FrameOpcode is only 4 bits");
+
+
+private struct Frame {
+@safe:
+	enum maxHeaderSize = 14;
+
+	bool fin;
+	FrameOpcode opcode;
+	ubyte[] payload;
+
+    /**
+     * Return the header length encoded with the expected amount of bits
+     *
+     * The WebSocket RFC define a variable-length payload length.
+     * In short, it means that:
+     * - If the length is <= 125, it is stored as the 7 least significant
+     *   bits of the second header byte.  The first bit is reserved for MASK.
+     * - If the length is <= 65_536 (so it fits in 2 bytes), a magic value of
+     *   126 is stored in the aforementioned 7 bits, and the actual length
+     *   is stored in the next two bytes, resulting in a 4 bytes header
+     *   ( + masking key, if any).
+     * - If the length is > 65_536, a magic value of 127 will be used for
+     *   the 7-bit field, and the next 8 bytes are expected to be the length,
+     *   resulting in a 10 bytes header ( + masking key, if any).
+     *
+     * Those functions encapsulate all this logic and allow to just get the
+     * length with the desired size.
+     *
+     * Return:
+     * - For `ubyte`, the value to store in the 7 bits field, either the
+     *   length or a magic value (126 or 127).
+     * - For `ushort`, a value in the range [126; 65_536].
+     *   If payload.length is not in this bound, an assertion will be triggered.
+     * - For `ulong`, a value in the range [65_537; size_t.max].
+     *   If payload.length is not in this bound, an assertion will be triggered.
+     */
+	size_t getHeaderSize(bool mask)
+	{
+		size_t ret = 1;
+		if (payload.length < 126) ret += 1;
+		else if (payload.length < 65536) ret += 3;
+		else ret += 9;
+		if (mask) ret += 4;
+		return ret;
+	}
+
+	void writeHeader(ubyte[] dst, RandomNumberStream sys_rng)
+	{
+		ubyte[4] buff;
+		ubyte firstByte = cast(ubyte)opcode;
+		if (fin) firstByte |= 0x80;
+		dst[0] = firstByte;
+		dst = dst[1 .. $];
+
+		auto b1 = sys_rng ? 0x80 : 0x00;
+
+		if (payload.length < 126) {
+			dst[0] = cast(ubyte)(b1 | payload.length);
+			dst = dst[1 .. $];
+		} else if (payload.length < 65536) {
+			dst[0] = cast(ubyte) (b1 | 126);
+			dst[1 .. 3] = std.bitmanip.nativeToBigEndian(cast(ushort)payload.length);
+			dst = dst[3 .. $];
+		} else {
+			dst[0] = cast(ubyte) (b1 | 127);
+			dst[1 .. 9] = std.bitmanip.nativeToBigEndian(cast(ulong)payload.length);
+			dst = dst[9 .. $];
+		}
+
+		if (sys_rng) {
+			sys_rng.read(dst[0 .. 4]);
+			for (size_t i = 0; i < payload.length; i++)
+				payload[i] ^= dst[i % 4];
+		}
+	}
+
+	static Frame readFrame(InputStream stream)
+	{
+		Frame frame;
+		ubyte[8] data;
+
+		stream.read(data[0 .. 2]);
+		frame.fin = (data[0] & 0x80) != 0;
+		frame.opcode = cast(FrameOpcode)(data[0] & 0x0F);
+
+		bool masked = !!(data[1] & 0b1000_0000);
+
+		//parsing length
+		ulong length = data[1] & 0b0111_1111;
+		if (length == 126) {
+			stream.read(data[0 .. 2]);
+			length = bigEndianToNative!ushort(data[0 .. 2]);
+		} else if (length == 127) {
+			stream.read(data);
+			length = bigEndianToNative!ulong(data);
+
+			// RFC 6455, 5.2, 'Payload length': If 127, the following 8 bytes
+			// interpreted as a 64-bit unsigned integer (the most significant
+			// bit MUST be 0)
+			enforce!WebSocketException(!(length >> 63),
+				"Received length has a non-zero most significant bit");
+
+		}
+		logDebug("Read frame: %s %s %s length=%d",
+				 frame.opcode,
+				 frame.fin ? "final frame" : "continuation",
+				 masked ? "masked" : "not masked",
+				 length);
+
+		// Masking key is 32 bits / uint
+		if (masked)
+			stream.read(data[0 .. 4]);
+
+		// Read payload
+		// TODO: Provide a way to limit the size read, easy
+		// DOS for server code here (rejectedsoftware/vibe.d#1496).
+		enforce!WebSocketException(length <= size_t.max);
+		frame.payload = new ubyte[](cast(size_t)length);
+		stream.read(frame.payload);
+
+		//de-masking
+		if (masked)
+			foreach (size_t i; 0 .. cast(size_t)length)
+				frame.payload[i] = frame.payload[i] ^ data[i % 4];
+
+		return frame;
+	}
+}
+
+unittest {
+	import std.algorithm.searching : all;
+
+	final class DummyRNG : RandomNumberStream {
+	@safe:
+		@property bool empty() { return false; }
+		@property ulong leastSize() { return ulong.max; }
+		@property bool dataAvailableForRead() { return true; }
+		const(ubyte)[] peek() { return null; }
+		size_t read(scope ubyte[] buffer, IOMode mode) @trusted { buffer[] = 13; return buffer.length; }
+		alias read = RandomNumberStream.read;
+	}
+
+	ubyte[14] hdrbuf;
+	auto rng = new DummyRNG;
+
+	Frame f;
+	f.payload = new ubyte[125];
+
+	assert(f.getHeaderSize(false) == 2);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 2], null);
+	assert(hdrbuf[0 .. 2] == [0, 125]);
+
+	assert(f.getHeaderSize(true) == 6);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 6], rng);
+	assert(hdrbuf[0 .. 2] == [0, 128|125]);
+	assert(hdrbuf[2 .. 6].all!(b => b == 13));
+
+	f.payload = new ubyte[126];
+	assert(f.getHeaderSize(false) == 4);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 4], null);
+	assert(hdrbuf[0 .. 4] == [0, 126, 0, 126]);
+
+	assert(f.getHeaderSize(true) == 8);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 8], rng);
+	assert(hdrbuf[0 .. 4] == [0, 128|126, 0, 126]);
+	assert(hdrbuf[4 .. 8].all!(b => b == 13));
+
+	f.payload = new ubyte[65535];
+	assert(f.getHeaderSize(false) == 4);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 4], null);
+	assert(hdrbuf[0 .. 4] == [0, 126, 255, 255]);
+
+	assert(f.getHeaderSize(true) == 8);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 8], rng);
+	assert(hdrbuf[0 .. 4] == [0, 128|126, 255, 255]);
+	assert(hdrbuf[4 .. 8].all!(b => b == 13));
+
+	f.payload = new ubyte[65536];
+	assert(f.getHeaderSize(false) == 10);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 10], null);
+	assert(hdrbuf[0 .. 10] == [0, 127, 0, 0, 0, 0, 0, 1, 0, 0]);
+
+	assert(f.getHeaderSize(true) == 14);
+	hdrbuf[] = 0;
+	f.writeHeader(hdrbuf[0 .. 14], rng);
+	assert(hdrbuf[0 .. 10] == [0, 128|127, 0, 0, 0, 0, 0, 1, 0, 0]);
+	assert(hdrbuf[10 .. 14].all!(b => b == 13));
+}
+
+/**
+ * Generate a challenge key for the protocol upgrade phase.
+ */
+private string generateChallengeKey(RandomNumberStream rng)
+{
+	ubyte[16] buffer;
+	rng.read(buffer);
+	return Base64.encode(buffer);
+}
+
+private string computeAcceptKey(string challengekey)
+{
+	immutable(ubyte)[] b = challengekey.representation;
+	immutable(ubyte)[] a = s_webSocketGuid.representation;
+	SHA1 hash;
+	hash.start();
+	hash.put(b);
+	hash.put(a);
+	auto result = Base64.encode(hash.finish());
+	return to!(string)(result);
+}