1 /**
2  * A simple HTTP server.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Stéphan Kochen <stephan@kochen.nl>
12  *   Vladimir Panteleev <ae@cy.md>
13  *   Simon Arlott
14  */
15 
16 module ae.net.http.server;
17 
18 import std.algorithm.mutation : move;
19 import std.conv;
20 import std.datetime;
21 import std.exception;
22 import std.range;
23 import std.socket;
24 import std.string;
25 import std.uri;
26 
27 import ae.net.asockets;
28 import ae.net.ietf.headerparse;
29 import ae.net.ietf.headers;
30 import ae.net.ssl;
31 import ae.sys.data;
32 import ae.sys.dataset : bytes, shift, DataVec, joinToGC;
33 import ae.sys.log;
34 import ae.utils.array;
35 import ae.utils.container.listnode;
36 import ae.utils.exception;
37 import ae.utils.text;
38 import ae.utils.textout;
39 
40 public import ae.net.http.common;
41 
42 debug(HTTP) import std.stdio : stderr;
43 
44 // TODO:
45 // - Decouple protocol from network operations.
46 //   This should work on IConnection everywhere.
47 // - Unify HTTP client and server connections.
48 //   Aside the first line, these are pretty much the same protocols.
49 // - We have more than one axis of parameters:
50 //   transport socket type, whether TLS is enabled, possibly more.
51 //   We have only one axis of polymorphism (class inheritance),
52 //   so combinations such as UNIX TLS HTTP server are difficult to represent.
53 //   Refactor to fix this.
54 // - HTTP bodies have stream semantics, and should be represented as such.
55 
56 /// The base class for an incoming connection to a HTTP server,
57 /// unassuming of transport.
58 class BaseHttpServerConnection
59 {
60 public:
61 	TimeoutAdapter timer; /// Time-out adapter.
62 	IConnection conn; /// Connection used for this HTTP connection.
63 
64 	HttpRequest currentRequest; /// The current in-flight request.
65 	bool persistent; /// Whether we will keep the connection open after the request is handled.
66 	bool optimizeResponses = true; /// Whether we should compress responses according to the request headers.
67 	bool satisfyRangeRequests = true; /// Whether we should follow "Range" request headers.
68 
69 	bool connected = true; /// Are we connected now?
70 	Logger log; /// Optional HTTP log.
71 
72 	void delegate(HttpRequest request) handleRequest; /// Callback to handle a fully received request.
73 
74 protected:
75 	string protocol;
76 	DataVec inBuffer;
77 	sizediff_t expect;
78 	size_t responseSize;
79 	bool requestProcessing; // user code is asynchronously processing current request
80 	bool firstRequest = true;
81 	Duration timeout = HttpServer.defaultTimeout;
82 	bool timeoutActive;
83 	string banner;
84 
85 	this(IConnection c)
86 	{
87 		debug (HTTP) debugLog("New connection from %s", remoteAddressStr(null));
88 
89 		if (timeout != Duration.zero)
90 		{
91 			timer = new TimeoutAdapter(c);
92 			timer.setIdleTimeout(timeout);
93 			c = timer;
94 		}
95 
96 		this.conn = c;
97 		conn.handleReadData = &onNewRequest;
98 		conn.handleDisconnect = &onDisconnect;
99 
100 		timeoutActive = true;
101 	}
102 
103 	debug (HTTP)
104 	final void debugLog(Args...)(Args args)
105 	{
106 		stderr.writef("[%s %s] ", Clock.currTime(), cast(void*)this);
107 		stderr.writefln(args);
108 	}
109 
110 	final void onNewRequest(Data data)
111 	{
112 		try
113 		{
114 			inBuffer ~= data;
115 			debug (HTTP) debugLog("Receiving start of request (%d new bytes, %d total)", data.length, inBuffer.bytes.length);
116 
117 			string reqLine;
118 			Headers headers;
119 
120 			if (!parseHeaders(inBuffer, reqLine, headers))
121 			{
122 				debug (HTTP) debugLog("Headers not yet received. Data in buffer:\n%s---", inBuffer.joinToGC().as!string);
123 				return;
124 			}
125 
126 			debug (HTTP)
127 			{
128 				debugLog("Headers received:");
129 				debugLog("> %s", reqLine);
130 				foreach (name, value; headers)
131 					debugLog("> %s: %s", name, value);
132 			}
133 
134 			currentRequest = new HttpRequest;
135 			currentRequest.protocol = protocol;
136 			currentRequest.parseRequestLine(reqLine);
137 			currentRequest.headers = headers;
138 
139 			auto connection = toLower(currentRequest.headers.get("Connection", null));
140 			switch (currentRequest.protocolVersion)
141 			{
142 				case "1.0":
143 					persistent = connection == "keep-alive";
144 					break;
145 				default: // 1.1+
146 					persistent = connection != "close";
147 					break;
148 			}
149 			debug (HTTP) debugLog("This %s connection %s persistent", currentRequest.protocolVersion, persistent ? "IS" : "is NOT");
150 
151 			expect = 0;
152 			if ("Content-Length" in currentRequest.headers)
153 				expect = to!size_t(currentRequest.headers["Content-Length"]);
154 
155 			if (expect > 0)
156 			{
157 				if (expect > inBuffer.bytes.length)
158 					conn.handleReadData = &onContinuation;
159 				else
160 					processRequest(inBuffer.shift(expect));
161 			}
162 			else
163 				processRequest(DataVec.init);
164 		}
165 		catch (CaughtException e)
166 		{
167 			debug (HTTP) debugLog("Exception onNewRequest: %s", e);
168 			if (conn && conn.state == ConnectionState.connected)
169 			{
170 				HttpResponse response;
171 				debug
172 				{
173 					response = new HttpResponse();
174 					response.status = HttpStatusCode.InternalServerError;
175 					response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError);
176 					response.headers["Content-Type"] = "text/plain";
177 					response.data = DataVec(Data(e.toString().asBytes));
178 				}
179 				sendResponse(response);
180 			}
181 			else
182 				assert(false, "Unhandled HTTP exception after disconnect");
183 		}
184 	}
185 
186 	void onDisconnect(string reason, DisconnectType type)
187 	{
188 		debug (HTTP) debugLog("Disconnect: %s", reason);
189 		connected = false;
190 	}
191 
192 	final void onContinuation(Data data)
193 	{
194 		debug (HTTP) debugLog("Receiving continuation of request: \n%s---", cast(string)data.unsafeContents);
195 		inBuffer ~= data;
196 
197 		if (!requestProcessing && inBuffer.bytes.length >= expect)
198 		{
199 			debug (HTTP) debugLog("%s/%s", inBuffer.bytes.length, expect);
200 			processRequest(inBuffer.shift(expect));
201 		}
202 	}
203 
204 	final void processRequest(DataVec data)
205 	{
206 		debug (HTTP) debugLog("processRequest (%d bytes)", data.bytes.length);
207 		currentRequest.data = move(data);
208 		timeoutActive = false;
209 		if (timer)
210 			timer.cancelIdleTimeout();
211 		if (handleRequest)
212 		{
213 			// Log unhandled exceptions, but don't mess up the stack trace
214 			//scope(failure) logRequest(currentRequest, null);
215 
216 			// sendResponse may be called immediately, or later
217 			requestProcessing = true;
218 			handleRequest(currentRequest);
219 		}
220 	}
221 
222 	final void logRequest(HttpRequest request, HttpResponse response)
223 	{
224 		debug // avoid linewrap in terminal during development
225 			enum DEBUG = true;
226 		else
227 			enum DEBUG = false;
228 
229 		if (log) log(([
230 			"", // align IP to tab
231 			remoteAddressStr(request),
232 			response ? text(cast(ushort)response.status) : "-",
233 			request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-",
234 			request ? request.method : "-",
235 			request ? formatLocalAddress(request) ~ request.resource : "-",
236 			response ? response.headers.get("Content-Type", "-") : "-",
237 		] ~ (DEBUG ? [] : [
238 			request ? request.headers.get("Referer", "-") : "-",
239 			request ? request.headers.get("User-Agent", "-") : "-",
240 		])).join("\t"));
241 	}
242 
243 	abstract string formatLocalAddress(HttpRequest r);
244 
245 	/// Idle connections are those which can be closed when the server
246 	/// is shutting down.
247 	final @property bool idle()
248 	{
249 		// Technically, with a persistent connection, we never know if
250 		// there is a request on the wire on the way to us which we
251 		// haven't received yet, so it's not possible to truly know
252 		// when the connection is idle and can be safely closed.
253 		// However, we do have the ability to do that for
254 		// non-persistent connections - assume that a connection is
255 		// never idle until we receive (and process) the first
256 		// request.  Therefore, in deployments where clients require
257 		// that an outstanding request is always processed before the
258 		// server is shut down, non-persistent connections can be used
259 		// (i.e. no attempt to reuse `HttpClient`) to achieve this.
260 		if (firstRequest)
261 			return false;
262 
263 		if (requestProcessing)
264 			return false;
265 
266 		foreach (datum; inBuffer)
267 			if (datum.length)
268 				return false;
269 
270 		return true;
271 	}
272 
273 	/// Send the given HTTP response, and do nothing else.
274 	final void writeResponse(HttpResponse response)
275 	{
276 		assert(response.status != 0);
277 
278 		if (currentRequest)
279 		{
280 			if (optimizeResponses)
281 				response.optimizeData(currentRequest.headers);
282 			if (satisfyRangeRequests)
283 				response.sliceData(currentRequest.headers);
284 		}
285 
286 		if ("Content-Length" !in response.headers)
287 			response.headers["Content-Length"] = text(response.data.bytes.length);
288 
289 		sendHeaders(response);
290 
291 		bool isHead = currentRequest ? currentRequest.method == "HEAD" : false;
292 		if (response && response.data.length && !isHead)
293 			sendData(response.data[]);
294 
295 		responseSize = response ? response.data.bytes.length : 0;
296 		debug (HTTP) debugLog("Sent response (%d bytes data)", responseSize);
297 	}
298 
299 public:
300 	/// Send the given HTTP response.
301 	final void sendResponse(HttpResponse response)
302 	{
303 		requestProcessing = false;
304 		if (!response)
305 		{
306 			debug (HTTP) debugLog("sendResponse(null) - generating dummy response");
307 			response = new HttpResponse();
308 			response.status = HttpStatusCode.InternalServerError;
309 			response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError);
310 			response.data = DataVec(Data("Internal Server Error".asBytes));
311 		}
312 		writeResponse(response);
313 
314 		closeResponse();
315 
316 		logRequest(currentRequest, response);
317 	}
318 
319 	/// Switch protocols.
320 	/// If `response` is given, send that first.
321 	/// Then, release the connection and return it.
322 	final Upgrade upgrade(HttpResponse response = null)
323 	{
324 		requestProcessing = false;
325 		if (response)
326 			writeResponse(response);
327 
328 		conn.handleReadData = null;
329 		conn.handleDisconnect = null;
330 
331 		Upgrade upgrade;
332 		upgrade.conn = conn;
333 		upgrade.initialData = move(inBuffer);
334 
335 		this.conn = null;
336 		assert(!timeoutActive);
337 
338 		logRequest(currentRequest, response);
339 		return upgrade;
340 	}
341 
342 	struct Upgrade
343 	{
344 		IConnection conn; /// The connection.
345 
346 		/// Any data that came after the request.
347 		/// It is almost surely part of the protocol being upgraded to,
348 		/// so it should be parsed as such.
349 		DataVec initialData;
350 	} /// ditto
351 
352 	/// Send these headers only.
353 	/// Low-level alternative to `sendResponse`.
354 	final void sendHeaders(Headers headers, HttpStatusCode status, string statusMessage = null)
355 	{
356 		assert(status, "Unset status code");
357 
358 		if (!statusMessage)
359 			statusMessage = HttpResponse.getStatusMessage(status);
360 
361 		StringBuilder respMessage;
362 		auto protocolVersion = currentRequest ? currentRequest.protocolVersion : "1.0";
363 		respMessage.put("HTTP/", protocolVersion, " ");
364 
365 		if (banner && "X-Powered-By" !in headers)
366 			headers["X-Powered-By"] = banner;
367 
368 		if ("Date" !in headers)
369 			headers["Date"] = httpTime(Clock.currTime());
370 
371 		if ("Connection" !in headers)
372 		{
373 			if (persistent && protocolVersion=="1.0")
374 				headers["Connection"] = "Keep-Alive";
375 			else
376 			if (!persistent && protocolVersion=="1.1")
377 				headers["Connection"] = "close";
378 		}
379 
380 		respMessage.put("%d %s\r\n".format(status, statusMessage));
381 		foreach (string header, string value; headers)
382 			respMessage.put(header, ": ", value, "\r\n");
383 
384 		debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> "));
385 
386 		respMessage.put("\r\n");
387 		conn.send(Data(respMessage.get().asBytes));
388 	}
389 
390 	/// ditto
391 	final void sendHeaders(HttpResponse response)
392 	{
393 		sendHeaders(response.headers, response.status, response.statusMessage);
394 	}
395 
396 	/// Send this data only.
397 	/// Headers should have already been sent.
398 	/// Low-level alternative to `sendResponse`.
399 	final void sendData(scope Data[] data)
400 	{
401 		conn.send(data);
402 	}
403 
404 	/// Accept more requests on the same connection?
405 	protected bool acceptMore() { return true; }
406 
407 	/// Finalize writing the response.
408 	/// Headers and data should have already been sent.
409 	/// Low-level alternative to `sendResponse`.
410 	final void closeResponse()
411 	{
412 		firstRequest = false;
413 		if (persistent && acceptMore)
414 		{
415 			// reset for next request
416 			debug (HTTP) debugLog("  Waiting for next request.");
417 			conn.handleReadData = &onNewRequest;
418 			if (!timeoutActive)
419 			{
420 				// Give the client time to download large requests.
421 				// Assume a minimal speed of 1kb/s.
422 				if (timer)
423 					timer.setIdleTimeout(timeout + (responseSize / 1024).seconds);
424 				timeoutActive = true;
425 			}
426 			if (inBuffer.bytes.length) // a second request has been pipelined
427 			{
428 				debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length);
429 				onNewRequest(Data());
430 			}
431 		}
432 		else
433 		{
434 			string reason = persistent ? "Server has been shut down" : "Non-persistent connection";
435 			debug (HTTP) debugLog("  Closing connection (%s).", reason);
436 			conn.disconnect(reason);
437 		}
438 	}
439 
440 	/// Retrieve the remote address of the peer, as a string.
441 	abstract @property string remoteAddressStr(HttpRequest r);
442 }
443 
444 /// Basic unencrypted HTTP 1.0/1.1 server.
445 class HttpServer
446 {
447 	enum defaultTimeout = 30.seconds; /// The default timeout used for incoming connections.
448 
449 // public:
450 	this(Duration timeout = defaultTimeout)
451 	{
452 		assert(timeout > Duration.zero);
453 		this.timeout = timeout;
454 
455 		conn = new TcpServer();
456 		conn.handleClose = &onClose;
457 		conn.handleAccept = &onAccept;
458 	} ///
459 
460 	/// Listen on the given TCP address and port.
461 	/// If port is 0, listen on a random available port.
462 	/// Returns the port that the server is actually listening on.
463 	ushort listen(ushort port, string addr = null)
464 	{
465 		port = conn.listen(port, addr);
466 		if (log)
467 			foreach (address; conn.localAddresses)
468 				log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
469 		return port;
470 	}
471 
472 	/// Listen on the given addresses.
473 	void listen(AddressInfo[] addresses)
474 	{
475 		conn.listen(addresses);
476 		if (log)
477 			foreach (address; conn.localAddresses)
478 				log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
479 	}
480 
481 	/// Get listen addresses.
482 	@property Address[] localAddresses() { return conn.localAddresses; }
483 
484 	/// Stop listening, and close idle client connections.
485 	void close()
486 	{
487 		debug(HTTP) stderr.writeln("Shutting down");
488 		if (log) log("Shutting down.");
489 		conn.close();
490 
491 		debug(HTTP) stderr.writefln("There still are %d active connections", connections.iterator.walkLength);
492 
493 		// Close idle connections
494 		foreach (connection; connections.iterator.array)
495 			if (connection.idle && connection.conn.state == ConnectionState.connected)
496 				connection.conn.disconnect("HTTP server shutting down");
497 	}
498 
499 	/// Optional HTTP request log.
500 	Logger log;
501 
502 	/// Single-ended doubly-linked list of active connections
503 	SEDListContainer!HttpServerConnection connections;
504 
505 	/// Callback for when the socket was closed.
506 	void delegate() handleClose;
507 	/// Callback for an incoming request.
508 	void delegate(HttpRequest request, HttpServerConnection conn) handleRequest;
509 
510 	/// What to send in the `"X-Powered-By"` header.
511 	string banner = "ae.net.http.server (+https://github.com/CyberShadow/ae)";
512 
513 	/// If set, the name of the header which will be used to obtain
514 	/// the actual IP of the connecting peer.  Useful when this
515 	/// `HttpServer` is behind a reverse proxy.
516 	string remoteIPHeader;
517 
518 protected:
519 	TcpServer conn;
520 	Duration timeout;
521 
522 	void onClose()
523 	{
524 		if (handleClose)
525 			handleClose();
526 	}
527 
528 	IConnection adaptConnection(IConnection transport)
529 	{
530 		return transport;
531 	}
532 
533 	@property string protocol() { return "http"; }
534 
535 	void onAccept(TcpConnection incoming)
536 	{
537 		try
538 			new HttpServerConnection(this, incoming, adaptConnection(incoming), protocol);
539 		catch (Exception e)
540 		{
541 			if (log)
542 				log("Error accepting connection: " ~ e.msg);
543 			if (incoming.state == ConnectionState.connected)
544 				incoming.disconnect();
545 		}
546 	}
547 }
548 
549 /**
550    HTTPS server. Set SSL parameters on ctx after instantiation.
551 
552    Example:
553    ---
554    auto s = new HttpsServer();
555    s.ctx.enableDH(4096);
556    s.ctx.enableECDH();
557    s.ctx.setCertificate("server.crt");
558    s.ctx.setPrivateKey("server.key");
559    ---
560 */
561 class HttpsServer : HttpServer
562 {
563 	SSLContext ctx; /// The SSL context.
564 
565 	this()
566 	{
567 		ctx = ssl.createContext(SSLContext.Kind.server);
568 	} ///
569 
570 protected:
571 	override @property string protocol() { return "https"; }
572 
573 	override IConnection adaptConnection(IConnection transport)
574 	{
575 		return ssl.createAdapter(ctx, transport);
576 	}
577 }
578 
579 /// Standard socket-based HTTP server connection.
580 final class HttpServerConnection : BaseHttpServerConnection
581 {
582 	SocketConnection socket; /// The socket transport.
583 	HttpServer server; /// `HttpServer` owning this connection.
584 	/// Cached local and remote addresses.
585 	Address localAddress, remoteAddress;
586 
587 	mixin DListLink;
588 
589 	/// Retrieves the remote peer address, honoring `remoteIPHeader` if set.
590 	override @property string remoteAddressStr(HttpRequest r)
591 	{
592 		if (server.remoteIPHeader)
593 		{
594 			if (r)
595 				if (auto p = server.remoteIPHeader in r.headers)
596 					return (*p).split(",")[$ - 1];
597 
598 			return "[local:" ~ remoteAddress.toAddrString() ~ "]";
599 		}
600 
601 		return remoteAddress.toAddrString();
602 	}
603 
604 protected:
605 	this(HttpServer server, SocketConnection socket, IConnection c, string protocol = "http")
606 	{
607 		this.server = server;
608 		this.socket = socket;
609 		this.log = server.log;
610 		this.protocol = protocol;
611 		this.banner = server.banner;
612 		this.timeout = server.timeout;
613 		this.handleRequest = (HttpRequest r) => server.handleRequest(r, this);
614 		this.localAddress = socket.localAddress;
615 		this.remoteAddress = socket.remoteAddress;
616 
617 		super(c);
618 
619 		server.connections.pushFront(this);
620 	}
621 
622 	override void onDisconnect(string reason, DisconnectType type)
623 	{
624 		super.onDisconnect(reason, type);
625 		server.connections.remove(this);
626 	}
627 
628 	override bool acceptMore() { return server.conn.isListening; }
629 	override string formatLocalAddress(HttpRequest r) { return formatAddress(protocol, localAddress, r.host, r.port); }
630 }
631 
632 /// `BaseHttpServerConnection` implementation with files, allowing to
633 /// e.g. read a request from standard input and write the response to
634 /// standard output.
635 version (Posix)
636 class FileHttpServerConnection : BaseHttpServerConnection
637 {
638 	this(File input = stdin, File output = stdout, string protocol = "stdin")
639 	{
640 		this.protocol = protocol;
641 
642 		auto c = new Duplex(
643 			new FileConnection(input.fileno),
644 			new FileConnection(output.fileno),
645 		);
646 
647 		super(c);
648 	} ///
649 
650 	override @property string remoteAddressStr(HttpRequest r) { return "-"; } /// Stub.
651 
652 protected:
653 	import std.stdio : File, stdin, stdout;
654 
655 	string protocol;
656 
657 	override string formatLocalAddress(HttpRequest r) { return protocol ~ "://"; }
658 }
659 
660 /// Formats a remote address for logging.
661 string formatAddress(string protocol, Address address, string vhost = null, ushort logPort = 0)
662 {
663 	string addr = address.toAddrString();
664 	string port =
665 		address.addressFamily == AddressFamily.UNIX ? null :
666 		logPort ? text(logPort) :
667 		address.toPortString();
668 	return protocol ~ "://" ~
669 		(vhost ? vhost : addr == "0.0.0.0" || addr == "::" ? "*" : addr.contains(":") ? "[" ~ addr ~ "]" : addr) ~
670 		(port is null || port == "80" ? "" : ":" ~ port);
671 }
672 
673 debug (ae_unittest) import ae.net.http.client;
674 debug (ae_unittest) import ae.net.http.responseex;
675 debug(ae_unittest) unittest
676 {
677 	int[] replies;
678 	int closeAfter;
679 
680 	// Sum "a" from GET and "b" from POST
681 	auto s = new HttpServer;
682 	s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
683 		auto get  = request.urlParameters;
684 		auto post = request.decodePostData();
685 		auto response = new HttpResponseEx;
686 		auto result = to!int(get["a"]) + to!int(post["b"]);
687 		replies ~= result;
688 		conn.sendResponse(response.serveJson(result));
689 		if (--closeAfter == 0)
690 			s.close();
691 	};
692 
693 	// Test server, client, parameter encoding
694 	replies = null;
695 	closeAfter = 1;
696 	auto port = s.listen(0, "127.0.0.1");
697 	httpPost("http://127.0.0.1:" ~ to!string(port) ~ "/?" ~ encodeUrlParameters(["a":"2"]), UrlParameters(["b":"3"]), (string s) { assert(s=="5"); }, null);
698 	socketManager.loop();
699 
700 	// Test pipelining, protocol errors
701 	replies = null;
702 	closeAfter = 2;
703 	port = s.listen(0, "127.0.0.1");
704 	TcpConnection c = new TcpConnection;
705 	c.handleConnect = {
706 		c.send(Data((
707 "GET /?a=123456 HTTP/1.1
708 Content-length: 8
709 Content-type: application/x-www-form-urlencoded
710 
711 b=654321" ~
712 "GET /derp HTTP/1.1
713 Content-length: potato
714 
715 " ~
716 "GET /?a=1234567 HTTP/1.1
717 Content-length: 9
718 Content-type: application/x-www-form-urlencoded
719 
720 b=7654321").asBytes));
721 		c.disconnect();
722 	};
723 	c.connect("127.0.0.1", port);
724 
725 	socketManager.loop();
726 
727 	assert(replies == [777777, 8888888]);
728 
729 	// Test bad headers
730 	s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
731 		auto response = new HttpResponseEx;
732 		conn.sendResponse(response.serveText("OK"));
733 		if (--closeAfter == 0)
734 			s.close();
735 	};
736 	closeAfter = 1;
737 
738 	port = s.listen(0, "127.0.0.1");
739 	c = new TcpConnection;
740 	c.handleConnect = {
741 		c.send(Data("\n\n\n\n\n".asBytes));
742 		c.disconnect();
743 
744 		// Now send a valid request to end the loop
745 		c = new TcpConnection;
746 		c.handleConnect = {
747 			c.send(Data("GET / HTTP/1.0\n\n".asBytes));
748 			c.disconnect();
749 		};
750 		c.connect("127.0.0.1", port);
751 	};
752 	c.connect("127.0.0.1", port);
753 
754 	socketManager.loop();
755 
756 /+
757 	void testFile(string fn)
758 	{
759 		std.file.write(fn, "42");
760 		s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
761 			auto response = new HttpResponseEx;
762 			conn.sendResponse(response.serveFile(request.resource[1..$], ""));
763 			if (--closeAfter == 0)
764 				s.close();
765 		};
766 		port = s.listen(0, "127.0.0.1");
767 		closeAfter = 1;
768 		httpGet("http://127.0.0.1:" ~ to!string(port) ~ "/" ~ fn, (string s) { assert(s=="42"); }, null);
769 		socketManager.loop();
770 		std.file.remove(fn);
771 	}
772 
773 	testFile("http-test.bin");
774 	testFile("http-test.txt");
775 +/
776 }
777 
778 // Test form-data
779 debug(ae_unittest) unittest
780 {
781 	bool ok;
782 	auto s = new HttpServer;
783 	s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
784 		auto post = request.decodePostData();
785 		assert(post["a"] == "b");
786 		assert(post["c"] == "d");
787 		assert(post["e"] == "f");
788 		ok = true;
789 		conn.conn.disconnect();
790 		s.close();
791 	};
792 	auto port = s.listen(0, "127.0.0.1");
793 
794 	TcpConnection c = new TcpConnection;
795 	c.handleConnect = {
796 		c.send(Data((q"EOF
797 POST / HTTP/1.1
798 Host: google.com
799 User-Agent: curl/8.1.2
800 Accept: */*
801 Content-Length: 319
802 Content-Type: multipart/form-data; boundary=------------------------f7d0ffeae587957a
803 
804 --------------------------f7d0ffeae587957a
805 Content-Disposition: form-data; name="a"
806 
807 b
808 --------------------------f7d0ffeae587957a
809 Content-Disposition: form-data; name="c"
810 
811 d
812 --------------------------f7d0ffeae587957a
813 Content-Disposition: form-data; name="e"
814 
815 f
816 --------------------------f7d0ffeae587957a--
817 EOF".replace("\n", "\r\n")).asBytes));
818 		c.disconnect();
819 	};
820 	c.connect("127.0.0.1", port);
821 
822 	socketManager.loop();
823 
824 	assert(ok);
825 }