Overview

Namespaces

  • PHPFastCGI
    • FastCGIDaemon
      • Command
      • Connection
      • ConnectionHandler
      • Exception
      • Http

Classes

  • PHPFastCGI\FastCGIDaemon\ApplicationFactory
  • PHPFastCGI\FastCGIDaemon\CallbackWrapper
  • PHPFastCGI\FastCGIDaemon\Command\DaemonRunCommand
  • PHPFastCGI\FastCGIDaemon\Connection\StreamSocketConnection
  • PHPFastCGI\FastCGIDaemon\Connection\StreamSocketConnectionPool
  • PHPFastCGI\FastCGIDaemon\ConnectionHandler\ConnectionHandler
  • PHPFastCGI\FastCGIDaemon\ConnectionHandler\ConnectionHandlerFactory
  • PHPFastCGI\FastCGIDaemon\Daemon
  • PHPFastCGI\FastCGIDaemon\DaemonFactory
  • PHPFastCGI\FastCGIDaemon\Http\Request

Interfaces

  • PHPFastCGI\FastCGIDaemon\ApplicationFactoryInterface
  • PHPFastCGI\FastCGIDaemon\Connection\ConnectionInterface
  • PHPFastCGI\FastCGIDaemon\Connection\ConnectionPoolInterface
  • PHPFastCGI\FastCGIDaemon\ConnectionHandler\ConnectionHandlerFactoryInterface
  • PHPFastCGI\FastCGIDaemon\ConnectionHandler\ConnectionHandlerInterface
  • PHPFastCGI\FastCGIDaemon\DaemonFactoryInterface
  • PHPFastCGI\FastCGIDaemon\DaemonInterface
  • PHPFastCGI\FastCGIDaemon\Http\RequestInterface
  • PHPFastCGI\FastCGIDaemon\KernelInterface

