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 <ae@cy.md>
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 /// Describes one configuration for how a listener can receive
44 /// HTTP requests and send responses.
45 struct ServerConfig
46 {
47 	/// Low-level transport to listen on.
48 	enum Transport
49 	{
50 		inet,   /// Internet Protocol (TCP)
51 		unix,   /// UNIX socket
52 		stdin,  /// Standard input (like CGI)
53 		accept, /// A socket that can be accepted on standard input (like FastCGI)
54 	}
55 	Transport transport; /// ditto
56 
57 	/// Listen parameters
58 	struct Listen
59 	{
60 		/// Local address to bind to
61 		string addr;
62 		/// Port number to listen on
63 		ushort port;
64 		/// Path to UNIX socket to listen on
65 		string socketPath;
66 	}
67 	Listen listen; /// ditto
68 
69 	/// Protocol in which requests arrive and responses should be sent in
70 	enum Protocol
71 	{
72 		http,     /// Standard HTTP
73 		cgi,      /// CGI
74 		scgi,     /// SCGI
75 		fastcgi,  /// FastCGI
76 	}
77 	Protocol protocol; /// ditto
78 
79 	/// Whether "No Parsed Headers" mode is enabled or not (null = autodetect).
80 	Nullable!bool nph;
81 
82 	/// SSL parameters. If set, enables TLS.
83 	struct SSL
84 	{
85 		/// Path to a PEM-encoded file containing the public part of the certificate.
86 		string cert;
87 		/// Path to a PEM-encoded file containing the certificate's private key.
88 		string key;
89 	}
90 	SSL ssl; /// ditto
91 
92 	string logDir; /// Directory where logs are to be saved. If null, just write to stderr.
93 	string prefix = "/"; /// URL prefix that requests are expected to start with.
94 	string username; /// Require this username via HTTP basic authentication, if set.
95 	string password; /// Require this password via HTTP basic authentication, if set.
96 }
97 
98 /// An object which receives HTTP requests through one or more
99 /// channels and sends replies according to a user-supplied handler.
100 /// Params:
101 ///  useSSL = Whether to compile SSL support (using ae.net.ssl).
102 struct Server(bool useSSL)
103 {
104 	static if (useSSL)
105 	{
106 		static import ae.net.ssl.openssl;
107 		mixin ae.net.ssl.openssl.SSLUseLib;
108 	}
109 
110 	/// Configuration (as specified when constructing).
111 	immutable ServerConfig[string] config;
112 
113 	/// Constructor.
114 	/// Params:
115 	///  config = An associative array from listener names to their
116 	///           listen configuration.
117 	this(immutable ServerConfig[string] config)
118 	{
119 		this.config = config;
120 	}
121 
122 	/// The use-specified request handler.
123 	/// Params:
124 	///  request        = The HTTP request object to respond to.
125 	///  serverConfig   = The configuration of the listener that
126 	///                   received this request.
127 	///  handleResponse = Call this to send a response.
128 	///  log            = The Logger object associated with this
129 	///                   server.  Can be used to log more
130 	///                   information.
131 	void delegate(
132 		HttpRequest request,
133 		immutable ref ServerConfig serverConfig,
134 		void delegate(HttpResponse) handleResponse,
135 		ref Logger log,
136 	) handleRequest;
137 
138 	/// Send this as the X-Powered-By header.
139 	string banner;
140 
141 	/// Start only the server with the specified name.
142 	/// Does not start an event loop, and returns immediately.
143 	void startServer(string serverName)
144 	{
145 		auto pserverConfig = serverName in config;
146 		pserverConfig.enforce(format!"Did not find a server named %s."(serverName));
147 		startServer(serverName, *pserverConfig, true);
148 	}
149 
150 	/// Start all configured servers, and runs an event loop.
151 	/// Shutdown handlers will be registered, so calling
152 	/// `ae.net.shutdown.shutdown` will stop all servers.
153 	void startServers()
154 	{
155 		enforce(config.length, "No servers are configured.");
156 
157 		foreach (name, serverConfig; config)
158 			startServer(name, serverConfig, false);
159 
160 		enforce(socketManager.size(), "No servers to start!");
161 		socketManager.loop();
162 	}
163 
164 	/// If the current process is being run in a certain environment
165 	/// that implies a particular way of handling requests, run an
166 	/// appropriate server until termination and return true.
167 	/// Otherwise, return false.
168 	bool runImplicitServer()
169 	{
170 		if (inCGI())
171 		{
172 			runImplicitServer(
173 				ServerConfig.Transport.stdin,
174 				ServerConfig.Protocol.cgi,
175 				"cgi",
176 				"CGI",
177 				"a CGI script");
178 			return true;
179 		}
180 
181 		if (inFastCGI())
182 		{
183 			runImplicitServer(
184 				ServerConfig.Transport.accept,
185 				ServerConfig.Protocol.fastcgi,
186 				"fastcgi",
187 				"FastCGI",
188 				"a FastCGI application");
189 			return true;
190 		}
191 
192 		return false;
193 	}
194 
195 private:
196 
197 	void runImplicitServer(
198 		ServerConfig.Transport transport,
199 		ServerConfig.Protocol protocol,
200 		string serverName,
201 		string protocolText,
202 		string kindText,
203 	)
204 	{
205 		auto pserverConfig = serverName in config;
206 		enum errorFmt =
207 			"This program was invoked as %2$s, but no \"%1$s\" server is configured.\n\n" ~
208 			"Please configure a server named %1$s.";
209 		enforce(pserverConfig, format!errorFmt(serverName, kindText));
210 		enforce(pserverConfig.transport == transport,
211 			format!"The transport must be set to %s in the %s server for implicit %s requests."
212 			(transport, serverName, protocolText));
213 		enforce(pserverConfig.protocol == protocol,
214 			format!"The protocol must be set to %s in the %s server for implicit %s requests."
215 			(protocol, serverName, protocolText));
216 
217 		startServer(serverName, *pserverConfig, true);
218 		socketManager.loop();
219 	}
220 
221 
222 	void startServer(string name, immutable ServerConfig serverConfig, bool exclusive)
223 	{
224 		scope(failure) stderr.writefln("Error with server %s:", name);
225 
226 		auto isSomeCGI = serverConfig.protocol.among(
227 			ServerConfig.Protocol.cgi,
228 			ServerConfig.Protocol.scgi,
229 			ServerConfig.Protocol.fastcgi);
230 
231 		// Check options
232 		if (serverConfig.listen.addr)
233 			enforce(serverConfig.transport == ServerConfig.Transport.inet,
234 				"listen.addr should only be set with transport = inet");
235 		if (serverConfig.listen.port)
236 			enforce(serverConfig.transport == ServerConfig.Transport.inet,
237 				"listen.port should only be set with transport = inet");
238 		if (serverConfig.listen.socketPath)
239 			enforce(serverConfig.transport == ServerConfig.Transport.unix,
240 				"listen.socketPath should only be set with transport = unix");
241 		if (serverConfig.protocol == ServerConfig.Protocol.cgi)
242 			enforce(serverConfig.transport == ServerConfig.Transport.stdin,
243 				"CGI can only be used with transport = stdin");
244 		if (serverConfig.ssl.cert || serverConfig.ssl.key)
245 			enforce(serverConfig.protocol == ServerConfig.Protocol.http,
246 				"SSL can only be used with protocol = http");
247 		if (!serverConfig.nph.isNull)
248 			enforce(isSomeCGI,
249 				"Setting NPH only makes sense with protocol = cgi, scgi, or fastcgi");
250 		enforce(serverConfig.prefix.startsWith("/") && serverConfig.prefix.endsWith("/"),
251 			"Server prefix should start and end with /");
252 
253 		if (!exclusive && serverConfig.transport.among(
254 				ServerConfig.Transport.stdin,
255 				ServerConfig.Transport.accept))
256 		{
257 			stderr.writefln("Skipping exclusive server %1$s.", name);
258 			return;
259 		}
260 
261 		static if (useSSL) SSLContext ctx;
262 		if (serverConfig.ssl !is ServerConfig.SSL.init)
263 		{
264 			static if (useSSL)
265 			{
266 				ctx = ssl.createContext(SSLContext.Kind.server);
267 				ctx.setCertificate(serverConfig.ssl.cert);
268 				ctx.setPrivateKey(serverConfig.ssl.key);
269 			}
270 			else
271 				throw new Exception("This executable was built without SSL support. Cannot use SSL, sorry!");
272 		}
273 
274 		// Place on heap to extend lifetime past scope,
275 		// even though this function creates a closure
276 		Logger* log = {
277 			Logger log;
278 			auto logName = "Server-" ~ name;
279 			string logDir = serverConfig.logDir;
280 			if (logDir is null)
281 				logDir = "/dev/stderr";
282 			switch (logDir)
283 			{
284 				case "/dev/stderr":
285 					log = consoleLogger(logName);
286 					break;
287 				case "/dev/null":
288 					log = nullLogger();
289 					break;
290 				default:
291 					log = fileLogger(logDir ~ "/" ~ logName);
292 					break;
293 			}
294 			return [log].ptr;
295 		}();
296 
297 		SocketServer server;
298 		string protocol = join(
299 			(serverConfig.transport == ServerConfig.Transport.inet ? [] : [serverConfig.transport.text]) ~
300 			(
301 				(serverConfig.protocol == ServerConfig.Protocol.http && serverConfig.ssl !is ServerConfig.SSL.init)
302 				? ["https"]
303 				: (
304 					[serverConfig.protocol.text] ~
305 					(serverConfig.ssl is ServerConfig.SSL.init ? [] : ["tls"])
306 				)
307 			),
308 			"+");
309 
310 		bool nph;
311 		if (isSomeCGI)
312 			nph = serverConfig.nph.isNull ? isNPH() : serverConfig.nph.get;
313 
314 		string[] serverAddrs;
315 		if (serverConfig.protocol == ServerConfig.Protocol.fastcgi)
316 			serverAddrs = environment.get("FCGI_WEB_SERVER_ADDRS", null).split(",");
317 
318 		void handleConnection(IConnection c, string localAddressStr, string remoteAddressStr)
319 		{
320 			static if (useSSL) if (ctx)
321 				c = ssl.createAdapter(ctx, c);
322 
323 			void handleRequest(HttpRequest request, void delegate(HttpResponse) handleResponse)
324 			{
325 				void logAndHandleResponse(HttpResponse response)
326 				{
327 					log.log([
328 						"", // align IP to tab
329 						remoteAddressStr,
330 						response ? text(cast(ushort)response.status) : "-",
331 						request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-",
332 						request ? request.method : "-",
333 						request ? protocol ~ "://" ~ localAddressStr ~ request.resource : "-",
334 						response ? response.headers.get("Content-Type", "-") : "-",
335 						request ? request.headers.get("Referer", "-") : "-",
336 						request ? request.headers.get("User-Agent", "-") : "-",
337 					].join("\t"));
338 
339 					handleResponse(response);
340 				}
341 
342 				this.handleRequest(request, serverConfig, &logAndHandleResponse, *log);
343 			}
344 
345 			final switch (serverConfig.protocol)
346 			{
347 				case ServerConfig.Protocol.cgi:
348 				{
349 					prepareCGIFDs();
350 					auto cgiRequest = readCGIRequest();
351 					auto request = new CGIHttpRequest(cgiRequest);
352 					bool responseWritten;
353 					void handleResponse(HttpResponse response)
354 					{
355 						if (nph)
356 							writeNPHResponse(response);
357 						else
358 							writeCGIResponse(response);
359 						responseWritten = true;
360 					}
361 
362 					handleRequest(request, &handleResponse);
363 					assert(responseWritten);
364 					break;
365 				}
366 				case ServerConfig.Protocol.scgi:
367 				{
368 					auto conn = new SCGIConnection(c);
369 					conn.log = *log;
370 					conn.nph = nph;
371 					void handleSCGIRequest(ref CGIRequest cgiRequest)
372 					{
373 						auto request = new CGIHttpRequest(cgiRequest);
374 						handleRequest(request, &conn.sendResponse);
375 					}
376 					conn.handleRequest = &handleSCGIRequest;
377 					break;
378 				}
379 				case ServerConfig.Protocol.fastcgi:
380 				{
381 					if (serverAddrs && !serverAddrs.canFind(remoteAddressStr))
382 					{
383 						log.log("Address not in FCGI_WEB_SERVER_ADDRS, rejecting");
384 						c.disconnect("Forbidden by FCGI_WEB_SERVER_ADDRS");
385 						return;
386 					}
387 					auto fconn = new FastCGIResponderConnection(c);
388 					fconn.log = *log;
389 					fconn.nph = nph;
390 					void handleCGIRequest(ref CGIRequest cgiRequest, void delegate(HttpResponse) handleResponse)
391 					{
392 						auto request = new CGIHttpRequest(cgiRequest);
393 						handleRequest(request, handleResponse);
394 					}
395 					fconn.handleRequest = &handleCGIRequest;
396 					break;
397 				}
398 				case ServerConfig.Protocol.http:
399 				{
400 					alias connRemoteAddressStr = remoteAddressStr;
401 					alias handleServerRequest = handleRequest;
402 					auto self = &this;
403 
404 					final class HttpConnection : BaseHttpServerConnection
405 					{
406 					protected:
407 						this()
408 						{
409 							this.log = log;
410 							if (self.banner)
411 								this.banner = self.banner;
412 							this.handleRequest = &onRequest;
413 
414 							super(c);
415 						}
416 
417 						void onRequest(HttpRequest request)
418 						{
419 							handleServerRequest(request, &sendResponse);
420 						}
421 
422 						override bool acceptMore() { return server.isListening; }
423 						override string formatLocalAddress(HttpRequest r) { return protocol ~ "://" ~ localAddressStr; }
424 						override @property string remoteAddressStr(HttpRequest r) { return connRemoteAddressStr; }
425 					}
426 					new HttpConnection();
427 					break;
428 				}
429 			}
430 		}
431 
432 		final switch (serverConfig.transport)
433 		{
434 			case ServerConfig.Transport.stdin:
435 				static if (is(FileConnection))
436 				{
437 					import std.stdio : stdin, stdout;
438 					import core.sys.posix.unistd : dup;
439 					auto c = new Duplex(
440 						new FileConnection(stdin.fileno.dup),
441 						new FileConnection(stdout.fileno.dup),
442 					);
443 					handleConnection(c,
444 						environment.get("REMOTE_ADDR", "-"),
445 						environment.get("SERVER_NAME", "-"));
446 					c.disconnect();
447 					return;
448 				}
449 				else
450 					throw new Exception("Sorry, transport = stdin is not supported on this platform!");
451 			case ServerConfig.Transport.accept:
452 				server = SocketServer.fromStdin();
453 				break;
454 			case ServerConfig.Transport.inet:
455 			{
456 				auto tcpServer = new TcpServer();
457 				tcpServer.listen(serverConfig.listen.port, serverConfig.listen.addr);
458 				server = tcpServer;
459 				break;
460 			}
461 			case ServerConfig.Transport.unix:
462 			{
463 				server = new SocketServer();
464 				static if (is(UnixAddress))
465 				{
466 					string socketPath = serverConfig.listen.socketPath;
467 					// Work around "path too long" errors with long $PWD
468 					{
469 						import std.path : relativePath;
470 						auto relPath = relativePath(socketPath);
471 						if (relPath.length < socketPath.length)
472 							socketPath = relPath;
473 					}
474 					socketPath.remove().collectException();
475 
476 					AddressInfo ai;
477 					ai.family = AddressFamily.UNIX;
478 					ai.type = SocketType.STREAM;
479 					ai.address = new UnixAddress(socketPath);
480 					server.listen([ai]);
481 
482 					addShutdownHandler((scope const(char)[] /*reason*/) { socketPath.remove(); });
483 				}
484 				else
485 					throw new Exception("UNIX sockets are not available on this platform");
486 			}
487 		}
488 
489 		addShutdownHandler((scope const(char)[] /*reason*/) { server.close(); });
490 
491 		server.handleAccept =
492 			(SocketConnection incoming)
493 			{
494 				handleConnection(incoming, incoming.localAddressStr, incoming.remoteAddressStr);
495 			};
496 
497 		foreach (address; server.localAddresses)
498 			log.log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]");
499 	}
500 }
501 
502 unittest
503 {
504 	alias Test = Server!false;
505 }