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 }