Exceptions

  • PHPFastCGI\FastCGIDaemon\Exception\ConnectionException
  • PHPFastCGI\FastCGIDaemon\Exception\DaemonException
  • PHPFastCGI\FastCGIDaemon\Exception\ProtocolException
  • PHPFastCGI\FastCGIDaemon\Exception\ShutdownException
  • Overview
  • Namespace
  • Class
  1: <?php
  2: 
  3: namespace PHPFastCGI\FastCGIDaemon\ConnectionHandler;
  4: 
  5: use PHPFastCGI\FastCGIDaemon\Connection\ConnectionInterface;
  6: use PHPFastCGI\FastCGIDaemon\DaemonInterface;
  7: use PHPFastCGI\FastCGIDaemon\Exception\DaemonException;
  8: use PHPFastCGI\FastCGIDaemon\Exception\ProtocolException;
  9: use PHPFastCGI\FastCGIDaemon\Http\Request;
 10: use PHPFastCGI\FastCGIDaemon\KernelInterface;
 11: use Psr\Http\Message\ResponseInterface;
 12: use Psr\Http\Message\StreamInterface;
 13: use Psr\Log\LoggerAwareInterface;
 14: use Psr\Log\LoggerAwareTrait;
 15: use Psr\Log\LoggerInterface;
 16: use Psr\Log\NullLogger;
 17: use Symfony\Component\HttpFoundation\Response as HttpFoundationResponse;
 18: use Zend\Diactoros\Stream;
 19: 
 20: /**
 21:  * The default implementation of the ConnectionHandlerInterface.
 22:  */
 23: class ConnectionHandler implements ConnectionHandlerInterface, LoggerAwareInterface
 24: {
 25:     use LoggerAwareTrait;
 26: 
 27:     const READ_LENGTH = 4096;
 28: 
 29:     /**
 30:      * @var bool
 31:      */
 32:     private $shutdown;
 33: 
 34:     /**
 35:      * @var KernelInterface
 36:      */
 37:     private $kernel;
 38: 
 39:     /**
 40:      * @var ConnectionInterface
 41:      */
 42:     private $connection;
 43: 
 44:     /**
 45:      * @var array
 46:      */
 47:     private $requests;
 48: 
 49:     /**
 50:      * @var string
 51:      */
 52:     private $buffer;
 53: 
 54:     /**
 55:      * @var int
 56:      */
 57:     private $bufferLength;
 58: 
 59:     /**
 60:      * Constructor.
 61:      *
 62:      * @param KernelInterface     $kernel     The kernel to use to handle requests
 63:      * @param ConnectionInterface $connection The connection to handle
 64:      * @param LoggerInterface     $logger     A logger to use
 65:      */
 66:     public function __construct(KernelInterface $kernel, ConnectionInterface $connection, LoggerInterface $logger = null)
 67:     {
 68:         $this->setLogger((null === $logger) ? new NullLogger() : $logger);
 69: 
 70:         $this->shutdown     = false;
 71:         $this->kernel       = $kernel;
 72:         $this->connection   = $connection;
 73:         $this->requests     = [];
 74:         $this->buffer       = '';
 75:         $this->bufferLength = 0;
 76:     }
 77: 
 78:     /**
 79:      * {@inheritdoc}
 80:      */
 81:     public function ready()
 82:     {
 83:         try {
 84:             $data = $this->connection->read(self::READ_LENGTH);
 85:             $dataLength = strlen($data);
 86: 
 87:             $this->buffer       .= $data;
 88:             $this->bufferLength += $dataLength;
 89: 
 90:             while (null !== ($record = $this->readRecord())) {
 91:                 $this->processRecord($record);
 92:             }
 93:         } catch (DaemonException $exception) {
 94:             $this->logger->error($exception->getMessage());
 95: 
 96:             $this->close();
 97:         }
 98:     }
 99: 
100:     /**
101:      * {@inheritdoc}
102:      */
103:     public function shutdown()
104:     {
105:         $this->shutdown = true;
106:     }
107: 
108:     /**
109:      * {@inheritdoc}
110:      */
111:     public function close()
112:     {
113:         $this->buffer       = null;
114:         $this->bufferLength = 0;
115: 
116:         foreach ($this->requests as $request) {
117:             fclose($request['stdin']);
118:         }
119: 
120:         $this->requests = [];
121: 
122:         $this->connection->close();
123:     }
124: 
125:     /**
126:      * Read a record from the connection.
127:      *
128:      * @return array|null The record that has been read
129:      */
130:     private function readRecord()
131:     {
132:         // Not enough data to read header
133:         if ($this->bufferLength < 8) {
134:             return;
135:         }
136: 
137:         $headerData = substr($this->buffer, 0, 8);
138: 
139:         $headerFormat = 'Cversion/Ctype/nrequestId/ncontentLength/CpaddingLength/x';
140: 
141:         $record = unpack($headerFormat, $headerData);
142: 
143:         // Not enough data to read rest of record
144:         if ($this->bufferLength - 8 < $record['contentLength'] + $record['paddingLength']) {
145:             return;
146:         }
147: 
148:         $record['contentData'] = substr($this->buffer, 8, $record['contentLength']);
149: 
150:         // Remove the record from the buffer
151:         $recordSize = 8 + $record['contentLength'] + $record['paddingLength'];
152: 
153:         $this->buffer        = substr($this->buffer, $recordSize);
154:         $this->bufferLength -= $recordSize;
155: 
156:         return $record;
157:     }
158: 
159:     /**
160:      * Process a record.
161:      *
162:      * @param array $record The record that has been read
163:      *
164:      * @throws ProtocolException If the client sends an unexpected record.
165:      */
166:     private function processRecord(array $record)
167:     {
168:         $requestId = $record['requestId'];
169: 
170:         $content = 0 === $record['contentLength'] ? null : $record['contentData'];
171: 
172:         switch ($record['type']) {
173:             case DaemonInterface::FCGI_BEGIN_REQUEST:
174:                 $this->processBeginRequestRecord($requestId, $content);
175:                 break;
176: 
177:             case DaemonInterface::FCGI_PARAMS:
178:                 $this->processParamsRecord($requestId, $content);
179:                 break;
180: 
181:             case DaemonInterface::FCGI_STDIN:
182:                 $this->processStdinRecord($requestId, $content);
183:                 break;
184: 
185:             case DaemonInterface::FCGI_ABORT_REQUEST:
186:                 $this->processAbortRequestRecord($requestId);
187:                 break;
188: 
189:             default:
190:                 throw new ProtocolException('Unexpected packet of unkown type: '.$record['type']);
191:         }
192:     }
193: 
194:     /**
195:      * Process a FCGI_BEGIN_REQUEST record.
196:      *
197:      * @param int    $requestId   The request id sending the record
198:      * @param string $contentData The content of the record
199:      *
200:      * @throws ProtocolException If the FCGI_BEGIN_REQUEST record is unexpected
201:      */
202:     private function processBeginRequestRecord($requestId, $contentData)
203:     {
204:         if (isset($this->requests[$requestId])) {
205:             throw new ProtocolException('Unexpected FCGI_BEGIN_REQUEST record');
206:         }
207: 
208:         $contentFormat = 'nrole/Cflags/x5';
209: 
210:         $content = unpack($contentFormat, $contentData);
211: 
212:         $keepAlive = DaemonInterface::FCGI_KEEP_CONNECTION & $content['flags'];
213: 
214:         $this->requests[$requestId] = [
215:             'keepAlive' => $keepAlive,
216:             'stdin'     => fopen('php://temp', 'r+'),
217:             'params'    => [],
218:         ];
219: 
220:         if ($this->shutdown) {
221:             $this->endRequest($requestId, 0, DaemonInterface::FCGI_OVERLOADED);
222:             return;
223:         }
224: 
225:         if (DaemonInterface::FCGI_RESPONDER !== $content['role']) {
226:             $this->endRequest($requestId, 0, DaemonInterface::FCGI_UNKNOWN_ROLE);
227:             return;
228:         }
229:     }
230: 
231:     /**
232:      * Process a FCGI_PARAMS record.
233:      *
234:      * @param int    $requestId   The request id sending the record
235:      * @param string $contentData The content of the record
236:      *
237:      * @throws ProtocolException If the FCGI_PARAMS record is unexpected
238:      */
239:     private function processParamsRecord($requestId, $contentData)
240:     {
241:         if (!isset($this->requests[$requestId])) {
242:             throw new ProtocolException('Unexpected FCGI_PARAMS record');
243:         }
244: 
245:         if (null === $contentData) {
246:             return;
247:         }
248: 
249:         $initialBytes = unpack('C5', $contentData);
250: 
251:         $extendedLengthName  = $initialBytes[1] & 0x80;
252:         $extendedLengthValue = $extendedLengthName ? $initialBytes[5] & 0x80 : $initialBytes[2] & 0x80;
253: 
254:         $structureFormat = (
255:             ($extendedLengthName  ? 'N' : 'C').'nameLength/'.
256:             ($extendedLengthValue ? 'N' : 'C').'valueLength'
257:         );
258: 
259:         $structure = unpack($structureFormat, $contentData);
260: 
261:         if ($extendedLengthName) {
262:             $structure['nameLength'] &= 0x7FFFFFFF;
263:         }
264: 
265:         if ($extendedLengthValue) {
266:             $structure['valueLength'] &= 0x7FFFFFFF;
267:         }
268: 
269:         $skipLength = ($extendedLengthName ? 4 : 1) + ($extendedLengthValue ? 4 : 1);
270: 
271:         $contentFormat = (
272:             'x'.$skipLength.'/'.
273:             'a'.$structure['nameLength'].'name/'.
274:             'a'.$structure['valueLength'].'value/'
275:         );
276: 
277:         $content = unpack($contentFormat, $contentData);
278: 
279:         $this->requests[$requestId]['params'][$content['name']] = $content['value'];
280:     }
281: 
282:     /**
283:      * Process a FCGI_STDIN record.
284:      *
285:      * @param int    $requestId   The request id sending the record
286:      * @param string $contentData The content of the record
287:      *
288:      * @throws ProtocolException If the FCGI_STDIN record is unexpected
289:      */
290:     private function processStdinRecord($requestId, $contentData)
291:     {
292:         if (!isset($this->requests[$requestId])) {
293:             throw new ProtocolException('Unexpected FCGI_STDIN record');
294:         }
295: 
296:         if (null === $contentData) {
297:             $this->dispatchRequest($requestId);
298: 
299:             return;
300:         }
301: 
302:         fwrite($this->requests[$requestId]['stdin'], $contentData);
303:     }
304: 
305:     /**
306:      * Process a FCGI_ABORT_REQUEST record.
307:      *
308:      * @param int $requestId The request id sending the record
309:      *
310:      * @throws ProtocolException If the FCGI_ABORT_REQUEST record is unexpected
311:      */
312:     private function processAbortRequestRecord($requestId)
313:     {
314:         if (!isset($this->requests[$requestId])) {
315:             throw new ProtocolException('Unexpected FCG_ABORT_REQUEST record');
316:         }
317: 
318:         $this->endRequest($requestId);
319:     }
320:     
321:     /**
322:      * End the request by writing an FCGI_END_REQUEST record and then removing
323:      * the request from memory and closing the connection if necessary.
324:      *
325:      * @param int $requestId      The request id to end
326:      * @param int $appStatus      The application status to declare
327:      * @param int $protocolStatus The protocol status to declare
328:      */
329:     private function endRequest($requestId, $appStatus = 0, $protocolStatus = DaemonInterface::FCGI_REQUEST_COMPLETE)
330:     {
331:         $content = pack('NCx3', $appStatus, $protocolStatus);
332:         $this->writeRecord($requestId, DaemonInterface::FCGI_END_REQUEST, $content);
333: 
334:         $keepAlive = $this->requests[$requestId]['keepAlive'];
335: 
336:         fclose($this->requests[$requestId]['stdin']);
337: 
338:         unset($this->requests[$requestId]);
339: 
340:         if (!$keepAlive) {
341:             $this->close();
342:         }
343:     }
344: 
345:     /**
346:      * Write a record to the connection.
347:      *
348:      * @param int    $requestId The request id to write to
349:      * @param int    $type      The type of record
350:      * @param string $content   The content of the record
351:      */
352:     private function writeRecord($requestId, $type, $content = null)
353:     {
354:         $contentLength = null === $content ? 0 : strlen($content);
355: 
356:         $headerData = pack('CCnnxx', DaemonInterface::FCGI_VERSION_1, $type, $requestId, $contentLength);
357: 
358:         $this->connection->write($headerData);
359: 
360:         if (null !== $content) {
361:             $this->connection->write($content);
362:         }
363:     }
364: 
365:     /**
366:      * Write a response to the connection as FCGI_STDOUT records.
367:      *
368:      * @param int             $requestId  The request id to write to
369:      * @param string          $headerData The header data to write (including terminating CRLFCRLF)
370:      * @param StreamInterface $stream     The stream to write
371:      */
372:     private function writeResponse($requestId, $headerData, StreamInterface $stream)
373:     {
374:         $data = $headerData;
375:         $eof  = false;
376: 
377:         $stream->rewind();
378: 
379:         do {
380:             $dataLength = strlen($data);
381: 
382:             if ($dataLength < 65535 && !$eof && !($eof = $stream->eof())) {
383:                 $readLength  = 65535 - $dataLength;
384:                 $data       .= $stream->read($readLength);
385:                 $dataLength  = strlen($data);
386:             }
387: 
388:             $writeSize = min($dataLength, 65535);
389:             $writeData = substr($data, 0, $writeSize);
390:             $data      = substr($data, $writeSize);
391: 
392:             $this->writeRecord($requestId, DaemonInterface::FCGI_STDOUT, $writeData);
393:         } while ($writeSize === 65535);
394: 
395:         $this->writeRecord($requestId, DaemonInterface::FCGI_STDOUT);
396:     }
397: 
398:     /**
399:      * Dispatches a request to the kernel.
400:      *
401:      * @param int $requestId The request id that is ready to dispatch
402:      *
403:      * @throws DaemonException If there is an error dispatching the request
404:      */
405:     private function dispatchRequest($requestId)
406:     {
407:         $request = new Request(
408:             $this->requests[$requestId]['params'],
409:             $this->requests[$requestId]['stdin']
410:         );
411: 
412:         try {
413:             $response = $this->kernel->handleRequest($request);
414: 
415:             if ($response instanceof ResponseInterface) {
416:                 $this->sendResponse($requestId, $response);
417:             } elseif ($response instanceof HttpFoundationResponse) {
418:                 $this->sendHttpFoundationResponse($requestId, $response);
419:             } else {
420:                 throw new \LogicException('Kernel must return a PSR-7 or HttpFoundation response message');
421:             }
422: 
423:             $this->endRequest($requestId);
424:         } catch (\Exception $exception) {
425:             $this->logger->error($exception->getMessage());
426: 
427:             $this->endRequest($requestId);
428:         }
429:     }
430: 
431:     /**
432:      * Sends the response to the client.
433:      *
434:      * @param int               $requestId The request id to respond to
435:      * @param ResponseInterface $response  The PSR-7 HTTP response message
436:      */
437:     private function sendResponse($requestId, ResponseInterface $response)
438:     {
439:         $statusCode   = $response->getStatusCode();
440:         $reasonPhrase = $response->getReasonPhrase();
441: 
442:         $headerData = "Status: {$statusCode} {$reasonPhrase}\r\n";
443: 
444:         foreach ($response->getHeaders() as $name => $values) {
445:             $headerData .= $name.': '.implode(', ', $values)."\r\n";
446:         }
447: 
448:         $headerData .= "\r\n";
449: 
450:         $this->writeResponse($requestId, $headerData, $response->getBody());
451:     }
452: 
453:     /**
454:      * Send a HttpFoundation response to the client.
455:      * 
456:      * @param int                    $requestId The request id to respond to
457:      * @param HttpFoundationResponse $response  The HTTP foundation response message
458:      */
459:     private function sendHttpFoundationResponse($requestId, HttpFoundationResponse $response)
460:     {
461:         $statusCode = $response->getStatusCode();
462: 
463:         $headerData  = "Status: {$statusCode}\r\n";
464:         $headerData .= $response->headers . "\r\n";
465: 
466:         $stream = new Stream('php://memory', 'r+');
467:         $stream->write($response->getContent());
468:         $stream->rewind();
469: 
470:         $this->writeResponse($requestId, $headerData, $stream);
471:     }
472: }
473: 
API documentation generated by ApiGen