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