Browse Source

More stable while parsing the http request (#4821)

* Add hunt-http

* Try to parse more request data
Xueping 6 years ago
parent
commit
2ed990e556

+ 32 - 6
frameworks/D/hunt/dub.json

@@ -2,18 +2,44 @@
 	"name": "hunt-minihttp",
 	"targetType": "executable",
 	"description": "A mini http server powered by Hunt.",
-    "copyright": "Copyright (C) 2017-2019, HuntLabs",
-    "homepage": "https://www.huntlabs.net",
+	"copyright": "Copyright (C) 2017-2019, HuntLabs",
+	"homepage": "https://www.huntlabs.net",
 	"license": "Apache-2.0",
-	"libs-posix": [ "picohttpparser" ],
-	"lflags-posix": ["-Lpicohttpparser/"],
 	"dependencies": {
 		"hunt": "~>1.2.0",
 		"hunt-database": "~>1.2.0",
 		"std_data_json": "~>0.18.2"
 	},
-	"versions": ["POSTGRESQL"],
+	"versions": [
+		"POSTGRESQL"
+	],
+	"configurations": [
+		{
+			"name": "default",
+			"libs-posix": [
+				"http_parser"
+			],
+			"lflags-posix": [
+				"-Lhttp-parser/"
+			],
+			"versions": [
+				"HTTP"
+			]
+		},
+		{
+			"name": "minihttp",
+			"libs-posix": [
+				"picohttpparser"
+			],
+			"lflags-posix": [
+				"-Lpicohttpparser/"
+			],
+			"versions": [
+				"MINIHTTP"
+			]
+		}
+	],
 	"subConfigurations": {
 		"hunt-database": "postgresql"
 	}
-}
+}

+ 3 - 2
frameworks/D/hunt/hunt-dmd.dockerfile

@@ -8,9 +8,10 @@ RUN apt update -y && apt install -y --no-install-recommends git && apt install -
 RUN git clone https://github.com/h2o/picohttpparser.git && \
     cp -rf patches/Makefile picohttpparser && \
     cd picohttpparser && \
-    make package
+    make package && \
+    cd ..
 
 RUN dub upgrade --verbose
-RUN dub build --build=release --arch=x86_64 --compiler=dmd -f
+RUN dub build --build=release --arch=x86_64 --compiler=dmd  -c=minihttp
 
 CMD ["./hunt-minihttp"]

+ 16 - 0
frameworks/D/hunt/hunt-http.dockerfile

@@ -0,0 +1,16 @@
+FROM dlangchina/dlang-ldc:latest
+
+ADD ./ /hunt
+WORKDIR /hunt
+
+RUN apt update -y && apt install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/* && rm -rf /var/cache/apt/*
+
+RUN git clone https://github.com/nodejs/http-parser.git && \
+    cd http-parser && \
+    make package && \
+    cd ..
+
+RUN dub upgrade --verbose
+RUN dub build --build=release --arch=x86_64 --compiler=ldc2
+
+CMD ["./hunt-minihttp"]

+ 3 - 2
frameworks/D/hunt/hunt.dockerfile

@@ -8,9 +8,10 @@ RUN apt update -y && apt install -y --no-install-recommends git && apt install -
 RUN git clone https://github.com/h2o/picohttpparser.git && \
     cp -rf patches/Makefile picohttpparser && \
     cd picohttpparser && \
-    make package
+    make package && \
+    cd ..
 
 RUN dub upgrade --verbose
-RUN dub build --build=release --arch=x86_64 --compiler=ldc2
+RUN dub build --build=release --arch=x86_64 --compiler=ldc2 -c=minihttp 
 
 CMD ["./hunt-minihttp"]

+ 47 - 3
frameworks/D/hunt/source/app.d

@@ -8,6 +8,49 @@
  * Licensed under the Apache-2.0 License.
  *
  */
+ version(MINIHTTP)
+ {
+	import std.getopt;
+	import std.stdio;
+
+	import hunt.database;
+	import hunt.io;
+	import hunt.system.Memory : totalCPUs;
+	import minihttp.Processor;
+	import minihttp.Server;
+	import minihttp.DemoProcessor;
+
+	void main(string[] args) {
+		ushort port = 8080;
+		GetoptResult o = getopt(args, "port|p", "Port (default 8080)", &port);
+		if (o.helpWanted) {
+			defaultGetoptPrinter("A mini-http server powered by Hunt!", o.options);
+			return;
+		}
+
+		version (POSTGRESQL) {
+			DatabaseOption options;
+			debug {
+				options = new DatabaseOption(
+						"postgresql://benchmarkdbuser:[email protected]:5432/hello_world?charset=utf-8");
+			} else {
+				options = new DatabaseOption(
+						"postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world?charset=utf-8");
+			}
+			options.setMinimumConnection(totalCPUs*3);
+			options.setMaximumConnection(totalCPUs*3);
+			dbConnection = new Database(options);
+		}
+
+		AbstractTcpServer httpServer = new HttpServer!(DemoProcessor)("0.0.0.0", port, totalCPUs);
+		writefln("listening on http://%s", httpServer.bindingAddress.toString());
+		httpServer.start();
+	}
+
+ }
+else
+{
+
 import std.getopt;
 import std.stdio;
 
@@ -16,7 +59,7 @@ import hunt.io;
 import hunt.system.Memory : totalCPUs;
 import http.Processor;
 import http.Server;
-import DemoProcessor;
+import http.DemoProcessor;
 
 void main(string[] args) {
 	ushort port = 8080;
@@ -35,8 +78,8 @@ void main(string[] args) {
 			options = new DatabaseOption(
 					"postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world?charset=utf-8");
 		}
-		options.setMinimumConnection(totalCPUs*3);
-		options.setMaximumConnection(totalCPUs*3);
+		options.setMinimumConnection(totalCPUs*2);
+		options.setMaximumConnection(totalCPUs*2);
 		dbConnection = new Database(options);
 	}
 
@@ -44,3 +87,4 @@ void main(string[] args) {
 	writefln("listening on http://%s", httpServer.bindingAddress.toString());
 	httpServer.start();
 }
+}

+ 4 - 4
frameworks/D/hunt/source/DemoProcessor.d → frameworks/D/hunt/source/http/DemoProcessor.d

@@ -1,11 +1,12 @@
-module DemoProcessor;
+module http.DemoProcessor;
+
+version(HTTP) :
 
 // import stdx.data.json;
 import std.json;
 
 import hunt.database;
 import hunt.io;
-import http.Common;
 import http.Processor;
 import http.HttpURI;
 import http.UrlEncoded;
@@ -39,8 +40,7 @@ class DemoProcessor : HttpProcessor {
         super(client);
     }
 
