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 	string protocol;
60 	DataVec inBuffer;
61 	sizediff_t expect;
62 	size_t responseSize;
63 	bool requestProcessing; // user code is asynchronously processing current request
64 	bool firstRequest = true;
65 	Duration timeout = HttpServer.defaultTimeout;
66 	bool timeoutActive;
67 	string banner;
68 
69 	this(IConnection c)
70 	{
71 		debug (HTTP) debugLog("New connection from %s", remoteAddressStr(null));
72 
73 		timer = new TimeoutAdapter(c);
74 		timer.setIdleTimeout(timeout);
75 		c = timer;
76 
77 		this.conn = c;
78 		conn.handleReadData = &onNewRequest;
79 		conn.handleDisconnect = &onDisconnect;
80 
81 		timeoutActive = true;
82 	}
83 
84 	debug (HTTP)
85 	final void debugLog(Args...)(Args args)
86 	{
87 		stderr.writef("[%s %s] ", Clock.currTime(), cast(void*)this);
88 		stderr.writefln(args);
89 	}
90 
91 	final void onNewRequest(Data data)
92 	{
93 		try
94 		{
95 			inBuffer ~= data;
96 			debug (HTTP) debugLog("Receiving start of request (%d new bytes, %d total)", data.length, inBuffer.bytes.length);
97 
98 			string reqLine;
99 			Headers headers;
100 
101 			if (!parseHeaders(inBuffer, reqLine, headers))
102 			{
103 				debug (HTTP) debugLog("Headers not yet received. Data in buffer:\n%s---", cast(string)inBuffer.joinToHeap());
104 				return;
105 			}
106 
107 			debug (HTTP)
108 			{
109 				debugLog("Headers received:");
110 				debugLog("> %s", reqLine);
111 				foreach (name, value; headers)
112 					debugLog("> %s: %s", name, value);
113 			}
114 
115 			currentRequest = new HttpRequest;
116 			currentRequest.protocol = protocol;
117 			currentRequest.parseRequestLine(reqLine);
118 			currentRequest.headers = headers;
119 
120 			auto connection = toLower(currentRequest.headers.get("Connection", null));
121 			switch (currentRequest.protocolVersion)
122 			{
123 				case "1.0":
124 					persistent = connection == "keep-alive";
125 					break;
126 				default: // 1.1+
127 					persistent = connection != "close";
128 					break;
129 			}
130 			debug (HTTP) debugLog("This %s connection %s persistent", currentRequest.protocolVersion, persistent ? "IS" : "is NOT");
131 
132 			expect = 0;
133 			if ("Content-Length" in currentRequest.headers)
134 				expect = to!size_t(currentRequest.headers["Content-Length"]);
135 
136 			if (expect > 0)
137 			{
138 				if (expect > inBuffer.bytes.length)
139 					conn.handleReadData = &onContinuation;
140 				else
141 					processRequest(inBuffer.shift(expect));
142 			}
143 			else
144 				processRequest(DataVec.init);
145 		}
146 		catch (CaughtException e)
147 		{
148 			debug (HTTP) debugLog("Exception onNewRequest: %s", e);
149 			HttpResponse response;
150 			debug
151 			{
152 				response = new HttpResponse();
153 				response.status = HttpStatusCode.InternalServerError;
154 				response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError);
155 				response.headers["Content-Type"] = "text/plain";
156 				response.data = DataVec(Data(e.toString()));
157 			}
158 			sendResponse(response);
159 		}
160 	}
161 
162 	void onDisconnect(string reason, DisconnectType type)
163 	{
164 		debug (HTTP) debugLog("Disconnect: %s", reason);
165 		connected = false;
166 	}
167 
168 	final void onContinuation(Data data)
169 	{
170 		debug (HTTP) debugLog("Receiving continuation of request: \n%s---", cast(string)data.contents);
171 		inBuffer ~= data;
172 
173 		if (!requestProcessing && inBuffer.bytes.length >= expect)
174 		{
175 			debug (HTTP) debugLog("%s/%s", inBuffer.bytes.length, expect);
176 			processRequest(inBuffer.shift(expect));
177 		}
178 	}
179 
180 	final void processRequest(DataVec data)
181 	{
182 		debug (HTTP) debugLog("processRequest (%d bytes)", data.bytes.length);
183 		currentRequest.data = move(data);
184 		timeoutActive = false;
185 		timer.cancelIdleTimeout();
186 		if (handleRequest)
187 		{
188 			// Log unhandled exceptions, but don't mess up the stack trace
189 			//scope(failure) logRequest(currentRequest, null);
190 
191 			// sendResponse may be called immediately, or later
192 			requestProcessing = true;
193 			handleRequest(currentRequest);
194 		}
195 	}
196 
197 	final void logRequest(HttpRequest request, HttpResponse response)
198 	{
199 		debug // avoid linewrap in terminal during development
200 			enum DEBUG = true;
201 		else
202 			enum DEBUG = false;
203 
204 		if (log) log(([
205 			"", // align IP to tab
206 			remoteAddressStr(request),
207 			response ? text(cast(ushort)response.status) : "-",
208 			request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-",
209 			request ? request.method : "-",
210 			request ? formatLocalAddress(request) ~ request.resource : "-",
211 			response ? response.headers.get("Content-Type", "-") : "-",
212 		] ~ (DEBUG ? [] : [
213 			request ? request.headers.get("Referer", "-") : "-",
214 			request ? request.headers.get("User-Agent", "-") : "-",
215 		])).join("\t"));
216 	}
217 
218 	abstract string formatLocalAddress(HttpRequest r);
219 
220 	/// Idle connections are those which can be closed when the server
221 	/// is shutting down.
222 	final @property bool idle()
223 	{
224 		// Technically, with a persistent connection, we never know if
225 		// there is a request on the wire on the way to us which we
226 		// haven't received yet, so it's not possible to truly know
227 		// when the connection is idle and can be safely closed.
228 		// However, we do have the ability to do that for
229 		// non-persistent connections - assume that a connection is
230 		// never idle until we receive (and process) the first
231 		// request.  Therefore, in deployments where clients require
232 		// that an outstanding request is always processed before the
233 		// server is shut down, non-persistent connections can be used
234 		// (i.e. no attempt to reuse `HttpClient`) to achieve this.
235 		if (firstRequest)
236 			return false;
237 
238 		if (requestProcessing)
239 			return false;
240 
241 		foreach (datum; inBuffer)
242 			if (datum.length)
243 				return false;
244 
245 		return true;
246 	}
247 
248 public:
249 	/// Send the given HTTP response.
250 	final void sendResponse(HttpResponse response)
251 	{
252 		requestProcessing = false;
253 		if (!response)
254 		{
255 			debug (HTTP) debugLog("sendResponse(null) - generating dummy response");
256 			response = new HttpResponse();
257 			response.status = HttpStatusCode.InternalServerError;
258 			response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError);
259 			response.data = DataVec(Data("Internal Server Error"));
260 		}
261 		assert(response.status != 0);
262 
263 		if (currentRequest)
264 		{
265 			response.optimizeData(currentRequest.headers);
266 			response.sliceData(currentRequest.headers);
267 		}
268 
269 		if ("Content-Length" !in response.headers)
270 			response.headers["Content-Length"] = text(response.data.bytes.length);
271 
272 		sendHeaders(response);
273 
274 		bool isHead = currentRequest ? currentRequest.method == "HEAD" : false;
275 		if (response && response.data.length && !isHead)
276 			sendData(response.data[]);
277 
278 		responseSize = response ? response.data.bytes.length : 0;
279 		debug (HTTP) debugLog("Sent response (%d bytes data)", responseSize);
280 
281 		closeResponse();
282 
283 		logRequest(currentRequest, response);
284 	}
285 
286 	/// Send these headers only.
287 	/// Low-level alternative to `sendResponse`.
288 	final void sendHeaders(Headers headers, HttpStatusCode status, string statusMessage = null)
289 	{
290 		assert(status, "Unset status code");
291 
292 		if (!statusMessage)
293 			statusMessage = HttpResponse.getStatusMessage(status);
294 
295 		StringBuilder respMessage;
296 		auto protocolVersion = currentRequest ? currentRequest.protocolVersion : "1.0";
297 		respMessage.put("HTTP/", protocolVersion, " ");
298 
299 		if (banner && "X-Powered-By" !in headers)
300 			headers["X-Powered-By"] = banner;
301 
302 		if ("Date" !in headers)
303 			headers["Date"] = httpTime(Clock.currTime());
304 
305 		if ("Connection" !in headers)
306 		{
307 			if (persistent && protocolVersion=="1.0")
308 				headers["Connection"] = "Keep-Alive";
309 			else
310 			if (!persistent && protocolVersion=="1.1")
311 				headers["Connection"] = "close";
312 		}
313 
314 		respMessage.put("%d %s\r\n".format(status, statusMessage));
315 		foreach (string header, string value; headers)
316 			respMessage.put(header, ": ", value, "\r\n");
317 
318 		debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> "));
319 
320 		respMessage.put("\r\n");
321 		conn.send(Data(respMessage.get()));
322 	}
323 
324 	/// ditto
325 	final void sendHeaders(HttpResponse response)
326 	{
327 		sendHeaders(response.headers, response.status, response.statusMessage);
328 	}
329 
330 	/// Send this data only.
331 	/// Headers should have already been sent.
332 	/// Low-level alternative to `sendResponse`.
333 	final void sendData(scope Data[] data)
334 	{
335 		conn.send(data);
336 	}
337 
338 	/// Accept more requests on the same connection?
339 	protected bool acceptMore() { return true; }
340 
341 	/// Finalize writing the response.
342 	/// Headers and data should have already been sent.
343 	/// Low-level alternative to `sendResponse`.
344 	final void closeResponse()
345 	{
346 		firstRequest = false;
347 		if (persistent && acceptMore)
348 		{
349 			// reset for next request
350 			debug (HTTP) debugLog("  Waiting for next request.");
351 			conn.handleReadData = &onNewRequest;
352 			if (!timeoutActive)
353 			{
354 				// Give the client time to download large requests.
355 				// Assume a minimal speed of 1kb/s.
356 				timer.setIdleTimeout(timeout + (responseSize / 1024).seconds);
357 				timeoutActive = true;
358 			}
359 			if (inBuffer.bytes.length) // a second request has been pipelined
360 			{
361 				debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length);
362 				onNewRequest(Data());
363 			}
364 		}
365 		else
366 		{
367 			string reason = persistent ? "Server has been shut down" : "Non-persistent connection";
368 			debug (HTTP) debugLog("  Closing connection (%s).", reason);
369 			conn.disconnect(reason);
370 		}
371 	}
372 
373 	/// Retrieve the remote address of the peer, as a string.
374 	abstract @property string remoteAddressStr(HttpRequest r);
375 }
376 
377 /// Basic unencrypted HTTP 1.0/1.1 server.
378 class HttpServer
379 {
380 	enum defaultTimeout = 30.seconds; /// The default timeout used for incoming connections.
381 
382 // public:
383 	this(Duration timeout = defaultTimeout)
384 	{
385 		assert(timeout > Duration.zero);
386 		this.timeout = timeout;
387 
388 		conn = new TcpServer();
389 		conn.handleClose = &onClose;
390 		conn.handleAccept = &onAccept;
391 	} ///
392 
393 	/// Listen on the given TCP address and port.
394 	/// If port is 0, listen on a random available port.
395 	/// Returns the port that the server is actually listening on.
396 	ushort listen(ushort port, string addr = null)
397 	{
398 		port = conn.listen(port, addr);
399 		if (log)
400 			foreach (address; conn.localAddresses)
401 				log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
402 		return port;
403 	}
404 
405 	/// Listen on the given addresses.
406 	void listen(AddressInfo[] addresses)
407 	{
408 		conn.listen(addresses);
409 		if (log)
410 			foreach (address; conn.localAddresses)
411 				log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
412 	}
413 
414 	/// Get listen addresses.
415 	@property Address[] localAddresses() { return conn.localAddresses; }
416 
417 	/// Stop listening, and close idle client connections.
418 	void close()
419 	{
420 		debug(HTTP) stderr.writeln("Shutting down");
421 		if (log) log("Shutting down.");
422 		conn.close();
423 
424 		debug(HTTP) stderr.writefln("There still are %d active connections", connections.iterator.walkLength);
425 
426 		// Close idle connections
427 		foreach (connection; connections.iterator.array)
428 			if (connection.idle && connection.conn.state == ConnectionState.connected)
429 				connection.conn.disconnect("HTTP server shutting down");
430 	}
431 
432 	/// Optional HTTP request log.
433 	Logger log;
434 
435 	/// Single-ended doubly-linked list of active connections
436 	SEDListContainer!HttpServerConnection connections;
437 
438 	/// Callback for when the socket was closed.
439 	void delegate() handleClose;
440 	/// Callback for an incoming request.
441 	void delegate(HttpRequest request, HttpServerConnection conn) handleRequest;
442 
443 	/// What to send in the `"X-Powered-By"` header.
444 	string banner = "ae.net.http.server (+https://github.com/CyberShadow/ae)";
445 
446 	/// If set, the name of the header which will be used to obtain
447 	/// the actual IP of the connecting peer.  Useful when this
448 	/// `HttpServer` is behind a reverse proxy.
449 	string remoteIPHeader;
450 
451 protected:
452 	TcpServer conn;
453 	Duration timeout;
454 
455 	void onClose()
456 	{
457 		if (handleClose)
458 			handleClose();
459 	}
460 
461 	IConnection createConnection(TcpConnection tcp)
462 	{
463 		return tcp;
464 	}
465 
466 	@property string protocol() { return "http"; }
467 
468 	void onAccept(TcpConnection incoming)
469 	{
470 		try
471 			new HttpServerConnection(this, incoming, createConnection(incoming), protocol);
472 		catch (Exception e)
473 		{
474 			if (log)
475 				log("Error accepting connection: " ~ e.msg);
476 			if (incoming.state == ConnectionState.connected)
477 				incoming.disconnect();
478 		}
479 	}
480 }
481 
482 /**
483    HTTPS server. Set SSL parameters on ctx after instantiation.
484 
485    Example:
486    ---
487    auto s = new HttpsServer();
488    s.ctx.enableDH(4096);
489    s.ctx.enableECDH();
490    s.ctx.setCertificate("server.crt");
491    s.ctx.setPrivateKey("server.key");
492    ---
493 */
494 class HttpsServer : HttpServer
495 {
496 	SSLContext ctx; /// The SSL context.
497 
498 	this()
499 	{
500 		ctx = ssl.createContext(SSLContext.Kind.server);
501 	} ///
502 
503 protected:
504 	override @property string protocol() { return "https"; }
505 
506 	override IConnection createConnection(TcpConnection tcp)
507 	{
508 		return ssl.createAdapter(ctx, tcp);
509 	}
510 }
511 
512 /// Standard TCP-based HTTP server connection.
513 final class HttpServerConnection : BaseHttpServerConnection
514 {
515 	TcpConnection tcp; /// The TCP transport.
516 	HttpServer server; /// `HttpServer` owning this connection.
517 	/// Cached local and remote addresses.
518 	Address localAddress, remoteAddress;
519 
520 	mixin DListLink;
521 
522 	/// Retrieves the remote peer address, honoring `remoteIPHeader` if set.
523 	override @property string remoteAddressStr(HttpRequest r)
524 	{
525 		if (server.remoteIPHeader)
526 		{
527 			if (r)
528 				if (auto p = server.remoteIPHeader in r.headers)
529 					return (*p).split(",")[$ - 1];
530 
531 			return "[local:" ~ remoteAddress.toAddrString() ~ "]";
532 		}
533 
534 		return remoteAddress.toAddrString();
535 	}
536 
537 protected:
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 }