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