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