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