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 }