-    override void onComplete(ref HttpRequest req) {
-
+    override void onComplete(HttpRequest req) {
         string path = req.uri;
         if(path.length == plaintextLength) { // plaintext
             respondWith("Hello, World!", 200, textHeader);

+ 2 - 0
frameworks/D/hunt/source/http/HttpURI.d

@@ -1,5 +1,7 @@
 module http.HttpURI;
 
+version(HTTP) :
+
 import hunt.collection.MultiMap;
 
 import hunt.Exceptions;

+ 223 - 128
frameworks/D/hunt/source/http/Parser.d

@@ -2,61 +2,157 @@
 /// Used for benchmarks with simple server
 module http.Parser;
 
-import http.Common;
+version(HTTP) :
+
+private:
 
-import hunt.logging.ConsoleLogger;
-import std.conv;
 import std.range.primitives;
 import core.stdc.string;
 
+alias http_data_cb = extern (C) int function(http_parser*, const ubyte* at, size_t length);
+alias http_cb = extern (C) int function(http_parser*);
 
+public enum HttpParserType : uint {
+	request = 0,
+	response = 1,
+	both = 2
+}
 
-/* contains name and value of a header (name == NULL if is a continuing line
- * of a multiline header */
-struct phr_header {
-    const char *name;
-    size_t name_len;
-    const char *value;
-    size_t value_len;
+public enum HttpMethod : uint {
+	DELETE = 0,
+	GET = 1,
+	HEAD = 2,
+	POST = 3,
+	PUT = 4,
+	/* pathological */
+	CONNECT = 5,
+	OPTIONS = 6,
+	TRACE = 7,
+	/* WebDAV */
+	COPY = 8,
+	LOCK = 9,
+	MKCOL = 10,
+	MOVE = 11,
+	PROPFIND = 12,
+	PROPPATCH = 13,
+	SEARCH = 14,
+	UNLOCK = 15,
+	BIND = 16,
+	REBIND = 17,
+	UNBIND = 18,
+	ACL = 19,
+	/* subversion */
+	REPORT = 20,
+	MKACTIVITY = 21,
+	CHECKOUT = 22,
+	MERGE = 23,
+	/* upnp */
+	MSEARCH = 24,
+	NOTIFY = 25,
+	SUBSCRIBE = 26,
+	UNSUBSCRIBE = 27,
+	/* RFC-5789 */
+	PATCH = 28,
+	PURGE = 29,
+	/* CalDAV */
+	MKCALENDAR = 30,
+	/* RFC-2068, section 19.6.1.2 */
+	LINK = 31,
+	UNLINK = 32,
+	/* icecast */
+	SOURCE = 33,
 }
 
-/* returns number of bytes consumed if successful, -2 if request is partial,
- * -1 if failed */
-extern (C) pure @nogc nothrow int phr_parse_request(const char *buf, size_t len, const char **method, 
-	size_t *method_len, const char **path, size_t *path_len,
-    int *minor_version, phr_header *headers, size_t *num_headers, size_t last_len);
-
-/* ditto */
-extern (C) pure @nogc nothrow int phr_parse_response(const char *_buf, size_t len, int *minor_version, 
-	int *status, const char **msg, size_t *msg_len,
-    phr_header *headers, size_t *num_headers, size_t last_len);
-
-/* ditto */
-extern (C) pure @nogc nothrow int phr_parse_headers(const char *buf, size_t len, 
-	phr_header *headers, size_t *num_headers, size_t last_len);
-
-/* should be zero-filled before start */
-struct phr_chunked_decoder {
-    size_t bytes_left_in_chunk; /* number of bytes left in current chunk */
-    char consume_trailer;       /* if trailing headers should be consumed */
-    char _hex_count;
-    char _state;
+enum HttpError : uint {
+	OK,
+	/* Callback-related errors */
+	CB_message_begin,
+	CB_url,
+	CB_header_field,
+	CB_header_value,
+	CB_headers_complete,
+	CB_body,
+	CB_message_complete,
+	CB_status,
+	CB_chunk_header,
+	CB_chunk_complete,
+	/* Parsing-related errors */
+	INVALID_EOF_STATE,
+	HEADER_OVERFLOW,
+	CLOSED_CONNECTION,
+	INVALID_VERSION,
+	INVALID_STATUS,
+	INVALID_METHOD,
+	INVALID_URL,
+	INVALID_HOST,
+	INVALID_PORT,
+	INVALID_PATH,
+	INVALID_QUERY_STRING,
+	INVALID_FRAGMENT,
+	LF_EXPECTED,
+	INVALID_HEADER_TOKEN,
+	INVALID_CONTENT_LENGTH,
+	UNEXPECTED_CONTENT_LENGTH,
+	INVALID_CHUNK_SIZE,
+	INVALID_CONSTANT,
+	INVALID_INTERNAL_STATE,
+	STRICT,
+	PAUSED,
+	UNKNOWN,
 }
 
-/* the function rewrites the buffer given as (buf, bufsz) removing the chunked-
- * encoding headers.  When the function returns without an error, bufsz is
- * updated to the length of the decoded data available.  Applications should
- * repeatedly call the function while it returns -2 (incomplete) every time
- * supplying newly arrived data.  If the end of the chunked-encoded data is
- * found, the function returns a non-negative number indicating the number of
- * octets left undecoded at the tail of the supplied buffer.  Returns -1 on
- * error.
- */
-extern (C) pure @nogc nothrow ptrdiff_t phr_decode_chunked(phr_chunked_decoder *decoder, char *buf, size_t *bufsz);
+struct http_parser {
+	/** PRIVATE **/
+	uint state; // bitfield
+	uint nread; /* # bytes read in various scenarios */
+	ulong content_length; /* # bytes in body (0 if no Content-Length header) */
+
+	/** READ-ONLY **/
+	ushort http_major;
+	ushort http_minor;
+	// bitfield
+	uint status_code_method_http_errono_upgrade;
+	/** PUBLIC **/
+	void* data; /* A pointer to get hook to the "connection" or "socket" object */
+}
+
+struct http_parser_settings {
+	http_cb on_message_begin;
+	http_data_cb on_url;
+	http_data_cb on_status;
+	http_data_cb on_header_field;
+	http_data_cb on_header_value;
+	http_cb on_headers_complete;
+	http_data_cb on_body;
+	http_cb on_message_complete;
+	/* When on_chunk_header is called, the current chunk length is stored
+   * in parser->content_length.
+   */
+	http_cb on_chunk_header;
+	http_cb on_chunk_complete;
+}
+
+extern (C) pure @nogc nothrow void http_parser_init(http_parser* parser, HttpParserType type);
+
+extern (C) pure @nogc nothrow int http_should_keep_alive(const http_parser* parser);
+
+/* Return a string description of the given error */
+extern (C) pure @nogc nothrow immutable(char)* http_errno_description(HttpError err);
+
+/* Checks if this is the final chunk of the body. */
+extern (C) pure @nogc nothrow int http_body_is_final(const http_parser* parser);
 
-/* returns if the chunked decoder is in middle of chunked data */
-extern (C) pure @nogc nothrow int phr_decode_chunked_is_in_data(phr_chunked_decoder *decoder);
+/* Executes the parser. Returns number of parsed bytes. Sets
+* `parser->http_errno` on error. */
+extern (C) pure @nogc nothrow size_t http_parser_execute(http_parser* parser,
+		const http_parser_settings* settings, const ubyte* data, size_t len);
 
+// extern (C) uint http_parser_flags(const http_parser* parser);
+
+uint http_parser_flags(const http_parser* parser) {
+	// return parser.status_code | (parser.method<<16) | (parser.http_errno << 24) | (parser.upgrade << 31);
+	return parser.status_code_method_http_errono_upgrade;
+}
 
 // =========== Public interface starts here =============
 
@@ -68,121 +164,120 @@ class HttpException : Exception {
 	pure @nogc nothrow this(HttpError error, string file = __FILE__,
 			size_t line = __LINE__, Throwable nextInChain = null) {
 		this.error = error;
-		super("Http exception", file, line, nextInChain);
+		immutable char* str = http_errno_description(error);
+		super(str[0 .. strlen(str)], file, line, nextInChain);
 	}
 }
 
 struct HttpParser(Interceptor) {
-	
-private {
+	http_parser parser;
+	http_parser_settings settings;
 	Interceptor interceptor;
 	Throwable failure;
-	phr_header[50] _headers;
-	char *_method;
-	char *path;
-
-	int minor_version;
-	size_t buflen = 0, prevbuflen = 0, method_len, path_len, num_headers;
-}
-
-
-	alias interceptor this;
-
-	this(Interceptor interceptor) {
-		this.interceptor = interceptor;
+	uint flags;
+
+	static generateCallback(string cName, string dName) {
+		import std.format;
+
+		return format(`
+			static if(__traits(hasMember, interceptor, "%2$s"))
+			{
+				extern(C) static int %1$s(http_parser* p) {
+					auto parser = cast(HttpParser*)p;
+					try {
+						parser.flags = http_parser_flags(p);
+						return parser.interceptor.%2$s(parser);
+					}
+					catch (Throwable t) {
+						parser.failure = t;
+						return 1;
+					}
+				}
+				settings.%1$s = &%1$s;
+			}
+		`, cName, dName);
 	}
 
-	@property bool status() pure @safe nothrow {
-		return failure is null;
+	static generateCallbackWithData(string cName, string dName) {
+		import std.format;
+
+		return format(`
+			static if(__traits(hasMember, interceptor, "%2$s"))
+			{
+				extern(C) static int %1$s(http_parser* p, const ubyte* at, size_t size) {
+					auto parser = cast(HttpParser*)p;
+					try {
+						parser.flags = http_parser_flags(p);
+						return parser.interceptor.%2$s(parser, at[0..size]);
+					}
+					catch (Throwable t) {
+						parser.failure = t;
+						return 1;
+					}
+				}
+				settings.%1$s = &%1$s;
+			}
+		`, cName, dName);
 	}
 
-	string uri(bool canCopy=false)() {
-		static if(canCopy) {
-			return cast(string)path[0..path_len].dup;
-		} else {
-			return cast(string)path[0..path_len];
-		}
+	@property HttpError errorCode() pure @safe nothrow {
+		return cast(HttpError)((flags >> 24) & 0x7f);
 	}
 
-	@property HttpMethod method() {
-		string s = cast(string)_method[0..method_len];
-		return to!HttpMethod(s);
-	}
+public:
+	alias interceptor this;
 
+	@property uint status() pure @safe nothrow {
+		return flags & 0xffff;
+	}
 
-	HttpHeader[] headers(bool canCopy=false)() {
-		HttpHeader[] hs = new HttpHeader[num_headers];
-		
-		for(int i; i<num_headers; i++) {
-			phr_header* h = &_headers[i];
-			static if(canCopy) {
-				hs[i].name = cast(string)h.name[0..h.name_len].idup;
-				hs[i].value = cast(string)h.value[0..h.value_len].idup;
-			} else {
-				hs[i].name = cast(string)h.name[0..h.name_len];
-				hs[i].value = cast(string)h.value[0..h.value_len];
-			}
-		}
+	@property HttpMethod method() pure @safe nothrow {
+		return cast(HttpMethod)((flags >> 16) & 0xFF);
+	}
 
-		return hs;
+	this(Interceptor interceptor, HttpParserType type) {
+		this.interceptor = interceptor;
+		http_parser_init(&parser, type);
+		mixin(generateCallback("on_message_begin", "onMessageBegin"));
+		mixin(generateCallbackWithData("on_url", "onUrl"));
+		mixin(generateCallbackWithData("on_status", "onStatus"));
+		mixin(generateCallbackWithData("on_body", "onBody"));
+		mixin(generateCallbackWithData("on_header_field", "onHeaderField"));
+		mixin(generateCallbackWithData("on_header_value", "onHeaderValue"));
+		mixin(generateCallback("on_headers_complete", "onHeadersComplete"));
+		mixin(generateCallback("on_message_complete", "onMessageComplete"));
 	}
 
 	@property bool shouldKeepAlive() pure nothrow {
-		return true;
+		return http_should_keep_alive(&parser) == 1;
 	}
 
 	@property ushort httpMajor() @safe pure nothrow {
-		return 1;
+		return parser.http_major;
 	}
 
 	@property ushort httpMinor() @safe pure nothrow {
-		return cast(ushort)minor_version;
-	}
-
-	void execute(const(char)[] str) {
-		execute(cast(const(ubyte)[]) str);
+		return parser.http_minor;
 	}
 
-	void execute(const(ubyte)[] chunk) {
-		failure = null;
-		num_headers = cast(int)_headers.length;
-		int pret = phr_parse_request(cast(const char*)chunk.ptr, cast(int)chunk.length, 
-					&_method, &method_len, 
-					&path, &path_len,
-					&minor_version, 
-					_headers.ptr, &num_headers,
-					0);
-		debug {
-			infof("buffer: %d bytes, request: %d bytes", chunk.length, pret);
-		} else {
-			if(pret < chunk.length)
-				infof("buffer: %d bytes, request: %d bytes", chunk.length, pret);
-		}
-
-		if(pret > 0) {
-			/* successfully parsed the request */
-			onMessageComplete();
-		} else if (pret == -1) {
-			num_headers = 0;
-			failure = new HttpException(HttpError.UNKNOWN);
-			throw failure;
+	size_t execute(const(ubyte)[] chunk) {
+		size_t size = http_parser_execute(&parser, &settings, chunk.ptr, chunk.length);
+		flags = http_parser_flags(&parser);
+		if (errorCode) {
+			auto f = failure;
+			failure = null;
+			if (f is null)
+				f = new HttpException(errorCode);
+			throw f;
 		}
+		return size;
 	}
 
-	void onMessageComplete() {
-		// interceptor.onHeadersComplete();
-		debug {
-			tracef("method is %s", _method[0..method_len]);
-			tracef("path is %s", path[0..path_len]);
-			tracef("HTTP version is 1.%d", minor_version);
-			foreach(ref phr_header h; _headers[0..num_headers]) {
-				tracef("Header: %s = %s", h.name[0..h.name_len], h.value[0..h.value_len]);
-			}
-		}
-		interceptor.onMessageComplete();
+	size_t execute(const(char)[] str) {
+		return execute(cast(const(ubyte)[]) str);
 	}
 }
 
-auto httpParser(Interceptor)(Interceptor interceptor) {
-	return HttpParser!Interceptor(interceptor);
+auto httpParser(Interceptor)(Interceptor interceptor, HttpParserType type) {
+	return HttpParser!Interceptor(interceptor, type);
 }

+ 131 - 35
frameworks/D/hunt/source/http/Processor.d

@@ -2,36 +2,27 @@
 ///
 module http.Processor;
 
-import std.conv;
+version(HTTP) :
+
 import std.array, std.exception, std.format, std.algorithm.mutation, std.socket;
 import core.stdc.stdlib;
 import core.thread, core.atomic;
 import http.Parser;
 
 import hunt.collection.ByteBuffer;
-import http.Common;
 import hunt.logging;
 import hunt.io;
 import hunt.util.DateTime;
 
 
-private	alias Parser = HttpParser!HttpProcessor;
-
+struct HttpHeader {
+	string name, value;
+}
 
 struct HttpRequest {
-	private Parser* parser;
-
-	HttpHeader[] headers(bool canCopy=false)() @property {
-		return parser.headers!canCopy();
-	}
-
-	HttpMethod method() @property {
-		return parser.method();
-	}
-
-	string uri(bool canCopy=false)() @property {
-		return parser.uri!(canCopy)();
-	}
+	HttpHeader[] headers;
+	HttpMethod method;
+	string uri;
 }
 
 version(NO_HTTPPARSER) {
@@ -39,12 +30,23 @@ enum string ResponseData = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\nConnection:
 }
 
 abstract class HttpProcessor {
-	
-package:
+private:
+	enum State {
+		url,
+		field,
+		value,
+		done
+	}
+
 	Appender!(char[]) outBuf;
 	HttpHeader[] headers; // buffer for headers
+	size_t header; // current header
+	string url; // url
+	alias Parser = HttpParser!HttpProcessor;
 	Parser parser;
+	ScratchPad pad;
 	HttpRequest request;
+	State state;
 	bool serving;
 	
 public:
@@ -54,8 +56,8 @@ public:
 		serving = true;
 		client = sock;
 		headers = new HttpHeader[1];
-		parser = httpParser(this);
-		request.parser = &parser;
+		pad = ScratchPad(16 * 1024);
+		parser = httpParser(this, HttpParserType.request);
 	}
 
 	void run() {
@@ -63,12 +65,9 @@ public:
 			version(NO_HTTPPARSER) {
 				client.write(cast(ubyte[])ResponseData);
 			} else {
-				try {
-					parser.execute(cast(ubyte[]) buffer.getRemaining());
-				} catch(Exception ex) {
-					respondWith(ex.msg, 500);
-				}
+				parser.execute(cast(ubyte[]) buffer.getRemaining());
 			}
+
 		})
 		.onClosed(() {
 			// notifyClientClosed();
@@ -106,24 +105,79 @@ public:
 		client.write(cast(ubyte[]) outBuf.data); // TODO: short-writes are quite possible
 	}
 
-	void onChunk(ref HttpRequest req, const(ubyte)[] chunk) {
-		// TODO: Tasks pending completion - 5/16/2019, 5:40:18 PM
-		// 
+	void onStart(HttpRequest req) {
 	}
 
-	void onComplete(ref HttpRequest req);
+	void onChunk(HttpRequest req, const(ubyte)[] chunk) {
+	}
+
+	void onComplete(HttpRequest req);
+
+	final int onMessageBegin(Parser* parser) {
+		outBuf.clear();
+		header = 0;
+		pad.reset();
+		state = State.url;
+		return 0;
+	}
 
+	final int onUrl(Parser* parser, const(ubyte)[] chunk) {
+		pad.put(chunk);
+		return 0;
+	}
 
 	final int onBody(Parser* parser, const(ubyte)[] chunk) {
 		onChunk(request, chunk);
 		return 0;
 	}
 
-	final int onMessageComplete() {
-		try {
-			onComplete(request);
-		} catch(Exception ex) {
-			respondWith(ex.msg, 500);
+	final int onHeaderField(Parser* parser, const(ubyte)[] chunk) {
+		final switch (state) {
+		case State.url:
+			url = pad.sliceStr;
+			break;
+		case State.value:
+			headers[header].value = pad.sliceStr;
+			header += 1;
+			if (headers.length <= header)
+				headers.length += 1;
+			break;
+		case State.field:
+		case State.done:
+			break;
+		}
+		state = State.field;
+		pad.put(chunk);
+		return 0;
+	}
+
+	final int onHeaderValue(Parser* parser, const(ubyte)[] chunk) {
+		if (state == State.field) {
+			headers[header].name = pad.sliceStr;
+		}
+		pad.put(chunk);
+		state = State.value;
+		return 0;
+	}
+
+	final int onHeadersComplete(Parser* parser) {
+		headers[header].value = pad.sliceStr;
+		header += 1;
+		request = HttpRequest(headers[0 .. header], parser.method, url);
+		onStart(request);
+		state = State.done;
+		return 0;
+	}
+
+	final int onMessageComplete(Parser* parser) {
+		import std.stdio;
+
+		if (state == State.done) {
+			try {
+				onComplete(request);
+			} catch(Exception ex) {
+				respondWith(ex.msg, 500);
+			}
 		}
 		if (!parser.shouldKeepAlive)
 			serving = false;
@@ -131,3 +185,45 @@ public:
 	}
 
 }
+
+// ==================================== IMPLEMENTATION DETAILS ==============================================
+private:
+
+struct ScratchPad {
+	ubyte* ptr;
+	size_t capacity;
+	size_t last, current;
+
+	this(size_t size) {
+		ptr = cast(ubyte*) malloc(size);
+		capacity = size;
+	}
+
+	void put(const(ubyte)[] slice) {
+		enforce(current + slice.length <= capacity, "HTTP headers too long");
+		ptr[current .. current + slice.length] = slice[];
+		current += slice.length;
+	}
+
+	const(ubyte)[] slice() {
+		auto data = ptr[last .. current];
+		last = current;
+		return data;
+	}
+
+	string sliceStr() {
+		return cast(string) slice;
+	}
+
+	void reset() {
+		current = 0;
+		last = 0;
+	}
+
+	@disable this(this);
+
+	~this() {
+		free(ptr);
+		ptr = null;
+	}
+}

+ 3 - 1
frameworks/D/hunt/source/http/Server.d

@@ -1,5 +1,7 @@
 module http.Server;
 
+version(HTTP) :
+
 import hunt.event;
 import hunt.io;
 import hunt.logging.ConsoleLogger;
@@ -54,7 +56,7 @@ abstract class AbstractTcpServer {
 		server.bind(new InternetAddress("0.0.0.0", 8080));
 		server.listen(8192);
 
-		trace("Launching server");
+		trace("Launching http server");
 		debug {
 			_group.start();
 		} else {

+ 2 - 0
frameworks/D/hunt/source/http/UrlEncoded.d

@@ -1,5 +1,7 @@
 module http.UrlEncoded;
 
+version(HTTP) :
+
 import hunt.collection.List;
 import hunt.collection.MultiMap;
 import hunt.collection.StringBuffer;

+ 2 - 1
frameworks/D/hunt/source/http/Common.d → frameworks/D/hunt/source/minihttp/Common.d

@@ -1,5 +1,6 @@
-module http.Common;
+module minihttp.Common;
 
+version(MINIHTTP):
 
 public enum HttpParserType : uint {
 	request = 0,

+ 254 - 0
frameworks/D/hunt/source/minihttp/DemoProcessor.d

@@ -0,0 +1,254 @@
+module minihttp.DemoProcessor;
+
+version(MINIHTTP):
+
+// import stdx.data.json;
+import std.json;
+
+import hunt.database;
+import hunt.io;
+import minihttp.Common;
+import minihttp.Processor;
+import minihttp.HttpURI;
+import minihttp.UrlEncoded;
+import hunt.logging.ConsoleLogger : trace, warning, tracef;
+
+import std.algorithm;
+import std.array;
+import std.exception;
+import std.random;
+import std.string;
+
+version (POSTGRESQL) {
+    __gshared Database dbConnection;
+}
+
+enum HttpHeader textHeader = HttpHeader("Content-Type", "text/plain; charset=UTF-8");
+enum HttpHeader htmlHeader = HttpHeader("Content-Type", "text/html; charset=UTF-8");
+enum HttpHeader jsonHeader = HttpHeader("Content-Type", "application/json; charset=UTF-8");
+
+
+enum plaintextLength = "/plaintext".length;
+enum jsonLength = "/json".length;
+enum dbLength = "/db".length;
+enum fortunesLength = "/fortunes".length;
+
+class DemoProcessor : HttpProcessor {
+    version (POSTGRESQL) HttpURI uri;
+
+    this(TcpStream client) {
+        version (POSTGRESQL) uri = new HttpURI();
+        super(client);
+    }
+
+    override void onComplete(ref HttpRequest req) {
+
+        string path = req.uri;
+        if(path.length == plaintextLength) { // plaintext
+            respondWith("Hello, World!", 200, textHeader);
+        } else if(path.length == jsonLength) { // json
+            JSONValue js = JSONValue(["message" : JSONValue("Hello, World!")]);
+            respondWith(js.toJSON(), 200, jsonHeader);
+        } else {
+
+        version (POSTGRESQL) {
+            if(path.length == dbLength) {
+                respondSingleQuery();
+            } else if(path.length == fortunesLength) {
+                respondFortunes();
+            } else {
+                handleDbUpdate(path);
+            }
+
+        } else {
+            respondWith404();
+        }
+        }    
+    }
+
+
+    private void respondWith404() {
+        version (POSTGRESQL) {
+            respondWith("The available paths are: /plaintext, /json, /db, /fortunes," ~
+             " /queries?queries=number, /updates?queries=number", 404);
+        } else {
+            respondWith("The available paths are: /plaintext, /json", 404);
+        }
+    }
+
+    version (POSTGRESQL) {
+        private void handleDbUpdate(string url) {
+            uri.parse(url);
+            
+            switch(uri.getPath()) {
+            case "/queries":
+                UrlEncoded queriesMap = new UrlEncoded();
+                uri.decodeQueryTo(queriesMap);
+                int number = 1;
+                debug {
+                    trace(queriesMap.toString());
+                    if (!queriesMap.containsKey("queries")) {
+                        respondWith404();
+                        return;
+                    }
+
+                    string v = queriesMap.getValue("queries", 0);
+                    if (!v.empty) {
+                        try {
+                            number = to!int(v);
+                        } catch (Exception ex) {
+                            warning(ex.msg);
+                        }
+                    }
+                } else {
+                    string v = queriesMap.getValue("queries", 0);
+                    if (!v.empty) {
+                        try {
+                            number = to!int(v);
+                        } catch (Exception ex) {
+                        }
+                    }
+                }
+
+                respondMultipleQuery(number);
+                break;
+
+
+            case "/updates":
+                UrlEncoded queriesMap = new UrlEncoded();
+                uri.decodeQueryTo(queriesMap);
+                int number = 1;
+                debug {
+                    if (!queriesMap.containsKey("queries")) {
+                        respondWith404();
+                        return;
+                    }
+
+                    string v = queriesMap.getValue("queries", 0);
+                    if (!v.empty) {
+                        try {
+                            number = to!int(v);
+                        } catch (Exception ex) {
+                            warning(ex.msg);
+                        }
+                    }
+                } else {
+                    string v = queriesMap.getValue("queries", 0);
+                    if (!v.empty) {
+                        try {
+                            number = to!int(v);
+                        } catch (Exception ex) {
+                        }
+                    }
+                }
+                respondUpdates(number);
+                break;
+
+            default:
+                respondWith404();
+                break;
+            }
+        }
+
+
+        private void respondSingleQuery() {
+            int id = uniform(1, 10000);
+            string query = "SELECT randomNumber FROM world WHERE id = " ~ id.to!string;
+            ResultSet rs = dbConnection.query(query);
+
+            JSONValue js = JSONValue(["id" : JSONValue(id), "randomNumber"
+                    : JSONValue(to!int(rs.front()[0]))]);
+
+            respondWith(js.toJSON(), 200, jsonHeader);
+        }
+
+        private void respondMultipleQuery(int queries) {
+            if (queries < 1)
+                queries = 1;
+            else if (queries > 500)
+                queries = 500;
+
+            JSONValue[] arr = new JSONValue[queries];
+            for (int i = 0; i < queries; i++) {
+                immutable id = uniform(1, 10000);
+                immutable query = "SELECT randomNumber FROM world WHERE id = " ~ id.to!string;
+                ResultSet rs = dbConnection.query(query);
+
+                arr[i] = JSONValue(["id" : JSONValue(id), "randomNumber"
+                        : JSONValue(to!int(rs.front()[0]))]);
+            }
+            JSONValue js = JSONValue(arr);
+            respondWith(js.toJSON(), 200, jsonHeader);
+        }
+
+        private void respondFortunes() {
+            immutable query = "SELECT id, message::text FROM Fortune";
+            ResultSet rs = dbConnection.query(query);
+            FortuneModel[] data = rs.map!(f => FortuneModel(f["id"].to!int, f["message"])).array;
+            data ~= FortuneModel(0, "Additional fortune added at request time.");
+            data.sort!((a, b) => a.message < b.message);
+            // trace(data);
+
+            respondWith(randerFortunes(data), 200, htmlHeader);
+        }
+
+        static string randerFortunes(FortuneModel[] data) {
+            Appender!string sb;
+            sb.put(`<!DOCTYPE html>
+<html>
+	<head>
+		<title>Fortunes</title>
+	</head>
+	<body>
+		<table>
+			<tr>
+				<th>id</th><th>message</th>
+			</tr>
+`);
+
+            foreach (FortuneModel f; data) {
+                string message = replace(f.message, ">", "&gt;");
+                message = replace(message, "<", "&lt;");
+                message = replace(message, "\"", "&quot;");
+                sb.put(format("			<tr>\n				<td>%d</td><td>%s</td>\n			</tr>\n", f.id, message));
+            }
+
+            sb.put("		</table>\n	</body>\n</html>");
+
+            return sb.data;
+        }
+
+        private void respondUpdates(int queries) {
+            if (queries < 1)
+                queries = 1;
+            else if (queries > 500)
+                queries = 500;
+
+            JSONValue[] arr = new JSONValue[queries];
+            for (int i = 0; i < queries; i++) {
+                immutable id = uniform(1, 10000);
+                immutable idString = id.to!string;
+                immutable query = "SELECT randomNumber FROM world WHERE id = " ~ idString;
+                ResultSet rs = dbConnection.query(query);
+                int randomNumber = to!int(rs.front()[0]);
+                debug tracef("id=%d, randomNumber=%d", id, randomNumber);
+
+                randomNumber = uniform(1, 10000);
+                string updateSql = "UPDATE world SET randomNumber = "
+                    ~ randomNumber.to!string ~ "  WHERE id = " ~ idString;
+                int r = dbConnection.execute(updateSql);
+                // debug tracef("r=%d", r);
+
+                arr[i] = JSONValue(["id" : JSONValue(id), "randomNumber" : JSONValue(randomNumber)]);
+            }
+
+            JSONValue js = JSONValue(arr);
+            respondWith(js.toJSON(), 200, jsonHeader);
+        }
+    }
+}
+
+struct FortuneModel {
+    int id;
+    string message;
+}

+ 1168 - 0
frameworks/D/hunt/source/minihttp/HttpURI.d

@@ -0,0 +1,1168 @@
+module minihttp.HttpURI;
+
+version(MINIHTTP):
+
+import hunt.collection.MultiMap;
+
+import hunt.Exceptions;
+import hunt.text.Charset;
+import hunt.text.Common;
+import hunt.text.StringBuilder;
+import hunt.util.TypeUtils;
+import minihttp.UrlEncoded;
+
+import std.array;
+import std.conv;
+import std.string;
+
+import hunt.logging;
+
+
+/**
+ * Http URI. Parse a HTTP URI from a string or byte array. Given a URI
+ * <code>http://user@host:port/path/info;param?query#fragment</code> this class
+ * will split it into the following undecoded optional elements:
+ * <ul>
+ * <li>{@link #getScheme()} - http:</li>
+ * <li>{@link #getAuthority()} - //name@host:port</li>
+ * <li>{@link #getHost()} - host</li>
+ * <li>{@link #getPort()} - port</li>
+ * <li>{@link #getPath()} - /path/info</li>
+ * <li>{@link #getParam()} - param</li>
+ * <li>{@link #getQuery()} - query</li>
+ * <li>{@link #getFragment()} - fragment</li>
+ * </ul>
+ * 
+	https://bob:[email protected]:8080/file;p=1?q=2#third
+	\___/   \_/ \___/ \______________/ \__/\_______/ \_/ \___/
+	|      |    |          |          |      | \_/  |    |
+	Scheme User Password    Host       Port  Path |   | Fragment
+			\_____________________________/       | Query
+						|               Path parameter
+					Authority 
+ * <p>
+ * Any parameters will be returned from {@link #getPath()}, but are excluded
+ * from the return value of {@link #getDecodedPath()}. If there are multiple
+ * parameters, the {@link #getParam()} method returns only the last one.
+ * 
+ * See_Also:
+ *	 https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20
+ *   https://web.archive.org/web/20151218094722/http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding
+ */
+class HttpURI {
+	private enum State {
+		START, HOST_OR_PATH, SCHEME_OR_PATH, HOST, IPV6, PORT, PATH, PARAM, QUERY, FRAGMENT, ASTERISK
+	}
+
+	private string _scheme;
+	private string _user;
+	private string _host;
+	private int _port;
+	private string _path;
+	private string _param;
+	private string _query;
+	private string _fragment;
+
+	string _uri;
+	string _decodedPath;
+
+	/**
+	 * Construct a normalized URI. Port is not set if it is the default port.
+	 * 
+	 * @param scheme
+	 *            the URI scheme
+	 * @param host
+	 *            the URI hose
+	 * @param port
+	 *            the URI port
+	 * @param path
+	 *            the URI path
+	 * @param param
+	 *            the URI param
+	 * @param query
+	 *            the URI query
+	 * @param fragment
+	 *            the URI fragment
+	 * @return the normalized URI
+	 */
+	static HttpURI createHttpURI(string scheme, string host, int port, string path, string param, string query,
+			string fragment) {
+		if (port == 80 && (scheme == "http"))
+			port = 0;
+		if (port == 443 && (scheme == "https"))
+			port = 0;
+		return new HttpURI(scheme, host, port, path, param, query, fragment);
+	}
+
+	this() {
+	}
+
+	this(string scheme, string host, int port, string path, string param, string query, string fragment) {
+		_scheme = scheme;
+		_host = host;
+		_port = port;
+		_path = path;
+		_param = param;
+		_query = query;
+		_fragment = fragment;
+	}
+
+	this(HttpURI uri) {
+		this(uri._scheme, uri._host, uri._port, uri._path, uri._param, uri._query, uri._fragment);
+		_uri = uri._uri;
+	}
+
+	this(string uri) {
+		_port = -1;
+		parse(State.START, uri);
+	}
+
+	// this(URI uri) {
+	// 	_uri = null;
+
+	// 	_scheme = uri.getScheme();
+	// 	_host = uri.getHost();
+	// 	if (_host == null && uri.getRawSchemeSpecificPart().startsWith("//"))
+	// 		_host = "";
+	// 	_port = uri.getPort();
+	// 	_user = uri.getUserInfo();
+	// 	_path = uri.getRawPath();
+
+	// 	_decodedPath = uri.getPath();
+	// 	if (_decodedPath != null) {
+	// 		int p = _decodedPath.lastIndexOf(';');
+	// 		if (p >= 0)
+	// 			_param = _decodedPath.substring(p + 1);
+	// 	}
+	// 	_query = uri.getRawQuery();
+	// 	_fragment = uri.getFragment();
+
+	// 	_decodedPath = null;
+	// }
+
+	this(string scheme, string host, int port, string pathQuery) {
+		_uri = null;
+
+		_scheme = scheme;
+		_host = host;
+		_port = port;
+
+		parse(State.PATH, pathQuery);
+
+	}
+
+	void parse(string uri) {
+		clear();
+		_uri = uri;
+		parse(State.START, uri);
+	}
+
+	/**
+	 * Parse according to https://tools.ietf.org/html/rfc7230#section-5.3
+	 * 
+	 * @param method
+	 *            the request method
+	 * @param uri
+	 *            the request uri
+	 */
+	void parseRequestTarget(string method, string uri) {
+		clear();
+		_uri = uri;
+
+		if (method == "CONNECT")
+			_path = uri;
+		else
+			parse(uri.startsWith("/") ? State.PATH : State.START, uri);
+	}
+
+	// deprecated("")
+	// void parseConnect(string uri) {
+	// 	clear();
+	// 	_uri = uri;
+	// 	_path = uri;
+	// }
+
+	void parse(string uri, int offset, int length) {
+		clear();
+		int end = offset + length;
+		_uri = uri.substring(offset, end);
+		parse(State.START, uri);
+	}
+
+	private void parse(State state, string uri) {
+		bool encoded = false;
+		int end = cast(int)uri.length;
+		int mark = 0;
+		int path_mark = 0;
+		char last = '/';
+		for (int i = 0; i < end; i++) {
+			char c = uri[i];
+
+			final switch (state) {
+			case State.START: {
+				switch (c) {
+				case '/':
+					mark = i;
+					state = State.HOST_OR_PATH;
+					break;
+				case ';':
+					mark = i + 1;
+					state = State.PARAM;
+					break;
+				case '?':
+					// assume empty path (if seen at start)
+					_path = "";
+					mark = i + 1;
+					state = State.QUERY;
+					break;
+				case '#':
+					mark = i + 1;
+					state = State.FRAGMENT;
+					break;
+				case '*':
+					_path = "*";
+					state = State.ASTERISK;
+					break;
+
+				case '.':
+					path_mark = i;
+					state = State.PATH;
+					encoded = true;
+					break;
+
+				default:
+					mark = i;
+					if (_scheme == null)
+						state = State.SCHEME_OR_PATH;
+					else {
+						path_mark = i;
+						state = State.PATH;
+					}
+					break;
+				}
+
+				continue;
+			}
+
+			case State.SCHEME_OR_PATH: {
+				switch (c) {
+				case ':':
+					// must have been a scheme
+					_scheme = uri.substring(mark, i);
+					// Start again with scheme set
+					state = State.START;
+					break;
+
+				case '/':
+					// must have been in a path and still are
+					state = State.PATH;
+					break;
+
+				case ';':
+					// must have been in a path
+					mark = i + 1;
+					state = State.PARAM;
+					break;
+
+				case '?':
+					// must have been in a path
+					_path = uri.substring(mark, i);
+					mark = i + 1;
+					state = State.QUERY;
+					break;
+
+				case '%':
+					// must have be in an encoded path
+					encoded = true;
+					state = State.PATH;
+					break;
+
+				case '#':
+					// must have been in a path
+					_path = uri.substring(mark, i);
+					state = State.FRAGMENT;
+					break;
+
+				default:
+					break;
+				}
+				continue;
+			}
+
+			case State.HOST_OR_PATH: {
+				switch (c) {
+				case '/':
+					_host = "";
+					mark = i + 1;
+					state = State.HOST;
+					break;
+
+				case '@':
+				case ';':
+				case '?':
+				case '#':
+					// was a path, look again
+					i--;
+					path_mark = mark;
+					state = State.PATH;
+					break;
+
+				case '.':
+					// it is a path
+					encoded = true;
+					path_mark = mark;
+					state = State.PATH;
+					break;
+
+				default:
+					// it is a path
+					path_mark = mark;
+					state = State.PATH;
+				}
+				continue;
+			}
+
+			case State.HOST: {
+				switch (c) {
+				case '/':
+					_host = uri.substring(mark, i);
+					path_mark = mark = i;
+					state = State.PATH;
+					break;
+				case ':':
+					if (i > mark)
+						_host = uri.substring(mark, i);
+					mark = i + 1;
+					state = State.PORT;
+					break;
+				case '@':
+					if (_user != null)
+						throw new IllegalArgumentException("Bad authority");
+					_user = uri.substring(mark, i);
+					mark = i + 1;
+					break;
+
+				case '[':
+					state = State.IPV6;
+					break;
+					
+				default:
+					break;
+				}
+				break;
+			}
+
+			case State.IPV6: {
+				switch (c) {
+				case '/':
+					throw new IllegalArgumentException("No closing ']' for ipv6 in " ~ uri);
+				case ']':
+					c = uri.charAt(++i);
+					_host = uri.substring(mark, i);
+					if (c == ':') {
+						mark = i + 1;
+						state = State.PORT;
+					} else {
+						path_mark = mark = i;
+						state = State.PATH;
+					}
+					break;
+					
+				default:
+					break;
+				}
+
+				break;
+			}
+
+			case State.PORT: {
+				if (c == '@') {
+					if (_user != null)
+						throw new IllegalArgumentException("Bad authority");
+					// It wasn't a port, but a password!
+					_user = _host ~ ":" ~ uri.substring(mark, i);
+					mark = i + 1;
+					state = State.HOST;
+				} else if (c == '/') {
+					// _port = TypeUtils.parseInt(uri, mark, i - mark, 10);
+					_port = to!int(uri[mark .. i], 10);
+					path_mark = mark = i;
+					state = State.PATH;
+				}
+				break;
+			}
+
+			case State.PATH: {
+				switch (c) {
+				case ';':
+					mark = i + 1;
+					state = State.PARAM;
+					break;
+				case '?':
+					_path = uri.substring(path_mark, i);
+					mark = i + 1;
+					state = State.QUERY;
+					break;
+				case '#':
+					_path = uri.substring(path_mark, i);
+					mark = i + 1;
+					state = State.FRAGMENT;
+					break;
+				case '%':
+					encoded = true;
+					break;
+				case '.':
+					if ('/' == last)
+						encoded = true;
+					break;
+					
+				default:
+					break;
+				}
+				break;
+			}
+
+			case State.PARAM: {
+				switch (c) {
+				case '?':
+					_path = uri.substring(path_mark, i);
+					_param = uri.substring(mark, i);
+					mark = i + 1;
+					state = State.QUERY;
+					break;
+				case '#':
+					_path = uri.substring(path_mark, i);
+					_param = uri.substring(mark, i);
+					mark = i + 1;
+					state = State.FRAGMENT;
+					break;
+				case '/':
+					encoded = true;
+					// ignore internal params
+					state = State.PATH;
+					break;
+				case ';':
+					// multiple parameters
+					mark = i + 1;
+					break;
+					
+				default:
+					break;
+				}
+				break;
+			}
+
+			case State.QUERY: {
+				if (c == '#') {
+					_query = uri.substring(mark, i);
+					mark = i + 1;
+					state = State.FRAGMENT;
+				}
+				break;
+			}
+
+			case State.ASTERISK: {
+				throw new IllegalArgumentException("Bad character '*'");
+			}
+
+			case State.FRAGMENT: {
+				_fragment = uri.substring(mark, end);
+				i = end;
+				break;
+			}
+			}
+			last = c;
+		}
+
+		final switch (state) {
+		case State.START:
+			break;
+		case State.SCHEME_OR_PATH:
+			_path = uri.substring(mark, end);
+			break;
+
+		case State.HOST_OR_PATH:
+			_path = uri.substring(mark, end);
+			break;
+
+		case State.HOST:
+			if (end > mark)
+				_host = uri.substring(mark, end);
+			break;
+
+		case State.IPV6:
+			throw new IllegalArgumentException("No closing ']' for ipv6 in " ~ uri);
+
+		case State.PORT:
+			// _port = TypeUtils.parseInt(uri, mark, end - mark, 10);
+			_port = to!int(uri[mark .. end], 10);
+			break;
+
+		case State.ASTERISK:
+			break;
+
+		case State.FRAGMENT:
+			_fragment = uri.substring(mark, end);
+			break;
+
+		case State.PARAM:
+			_path = uri.substring(path_mark, end);
+			_param = uri.substring(mark, end);
+			break;
+
+		case State.PATH:
+			_path = uri.substring(path_mark, end);
+			break;
+
+		case State.QUERY:
+			_query = uri.substring(mark, end);
+			break;
+		}
+
+		if (!encoded) {
+			if (_param == null)
+				_decodedPath = _path;
+			else
+				_decodedPath = _path[0 .. _path.length - _param.length - 1];
+		}
+	}
+
+	string getScheme() {
+		return _scheme;
+	}
+
+	string getHost() {
+		// Return null for empty host to retain compatibility with java.net.URI
+		if (_host != null && _host.length == 0)
+			return null;
+		return _host;
+	}
+
+	int getPort() {
+		return _port;
+	}
+
+	/**
+	 * The parsed Path.
+	 * 
+	 * @return the path as parsed on valid URI. null for invalid URI.
+	 */
+	string getPath() {
+		return _path;
+	}
+
+	string getDecodedPath() {
+		if (_decodedPath.empty && !_path.empty)
+			_decodedPath = URIUtils.canonicalPath(URIUtils.decodePath(_path));
+		return _decodedPath;
+	}
+
+	string getParam() {
+		return _param;
+	}
+
+	string getQuery() {
+		return _query;
+	}
+
+	bool hasQuery() {
+		return _query != null && _query.length > 0;
+	}
+
+	string getFragment() {
+		return _fragment;
+	}
+
+	void decodeQueryTo(MultiMap!string parameters, string encoding = StandardCharsets.UTF_8) {
+		if (_query == _fragment)
+			return;
+
+		UrlEncoded.decodeTo(_query, parameters, encoding);
+	}
+
+	void clear() {
+		_uri = null;
+
+		_scheme = null;
+		_host = null;
+		_port = -1;
+		_path = null;
+		_param = null;
+		_query = null;
+		_fragment = null;
+
+		_decodedPath = null;
+	}
+
+	bool isAbsolute() {
+		return _scheme != null && _scheme.length > 0;
+	}
+
+	override
+	string toString() {
+		if (_uri is null) {
+			StringBuilder ot = new StringBuilder();
+
+			if (_scheme != null)
+				ot.append(_scheme).append(':');
+
+			if (_host != null) {
+				ot.append("//");
+				if (_user != null)
+					ot.append(_user).append('@');
+				ot.append(_host);
+			}
+
+			if (_port > 0)
+				ot.append(':').append(_port);
+
+			if (_path != null)
+				ot.append(_path);
+
+			if (_query != null)
+				ot.append('?').append(_query);
+
+			if (_fragment != null)
+				ot.append('#').append(_fragment);
+
+			if (ot.length > 0)
+				_uri = ot.toString();
+			else
+				_uri = "";
+		}
+		return _uri;
+	}
+
+	bool equals(Object o) {
+		if (o is this)
+			return true;
+		if (!(typeid(o) == typeid(HttpURI)))
+			return false;
+		return toString().equals(o.toString());
+	}
+
+	void setScheme(string scheme) {
+		_scheme = scheme;
+		_uri = null;
+	}
+
+	/**
+	 * @param host
+	 *            the host
+	 * @param port
+	 *            the port
+	 */
+	void setAuthority(string host, int port) {
+		_host = host;
+		_port = port;
+		_uri = null;
+	}
+
+	/**
+	 * @param path
+	 *            the path
+	 */
+	void setPath(string path) {
+		_uri = null;
+		_path = path;
+		_decodedPath = null;
+	}
+
+	/**
+	 * @param path
+	 *            the decoded path
+	 */
+	// void setDecodedPath(string path) {
+	// 	_uri = null;
+	// 	_path = URIUtils.encodePath(path);
+	// 	_decodedPath = path;
+	// }
+
+	void setPathQuery(string path) {
+		_uri = null;
+		_path = null;
+		_decodedPath = null;
+		_param = null;
+		_fragment = null;
+		if (path != null)
+			parse(State.PATH, path);
+	}
+
+	void setQuery(string query) {
+		_query = query;
+		_uri = null;
+	}
+
+	// URI toURI() {
+	// 	return new URI(_scheme, null, _host, _port, _path, _query == null ? null : UrlEncoded.decodestring(_query),
+	// 			_fragment);
+	// }
+
+	string getPathQuery() {
+		if (_query == null)
+			return _path;
+		return _path ~ "?" ~ _query;
+	}
+
+	bool hasAuthority() {
+		return _host != null;
+	}
+
+	string getAuthority() {
+		if (_port > 0)
+			return _host ~ ":" ~ to!string(_port);
+		return _host;
+	}
+
+	string getUser() {
+		return _user;
+	}
+
+}
+
+
+/**
+ * Parse an authority string into Host and Port
+ * <p>Parse a string in the form "host:port", handling IPv4 an IPv6 hosts</p>
+ *
+ */
+class URIUtils
+{
+	/* ------------------------------------------------------------ */
+    /* Decode a URI path and strip parameters
+     */
+    static string decodePath(string path) {
+        return decodePath(path, 0, cast(int)path.length);
+    }
+
+    /* ------------------------------------------------------------ */
+    /* Decode a URI path and strip parameters of UTF-8 path
+     */
+    static string decodePath(string path, int offset, int length) {
+        try {
+            StringBuilder builder = null;
+
+            int end = offset + length;
+            for (int i = offset; i < end; i++) {
+                char c = path[i];
+                switch (c) {
+                    case '%':
+                        if (builder is null) {
+                            builder = new StringBuilder(path.length);
+                            builder.append(path, offset, i - offset);
+                        }
+                        if ((i + 2) < end) {
+                            char u = path.charAt(i + 1);
+                            if (u == 'u') {
+                                // TODO this is wrong. This is a codepoint not a char
+                                builder.append(cast(char) (0xffff & TypeUtils.parseInt(path, i + 2, 4, 16)));
+                                i += 5;
+                            } else {
+                                builder.append(cast(byte) (0xff & (TypeUtils.convertHexDigit(u) * 16 + TypeUtils.convertHexDigit(path.charAt(i + 2)))));
+                                i += 2;
+                            }
+                        } else {
+                            throw new IllegalArgumentException("Bad URI % encoding");
+                        }
+
+                        break;
+
+                    case ';':
+                        if (builder is null) {
+                            builder = new StringBuilder(path.length);
+                            builder.append(path, offset, i - offset);
+                        }
+
+                        while (++i < end) {
+                            if (path[i] == '/') {
+                                builder.append('/');
+                                break;
+                            }
+                        }
+
+                        break;
+
+                    default:
+                        if (builder !is null)
+                            builder.append(c);
+                        break;
+                }
+            }
+
+            if (builder !is null)
+                return builder.toString();
+            if (offset == 0 && length == path.length)
+                return path;
+            return path.substring(offset, end);
+        } catch (Exception e) {
+            // System.err.println(path.substring(offset, offset + length) + " " + e);
+			error(e.toString);
+            return decodeISO88591Path(path, offset, length);
+        }
+    }
+
+
+    /* ------------------------------------------------------------ */
+    /* Decode a URI path and strip parameters of ISO-8859-1 path
+     */
+    private static string decodeISO88591Path(string path, int offset, int length) {
+        StringBuilder builder = null;
+        int end = offset + length;
+        for (int i = offset; i < end; i++) {
+            char c = path[i];
+            switch (c) {
+                case '%':
+                    if (builder is null) {
+                        builder = new StringBuilder(path.length);
+                        builder.append(path, offset, i - offset);
+                    }
+                    if ((i + 2) < end) {
+                        char u = path.charAt(i + 1);
+                        if (u == 'u') {
+                            // TODO this is wrong. This is a codepoint not a char
+                            builder.append(cast(char) (0xffff & TypeUtils.parseInt(path, i + 2, 4, 16)));
+                            i += 5;
+                        } else {
+                            builder.append(cast(byte) (0xff & (TypeUtils.convertHexDigit(u) * 16 + TypeUtils.convertHexDigit(path.charAt(i + 2)))));
+                            i += 2;
+                        }
+                    } else {
+                        throw new IllegalArgumentException("");
+                    }
+
+                    break;
+
+                case ';':
+                    if (builder is null) {
+                        builder = new StringBuilder(path.length);
+                        builder.append(path, offset, i - offset);
+                    }
+                    while (++i < end) {
+                        if (path[i] == '/') {
+                            builder.append('/');
+                            break;
+                        }
+                    }
+                    break;
+
+
+                default:
+                    if (builder !is null)
+                        builder.append(c);
+                    break;
+            }
+        }
+
+        if (builder !is null)
+            return builder.toString();
+        if (offset == 0 && length == path.length)
+            return path;
+        return path.substring(offset, end);
+    }
+
+	/* ------------------------------------------------------------ */
+
+    /**
+     * Convert a decoded path to a canonical form.
+     * <p>
+     * All instances of "." and ".." are factored out.
+     * </p>
+     * <p>
+     * Null is returned if the path tries to .. above its root.
+     * </p>
+     *
+     * @param path the path to convert, decoded, with path separators '/' and no queries.
+     * @return the canonical path, or null if path traversal above root.
+     */
+    static string canonicalPath(string path) {
+        if (path.empty)
+            return path;
+
+        bool slash = true;
+        int end = cast(int)path.length;
+        int i = 0;
+
+        loop:
+        while (i < end) {
+            char c = path[i];
+            switch (c) {
+                case '/':
+                    slash = true;
+                    break;
+
+                case '.':
+                    if (slash)
+                        break loop;
+                    slash = false;
+                    break;
+
+                default:
+                    slash = false;
+            }
+
+            i++;
+        }
+
+        if (i == end)
+            return path;
+
+        StringBuilder canonical = new StringBuilder(path.length);
+        canonical.append(path, 0, i);
+
+        int dots = 1;
+        i++;
+        while (i <= end) {
+            char c = i < end ? path[i] : '\0';
+            switch (c) {
+                case '\0':
+                case '/':
+                    switch (dots) {
+                        case 0:
+                            if (c != '\0')
+                                canonical.append(c);
+                            break;
+
+                        case 1:
+                            break;
+
+                        case 2:
+                            if (canonical.length < 2)
+                                return null;
+                            canonical.setLength(canonical.length - 1);
+                            canonical.setLength(canonical.lastIndexOf("/") + 1);
+                            break;
+
+                        default:
+                            while (dots-- > 0)
+                                canonical.append('.');
+                            if (c != '\0')
+                                canonical.append(c);
+                    }
+
+                    slash = true;
+                    dots = 0;
+                    break;
+
+                case '.':
+                    if (dots > 0)
+                        dots++;
+                    else if (slash)
+                        dots = 1;
+                    else
+                        canonical.append('.');
+                    slash = false;
+                    break;
+
+                default:
+                    while (dots-- > 0)
+                        canonical.append('.');
+                    canonical.append(c);
+                    dots = 0;
+                    slash = false;
+            }
+
+            i++;
+        }
+        return canonical.toString();
+    }
+
+
+    /* ------------------------------------------------------------ */
+
+    /**
+     * Convert a path to a cananonical form.
+     * <p>
+     * All instances of "." and ".." are factored out.
+     * </p>
+     * <p>
+     * Null is returned if the path tries to .. above its root.
+     * </p>
+     *
+     * @param path the path to convert (expects URI/URL form, encoded, and with path separators '/')
+     * @return the canonical path, or null if path traversal above root.
+     */
+    static string canonicalEncodedPath(string path) {
+        if (path.empty)
+            return path;
+
+        bool slash = true;
+        int end = cast(int)path.length;
+        int i = 0;
+
+        loop:
+        while (i < end) {
+            char c = path[i];
+            switch (c) {
+                case '/':
+                    slash = true;
+                    break;
+
+                case '.':
+                    if (slash)
+                        break loop;
+                    slash = false;
+                    break;
+
+                case '?':
+                    return path;
+
+                default:
+                    slash = false;
+            }
+
+            i++;
+        }
+
+        if (i == end)
+            return path;
+
+        StringBuilder canonical = new StringBuilder(path.length);
+        canonical.append(path, 0, i);
+
+        int dots = 1;
+        i++;
+        while (i <= end) {
+            char c = i < end ? path[i] : '\0';
+            switch (c) {
+                case '\0':
+                case '/':
+                case '?':
+                    switch (dots) {
+                        case 0:
+                            if (c != '\0')
+                                canonical.append(c);
+                            break;
+
+                        case 1:
+                            if (c == '?')
+                                canonical.append(c);
+                            break;
+
+                        case 2:
+                            if (canonical.length < 2)
+                                return null;
+                            canonical.setLength(canonical.length - 1);
+                            canonical.setLength(canonical.lastIndexOf("/") + 1);
+                            if (c == '?')
+                                canonical.append(c);
+                            break;
+                        default:
+                            while (dots-- > 0)
+                                canonical.append('.');
+                            if (c != '\0')
+                                canonical.append(c);
+                    }
+
+                    slash = true;
+                    dots = 0;
+                    break;
+
+                case '.':
+                    if (dots > 0)
+                        dots++;
+                    else if (slash)
+                        dots = 1;
+                    else
+                        canonical.append('.');
+                    slash = false;
+                    break;
+
+                default:
+                    while (dots-- > 0)
+                        canonical.append('.');
+                    canonical.append(c);
+                    dots = 0;
+                    slash = false;
+            }
+
+            i++;
+        }
+        return canonical.toString();
+    }
+
+
+
+    /* ------------------------------------------------------------ */
+
+    /**
+     * Convert a path to a compact form.
+     * All instances of "//" and "///" etc. are factored out to single "/"
+     *
+     * @param path the path to compact
+     * @return the compacted path
+     */
+    static string compactPath(string path) {
+        if (path == null || path.length == 0)
+            return path;
+
+        int state = 0;
+        int end = cast(int)path.length;
+        int i = 0;
+
+        loop:
+        while (i < end) {
+            char c = path[i];
+            switch (c) {
+                case '?':
+                    return path;
+                case '/':
+                    state++;
+                    if (state == 2)
+                        break loop;
+                    break;
+                default:
+                    state = 0;
+            }
+            i++;
+        }
+
+        if (state < 2)
+            return path;
+
+        StringBuilder buf = new StringBuilder(path.length);
+        buf.append(path, 0, i);
+
+        loop2:
+        while (i < end) {
+            char c = path[i];
+            switch (c) {
+                case '?':
+                    buf.append(path, i, end);
+                    break loop2;
+                case '/':
+                    if (state++ == 0)
+                        buf.append(c);
+                    break;
+                default:
+                    state = 0;
+                    buf.append(c);
+            }
+            i++;
+        }
+
+        return buf.toString();
+    }
+
+    /* ------------------------------------------------------------ */
+
+    /**
+     * @param uri URI
+     * @return True if the uri has a scheme
+     */
+    static bool hasScheme(string uri) {
+        for (int i = 0; i < uri.length; i++) {
+            char c = uri[i];
+            if (c == ':')
+                return true;
+            if (!(c >= 'a' && c <= 'z' ||
+                    c >= 'A' && c <= 'Z' ||
+                    (i > 0 && (c >= '0' && c <= '9' ||
+                            c == '.' ||
+                            c == '+' ||
+                            c == '-'))
+            ))
+                break;
+        }
+        return false;
+    }
+}

+ 198 - 0
frameworks/D/hunt/source/minihttp/Parser.d

@@ -0,0 +1,198 @@
+/// Minimalistic low-overhead wrapper for nodejs/http-parser
+/// Used for benchmarks with simple server
+module minihttp.Parser;
+
+version(MINIHTTP):
+
+import minihttp.Common;
+
+import hunt.logging.ConsoleLogger;
+import std.conv;
+import std.range.primitives;
+import core.stdc.string;
+
+
+
+/* contains name and value of a header (name == NULL if is a continuing line
+ * of a multiline header */
+struct phr_header {
+    const char *name;
+    size_t name_len;
+    const char *value;
+    size_t value_len;
+}
+
+/* returns number of bytes consumed if successful, -2 if request is partial,
+ * -1 if failed */
+extern (C) pure @nogc nothrow int phr_parse_request(const char *buf, size_t len, const char **method, 
+	size_t *method_len, const char **path, size_t *path_len,
+    int *minor_version, phr_header *headers, size_t *num_headers, size_t last_len);
+
+/* ditto */
+extern (C) pure @nogc nothrow int phr_parse_response(const char *_buf, size_t len, int *minor_version, 
+	int *status, const char **msg, size_t *msg_len,
+    phr_header *headers, size_t *num_headers, size_t last_len);
+
+/* ditto */
+extern (C) pure @nogc nothrow int phr_parse_headers(const char *buf, size_t len, 
+	phr_header *headers, size_t *num_headers, size_t last_len);
+
+/* should be zero-filled before start */
+struct phr_chunked_decoder {
+    size_t bytes_left_in_chunk; /* number of bytes left in current chunk */
+    char consume_trailer;       /* if trailing headers should be consumed */
+    char _hex_count;
+    char _state;
+}
+
+/* the function rewrites the buffer given as (buf, bufsz) removing the chunked-
+ * encoding headers.  When the function returns without an error, bufsz is
+ * updated to the length of the decoded data available.  Applications should
+ * repeatedly call the function while it returns -2 (incomplete) every time
+ * supplying newly arrived data.  If the end of the chunked-encoded data is
+ * found, the function returns a non-negative number indicating the number of
+ * octets left undecoded at the tail of the supplied buffer.  Returns -1 on
+ * error.
+ */
+extern (C) pure @nogc nothrow ptrdiff_t phr_decode_chunked(phr_chunked_decoder *decoder, char *buf, size_t *bufsz);
+
+/* returns if the chunked decoder is in middle of chunked data */
+extern (C) pure @nogc nothrow int phr_decode_chunked_is_in_data(phr_chunked_decoder *decoder);
+
+
+// =========== Public interface starts here =============
+
+public:
+
+class HttpException : Exception {
+	HttpError error;
+
+	pure @nogc nothrow this(HttpError error, string file = __FILE__,
+			size_t line = __LINE__, Throwable nextInChain = null) {
+		this.error = error;
+		super("Http exception", file, line, nextInChain);
+	}
+}
+
+struct HttpParser(Interceptor) {
+	
+private {
+	Interceptor interceptor;
+	Throwable failure;
+	phr_header[50] _headers;
+	char *_method;
+	char *path;
+
+	int minor_version;
+	size_t buflen = 0, prevbuflen = 0, method_len, path_len, num_headers;
+}
+
+
+	alias interceptor this;
+
+	this(Interceptor interceptor) {
+		this.interceptor = interceptor;
+	}
+
+	@property bool status() pure @safe nothrow {
+		return failure is null;
+	}
+
+	string uri(bool canCopy=false)() {
+		static if(canCopy) {
+			return cast(string)path[0..path_len].dup;
+		} else {
+			return cast(string)path[0..path_len];
+		}
+	}
+
+	@property HttpMethod method() {
+		string s = cast(string)_method[0..method_len];
+		return to!HttpMethod(s);
+	}
+
+
+	HttpHeader[] headers(bool canCopy=false)() {
+		HttpHeader[] hs = new HttpHeader[num_headers];
+		
+		for(int i; i<num_headers; i++) {
+			phr_header* h = &_headers[i];
+			static if(canCopy) {
+				hs[i].name = cast(string)h.name[0..h.name_len].idup;
+				hs[i].value = cast(string)h.value[0..h.value_len].idup;
+			} else {
+				hs[i].name = cast(string)h.name[0..h.name_len];
+				hs[i].value = cast(string)h.value[0..h.value_len];
+			}
+		}
+
+		return hs;
+	}
+
+	@property bool shouldKeepAlive() pure nothrow {
+		return true;
+	}
+
+	@property ushort httpMajor() @safe pure nothrow {
+		return 1;
+	}
+
+	@property ushort httpMinor() @safe pure nothrow {
+		return cast(ushort)minor_version;
+	}
+
+	void execute(const(char)[] str) {
+		execute(cast(const(ubyte)[]) str);
+	}
+
+	void execute(const(ubyte)[] chunk) {
+		failure = null;
+		num_headers = cast(int)_headers.length;
+		int pret = phr_parse_request(cast(const char*)chunk.ptr, cast(int)chunk.length, 
+					&_method, &method_len, 
+					&path, &path_len,
+					&minor_version, 
+					_headers.ptr, &num_headers,
+					0);
+		debug {
+			infof("buffer: %d bytes, request: %d bytes", chunk.length, pret);
+		} 
+
+		if(pret > 0) {
+			/* successfully parsed the request */
+			onMessageComplete();
+
+			if(pret < chunk.length) {
+				debug infof("try to parse next request");
+				execute(chunk[pret .. $]); // try to parse next http request data
+			}
+		} else if (pret == -1) {
+			warning("wrong data format");
+			num_headers = 0;
+			failure = new HttpException(HttpError.UNKNOWN);
+			throw failure;
+		} else if(pret == -2) {
+			warning("parsing incomplete");
+			num_headers = 0;
+			failure = new HttpException(HttpError.UNKNOWN);
+			throw failure;
+		}
+	}
+
+	void onMessageComplete() {
+		// interceptor.onHeadersComplete();
+		debug {
+			tracef("method is %s", _method[0..method_len]);
+			tracef("path is %s", path[0..path_len]);
+			tracef("HTTP version is 1.%d", minor_version);
+			foreach(ref phr_header h; _headers[0..num_headers]) {
+				tracef("Header: %s = %s", h.name[0..h.name_len], h.value[0..h.value_len]);
+			}
+		}
+		interceptor.onMessageComplete();
+	}
+}
+
+auto httpParser(Interceptor)(Interceptor interceptor) {
+	return HttpParser!Interceptor(interceptor);
+}

+ 135 - 0
frameworks/D/hunt/source/minihttp/Processor.d

@@ -0,0 +1,135 @@
+/// An example "HTTP server" with poor usability but sensible performance
+///
+module minihttp.Processor;
+
+version(MINIHTTP):
+
+import std.conv;
+import std.array, std.exception, std.format, std.algorithm.mutation, std.socket;
+import core.stdc.stdlib;
+import core.thread, core.atomic;
+import minihttp.Parser;
+
+import hunt.collection.ByteBuffer;
+import minihttp.Common;
+import hunt.logging;
+import hunt.io;
+import hunt.util.DateTime;
+
+
+private	alias Parser = HttpParser!HttpProcessor;
+
+
+struct HttpRequest {
+	private Parser* parser;
+
+	HttpHeader[] headers(bool canCopy=false)() @property {
+		return parser.headers!canCopy();
+	}
+
+	HttpMethod method() @property {
+		return parser.method();
+	}
+
+	string uri(bool canCopy=false)() @property {
+		return parser.uri!(canCopy)();
+	}
+}
+
+version(NO_HTTPPARSER) {
+enum string ResponseData = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\nConnection: Keep-Alive\r\nContent-Type: text/plain\r\nServer: Hunt/1.0\r\nDate: Wed, 17 Apr 2013 12:00:00 GMT\r\n\r\nHello, World!";
+}
+
+abstract class HttpProcessor {
+	
+package:
+	Appender!(char[]) outBuf;
+	HttpHeader[] headers; // buffer for headers
+	Parser parser;
+	HttpRequest request;
+	bool serving;
+	
+public:
+	TcpStream client;
+
+	this(TcpStream sock) {
+		serving = true;
+		client = sock;
+		headers = new HttpHeader[1];
+		parser = httpParser(this);
+		request.parser = &parser;
+	}
+
+	void run() {
+		client.onReceived((ByteBuffer buffer) { 
+			version(NO_HTTPPARSER) {
+				client.write(cast(ubyte[])ResponseData);
+			} else {
+				try {
+					parser.execute(cast(ubyte[]) buffer.getRemaining());
+				} catch(Exception ex) {
+					respondWith(ex.msg, 500);
+				}
+			}
+		})
+		.onClosed(() {
+			// notifyClientClosed();
+		})
+		.onError((string msg) { 
+			debug warning("Error: ", msg); 
+		})
+		.start();
+	}
+
+	protected void notifyClientClosed() {
+		debug tracef("The connection[%s] is closed", client.remoteAddress());
+	}
+
+	void respondWith(string _body, uint status, HttpHeader[] headers...) {
+		return respondWith(cast(const(ubyte)[]) _body, status, headers);
+	}
+
+	void respondWith(const(ubyte)[] _body, uint status, HttpHeader[] headers...) {
+		outBuf.clear();
+		formattedWrite(outBuf, "HTTP/1.1 %s OK\r\n", status);
+		outBuf.put("Server: Hunt/1.0\r\n");
+
+		formattedWrite(outBuf, "Date: %s\r\n", DateTimeHelper.getDateAsGMT());
+		if (!parser.shouldKeepAlive)
+			outBuf.put("Connection: close\r\n");
+		foreach (ref hdr; headers) {
+			outBuf.put(hdr.name);
+			outBuf.put(": ");
+			outBuf.put(hdr.value);
+			outBuf.put("\r\n");
+		}
+		formattedWrite(outBuf, "Content-Length: %d\r\n\r\n", _body.length);
+		outBuf.put(cast(string) _body);
+		client.write(cast(ubyte[]) outBuf.data); // TODO: short-writes are quite possible
+	}
+
+	void onChunk(ref HttpRequest req, const(ubyte)[] chunk) {
+		// TODO: Tasks pending completion - 5/16/2019, 5:40:18 PM
+		// 
+	}
+
+	void onComplete(ref HttpRequest req);
+
+
+	final int onBody(Parser* parser, const(ubyte)[] chunk) {
+		onChunk(request, chunk);
+		return 0;
+	}
+
+	final int onMessageComplete() {
+		try {
+			onComplete(request);
+		} catch(Exception ex) {
+			respondWith(ex.msg, 500);
+		}
+		if (!parser.shouldKeepAlive)
+			serving = false;
+		return 0;
+	}
+
+}

+ 124 - 0
frameworks/D/hunt/source/minihttp/Server.d

@@ -0,0 +1,124 @@
+module minihttp.Server;
+
+version(MINIHTTP):
+
+import hunt.event;
+import hunt.io;
+import hunt.logging.ConsoleLogger;
+import hunt.system.Memory : totalCPUs;
+import hunt.util.DateTime;
+
+import std.array;
+import std.conv;
+import std.json;
+import std.socket;
+import std.string;
+import std.stdio;
+
+import minihttp.Parser;
+import minihttp.Processor;
+
+shared static this() {
+	DateTimeHelper.startClock();
+}
+
+import hunt.io.channel;
+
+/**
+*/
+abstract class AbstractTcpServer {
+	protected EventLoopGroup _group = null;
+	protected bool _isStarted = false;
+	protected Address _address;
+	protected int _workersCount;
+	TcpStreamOption _tcpStreamoption;
+
+	this(Address address, int thread = (totalCPUs - 1), int workersCount = 0) {
+		this._address = address;
+		_tcpStreamoption = TcpStreamOption.createOption();
+		_tcpStreamoption.bufferSize = 1024 * 2;
+		_tcpStreamoption.isKeepalive = false;
+		_group = new EventLoopGroup(cast(uint) thread);
+		this._workersCount = workersCount;
+	}
+
+	@property Address bindingAddress() {
+		return _address;
+	}
+
+	void start() {
+		if (_isStarted)
+			return;
+		_isStarted = true;
+
+		Socket server = new TcpSocket();
+		server.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
+		server.bind(new InternetAddress("0.0.0.0", 8080));
+		server.listen(8192);
+
+		trace("Launching mini-http server");
+		debug {
+			_group.start();
+		} else {
+			_group.start(100);
+		}
+
+		if (_workersCount) {
+			defaultPoolThreads = _workersCount;
+			workerPool(); // Initilize worker poll
+		}
+		writefln("worker count: %d", _workersCount);
+		writefln("IO thread: %d", _group.size);
+
+		while (true) {
+			try {
+				version (HUNT_DEBUG)
+					trace("Waiting for server.accept()");
+
+				Socket socket = server.accept();
+				version (HUNT_DEBUG) {
+					infof("new connection from %s, fd=%d",
+							socket.remoteAddress.toString(), socket.handle());
+				}
+				// EventLoop loop = _group.nextLoop();
+				EventLoop loop = _group.nextLoop(socket.handle);
+				TcpStream stream = new TcpStream(loop, socket, _tcpStreamoption);
+				onConnectionAccepted(stream);
+			} catch (Exception e) {
+				warningf("Failure on accepting %s", e);
+				break;
+			}
+		}
+		_isStarted = false;
+	}
+
+	protected void onConnectionAccepted(TcpStream client);
+
+	void stop() {
+		if (!_isStarted)
+			return;
+		_isStarted = false;
+		_group.stop();
+	}
+}
+
+alias ProcessorCreater = HttpProcessor delegate(TcpStream client);
+
+/**
+*/
+class HttpServer(T) : AbstractTcpServer if (is(T : HttpProcessor)) {
+
+	this(string ip, ushort port, int thread = (totalCPUs - 1)) {
+		super(new InternetAddress(ip, port), thread);
+	}
+
+	this(Address address, int thread = (totalCPUs - 1)) {
+		super(address, thread);
+	}
+
+	override protected void onConnectionAccepted(TcpStream client) {
+		HttpProcessor httpProcessor = new T(client);
+		httpProcessor.run();
+	}
+
+}

+ 363 - 0
frameworks/D/hunt/source/minihttp/UrlEncoded.d

@@ -0,0 +1,363 @@
+module minihttp.UrlEncoded;
+
+version(MINIHTTP):
+
+import hunt.collection.List;
+import hunt.collection.MultiMap;
+import hunt.collection.StringBuffer;
+import hunt.Exceptions;
+import hunt.logging;
+import hunt.text.Charset;
+import hunt.text.Common;
+import hunt.text.StringBuilder;
+import hunt.util.TypeUtils;
+
+import std.conv;
+import std.array;
+
+
+/**
+ * Handles coding of MIME "x-www-form-urlencoded".
+ * <p>
+ * This class handles the encoding and decoding for either the query string of a
+ * URL or the _content of a POST HTTP request.
+ * </p>
+ * <b>Notes</b>
+ * <p>
+ * The UTF-8 charset is assumed, unless otherwise defined by either passing a
+ * parameter or setting the "org.hunt.utils.UrlEncoding.charset" System
+ * property.
+ * </p>
+ * <p>
+ * The hashtable either contains string single values, vectors of string or
+ * arrays of Strings.
+ * </p>
+ * <p>
+ * This class is only partially synchronised. In particular, simple get
+ * operations are not protected from concurrent updates.
+ * </p>
+ *
+ * @see java.net.URLEncoder
+ */
+class UrlEncoded  : MultiMap!string { 
+    
+    enum string ENCODING = StandardCharsets.UTF_8;
+
+
+    this() {
+    }
+
+    this(string query) {
+        decodeTo(query, this, ENCODING);
+    }
+
+    void decode(string query) {
+        decodeTo(query, this, ENCODING);
+    }
+
+    void decode(string query, string charset) {
+        decodeTo(query, this, charset);
+    }
+
+    /**
+     * Encode MultiMap with % encoding for UTF8 sequences.
+     *
+     * @return the MultiMap as a string with % encoding
+     */
+    string encode() {
+        return encode(ENCODING, false);
+    }
+
+    /**
+     * Encode MultiMap with % encoding for arbitrary string sequences.
+     *
+     * @param charset the charset to use for encoding
+     * @return the MultiMap as a string encoded with % encodings
+     */
+    string encode(string charset) {
+        return encode(charset, false);
+    }
+
+    /**
+     * Encode MultiMap with % encoding.
+     *
+     * @param charset            the charset to encode with
+     * @param equalsForNullValue if True, then an '=' is always used, even
+     *                           for parameters without a value. e.g. <code>"blah?a=&amp;b=&amp;c="</code>.
+     * @return the MultiMap as a string encoded with % encodings
+     */
+    string encode(string charset, bool equalsForNullValue) {
+        return encode(this, charset, equalsForNullValue);
+    }
+
+    /**
+     * Encode MultiMap with % encoding.
+     *
+     * @param map                the map to encode
+     * @param charset            the charset to use for encoding (uses default encoding if null)
+     * @param equalsForNullValue if True, then an '=' is always used, even
+     *                           for parameters without a value. e.g. <code>"blah?a=&amp;b=&amp;c="</code>.
+     * @return the MultiMap as a string encoded with % encodings.
+     */
+    static string encode(MultiMap!string map, string charset, bool equalsForNullValue) {
+        if (charset is null)
+            charset = ENCODING;
+
+        StringBuilder result = new StringBuilder(128);
+
+        bool delim = false;
+        foreach(string key, List!string list; map)
+        {
+            int s = list.size();
+
+            if (delim) {
+                result.append('&');
+            }
+
+            if (s == 0) {
+                result.append(encodeString(key, charset));
+                if (equalsForNullValue)
+                    result.append('=');
+            } else {
+                for (int i = 0; i < s; i++) {
+                    if (i > 0)
+                        result.append('&');
+                    string val = list.get(i);
+                    result.append(encodeString(key, charset));
+
+                    if (val != null) {
+                        if (val.length > 0) {
+                            result.append('=');
+                            result.append(encodeString(val, charset));
+                        } else if (equalsForNullValue)
+                            result.append('=');
+                    } else if (equalsForNullValue)
+                        result.append('=');
+                }
+            }
+            delim = true;
+        }
+        return result.toString();
+    }
+
+    /**
+     * Decoded parameters to Map.
+     *
+     * @param content the string containing the encoded parameters
+     * @param map     the MultiMap to put parsed query parameters into
+     * @param charset the charset to use for decoding
+     */
+    static void decodeTo(string content, MultiMap!string map, string charset = ENCODING) {
+        if (charset.empty)
+            charset = ENCODING;
+
+        synchronized (map) {
+            string key = null;
+            string value = null;
+            int mark = -1;
+            bool encoded = false;
+            for (int i = 0; i < content.length; i++) {
+                char c = content[i];
+                switch (c) {
+                    case '&':
+                        int l = i - mark - 1;
+                        value = l == 0 ? "" :
+                                (encoded ? decodeString(content, mark + 1, l) : content.substring(mark + 1, i));
+                        mark = i;
+                        encoded = false;
+                        if (key != null) {
+                            map.add(key, value);
+                        } else if (value != null && value.length > 0) {
+                            map.add(value, "");
+                        }
+                        key = null;
+                        value = null;
+                        break;
+                    case '=':
+                        if (key != null)
+                            break;
+                        key = encoded ? decodeString(content, mark + 1, i - mark - 1) : content.substring(mark + 1, i);
+                        mark = i;
+                        encoded = false;
+                        break;
+                    case '+':
+                        encoded = true;
+                        break;
+                    case '%':
+                        encoded = true;
+                        break;
+                    default: break;
+                }
+            }
+
+            int contentLen = cast(int)content.length;
+
+            if (key != null) {
+                int l =  contentLen - mark - 1;
+                value = l == 0 ? "" : (encoded ? decodeString(content, mark + 1, l) : content.substring(mark + 1));
+                version(HUNT_DEBUG) tracef("key=%s, value=%s", key, value);
+                map.add(key, value);
+            } else if (mark < contentLen) {
+                version(HUNT_DEBUG) tracef("empty value: content=%s, key=%s", content, key);
+                key = encoded
+                        ? decodeString(content, mark + 1, contentLen - mark - 1, charset)
+                        : content.substring(mark + 1);
+                if (!key.empty) {
+                    map.add(key, "");
+                }
+            } else {
+                warningf("No key found.");
+            }
+        }
+    }
+
+    /**
+     * Decode string with % encoding.
+     * This method makes the assumption that the majority of calls
+     * will need no decoding.
+     *
+     * @param encoded the encoded string to decode
+     * @return the decoded string
+     */
+    static string decodeString(string encoded) {
+        return decodeString(encoded, 0, cast(int)encoded.length);
+    }
+
+    /**
+     * Decode string with % encoding.
+     * This method makes the assumption that the majority of calls
+     * will need no decoding.
+     *
+     * @param encoded the encoded string to decode
+     * @param offset  the offset in the encoded string to decode from
+     * @param length  the length of characters in the encoded string to decode
+     * @param charset the charset to use for decoding
+     * @return the decoded string
+     */
+    static string decodeString(string encoded, int offset, int length, string charset = ENCODING) {
+        StringBuffer buffer = null;
+
+        for (int i = 0; i < length; i++) {
+            char c = encoded.charAt(offset + i);
+            if (c < 0 || c > 0xff) {
+                if (buffer is null) {
+                    buffer = new StringBuffer(length);
+                    buffer.append(encoded, offset, offset + i + 1);
+                } else
+                    buffer.append(c);
+            } else if (c == '+') {
+                if (buffer is null) {
+                    buffer = new StringBuffer(length);
+                    buffer.append(encoded, offset, offset + i);
+                }
+
+                buffer.append(' ');
+            } else if (c == '%') {
+                if (buffer is null) {
+                    buffer = new StringBuffer(length);
+                    buffer.append(encoded, offset, offset + i);
+                }
+
+                byte[] ba = new byte[length];
+                int n = 0;
+                while (c >= 0 && c <= 0xff) {
+                    if (c == '%') {
+                        if (i + 2 < length) {
+                            int o = offset + i + 1;
+                            i += 3;
+                            ba[n] = cast(byte) TypeUtils.parseInt(encoded, o, 2, 16);
+                            n++;
+                        } else {
+                            ba[n++] = cast(byte) '?';
+                            i = length;
+                        }
+                    } else if (c == '+') {
+                        ba[n++] = cast(byte) ' ';
+                        i++;
+                    } else {
+                        ba[n++] = cast(byte) c;
+                        i++;
+                    }
+
+                    if (i >= length)
+                        break;
+                    c = encoded.charAt(offset + i);
+                }
+
+                i--;
+                buffer.append(cast(string)(ba[0 .. n]));
+
+            } else if (buffer !is null)
+                buffer.append(c);
+        }
+
+        if (buffer is null) {
+            if (offset == 0 && encoded.length == length)
+                return encoded;
+            return encoded.substring(offset, offset + length);
+        }
+
+        return buffer.toString();
+    }
+
+
+    /**
+     * Perform URL encoding.
+     *
+     * @param string the string to encode
+     * @return encoded string.
+     */
+    static string encodeString(string string) {
+        return encodeString(string, ENCODING);
+    }
+
+    /**
+     * Perform URL encoding.
+     *
+     * @param string  the string to encode
+     * @param charset the charset to use for encoding
+     * @return encoded string.
+     */
+    static string encodeString(string str, string charset) {
+        if (charset is null)
+            charset = ENCODING;
+        byte[] bytes = cast(byte[])str;
+        // bytes = string.getBytes(charset);
+
+        int len = cast(int)bytes.length;
+        byte[] encoded = new byte[bytes.length * 3];
+        int n = 0;
+        bool noEncode = true;
+
+        for (int i = 0; i < len; i++) {
+            byte b = bytes[i];
+
+            if (b == ' ') {
+                noEncode = false;
+                encoded[n++] = cast(byte) '+';
+            } else if (b >= 'a' && b <= 'z' ||
+                    b >= 'A' && b <= 'Z' ||
+                    b >= '0' && b <= '9') {
+                encoded[n++] = b;
+            } else {
+                noEncode = false;
+                encoded[n++] = cast(byte) '%';
+                byte nibble = cast(byte) ((b & 0xf0) >> 4);
+                if (nibble >= 10)
+                    encoded[n++] = cast(byte) ('A' + nibble - 10);
+                else
+                    encoded[n++] = cast(byte) ('0' + nibble);
+                nibble = cast(byte) (b & 0xf);
+                if (nibble >= 10)
+                    encoded[n++] = cast(byte) ('A' + nibble - 10);
+                else
+                    encoded[n++] = cast(byte) ('0' + nibble);
+            }
+        }
+
+        if (noEncode)
+            return str;
+
+        return cast(string)(encoded[0 .. n]);
+    }
+}