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: 22:
23: class ConnectionHandler implements ConnectionHandlerInterface, LoggerAwareInterface
24: {
25: use LoggerAwareTrait;
26:
27: const READ_LENGTH = 4096;
28:
29: 30: 31:
32: private $shutdown;
33:
34: 35: 36:
37: private $kernel;
38:
39: 40: 41:
42: private $connection;
43:
44: 45: 46:
47: private $requests;
48:
49: 50: 51:
52: private $buffer;
53:
54: 55: 56:
57: private $bufferLength;
58:
59: 60: 61: 62: 63: 64: 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: 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: 102:
103: public function shutdown()
104: {
105: $this->shutdown = true;
106: }
107:
108: 109: 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: 127: 128: 129:
130: private function readRecord()
131: {
132:
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:
144: if ($this->bufferLength - 8 < $record['contentLength'] + $record['paddingLength']) {
145: return;
146: }
147:
148: $record['contentData'] = substr($this->buffer, 8, $record['contentLength']);
149:
150:
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: 161: 162: 163: 164: 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: 196: 197: 198: 199: 200: 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: 233: 234: 235: 236: 237: 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: 284: 285: 286: 287: 288: 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: 307: 308: 309: 310: 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: 323: 324: 325: 326: 327: 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: 347: 348: 349: 350: 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: 367: 368: 369: 370: 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: 400: 401: 402: 403: 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: 433: 434: 435: 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: 455: 456: 457: 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: