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