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