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 <vladimir@thecybershadow.net>
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 	Data buffer;
70 	IConnection connection;
71 	Logger log;
72 
73 	this(IConnection connection)
74 	{
75 		this.connection = connection;
76 		connection.handleReadData = &onReadData;
77 	}
78 
79 	void onReadData(Data data)
80 	{
81 		buffer ~= data;
82 
83 		while (true)
84 		{
85 			if (buffer.length < FCGI_RecordHeader.sizeof)
86 				return;
87 
88 			auto pheader = cast(FCGI_RecordHeader*)buffer.contents.ptr;
89 			auto totalLength = FCGI_RecordHeader.sizeof + pheader.contentLength + pheader.paddingLength;
90 			if (buffer.length < totalLength)
91 				return;
92 
93 			auto contentData = buffer[FCGI_RecordHeader.sizeof .. FCGI_RecordHeader.sizeof + pheader.contentLength];
94 
95 			try
96 				onRecord(*pheader, contentData);
97 			catch (Exception e)
98 			{
99 				if (log) log("Error handling record: " ~ e.toString());
100 				connection.disconnect(e.msg);
101 				return;
102 			}
103 
104 			buffer = buffer[totalLength .. $];
105 		}
106 	}
107 
108 	abstract void onRecord(ref FCGI_RecordHeader header, Data contentData);
109 }
110 
111 class FastCGIAppSocketServer /// ditto
112 {
113 	string[] serverAddrs;
114 	Logger log;
115 
116 	this()
117 	{
118 		serverAddrs = environment.get("FCGI_WEB_SERVER_ADDRS", null).split(",");
119 	}
120 
121 	final void listen(Socket socket = getListenSocket())
122 	{
123 		socket.blocking = false;
124 		auto listener = new TcpServer(socket);
125 		listener.handleAccept(&onAccept);
126 	}
127 
128 	final void onAccept(TcpConnection connection)
129 	{
130 		if (log) log("Accepted connection from " ~ connection.remoteAddressStr);
131 		if (serverAddrs && !serverAddrs.canFind(connection.remoteAddressStr))
132 		{
133 			if (log) log("Address not in FCGI_WEB_SERVER_ADDRS, rejecting");
134 			connection.disconnect("Forbidden by FCGI_WEB_SERVER_ADDRS");
135 			return;
136 		}
137 		createConnection(connection);
138 	}
139 
140 	abstract void createConnection(IConnection connection);
141 }
142 
143 /// Higher-level FastCGI app server implementation,
144 /// handling the various FastCGI response types.
145 class FastCGIProtoConnection : FastCGIConnection
146 {
147 	// Some conservative limits that are unlikely to run afoul of any
148 	// default limits such as file descriptor ulimit.
149 	size_t maxConns = 512;
150 	size_t maxReqs = 4096;
151 	bool mpxsConns = true;
152 
153 	this(IConnection connection) { super(connection); }
154 
155 	class Request
156 	{
157 		ushort id;
158 		FCGI_Role role;
159 		bool keepConn;
160 		Data paramBuf;
161 
162 		void begin() {}
163 		void abort() {}
164 		void param(const(char)[] name, const(char)[] value) {}
165 		void paramEnd() {}
166 		void stdin(Data datum) {}
167 		void stdinEnd() {}
168 		void data(Data datum) {}
169 		void dataEnd() {}
170 
171 	final:
172 		void stdout(Data datum) { assert(datum.length); sendRecord(FCGI_RecordType.stdout, id, datum); }
173 		void stdoutEnd() { sendRecord(FCGI_RecordType.stdout, id, Data.init); }
174 		void stderr(Data datum) { assert(datum.length); sendRecord(FCGI_RecordType.stderr, id, datum); }
175 		void stderrEnd() { sendRecord(FCGI_RecordType.stderr, id, Data.init); }
176 		void end(uint appStatus, FCGI_ProtocolStatus status)
177 		{
178 			FCGI_EndRequestBody data;
179 			data.appStatus = appStatus;
180 			data.protocolStatus = status;
181 			sendRecord(FCGI_RecordType.endRequest, id, Data(data.bytes));
182 			killRequest(id);
183 			if (!keepConn)
184 				connection.disconnect("End of request without FCGI_KEEP_CONN");
185 		}
186 	}
187 
188 	Request[] requests;
189 
190 	abstract Request createRequest();
191 
192 	Request getRequest(ushort requestId)
193 	{
194 		enforce(requestId > 0, "Unexpected null request ID");
195 		return requests.getExpand(requestId - 1);
196 	}
197 
198 	Request newRequest(ushort requestId)
199 	{
200 		enforce(requestId > 0, "Unexpected null request ID");
201 		auto request = createRequest();
202 		request.id = requestId;
203 		requests.putExpand(requestId - 1, request);
204 		return request;
205 	}
206 
207 	void killRequest(ushort requestId)
208 	{
209 		enforce(requestId > 0, "Unexpected null request ID");
210 		requests.putExpand(requestId - 1, null);
211 	}
212 
213 	final void sendRecord(ref FCGI_RecordHeader header, Data contentData)
214 	{
215 		connection.send(Data(header.bytes));
216 		connection.send(contentData);
217 	}
218 
219 	final void sendRecord(FCGI_RecordType type, ushort requestId, Data contentData)
220 	{
221 		FCGI_RecordHeader header;
222 		header.version_ = FCGI_VERSION_1;
223 		header.type = type;
224 		header.requestId = requestId;
225 		header.contentLength = contentData.length.to!ushort;
226 		sendRecord(header, contentData);
227 	}
228 
229 	override void onRecord(ref FCGI_RecordHeader header, Data contentData)
230 	{
231 		switch (header.type)
232 		{
233 			case FCGI_RecordType.beginRequest:
234 			{
235 				auto beginRequest = contentData.asStruct!FCGI_BeginRequestBody;
236 				auto request = newRequest(header.requestId);
237 				request.role = beginRequest.role;
238 				request.keepConn = !!(beginRequest.flags & FCGI_RequestFlags.keepConn);
239 				request.begin();
240 				break;
241 			}
242 			case FCGI_RecordType.abortRequest:
243 			{
244 				enforce(contentData.length == 0, "Expected no data after FCGI_ABORT_REQUEST");
245 				auto request = getRequest(header.requestId);
246 				if (!request)
247 					return;
248 				request.abort();
249 				break;
250 			}
251 			case FCGI_RecordType.params:
252 			{
253 				auto request = getRequest(header.requestId);
254 				if (!request)
255 					return;
256 				if (contentData.length)
257 				{
258 					request.paramBuf ~= contentData;
259 					char[] name, value;
260 					auto buf = request.paramBuf;
261 					while (buf.readNameValue(name, value))
262 					{
263 						request.param(name, value);
264 						request.paramBuf = buf;
265 					}
266 				}
267 				else
268 				{
269 					enforce(request.paramBuf.length == 0, "Slack data in FCGI_PARAMS");
270 					request.paramEnd();
271 				}
272 				break;
273 			}
274 			case FCGI_RecordType.stdin:
275 			{
276 				auto request = getRequest(header.requestId);
277 				if (!request)
278 					return;
279 				if (contentData.length)
280 					request.stdin(contentData);
281 				else
282 					request.stdinEnd();
283 				break;
284 			}
285 			case FCGI_RecordType.data:
286 			{
287 				auto request = getRequest(header.requestId);
288 				if (!request)
289 					return;
290 				if (contentData.length)
291 					request.data(contentData);
292 				else
293 					request.dataEnd();
294 				break;
295 			}
296 			case FCGI_RecordType.getValues:
297 			{
298 				FastAppender!ubyte result;
299 				while (contentData.length)
300 				{
301 					char[] name, dummyValue;
302 					contentData.readNameValue(name, dummyValue)
303 						.enforce("Incomplete FCGI_GET_VALUES");
304 					enforce(dummyValue.length == 0,
305 						"Present value in FCGI_GET_VALUES");
306 					auto value = getValue(name);
307 					if (value)
308 						result.putNameValue(name, value);
309 				}
310 				sendRecord(
311 					FCGI_RecordType.getValuesResult,
312 					FCGI_NULL_REQUEST_ID,
313 					Data(result.get),
314 				);
315 				break;
316 			}
317 			default:
318 			{
319 				FCGI_UnknownTypeBody data;
320 				data.type = header.type;
321 				sendRecord(
322 					FCGI_RecordType.unknownType,
323 					FCGI_NULL_REQUEST_ID,
324 					Data(data.bytes),
325 				);
326 				break;
327 			}
328 		}
329 	}
330 
331 	const(char)[] getValue(const(char)[] name)
332 	{
333 		switch (name)
334 		{
335 			case FCGI_MAX_CONNS:
336 				return maxConns.text;
337 			case FCGI_MAX_REQS:
338 				return maxReqs.text;
339 			case FCGI_MPXS_CONNS:
340 				return int(mpxsConns).text;
341 			default:
342 				return null;
343 		}
344 	}
345 }
346 
347 T* asStruct(T)(Data data)
348 {
349 	enforce(data.length == T.sizeof,
350 		format!"Expected data for %s (%d bytes), but got %d bytes"(
351 			T.stringof, T.sizeof, data.length,
352 		));
353 	return cast(T*)data.contents.ptr;
354 }
355 
356 bool readNameValue(ref Data data, ref char[] name, ref char[] value)
357 {
358 	uint nameLen, valueLen;
359 	if (!data.readVLInt(nameLen))
360 		return false;
361 	if (!data.readVLInt(valueLen))
362 		return false;
363 	auto totalLen = nameLen + valueLen;
364 	if (data.length < totalLen)
365 		return false;
366 	name  = cast(char[])data.contents[0 .. nameLen];
367 	value = cast(char[])data.contents[nameLen .. totalLen];
368 	data = data[totalLen .. $];
369 	return true;
370 }
371 
372 bool readVLInt(ref Data data, ref uint value)
373 {
374 	auto bytes = cast(ubyte[])data.contents;
375 	if (!bytes.length)
376 		return false;
377 	if ((bytes[0] & 0x80) == 0)
378 	{
379 		value = bytes[0];
380 		data = data[1..$];
381 		return true;
382 	}
383 	if (bytes.length < 4)
384 		return false;
385 	value = ((bytes[0] & 0x7F) << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3];
386 	data = data[4..$];
387 	return true;
388 }
389 
390 void putNameValue(W)(ref W writer, in char[] name, in char[] value)
391 {
392 	writer.putVLInt(name.length);
393 	writer.putVLInt(value.length);
394 	writer.put(cast(ubyte[])name);
395 	writer.put(cast(ubyte[])value);
396 }
397 
398 void putVLInt(W)(ref W writer, size_t value)
399 {
400 	enforce(value <= 0x7FFFFFFF, "FastCGI integer value overflow");
401 	if (value < 0x80)
402 		writer.put(cast(ubyte)value);
403 	else
404 		writer.put(
405 			ubyte((value >> 24) & 0xFF | 0x80),
406 			ubyte((value >> 16) & 0xFF),
407 			ubyte((value >>  8) & 0xFF),
408 			ubyte((value      ) & 0xFF),
409 		);
410 }
411 
412 /// FastCGI server for handling Responder requests.
413 class FastCGIResponderConnection : FastCGIProtoConnection
414 {
415 	this(IConnection connection) { super(connection); }
416 
417 	final class ResponderRequest : Request
418 	{
419 		string[string] params;
420 		Data[] inputData;
421 
422 		override void begin()
423 		{
424 			if (role != FCGI_Role.responder)
425 				return end(1, FCGI_ProtocolStatus.unknownRole);
426 		}
427 
428 		override void param(const(char)[] name, const(char)[] value)
429 		{
430 			params[name.idup] = value.idup;
431 		}
432 
433 		override void stdin(Data datum)
434 		{
435 			inputData ~= datum;
436 		}
437 
438 		override void stdinEnd()
439 		{
440 			auto request = CGIRequest.fromAA(params);
441 			request.data = inputData;
442 
443 			try
444 				this.outer.handleRequest(request, &sendResponse);
445 			catch (Exception e)
446 			{
447 				stderr(Data(e.toString()));
448 				stderrEnd();
449 				end(0, FCGI_ProtocolStatus.requestComplete);
450 			}
451 		}
452 
453 		void sendResponse(HttpResponse r)
454 		{
455 			FastAppender!char headers;
456 			if (this.outer.nph)
457 				writeNPHHeaders(r, headers);
458 			else
459 				writeCGIHeaders(r, headers);
460 			stdout(Data(headers.get));
461 
462 			foreach (datum; r.data)
463 				stdout(datum);
464 			stdoutEnd();
465 			end(0, FCGI_ProtocolStatus.requestComplete);
466 		}
467 
468 		override void data(Data datum) { throw new Exception("Unexpected FCGI_DATA"); }
469 		override void dataEnd() { throw new Exception("Unexpected FCGI_DATA"); }
470 	}
471 
472 	override Request createRequest() { return new ResponderRequest; }
473 
474 	void delegate(ref CGIRequest, void delegate(HttpResponse)) handleRequest;
475 	bool nph;
476 }
477 
478 class FastCGIResponderServer : FastCGIAppSocketServer /// ditto
479 {
480 	bool nph;
481 
482 	void delegate(ref CGIRequest, void delegate(HttpResponse)) handleRequest;
483 
484 	override void createConnection(IConnection connection)
485 	{
486 		auto fconn = new FastCGIResponderConnection(connection);
487 		fconn.log = this.log;
488 		fconn.nph = this.nph;
489 		fconn.handleRequest = this.handleRequest;
490 	}
491 }
492 
493 unittest
494 {
495 	new FastCGIResponderServer;
496 }