1 /**
2  * Support for implementing FastCGI application servers.
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  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.net.http.fastcgi.app;
15 
16 version (Windows)
17 {
18 	import core.sys.windows.winbase;
19 	import core.sys.windows.winsock2;
20 }
21 else
22 	import core.stdc.errno;
23 
24 import std.algorithm.searching;
25 import std.array;
26 import std.bitmanip;
27 import std.conv;
28 import std.exception;
29 import std.format;
30 import std.process : environment;
31 import std.socket;
32 
33 import ae.net.asockets;
34 import ae.net.http.common;
35 import ae.net.http.cgi.common;
36 import ae.net.http.cgi.script;
37 import ae.net.http.fastcgi.common;
38 import ae.sys.log;
39 import ae.utils.array;
40 
41 private Socket getListenSocket()
42 {
43 	socket_t socket;
44 	version (Windows)
45 		socket = cast(socket_t)GetStdHandle(STD_INPUT_HANDLE);
46 	else
47 		socket = cast(socket_t)FCGI_LISTENSOCK_FILENO;
48 
49 	return new Socket(socket, AddressFamily.UNSPEC);
50 }
51 
52 /// Return true if the current process was
53 /// likely invoked as a FastCGI application.
54 bool inFastCGI()
55 {
56 	auto socket = getListenSocket();
57 	try
58 	{
59 		socket.remoteAddress();
60 		return false;
61 	}
62 	catch (SocketOSException e)
63 		return e.errorCode == ENOTCONN;
64 }
65 
66 /// Base implementation of the low-level FastCGI protocol.
67 class FastCGIConnection
68 {
69 	private Data buffer;
70 	IConnection connection; /// Connection used to construct this object.
71 	Logger log;             /// Optional logger.
72 
73 	/// Constructor.
74 	/// Params:
75 	///  connection = Abstract connection used for communication.
76 	this(IConnection connection)
77 	{
78 		this.connection = connection;
79 		connection.handleReadData = &onReadData;
80 	}
81 
82 	protected void onReadData(Data data)
83 	{
84 		buffer ~= data;
85 
86 		while (true)
87 		{
88 			if (buffer.length < FCGI_RecordHeader.sizeof)
89 				return;
90 
91 			auto pheader = cast(FCGI_RecordHeader*)buffer.contents.ptr;
92 			auto totalLength = FCGI_RecordHeader.sizeof + pheader.contentLength + pheader.paddingLength;
93 			if (buffer.length < totalLength)
94 				return;
95 
96 			auto contentData = buffer[FCGI_RecordHeader.sizeof .. FCGI_RecordHeader.sizeof + pheader.contentLength];
97 
98 			try
99 				onRecord(*pheader, contentData);
100 			catch (Exception e)
101 			{
102 				if (log) log("Error handling record: " ~ e.toString());
103 				connection.disconnect(e.msg);
104 				return;
105 			}
106 
107 			buffer = buffer[totalLength .. $];
108 		}
109 	}
110 
111 	protected abstract void onRecord(ref FCGI_RecordHeader header, Data contentData);
112 }
113 
114 /// ditto
115 class FastCGIAppSocketServer
116 {
117 	/// Addresses to listen on.
118 	/// Populated from `FCGI_WEB_SERVER_ADDRS` by default.
119 	string[] serverAddrs;
120 	Logger log; /// Optional logger.
121 
122 	///
123 	this()
124 	{
125 		serverAddrs = environment.get("FCGI_WEB_SERVER_ADDRS", null).split(",");
126 	}
127 
128 	/// Begin listening.
129 	/// Params:
130 	///  socket = Listen on this socket instead of the default.
131 	final void listen(Socket socket = getListenSocket())
132 	{
133 		socket.blocking = false;
134 		auto listener = new TcpServer(socket);
135 		listener.handleAccept(&onAccept);
136 	}
137 
138 	protected final void onAccept(TcpConnection connection)
139 	{
140 		if (log) log("Accepted connection from " ~ connection.remoteAddressStr);
141 		if (serverAddrs && !serverAddrs.canFind(connection.remoteAddressStr))
142 		{
143 			if (log) log("Address not in FCGI_WEB_SERVER_ADDRS, rejecting");
144 			connection.disconnect("Forbidden by FCGI_WEB_SERVER_ADDRS");
145 			return;
146 		}
147 		createConnection(connection);
148 	}
149 
150 	protected abstract void createConnection(IConnection connection);
151 }
152 
153 /// Higher-level FastCGI app server implementation,
154 /// handling the various FastCGI response types.
155 class FastCGIProtoConnection : FastCGIConnection
156 {
157 	// Some conservative limits that are unlikely to run afoul of any
158 	// default limits such as file descriptor ulimit.
159 	size_t maxConns = 512; /// Maximum number of concurrent connections to advertise.
160 	size_t maxReqs = 4096; /// Maximum number of concurrent requests to advertise.
161 	bool mpxsConns = true; /// Whether to advertise support for multiplexing.
162 
163 	///
164 	this(IConnection connection) { super(connection); }
165 
166 	/// Base class for an abstract ongoing FastCGI request.
167 	class Request
168 	{
169 		ushort id;      /// FastCGI request ID.
170 		FCGI_Role role; /// FastCGI role. (Responder, authorizer...)
171 		bool keepConn;  /// Keep connection alive after handling request.
172 		Data paramBuf;  /// Buffer used to hold the parameters.
173 
174 		void begin() {} /// Handle the beginning of processing this request.
175 		void abort() {} /// Handle a request to abort processing this request.
176 		/// Handle an incoming request parameter.
177 		void param(const(char)[] name, const(char)[] value) {}
178 		void paramEnd() {} /// Handle the end of request parameters.
179 		void stdin(Data datum) {} /// Handle a chunk of input (i.e. request body) data.
180 		void stdinEnd() {} /// Handle the end of input data.
181 		void data(Data datum) {} /// Handle a chunk of additional data.
182 		void dataEnd() {} /// Handle the end of additional data.
183 
184 	final:
185 		/// Send output (response) data.
186 		void stdout(Data datum) { assert(datum.length); sendRecord(FCGI_RecordType.stdout, id, datum); }
187 		/// Finish sending output data.
188 		void stdoutEnd() { sendRecord(FCGI_RecordType.stdout, id, Data.init); }
189 		/// Send error data.
190 		void stderr(Data datum) { assert(datum.length); sendRecord(FCGI_RecordType.stderr, id, datum); }
191 		/// Finish sending error data.
192 		void stderrEnd() { sendRecord(FCGI_RecordType.stderr, id, Data.init); }
193 		/// Finish processing this request, with the indicated status codes.
194 		void end(uint appStatus, FCGI_ProtocolStatus status)
195 		{
196 			FCGI_EndRequestBody data;
197 			data.appStatus = appStatus;
198 			data.protocolStatus = status;
199 			sendRecord(FCGI_RecordType.endRequest, id, Data(data.bytes));
200 			killRequest(id);
201 			if (!keepConn)
202 				connection.disconnect("End of request without FCGI_KEEP_CONN");
203 		}
204 	}
205 
206 	/// In-flight requests.
207 	Request[] requests;
208 
209 	/// Override this method to provide a factory for your Request
210 	/// implementation.
211 	abstract Request createRequest();
212 
213 	/// Return the request with the given ID.
214 	Request getRequest(ushort requestId)
215 	{
216 		enforce(requestId > 0, "Unexpected null request ID");
217 		return requests.getExpand(requestId - 1);
218 	}
219 
220 	/// Create and return a request with the given ID.
221 	Request newRequest(ushort requestId)
222 	{
223 		enforce(requestId > 0, "Unexpected null request ID");
224 		auto request = createRequest();
225 		request.id = requestId;
226 		requests.putExpand(requestId - 1, request);
227 		return request;
228 	}
229 
230 	/// Clear the given request ID.
231 	void killRequest(ushort requestId)
232 	{
233 		enforce(requestId > 0, "Unexpected null request ID");
234 		requests.putExpand(requestId - 1, null);
235 	}
236 
237 	/// Write a raw FastCGI packet.
238 	final void sendRecord(ref FCGI_RecordHeader header, Data contentData)
239 	{
240 		connection.send(Data(header.bytes));
241 		connection.send(contentData);
242 	}
243 
244 	/// ditto
245 	final void sendRecord(FCGI_RecordType type, ushort requestId, Data contentData)
246 	{
247 		FCGI_RecordHeader header;
248 		header.version_ = FCGI_VERSION_1;
249 		header.type = type;
250 		header.requestId = requestId;
251 		header.contentLength = contentData.length.to!ushort;
252 		sendRecord(header, contentData);
253 	}
254 
255 	protected override void onRecord(ref FCGI_RecordHeader header, Data contentData)
256 	{
257 		switch (header.type)
258 		{
259 			case FCGI_RecordType.beginRequest:
260 			{
261 				auto beginRequest = contentData.asStruct!FCGI_BeginRequestBody;
262 				auto request = newRequest(header.requestId);
263 				request.role = beginRequest.role;
264 				request.keepConn = !!(beginRequest.flags & FCGI_RequestFlags.keepConn);
265 				request.begin();
266 				break;
267 			}
268 			case FCGI_RecordType.abortRequest:
269 			{
270 				enforce(contentData.length == 0, "Expected no data after FCGI_ABORT_REQUEST");
271 				auto request = getRequest(header.requestId);
272 				if (!request)
273 					return;
274 				request.abort();
275 				break;
276 			}
277 			case FCGI_RecordType.params:
278 			{
279 				auto request = getRequest(header.requestId);
280 				if (!request)
281 					return;
282 				if (contentData.length)
283 				{
284 					request.paramBuf ~= contentData;
285 					char[] name, value;
286 					auto buf = request.paramBuf;
287 					while (buf.readNameValue(name, value))
288 					{
289 						request.param(name, value);
290 						request.paramBuf = buf;
291 					}
292 				}
293 				else
294 				{
295 					enforce(request.paramBuf.length == 0, "Slack data in FCGI_PARAMS");
296 					request.paramEnd();
297 				}
298 				break;
299 			}
300 			case FCGI_RecordType.stdin:
301 			{
302 				auto request = getRequest(header.requestId);
303 				if (!request)
304 					return;
305 				if (contentData.length)
306 					request.stdin(contentData);
307 				else
308 					request.stdinEnd();
309 				break;
310 			}
311 			case FCGI_RecordType.data:
312 			{
313 				auto request = getRequest(header.requestId);
314 				if (!request)
315 					return;
316 				if (contentData.length)
317 					request.data(contentData);
318 				else
319 					request.dataEnd();
320 				break;
321 			}
322 			case FCGI_RecordType.getValues:
323 			{
324 				FastAppender!ubyte result;
325 				while (contentData.length)
326 				{
327 					char[] name, dummyValue;
328 					contentData.readNameValue(name, dummyValue)
329 						.enforce("Incomplete FCGI_GET_VALUES");
330 					enforce(dummyValue.length == 0,
331 						"Present value in FCGI_GET_VALUES");
332 					auto value = getValue(name);
333 					if (value)
334 						result.putNameValue(name, value);
335 				}
336 				sendRecord(
337 					FCGI_RecordType.getValuesResult,
338 					FCGI_NULL_REQUEST_ID,
339 					Data(result.get),
340 				);
341 				break;
342 			}
343 			default:
344 			{
345 				FCGI_UnknownTypeBody data;
346 				data.type = header.type;
347 				sendRecord(
348 					FCGI_RecordType.unknownType,
349 					FCGI_NULL_REQUEST_ID,
350 					Data(data.bytes),
351 				);
352 				break;
353 			}
354 		}
355 	}
356 
357 	private const(char)[] getValue(const(char)[] name)
358 	{
359 		switch (name)
360 		{
361 			case FCGI_MAX_CONNS:
362 				return maxConns.text;
363 			case FCGI_MAX_REQS:
364 				return maxReqs.text;
365 			case FCGI_MPXS_CONNS:
366 				return int(mpxsConns).text;
367 			default:
368 				return null;
369 		}
370 	}
371 }
372 
373 private T* asStruct(T)(Data data)
374 {
375 	enforce(data.length == T.sizeof,
376 		format!"Expected data for %s (%d bytes), but got %d bytes"(
377 			T.stringof, T.sizeof, data.length,
378 		));
379 	return cast(T*)data.contents.ptr;
380 }
381 
382 /// Parse a FastCGI-encoded name-value pair.
383 bool readNameValue(ref Data data, ref char[] name, ref char[] value)
384 {
385 	uint nameLen, valueLen;
386 	if (!data.readVLInt(nameLen))
387 		return false;
388 	if (!data.readVLInt(valueLen))
389 		return false;
390 	auto totalLen = nameLen + valueLen;
391 	if (data.length < totalLen)
392 		return false;
393 	name  = cast(char[])data.contents[0 .. nameLen];
394 	value = cast(char[])data.contents[nameLen .. totalLen];
395 	data = data[totalLen .. $];
396 	return true;
397 }
398 
399 /// Parse a FastCGI-encoded variable-length integer.
400 bool readVLInt(ref Data data, ref uint value)
401 {
402 	auto bytes = cast(ubyte[])data.contents;
403 	if (!bytes.length)
404 		return false;
405 	if ((bytes[0] & 0x80) == 0)
406 	{
407 		value = bytes[0];
408 		data = data[1..$];
409 		return true;
410 	}
411 	if (bytes.length < 4)
412 		return false;
413 	value = ((bytes[0] & 0x7F) << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3];
414 	data = data[4..$];
415 	return true;
416 }
417 
418 /// Write a FastCGI-encoded name-value pair.
419 void putNameValue(W)(ref W writer, in char[] name, in char[] value)
420 {
421 	writer.putVLInt(name.length);
422 	writer.putVLInt(value.length);
423 	writer.put(cast(ubyte[])name);
424 	writer.put(cast(ubyte[])value);
425 }
426 
427 /// Write a FastCGI-encoded variable-length integer.
428 void putVLInt(W)(ref W writer, size_t value)
429 {
430 	enforce(value <= 0x7FFFFFFF, "FastCGI integer value overflow");
431 	if (value < 0x80)
432 		writer.put(cast(ubyte)value);
433 	else
434 		writer.put(
435 			ubyte((value >> 24) & 0xFF | 0x80),
436 			ubyte((value >> 16) & 0xFF),
437 			ubyte((value >>  8) & 0xFF),
438 			ubyte((value      ) & 0xFF),
439 		);
440 }
441 
442 /// FastCGI server for handling Responder requests.
443 class FastCGIResponderConnection : FastCGIProtoConnection
444 {
445 	///
446 	this(IConnection connection) { super(connection); }
447 
448 	protected final class ResponderRequest : Request
449 	{
450 		string[string] params;
451 		Data[] inputData;
452 
453 		override void begin()
454 		{
455 			if (role != FCGI_Role.responder)
456 				return end(1, FCGI_ProtocolStatus.unknownRole);
457 		}
458 
459 		override void param(const(char)[] name, const(char)[] value)
460 		{
461 			params[name.idup] = value.idup;
462 		}
463 
464 		override void stdin(Data datum)
465 		{
466 			inputData ~= datum;
467 		}
468 
469 		override void stdinEnd()
470 		{
471 			auto request = CGIRequest.fromAA(params);
472 			request.data = inputData;
473 
474 			try
475 				this.outer.handleRequest(request, &sendResponse);
476 			catch (Exception e)
477 			{
478 				stderr(Data(e.toString()));
479 				stderrEnd();
480 				end(0, FCGI_ProtocolStatus.requestComplete);
481 			}
482 		}
483 
484 		void sendResponse(HttpResponse r)
485 		{
486 			FastAppender!char headers;
487 			if (this.outer.nph)
488 				writeNPHHeaders(r, headers);
489 			else
490 				writeCGIHeaders(r, headers);
491 			stdout(Data(headers.get));
492 
493 			foreach (datum; r.data)
494 				stdout(datum);
495 			stdoutEnd();
496 			end(0, FCGI_ProtocolStatus.requestComplete);
497 		}
498 
499 		override void data(Data datum) { throw new Exception("Unexpected FCGI_DATA"); }
500 		override void dataEnd() { throw new Exception("Unexpected FCGI_DATA"); }
501 	}
502 
503 	protected override Request createRequest() { return new ResponderRequest; }
504 
505 	/// User-supplied callback for handling incoming requests.
506 	void delegate(ref CGIRequest, void delegate(HttpResponse)) handleRequest;
507 
508 	/// Whether to operate in Non-Parsed Headers mode.
509 	bool nph;
510 }
511 
512 /// ditto
513 class FastCGIResponderServer : FastCGIAppSocketServer
514 {
515 	/// Whether to operate in Non-Parsed Headers mode.
516 	bool nph;
517 
518 	/// User-supplied callback for handling incoming requests.
519 	void delegate(ref CGIRequest, void delegate(HttpResponse)) handleRequest;
520 
521 	protected override void createConnection(IConnection connection)
522 	{
523 		auto fconn = new FastCGIResponderConnection(connection);
524 		fconn.log = this.log;
525 		fconn.nph = this.nph;
526 		fconn.handleRequest = this.handleRequest;
527 	}
528 }
529 
530 unittest
531 {
532 	new FastCGIResponderServer;
533 }