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