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