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);
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 			request ? request.remoteHosts(remoteAddressStr)[0] : remoteAddressStr,
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 	abstract @property string remoteAddressStr();
214 
215 	final @property bool idle()
216 	{
217 		if (requestProcessing)
218 			return false;
219 		foreach (datum; inBuffer)
220 			if (datum.length)
221 				return false;
222 		return true;
223 	}
224 
225 public:
226 	final void sendHeaders(Headers headers, HttpStatusCode status, string statusMessage = null)
227 	{
228 		assert(status, "Unset status code");
229 
230 		if (!statusMessage)
231 			statusMessage = HttpResponse.getStatusMessage(status);
232 
233 		StringBuilder respMessage;
234 		auto protocolVersion = currentRequest ? currentRequest.protocolVersion : "1.0";
235 		respMessage.put("HTTP/", protocolVersion, " ");
236 
237 		if (banner && "X-Powered-By" !in headers)
238 			headers["X-Powered-By"] = banner;
239 
240 		if ("Date" !in headers)
241 			headers["Date"] = httpTime(Clock.currTime());
242 
243 		if ("Connection" !in headers)
244 		{
245 			if (persistent && protocolVersion=="1.0")
246 				headers["Connection"] = "Keep-Alive";
247 			else
248 			if (!persistent && protocolVersion=="1.1")
249 				headers["Connection"] = "close";
250 		}
251 
252 		respMessage.put("%d %s\r\n".format(status, statusMessage));
253 		foreach (string header, string value; headers)
254 			respMessage.put(header, ": ", value, "\r\n");
255 
256 		debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> "));
257 
258 		respMessage.put("\r\n");
259 		conn.send(Data(respMessage.get()));
260 	}
261 
262 	final void sendHeaders(HttpResponse response)
263 	{
264 		sendHeaders(response.headers, response.status, response.statusMessage);
265 	}
266 
267 	final void sendResponse(HttpResponse response)
268 	{
269 		requestProcessing = false;
270 		if (!response)
271 		{
272 			debug (HTTP) debugLog("sendResponse(null) - generating dummy response");
273 			response = new HttpResponse();
274 			response.status = HttpStatusCode.InternalServerError;
275 			response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError);
276 			response.data = [Data("Internal Server Error")];
277 		}
278 
279 		if (currentRequest)
280 		{
281 			response.optimizeData(currentRequest.headers);
282 			response.sliceData(currentRequest.headers);
283 		}
284 
285 		if ("Content-Length" !in response.headers)
286 			response.headers["Content-Length"] = text(response.data.bytes.length);
287 
288 		sendHeaders(response);
289 
290 		bool isHead = currentRequest ? currentRequest.method == "HEAD" : false;
291 		if (response && response.data.length && !isHead)
292 			sendData(response.data);
293 
294 		responseSize = response ? response.data.bytes.length : 0;
295 		debug (HTTP) debugLog("Sent response (%d bytes data)", responseSize);
296 
297 		closeResponse();
298 
299 		logRequest(currentRequest, response);
300 	}
301 
302 	final void sendData(Data[] data)
303 	{
304 		conn.send(data);
305 	}
306 
307 	/// Accept more requests on the same connection?
308 	bool acceptMore() { return true; }
309 
310 	final void closeResponse()
311 	{
312 		if (persistent && acceptMore)
313 		{
314 			// reset for next request
315 			debug (HTTP) debugLog("  Waiting for next request.");
316 			conn.handleReadData = &onNewRequest;
317 			if (!timeoutActive)
318 			{
319 				// Give the client time to download large requests.
320 				// Assume a minimal speed of 1kb/s.
321 				timer.setIdleTimeout(timeout + (responseSize / 1024).seconds);
322 				timeoutActive = true;
323 			}
324 			if (inBuffer.bytes.length) // a second request has been pipelined
325 			{
326 				debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length);
327 				onNewRequest(Data());
328 			}
329 		}
330 		else
331 		{
332 			string reason = persistent ? "Server has been shut down" : "Non-persistent connection";
333 			debug (HTTP) debugLog("  Closing connection (%s).", reason);
334 			conn.disconnect(reason);
335 		}
336 	}
337 }
338 
339 class HttpServer
340 {
341 	enum defaultTimeout = 30.seconds;
342 public:
343 	this(Duration timeout = defaultTimeout)
344 	{
345 		assert(timeout > Duration.zero);
346 		this.timeout = timeout;
347 
348 		conn = new TcpServer();
349 		conn.handleClose = &onClose;
350 		conn.handleAccept = &onAccept;
351 	}
352 
353 	ushort listen(ushort port, string addr = null)
354 	{
355 		port = conn.listen(port, addr);
356 		if (log)
357 			foreach (address; conn.localAddresses)
358 				log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
359 		return port;
360 	}
361 
362 	void listen(AddressInfo[] addresses)
363 	{
364 		conn.listen(addresses);
365 		if (log)
366 			foreach (address; conn.localAddresses)
367 				log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
368 	}
369 
370 	void close()
371 	{
372 		debug(HTTP) stderr.writeln("Shutting down");
373 		if (log) log("Shutting down.");
374 		conn.close();
375 
376 		debug(HTTP) stderr.writefln("There still are %d active connections", connections.iterator.walkLength);
377 
378 		// Close idle connections
379 		foreach (connection; connections.iterator.array)
380 			if (connection.idle && connection.conn.state == ConnectionState.connected)
381 				connection.conn.disconnect("HTTP server shutting down");
382 	}
383 
384 	Logger log;
385 
386 	/// Single-ended doubly-linked list of active connections
387 	SEDListContainer!HttpServerConnection connections;
388 
389 	/// Callback for when the socket was closed.
390 	void delegate() handleClose;
391 	/// Callback for an incoming request.
392 	void delegate(HttpRequest request, HttpServerConnection conn) handleRequest;
393 
394 	string banner = "ae.net.http.server (+https://github.com/CyberShadow/ae)";
395 
396 protected:
397 	TcpServer conn;
398 	Duration timeout;
399 
400 	void onClose()
401 	{
402 		if (handleClose)
403 			handleClose();
404 	}
405 
406 	IConnection createConnection(TcpConnection tcp)
407 	{
408 		return tcp;
409 	}
410 
411 	@property string protocol() { return "http"; }
412 
413 	void onAccept(TcpConnection incoming)
414 	{
415 		try
416 			new HttpServerConnection(this, incoming, createConnection(incoming), protocol);
417 		catch (Exception e)
418 		{
419 			if (log)
420 				log("Error accepting connection: " ~ e.msg);
421 			if (incoming.state == ConnectionState.connected)
422 				incoming.disconnect();
423 		}
424 	}
425 }
426 
427 /// HTTPS server. Set SSL parameters on ctx after instantiation.
428 /// Example:
429 /// ---
430 ///	auto s = new HttpsServer();
431 ///	s.ctx.enableDH(4096);
432 ///	s.ctx.enableECDH();
433 ///	s.ctx.setCertificate("server.crt");
434 ///	s.ctx.setPrivateKey("server.key");
435 /// ---
436 class HttpsServer : HttpServer
437 {
438 	SSLContext ctx;
439 
440 	this()
441 	{
442 		ctx = ssl.createContext(SSLContext.Kind.server);
443 	}
444 
445 protected:
446 	override @property string protocol() { return "https"; }
447 
448 	override IConnection createConnection(TcpConnection tcp)
449 	{
450 		return ssl.createAdapter(ctx, tcp);
451 	}
452 }
453 
454 final class HttpServerConnection : BaseHttpServerConnection
455 {
456 	TcpConnection tcp;
457 	HttpServer server;
458 	Address localAddress, remoteAddress;
459 
460 	mixin DListLink;
461 
462 protected:
463 	string protocol;
464 
465 	this(HttpServer server, TcpConnection tcp, IConnection c, string protocol = "http")
466 	{
467 		this.server = server;
468 		this.tcp = tcp;
469 		this.log = server.log;
470 		this.protocol = protocol;
471 		this.banner = server.banner;
472 		this.timeout = server.timeout;
473 		this.handleRequest = (HttpRequest r) => server.handleRequest(r, this);
474 		this.localAddress = tcp.localAddress;
475 		this.remoteAddress = tcp.remoteAddress;
476 
477 		super(c);
478 
479 		server.connections.pushFront(this);
480 	}
481 
482 	override void onDisconnect(string reason, DisconnectType type)
483 	{
484 		super.onDisconnect(reason, type);
485 		server.connections.remove(this);
486 	}
487 
488 	override bool acceptMore() { return server.conn.isListening; }
489 	override string formatLocalAddress(HttpRequest r) { return formatAddress(protocol, localAddress, r.host, r.port); }
490 	override @property string remoteAddressStr() { return remoteAddress.toAddrString(); }
491 }
492 
493 version (Posix)
494 final class FileHttpServerConnection : BaseHttpServerConnection
495 {
496 	this(File input = stdin, File output = stdout, string protocol = "stdin")
497 	{
498 		this.protocol = protocol;
499 
500 		auto c = new Duplex(
501 			new FileConnection(input.fileno),
502 			new FileConnection(output.fileno),
503 		);
504 
505 		super(c);
506 	}
507 
508 protected:
509 	import std.stdio : File, stdin, stdout;
510 
511 	string protocol;
512 
513 	override string formatLocalAddress(HttpRequest r) { return protocol ~ "://"; }
514 	override @property string remoteAddressStr() { return "-"; }
515 }
516 
517 string formatAddress(string protocol, Address address, string vhost = null, ushort logPort = 0)
518 {
519 	string addr = address.toAddrString();
520 	string port =
521 		address.addressFamily == AddressFamily.UNIX ? null :
522 		logPort ? text(logPort) :
523 		address.toPortString();
524 	return protocol ~ "://" ~
525 		(vhost ? vhost : addr == "0.0.0.0" || addr == "::" ? "*" : addr.contains(":") ? "[" ~ addr ~ "]" : addr) ~
526 		(port is null || port == "80" ? "" : ":" ~ port);
527 }
528 
529 version (unittest) import ae.net.http.client;
530 version (unittest) import ae.net.http.responseex;
531 unittest
532 {
533 	int[] replies;
534 	int closeAfter;
535 
536 	// Sum "a" from GET and "b" from POST
537 	auto s = new HttpServer;
538 	s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
539 		auto get  = request.urlParameters;
540 		auto post = request.decodePostData();
541 		auto response = new HttpResponseEx;
542 		auto result = to!int(get["a"]) + to!int(post["b"]);
543 		replies ~= result;
544 		conn.sendResponse(response.serveJson(result));
545 		if (--closeAfter == 0)
546 			s.close();
547 	};
548 
549 	// Test server, client, parameter encoding
550 	replies = null;
551 	closeAfter = 1;
552 	auto port = s.listen(0, "127.0.0.1");
553 	httpPost("http://127.0.0.1:" ~ to!string(port) ~ "/?" ~ encodeUrlParameters(["a":"2"]), UrlParameters(["b":"3"]), (string s) { assert(s=="5"); }, null);
554 	socketManager.loop();
555 
556 	// Test pipelining, protocol errors
557 	replies = null;
558 	closeAfter = 2;
559 	port = s.listen(0, "127.0.0.1");
560 	TcpConnection c = new TcpConnection;
561 	c.handleConnect = {
562 		c.send(Data(
563 "GET /?a=123456 HTTP/1.1
564 Content-length: 8
565 Content-type: application/x-www-form-urlencoded
566 
567 b=654321" ~
568 "GET /derp HTTP/1.1
569 Content-length: potato
570 
571 " ~
572 "GET /?a=1234567 HTTP/1.1
573 Content-length: 9
574 Content-type: application/x-www-form-urlencoded
575 
576 b=7654321"));
577 		c.disconnect();
578 	};
579 	c.connect("127.0.0.1", port);
580 
581 	socketManager.loop();
582 
583 	assert(replies == [777777, 8888888]);
584 
585 	// Test bad headers
586 	s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
587 		auto response = new HttpResponseEx;
588 		conn.sendResponse(response.serveText("OK"));
589 		if (--closeAfter == 0)
590 			s.close();
591 	};
592 	closeAfter = 1;
593 
594 	port = s.listen(0, "127.0.0.1");
595 	c = new TcpConnection;
596 	c.handleConnect = {
597 		c.send(Data("\n\n\n\n\n"));
598 		c.disconnect();
599 
600 		// Now send a valid request to end the loop
601 		c = new TcpConnection;
602 		c.handleConnect = {
603 			c.send(Data("GET / HTTP/1.0\n\n"));
604 			c.disconnect();
605 		};
606 		c.connect("127.0.0.1", port);
607 	};
608 	c.connect("127.0.0.1", port);
609 
610 	socketManager.loop();
611 
612 /+
613 	void testFile(string fn)
614 	{
615 		std.file.write(fn, "42");
616 		s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
617 			auto response = new HttpResponseEx;
618 			conn.sendResponse(response.serveFile(request.resource[1..$], ""));
619 			if (--closeAfter == 0)
620 				s.close();
621 		};
622 		port = s.listen(0, "127.0.0.1");
623 		closeAfter = 1;
624 		httpGet("http://127.0.0.1:" ~ to!string(port) ~ "/" ~ fn, (string s) { assert(s=="42"); }, null);
625 		socketManager.loop();
626 		std.file.remove(fn);
627 	}
628 
629 	testFile("http-test.bin");
630 	testFile("http-test.txt");
631 +/
632 }