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