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