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 }