1 /**
2  * Flexible app server glue, supporting all
3  * protocols implemented in this library.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <vladimir@thecybershadow.net>
13  */
14 
15 module ae.net.http.app.server;
16 
17 debug version(unittest) version = SSL;
18 
19 import std.algorithm.comparison;
20 import std.algorithm.searching;
21 import std.array;
22 import std.conv;
23 import std.exception;
24 import std.file;
25 import std.format;
26 import std.process : environment;
27 import std.socket;
28 import std.stdio : stderr;
29 import std.typecons;
30 
31 import ae.net.asockets;
32 import ae.net.http.cgi.common;
33 import ae.net.http.cgi.script;
34 import ae.net.http.fastcgi.app;
35 import ae.net.http.responseex;
36 import ae.net.http.scgi.app;
37 import ae.net.http.server;
38 import ae.net.shutdown;
39 import ae.net.ssl;
40 import ae.sys.log;
41 import ae.utils.array;
42 
43 struct ServerConfig
44 {
45 	enum Transport
46 	{
47 		inet,
48 		unix,
49 		stdin,
50 		accept,
51 	}
52 	Transport transport;
53 
54 	struct Listen
55 	{
56 		string addr;
57 		ushort port;
58 		string socketPath;
59 	}
60 	Listen listen;
61 
62 	enum Protocol
63 	{
64 		http,
65 		cgi,
66 		scgi,
67 		fastcgi,
68 	}
69 	Protocol protocol;
70 
71 	Nullable!bool nph;
72 
73 	struct SSL
74 	{
75 		string cert, key;
76 	}
77 	SSL ssl;
78 
79 	string logDir;
80 	string prefix = "/";
81 	string username;
82 	string password;
83 }
84 
85 struct Server(bool useSSL)
86 {
87 	static if (useSSL)
88 	{
89 		import ae.net.ssl.openssl;
90 		mixin SSLUseLib;
91 	}
92 
93 	immutable ServerConfig[string] config;
94 
95 	this(immutable ServerConfig[string] config)
96 	{
97 		this.config = config;
98 	}
99 
100 	void delegate(
101 		HttpRequest request,
102 		immutable ref ServerConfig serverConfig,
103 		void delegate(HttpResponse) handleResponse,
104 		ref Logger log,
105 	) handleRequest;
106 
107 	string banner;
108 
109 	void startServer(string serverName)
110 	{
111 		auto pserverConfig = serverName in config;
112 		pserverConfig.enforce(format!"Did not find a server named %s."(serverName));
113 		startServer(serverName, *pserverConfig, true);
114 	}
115 
116 	void startServers()
117 	{
118 		enforce(config.length, "No servers are configured.");
119 
120 		foreach (name, serverConfig; config)
121 			startServer(name, serverConfig, false);
122 
123 		enforce(socketManager.size(), "No servers to start!");
124 		socketManager.loop();
125 	}
126 
127 	bool runImplicitServer()
128 	{
129 		if (inCGI())
130 		{
131 			runImplicitServer(
132 				ServerConfig.Transport.stdin,
133 				ServerConfig.Protocol.cgi,
134 				"cgi",
135 				"CGI",
136 				"a CGI script");
137 			return true;
138 		}
139 
140 		if (inFastCGI())
141 		{
142 			runImplicitServer(
143 				ServerConfig.Transport.accept,
144 				ServerConfig.Protocol.fastcgi,
145 				"fastcgi",
146 				"FastCGI",
147 				"a FastCGI application");
148 			return true;
149 		}
150 
151 		return false;
152 	}
153 
154 private:
155 
156 	void runImplicitServer(
157 		ServerConfig.Transport transport,
158 		ServerConfig.Protocol protocol,
159 		string serverName,
160 		string protocolText,
161 		string kindText,
162 	)
163 	{
164 		auto pserverConfig = serverName in config;
165 		enum errorFmt =
166 			"This program was invoked as %2$s, but no \"%1$s\" server is configured.\n\n" ~
167 			"Please configure a server named %1$s.";
168 		enforce(pserverConfig, format!errorFmt(serverName, kindText));
169 		enforce(pserverConfig.transport == transport,
170 			format!"The transport must be set to %s in the %s server for implicit %s requests."
171 			(transport, serverName, protocolText));
172 		enforce(pserverConfig.protocol == protocol,
173 			format!"The protocol must be set to %s in the %s server for implicit %s requests."
174 			(protocol, serverName, protocolText));
175 
176 		startServer(serverName, *pserverConfig, true);
177 		socketManager.loop();
178 	}
179 
180 
181 	void startServer(string name, immutable ServerConfig serverConfig, bool exclusive)
182 	{
183 		scope(failure) stderr.writefln("Error with server %s:", name);
184 
185 		auto isSomeCGI = serverConfig.protocol.among(
186 			ServerConfig.Protocol.cgi,
187 			ServerConfig.Protocol.scgi,
188 			ServerConfig.Protocol.fastcgi);
189 
190 		// Check options
191 		if (serverConfig.listen.addr)
192 			enforce(serverConfig.transport == ServerConfig.Transport.inet,
193 				"listen.addr should only be set with transport = inet");
194 		if (serverConfig.listen.port)
195 			enforce(serverConfig.transport == ServerConfig.Transport.inet,
196 				"listen.port should only be set with transport = inet");
197 		if (serverConfig.listen.socketPath)
198 			enforce(serverConfig.transport == ServerConfig.Transport.unix,
199 				"listen.socketPath should only be set with transport = unix");
200 		if (serverConfig.protocol == ServerConfig.Protocol.cgi)
201 			enforce(serverConfig.transport == ServerConfig.Transport.stdin,
202 				"CGI can only be used with transport = stdin");
203 		if (serverConfig.ssl.cert || serverConfig.ssl.key)
204 			enforce(serverConfig.protocol == ServerConfig.Protocol.http,
205 				"SSL can only be used with protocol = http");
206 		if (!serverConfig.nph.isNull)
207 			enforce(isSomeCGI,
208 				"Setting NPH only makes sense with protocol = cgi, scgi, or fastcgi");
209 		enforce(serverConfig.prefix.startsWith("/") && serverConfig.prefix.endsWith("/"),
210 			"Server prefix should start and end with /");
211 
212 		if (!exclusive && serverConfig.transport.among(
213 				ServerConfig.Transport.stdin,
214 				ServerConfig.Transport.accept))
215 		{
216 			stderr.writefln("Skipping exclusive server %1$s.", name);
217 			return;
218 		}
219 
220 		static if (useSSL) SSLContext ctx;
221 		if (serverConfig.ssl !is ServerConfig.SSL.init)
222 		{
223 			static if (useSSL)
224 			{
225 				ctx = ssl.createContext(SSLContext.Kind.server);
226 				ctx.setCertificate(serverConfig.ssl.cert);
227 				ctx.setPrivateKey(serverConfig.ssl.key);
228 			}
229 			else
230 				throw new Exception("This executable was built without SSL support. Cannot use SSL, sorry!");
231 		}
232 
233 		// Place on heap to extend lifetime past scope,
234 		// even though this function creates a closure
235 		Logger* log = {
236 			Logger log;
237 			auto logName = "Server-" ~ name;
238 			string logDir = serverConfig.logDir;
239 			if (logDir is null)
240 				logDir = "/dev/stderr";
241 			switch (logDir)
242 			{
243 				case "/dev/stderr":
244 					log = consoleLogger(logName);
245 					break;
246 				case "/dev/null":
247 					log = nullLogger();
248 					break;
249 				default:
250 					log = fileLogger(logDir ~ "/" ~ logName);
251 					break;
252 			}
253 			return [log].ptr;
254 		}();
255 
256 		TcpServer server;
257 		string protocol = join(
258 			(serverConfig.transport == ServerConfig.Transport.inet ? [] : [serverConfig.transport.text]) ~
259 			(
260 				(serverConfig.protocol == ServerConfig.Protocol.http && serverConfig.ssl !is ServerConfig.SSL.init)
261 				? ["https"]
262 				: (
263 					[serverConfig.protocol.text] ~
264 					(serverConfig.ssl is ServerConfig.SSL.init ? [] : ["tls"])
265 				)
266 			),
267 			"+");
268 
269 		bool nph;
270 		if (isSomeCGI)
271 			nph = serverConfig.nph.isNull ? isNPH() : serverConfig.nph.get;
272 
273 		string[] serverAddrs;
274 		if (serverConfig.protocol == ServerConfig.Protocol.fastcgi)
275 			serverAddrs = environment.get("FCGI_WEB_SERVER_ADDRS", null).split(",");
276 
277 		void handleConnection(IConnection c, string localAddressStr, string remoteAddressStr)
278 		{
279 			static if (useSSL) if (ctx)
280 				c = ssl.createAdapter(ctx, c);
281 
282 			void handleRequest(HttpRequest request, void delegate(HttpResponse) handleResponse)
283 			{
284 				void logAndHandleResponse(HttpResponse response)
285 				{
286 					log.log([
287 						"", // align IP to tab
288 						request ? request.remoteHosts(remoteAddressStr).get(0, remoteAddressStr) : remoteAddressStr,
289 						response ? text(cast(ushort)response.status) : "-",
290 						request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-",
291 						request ? request.method : "-",
292 						request ? protocol ~ "://" ~ localAddressStr ~ request.resource : "-",
293 						response ? response.headers.get("Content-Type", "-") : "-",
294 						request ? request.headers.get("Referer", "-") : "-",
295 						request ? request.headers.get("User-Agent", "-") : "-",
296 					].join("\t"));
297 
298 					handleResponse(response);
299 				}
300 
301 				this.handleRequest(request, serverConfig, &logAndHandleResponse, *log);
302 			}
303 
304 			final switch (serverConfig.protocol)
305 			{
306 				case ServerConfig.Protocol.cgi:
307 				{
308 					auto cgiRequest = readCGIRequest();
309 					auto request = new CGIHttpRequest(cgiRequest);
310 					bool responseWritten;
311 					void handleResponse(HttpResponse response)
312 					{
313 						if (nph)
314 							writeNPHResponse(response);
315 						else
316 							writeCGIResponse(response);
317 						responseWritten = true;
318 					}
319 
320 					handleRequest(request, &handleResponse);
321 					assert(responseWritten);
322 					break;
323 				}
324 				case ServerConfig.Protocol.scgi:
325 				{
326 					auto conn = new SCGIConnection(c);
327 					conn.log = *log;
328 					conn.nph = nph;
329 					void handleSCGIRequest(ref CGIRequest cgiRequest)
330 					{
331 						auto request = new CGIHttpRequest(cgiRequest);
332 						handleRequest(request, &conn.sendResponse);
333 					}
334 					conn.handleRequest = &handleSCGIRequest;
335 					break;
336 				}
337 				case ServerConfig.Protocol.fastcgi:
338 				{
339 					if (serverAddrs && !serverAddrs.canFind(remoteAddressStr))
340 					{
341 						log.log("Address not in FCGI_WEB_SERVER_ADDRS, rejecting");
342 						c.disconnect("Forbidden by FCGI_WEB_SERVER_ADDRS");
343 						return;
344 					}
345 					auto fconn = new FastCGIResponderConnection(c);
346 					fconn.log = *log;
347 					fconn.nph = nph;
348 					void handleCGIRequest(ref CGIRequest cgiRequest, void delegate(HttpResponse) handleResponse)
349 					{
350 						auto request = new CGIHttpRequest(cgiRequest);
351 						handleRequest(request, handleResponse);
352 					}
353 					fconn.handleRequest = &handleCGIRequest;
354 					break;
355 				}
356 				case ServerConfig.Protocol.http:
357 				{
358 					alias connRemoteAddressStr = remoteAddressStr;
359 					alias handleServerRequest = handleRequest;
360 					auto self = &this;
361 
362 					final class HttpConnection : BaseHttpServerConnection
363 					{
364 					protected:
365 						this()
366 						{
367 							this.log = log;
368 							if (self.banner)
369 								this.banner = self.banner;
370 							this.handleRequest = &onRequest;
371 
372 							super(c);
373 						}
374 
375 						void onRequest(HttpRequest request)
376 						{
377 							handleServerRequest(request, &sendResponse);
378 						}
379 
380 						override bool acceptMore() { return server.isListening; }
381 						override string formatLocalAddress(HttpRequest r) { return protocol ~ "://" ~ localAddressStr; }
382 						override @property string remoteAddressStr() { return connRemoteAddressStr; }
383 					}
384 					new HttpConnection();
385 					break;
386 				}
387 			}
388 		}
389 
390 		final switch (serverConfig.transport)
391 		{
392 			case ServerConfig.Transport.stdin:
393 				static if (is(FileConnection))
394 				{
395 					import std.stdio : stdin, stdout;
396 					import core.sys.posix.unistd : dup;
397 					auto c = new Duplex(
398 						new FileConnection(stdin.fileno.dup),
399 						new FileConnection(stdout.fileno.dup),
400 					);
401 					handleConnection(c,
402 						environment.get("REMOTE_ADDR", "-"),
403 						environment.get("SERVER_NAME", "-"));
404 					c.disconnect();
405 					return;
406 				}
407 				else
408 					throw new Exception("Sorry, transport = stdin is not supported on this platform!");
409 			case ServerConfig.Transport.accept:
410 				server = TcpServer.fromStdin();
411 				break;
412 			case ServerConfig.Transport.inet:
413 				server = new TcpServer();
414 				server.listen(serverConfig.listen.port, serverConfig.listen.addr);
415 				break;
416 			case ServerConfig.Transport.unix:
417 			{
418 				server = new TcpServer();
419 				static if (is(UnixAddress))
420 				{
421 					string socketPath = serverConfig.listen.socketPath;
422 					// Work around "path too long" errors with long $PWD
423 					{
424 						import std.path : relativePath;
425 						auto relPath = relativePath(socketPath);
426 						if (relPath.length < socketPath.length)
427 							socketPath = relPath;
428 					}
429 					socketPath.remove().collectException();
430 
431 					AddressInfo ai;
432 					ai.family = AddressFamily.UNIX;
433 					ai.type = SocketType.STREAM;
434 					ai.address = new UnixAddress(socketPath);
435 					server.listen([ai]);
436 
437 					addShutdownHandler({ socketPath.remove(); });
438 				}
439 				else
440 					throw new Exception("UNIX sockets are not available on this platform");
441 			}
442 		}
443 
444 		addShutdownHandler({ server.close(); });
445 
446 		server.handleAccept =
447 			(TcpConnection incoming)
448 			{
449 				handleConnection(incoming, incoming.localAddressStr, incoming.remoteAddressStr);
450 			};
451 
452 		foreach (address; server.localAddresses)
453 			log.log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
454 	}
455 }
456 
457 unittest
458 {
459 	Server!false testServer;
460 }