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.remoteHosts(remoteAddress.toAddrString())[0],
320 			response ? text(cast(ushort)response.status) : "-",
321 			format("%9.2f ms", request.age.total!"usecs" / 1000f),
322 			request.method,
323 			formatAddress(protocol, localAddress, request.host, request.port) ~ request.resource,
324 			response ? response.headers.get("Content-Type", "-") : "-",
325 		] ~ (DEBUG ? [] : [
326 			request.headers.get("Referer", "-"),
327 			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 		respMessage.put("HTTP/", currentRequest.protocolVersion, " ");
351 
352 		if ("X-Powered-By" !in headers)
353 			headers["X-Powered-By"] = "ae.net.http.server (+https://github.com/CyberShadow/ae)";
354 
355 		headers["Date"] = httpTime(Clock.currTime());
356 		if (persistent && currentRequest.protocolVersion=="1.0")
357 			headers["Connection"] = "Keep-Alive";
358 		else
359 		if (!persistent && currentRequest.protocolVersion=="1.1")
360 			headers["Connection"] = "close";
361 		else
362 			headers.remove("Connection");
363 
364 		respMessage.put("%d %s\r\n".format(status, statusMessage));
365 		foreach (string header, string value; headers)
366 			respMessage.put(header, ": ", value, "\r\n");
367 
368 		debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> "));
369 
370 		respMessage.put("\r\n");
371 		conn.send(Data(respMessage.get()));
372 	}
373 
374 	void sendHeaders(HttpResponse response)
375 	{
376 		sendHeaders(response.headers, response.status, response.statusMessage);
377 	}
378 
379 	void sendResponse(HttpResponse response)
380 	{
381 		requestProcessing = false;
382 		if (!response)
383 		{
384 			debug (HTTP) debugLog("sendResponse(null) - generating dummy response");
385 			response = new HttpResponse();
386 			response.status = HttpStatusCode.InternalServerError;
387 			response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError);
388 			response.data = [Data("Internal Server Error")];
389 		}
390 
391 		response.optimizeData(currentRequest.headers);
392 		response.sliceData(currentRequest.headers);
393 
394 		if ("Content-Length" !in response.headers)
395 			response.headers["Content-Length"] = text(response.data.bytes.length);
396 
397 		sendHeaders(response);
398 
399 		if (response && response.data.length && currentRequest.method != "HEAD")
400 			sendData(response.data);
401 
402 		debug (HTTP) debugLog("Sent response (%d bytes data)",
403 			response ? response.data.bytes.length : 0);
404 
405 		closeResponse();
406 
407 		logRequest(currentRequest, response);
408 	}
409 
410 	void sendData(Data[] data)
411 	{
412 		conn.send(data);
413 	}
414 
415 	void closeResponse()
416 	{
417 		if (persistent && server.conn.isListening)
418 		{
419 			// reset for next request
420 			debug (HTTP) debugLog("  Waiting for next request.");
421 			conn.handleReadData = &onNewRequest;
422 			if (!timeoutActive)
423 			{
424 				timer.resumeIdleTimeout();
425 				timeoutActive = true;
426 			}
427 			if (inBuffer.bytes.length) // a second request has been pipelined
428 			{
429 				debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length);
430 				onNewRequest(Data());
431 			}
432 		}
433 		else
434 		{
435 			string reason = persistent ? "Server has been shut down" : "Non-persistent connection";
436 			debug (HTTP) debugLog("  Closing connection (%s).", reason);
437 			conn.disconnect(reason);
438 		}
439 	}
440 }
441 
442 string formatAddress(string protocol, Address address, string vhost = null, ushort logPort = 0)
443 {
444 	string addr = address.toAddrString();
445 	string port =
446 		address.addressFamily == AddressFamily.UNIX ? null :
447 		logPort ? text(logPort) :
448 		address.toPortString();
449 	return protocol ~ "://" ~
450 		(vhost ? vhost : addr == "0.0.0.0" || addr == "::" ? "*" : addr.contains(":") ? "[" ~ addr ~ "]" : addr) ~
451 		(port is null || port == "80" ? "" : ":" ~ port);
452 }
453 
454 unittest
455 {
456 	import ae.net.http.client;
457 	import ae.net.http.responseex;
458 
459 	int[] replies;
460 	int closeAfter;
461 
462 	// Sum "a" from GET and "b" from POST
463 	auto s = new HttpServer;
464 	s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
465 		auto get  = request.urlParameters;
466 		auto post = request.decodePostData();
467 		auto response = new HttpResponseEx;
468 		auto result = to!int(get["a"]) + to!int(post["b"]);
469 		replies ~= result;
470 		conn.sendResponse(response.serveJson(result));
471 		if (--closeAfter == 0)
472 			s.close();
473 	};
474 
475 	// Test server, client, parameter encoding
476 	replies = null;
477 	closeAfter = 1;
478 	auto port = s.listen(0, "127.0.0.1");
479 	httpPost("http://127.0.0.1:" ~ to!string(port) ~ "/?" ~ encodeUrlParameters(["a":"2"]), UrlParameters(["b":"3"]), (string s) { assert(s=="5"); }, null);
480 	socketManager.loop();
481 
482 	// Test pipelining, protocol errors
483 	replies = null;
484 	closeAfter = 2;
485 	port = s.listen(0, "127.0.0.1");
486 	TcpConnection c = new TcpConnection;
487 	c.handleConnect = {
488 		c.send(Data(
489 "GET /?a=123456 HTTP/1.1
490 Content-length: 8
491 Content-type: application/x-www-form-urlencoded
492 
493 b=654321" ~
494 "GET /derp HTTP/1.1
495 Content-length: potato
496 
497 " ~
498 "GET /?a=1234567 HTTP/1.1
499 Content-length: 9
500 Content-type: application/x-www-form-urlencoded
501 
502 b=7654321"));
503 		c.disconnect();
504 	};
505 	c.connect("127.0.0.1", port);
506 
507 	socketManager.loop();
508 
509 	assert(replies == [777777, 8888888]);
510 
511 /+
512 	void testFile(string fn)
513 	{
514 		std.file.write(fn, "42");
515 		s.handleRequest = (HttpRequest request, HttpServerConnection conn) {
516 			auto response = new HttpResponseEx;
517 			conn.sendResponse(response.serveFile(request.resource[1..$], ""));
518 			if (--closeAfter == 0)
519 				s.close();
520 		};
521 		port = s.listen(0, "127.0.0.1");
522 		closeAfter = 1;
523 		httpGet("http://127.0.0.1:" ~ to!string(port) ~ "/" ~ fn, (string s) { assert(s=="42"); }, null);
524 		socketManager.loop();
525 		std.file.remove(fn);
526 	}
527 
528 	testFile("http-test.bin");
529 	testFile("http-test.txt");
530 +/
531 }