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