Completed
Push — master ( 57fcfe...4c8185 )
by Andrew
11s
created

ConnectionHandler::ready()   B

Complexity

Conditions 4
Paths 12

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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