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