xref: /php-src/sapi/fpm/tests/fcgi.inc (revision d02a8f4e)
1<?php
2/*
3 * This file is part of PHP-FastCGI-Client.
4 *
5 * (c) Pierrick Charron <pierrick@adoy.net>
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy of
8 * this software and associated documentation files (the "Software"), to deal in
9 * the Software without restriction, including without limitation the rights to
10 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11 * of the Software, and to permit persons to whom the Software is furnished to do
12 * so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in all
15 * copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 * SOFTWARE.
24 */
25namespace FPM\FastCGI;
26
27class TimedOutException extends \Exception {}
28class ForbiddenException extends \Exception {}
29class ReadLimitExceeded extends \Exception {}
30
31class TransportException extends \Exception {}
32
33interface Transport
34{
35    /**
36     * Connect to the application.
37     *
38     * @param string $host Host address.
39     * @param int $port Port number.
40     * @throws TransportException
41     */
42    public function connect(string $host, int $port, ?int $connectionTimeout): void;
43
44    /**
45     * Set keep alive.
46     *
47     * @param bool $keepAlive Whether to enable keep alive.
48     */
49    public function setKeepAlive(bool $keepAlive): void;
50
51    /**
52     * Set data reading and writing timeout.
53     *
54     * @param int $timeoutMs
55     * @return bool
56     */
57    public function setDataTimeout(int $timeoutMs): bool;
58
59    /**
60     * Read data.
61     *
62     * @param int $numBytes Number of bytes to read.
63     * @throws TransportException
64     * @return string
65     */
66    public function read(int $numBytes): string;
67
68    /**
69     * Write data.
70     *
71     * @param string $bytes Bytes to write.
72     * @throws TransportException
73     * @return int Number of bytes written.
74     */
75    public function write(string $bytes): int;
76
77    public function getMetaData(): array;
78
79    /**
80     * Flush data.
81     *
82     * @return bool
83     */
84    public function flush(): bool;
85
86    /**
87     * Close connection.
88     *
89     * @return bool
90     */
91    public function close(): bool;
92}
93
94/**
95 * Stream transport.
96 *
97 * Iis based on PHP streams and should be more reliable as it automatically handles timeouts and
98 * other features.
99 */
100class StreamTransport implements Transport
101{
102    /**
103     * @var resource|null|false
104     */
105    private $stream = null;
106
107    /**
108     * @var bool
109     */
110    private bool $keepAlive = false;
111
112    /**
113     * @inheritDoc
114     */
115    public function connect(string $host, int $port, ?int $connectionTimeout = 5000): void
116    {
117        if ($this->stream) {
118            return;
119        }
120        $this->stream = fsockopen(
121            $host,
122            $port,
123            $errno,
124            $errstr,
125            $connectionTimeout / 1000
126        );
127
128        if (!$this->stream) {
129            throw new TransportException('Unable to connect to FastCGI application: ' . $errstr);
130        }
131
132        if ($this->keepAlive) {
133            $this->setKeepAlive(true);
134        }
135    }
136
137    /**
138     * @inheritDoc
139     */
140    public function setDataTimeout(int $timeoutMs): bool
141    {
142        if (!$this->stream) {
143            return false;
144        }
145        return stream_set_timeout(
146            $this->stream,
147            floor($timeoutMs / 1000),
148            ($timeoutMs % 1000) * 1000
149        );
150    }
151
152    /**
153     * @inheritDoc
154     */
155    public function setKeepAlive(bool $keepAlive): void
156    {
157        $this->keepAlive = $keepAlive;
158        if (!$this->stream) {
159            return;
160        }
161        if ($keepAlive) {
162            $socket = socket_import_stream($this->stream);
163            if ($socket) {
164                socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
165            }
166        } else {
167            $this->close();
168        }
169    }
170
171    /**
172     * @inheritDoc
173     */
174    public function read(int $numBytes): string
175    {
176        $result = fread($this->stream, $numBytes);
177        if ($result === false) {
178            throw new TransportException('Reading from the stream failed');
179        }
180        return $result;
181    }
182
183    /**
184     * @inheritDoc
185     */
186    public function write(string $bytes): int
187    {
188        $result = fwrite($this->stream, $bytes);
189        if ($result === false) {
190            throw new TransportException('Writing to the stream failed');
191        }
192        return $result;
193    }
194
195    public function getMetaData(): array
196    {
197        return stream_get_meta_data($this->stream);
198    }
199
200    /**
201     * @inheritDoc
202     */
203    public function flush(): bool
204    {
205        return fflush($this->stream);
206    }
207
208    public function close(): bool
209    {
210        if ($this->stream) {
211            $result = fclose($this->stream);
212            $this->stream = null;
213            return $result;
214        }
215
216        return false;
217    }
218}
219
220/**
221 * Socket transport.
222 *
223 * This transport is more low level than stream and supports some extra socket options like
224 * SO_KEEPALIVE. However, it is currently less robust and missing some stream features like
225 * connection timeout. It should be used only for specific use cases.
226 */
227class SocketTransport implements Transport
228{
229    /**
230     * @var \Socket
231     */
232    private ?\Socket $socket = null;
233
234    /**
235     * @var int
236     */
237    protected int $dataTimeoutMs = 5000;
238
239    /**
240     * @var bool
241     */
242    private bool $keepAlive = false;
243
244    /**
245     * @inheritDoc
246     */
247    public function connect(string $host, int $port, ?int $connectionTimeout = 5000): void
248    {
249        if ($this->socket) {
250            return;
251        }
252        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
253        if (!$this->socket) {
254            throw new TransportException('Unable to create socket: ' . socket_strerror(socket_last_error()));
255        }
256
257        $ip = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host);
258
259        if (!socket_connect($this->socket, $ip, $port)) {
260            $error = socket_strerror(socket_last_error($this->socket));
261            throw new TransportException('Unable to connect to FastCGI application: ' . $error);
262        }
263
264        if ($this->keepAlive) {
265            $this->setKeepAlive(true);
266        }
267    }
268
269    /**
270     * @inheritDoc
271     */
272    public function setDataTimeout(int $timeoutMs): bool
273    {
274        $this->dataTimeoutMs = $timeoutMs;
275        return true;
276    }
277
278    /**
279     * @inheritDoc
280     */
281    public function setKeepAlive(bool $keepAlive): void
282    {
283        $this->keepAlive = $keepAlive;
284        if (!$this->socket) {
285            return;
286        }
287        if ($keepAlive) {
288            socket_set_option($this->socket, SOL_SOCKET, SO_KEEPALIVE, 1);
289        } else {
290            $this->close();
291        }
292    }
293
294    private function select(array $read, array $write = [], array $except = []): bool
295    {
296        return socket_select(
297            $read,
298            $write,
299            $except,
300            floor($this->dataTimeoutMs / 1000),
301            ($this->dataTimeoutMs % 1000) * 1000
302        );
303    }
304
305    /**
306     * @inheritDoc
307     */
308    public function read(int $numBytes): string
309    {
310        if ($this->select([$this->socket]) === false) {
311            throw new TimedOutException('Reading timeout');
312        }
313        $result = socket_read($this->socket, $numBytes);
314        if ($result === false) {
315            throw new TransportException('Reading from the stream failed');
316        }
317        return $result;
318    }
319
320    /**
321     * @inheritDoc
322     */
323    public function write(string $bytes): int
324    {
325        if ($this->select([], [$this->socket]) === false) {
326            throw new TimedOutException('Writing timeout');
327        }
328        $result = socket_write($this->socket, $bytes);
329        if ($result === false) {
330            throw new TransportException('Writing to the stream failed');
331        }
332        return $result;
333    }
334
335    public function getMetaData(): array
336    {
337        return [];
338    }
339
340    /**
341     * @inheritDoc
342     */
343    public function flush(): bool
344    {
345        return true;
346    }
347
348    public function close(): bool
349    {
350        if ($this->socket) {
351            socket_close($this->socket);
352            $this->socket = null;
353            return true;
354        }
355
356        return false;
357    }
358}
359
360/**
361 * Handles communication with a FastCGI application
362 *
363 * @author      Pierrick Charron <pierrick@adoy.net>, Jakub Zelenka <bukka@php.net>
364 * @version     2.0
365 */
366class Client
367{
368    const VERSION_1            = 1;
369
370    const BEGIN_REQUEST        = 1;
371    const ABORT_REQUEST        = 2;
372    const END_REQUEST          = 3;
373    const PARAMS               = 4;
374    const STDIN                = 5;
375    const STDOUT               = 6;
376    const STDERR               = 7;
377    const DATA                 = 8;
378    const GET_VALUES           = 9;
379    const GET_VALUES_RESULT    = 10;
380    const UNKNOWN_TYPE         = 11;
381    const MAXTYPE              = self::UNKNOWN_TYPE;
382
383    const RESPONDER            = 1;
384    const AUTHORIZER           = 2;
385    const FILTER               = 3;
386
387    const REQUEST_COMPLETE     = 0;
388    const CANT_MPX_CONN        = 1;
389    const OVERLOADED           = 2;
390    const UNKNOWN_ROLE         = 3;
391
392    const MAX_CONNS            = 'FCGI_MAX_CONNS';
393    const MAX_REQS             = 'FCGI_MAX_REQS';
394    const MPXS_CONNS           = 'FCGI_MPXS_CONNS';
395
396    const HEADER_LEN           = 8;
397
398    const REQ_STATE_WRITTEN    = 1;
399    const REQ_STATE_OK         = 2;
400    const REQ_STATE_ERR        = 3;
401    const REQ_STATE_TIMED_OUT  = 4;
402
403    /**
404     * Host
405     * @var string
406     */
407    private $_host = null;
408
409    /**
410     * Port
411     * @var int
412     */
413    private $_port = null;
414
415    /**
416     * Keep Alive
417     * @var bool
418     */
419    private $_keepAlive = false;
420
421    /**
422     * Outstanding request statuses keyed by request id
423     *
424     * Each request is an array with following form:
425     *
426     *  array(
427     *    'state' => REQ_STATE_*
428     *    'response' => null | string
429     *  )
430     *
431     * @var array
432     */
433    private $_requests = array();
434
435    /**
436     * Connect timeout in milliseconds
437     * @var int
438     */
439    private $_connectTimeout = 5000;
440
441    /**
442     * Read/Write timeout in milliseconds
443     * @var int
444     */
445    private $_readWriteTimeout = 5000;
446
447    /**
448     * Data transport instance
449     * @var Transport
450     */
451    private Transport $transport;
452
453    /**
454     * Constructor
455     *
456     * @param string $host Host of the FastCGI application
457     * @param int $port Port of the FastCGI application
458     * @param Transport $transport Transport
459     */
460    public function __construct($host, $port, Transport $transport)
461    {
462        $this->_host = $host;
463        $this->_port = $port;
464
465        $this->transport = $transport;
466    }
467
468    /**
469     * Get host.
470     *
471     * @return string
472     */
473    public function getHost()
474    {
475        return $this->_host;
476    }
477
478    /**
479     * Define whether the FastCGI application should keep the connection
480     * alive at the end of a request and additionally set SO_KEEPALIVE or not.
481     *
482     * @param bool $connKeepAlive true if the connection should stay alive, false otherwise
483     * @param bool $socketKeepAlive true if the socket SO_KEEPALIVE should be set, false otherwise
484     */
485    public function setKeepAlive(bool $connKeepAlive, bool $socketKeepAlive)
486    {
487        $this->_keepAlive = $connKeepAlive;
488        $this->transport->setKeepAlive($socketKeepAlive);
489    }
490
491    /**
492     * Get the keep alive status
493     *
494     * @return bool true if the connection should stay alive, false otherwise
495     */
496    public function getKeepAlive()
497    {
498        return $this->_keepAlive;
499    }
500
501    /**
502     * Set the connect timeout
503     *
504     * @param int  number of milliseconds before connect will timeout
505     */
506    public function setConnectTimeout($timeoutMs)
507    {
508        $this->_connectTimeout = $timeoutMs;
509    }
510
511    /**
512     * Get the connect timeout
513     *
514     * @return int  number of milliseconds before connect will timeout
515     */
516    public function getConnectTimeout()
517    {
518        return $this->_connectTimeout;
519    }
520
521    /**
522     * Set the read/write timeout
523     *
524     * @param int  number of milliseconds before read or write call will timeout
525     */
526    public function setReadWriteTimeout($timeoutMs)
527    {
528        $this->_readWriteTimeout = $timeoutMs;
529        $this->transport->setDataTimeout($this->_readWriteTimeout);
530    }
531
532    /**
533     * Get the read timeout
534     *
535     * @return int  number of milliseconds before read will timeout
536     */
537    public function getReadWriteTimeout()
538    {
539        return $this->_readWriteTimeout;
540    }
541
542    /**
543     * Create a connection to the FastCGI application
544     */
545    private function connect()
546    {
547        $this->transport->connect($this->_host, $this->_port, $this->_connectTimeout);
548        $this->transport->setDataTimeout($this->_readWriteTimeout);
549    }
550
551    /**
552     * Build a FastCGI packet
553     *
554     * @param int $type Type of the packet
555     * @param string $content Content of the packet
556     * @param int $requestId RequestId
557     * @return string
558     */
559    private function buildPacket($type, $content, $requestId = 1)
560    {
561        $clen = strlen($content);
562        return chr(self::VERSION_1)         /* version */
563            . chr($type)                    /* type */
564            . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
565            . chr($requestId & 0xFF)        /* requestIdB0 */
566            . chr(($clen >> 8 ) & 0xFF)     /* contentLengthB1 */
567            . chr($clen & 0xFF)             /* contentLengthB0 */
568            . chr(0)                        /* paddingLength */
569            . chr(0)                        /* reserved */
570            . $content;                     /* content */
571    }
572
573    /**
574     * Build an FastCGI Name value pair
575     *
576     * @param string $name Name
577     * @param string $value Value
578     * @return string FastCGI Name value pair
579     */
580    private function buildNvpair($name, $value)
581    {
582        $nlen = strlen($name);
583        $vlen = strlen($value);
584        if ($nlen < 128) {
585            /* nameLengthB0 */
586            $nvpair = chr($nlen);
587        } else {
588            /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
589            $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF)
590                . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
591        }
592        if ($vlen < 128) {
593            /* valueLengthB0 */
594            $nvpair .= chr($vlen);
595        } else {
596            /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
597            $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF)
598                . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
599        }
600        /* nameData & valueData */
601        return $nvpair . $name . $value;
602    }
603
604    /**
605     * Read a set of FastCGI Name value pairs
606     *
607     * @param string $data Data containing the set of FastCGI NVPair
608     * @return array of NVPair
609     */
610    private function readNvpair($data, $length = null)
611    {
612        $array = array();
613
614        if ($length === null) {
615            $length = strlen($data);
616        }
617
618        $p = 0;
619
620        while ($p != $length) {
621
622            $nlen = ord($data[$p++]);
623            if ($nlen >= 128) {
624                $nlen = ($nlen & 0x7F << 24);
625                $nlen |= (ord($data[$p++]) << 16);
626                $nlen |= (ord($data[$p++]) << 8);
627                $nlen |= (ord($data[$p++]));
628            }
629            $vlen = ord($data[$p++]);
630            if ($vlen >= 128) {
631                $vlen = ($nlen & 0x7F << 24);
632                $vlen |= (ord($data[$p++]) << 16);
633                $vlen |= (ord($data[$p++]) << 8);
634                $vlen |= (ord($data[$p++]));
635            }
636            $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
637            $p += ($nlen + $vlen);
638        }
639
640        return $array;
641    }
642
643    /**
644     * Decode a FastCGI Packet
645     *
646     * @param string $data string containing all the packet
647     * @return array
648     */
649    private function decodePacketHeader($data)
650    {
651        $ret = array();
652        $ret['version']       = ord($data[0]);
653        $ret['type']          = ord($data[1]);
654        $ret['requestId']     = (ord($data[2]) << 8) + ord($data[3]);
655        $ret['contentLength'] = (ord($data[4]) << 8) + ord($data[5]);
656        $ret['paddingLength'] = ord($data[6]);
657        $ret['reserved']      = ord($data[7]);
658        return $ret;
659    }
660
661    /**
662     * Read a FastCGI Packet
663     *
664     * @param int $readLimit max content size
665     * @return array
666     * @throws ReadLimitExceeded
667     * @throws TransportException
668     */
669    private function readPacket($readLimit = -1)
670    {
671        if ($packet = $this->transport->read(self::HEADER_LEN)) {
672            $resp = $this->decodePacketHeader($packet);
673            $resp['content'] = '';
674            if ($resp['contentLength']) {
675                $len = $resp['contentLength'];
676                if ($readLimit >= 0 && $len > $readLimit) {
677                    // close connection so it can be re-set reset and throw an error
678                    $this->transport->close();
679                    throw new ReadLimitExceeded("Content has $len bytes but the limit is $readLimit bytes");
680                }
681                while ($len && $buf = $this->transport->read($len)) {
682                    $len -= strlen($buf);
683                    $resp['content'] .= $buf;
684                }
685            }
686            if ($resp['paddingLength']) {
687                $this->transport->read($resp['paddingLength']);
688            }
689            return $resp;
690        } else {
691            return false;
692        }
693    }
694
695    /**
696     * Get Information on the FastCGI application
697     *
698     * @param array $requestedInfo information to retrieve
699     * @return array
700     * @throws \Exception
701     */
702    public function getValues(array $requestedInfo)
703    {
704        $this->connect();
705
706        $request = '';
707        foreach ($requestedInfo as $info) {
708            $request .= $this->buildNvpair($info, '');
709        }
710        $this->transport->write($this->buildPacket(self::GET_VALUES, $request, 0));
711
712        $resp = $this->readPacket();
713        if (isset($resp['type']) && $resp['type'] == self::GET_VALUES_RESULT) {
714            return $this->readNvpair($resp['content'], $resp['contentLength']);
715        } else {
716            throw new \Exception('Unexpected response type, expecting GET_VALUES_RESULT');
717        }
718    }
719
720    /**
721     * Execute a request to the FastCGI application and return response body
722     *
723     * @param array $params Array of parameters
724     * @param string $stdin Content
725     * @return string
726     * @throws ForbiddenException
727     * @throws TimedOutException
728     * @throws \Exception
729     */
730    public function request(array $params, $stdin)
731    {
732        $id = $this->async_request($params, $stdin);
733        return $this->wait_for_response($id);
734    }
735
736    /**
737     * Execute a request to the FastCGI application and return request data
738     *
739     * @param array $params Array of parameters
740     * @param string $stdin Content
741     * @param int $readLimit [optional] the number of bytes to accept in a single packet or -1 if unlimited
742     * @param int $writeDelayMs Number of milliseconds to wait before write
743     * @return array
744     * @throws ForbiddenException
745     * @throws TimedOutException
746     * @throws \Exception
747     */
748    public function request_data(array $params, $stdin, int $readLimit = -1, int $timoutMs = 0, int $writeDelayMs = 0)
749    {
750        $id = $this->async_request($params, $stdin, $writeDelayMs);
751        return $this->wait_for_response_data($id, $timoutMs, $readLimit);
752    }
753
754    /**
755     * Execute a request to the FastCGI application asynchronously
756     *
757     * This sends request to application and returns the assigned ID for that request.
758     *
759     * You should keep this id for later use with wait_for_response(). Ids are chosen randomly
760     * rather than sequentially to guard against false-positives when using persistent sockets.
761     * In that case it is possible that a delayed response to a request made by a previous script
762     * invocation comes back on this socket and is mistaken for response to request made with same
763     * ID during this request.
764     *
765     * @param array $params Array of parameters
766     * @param string $stdin Content
767     * @param int $writeDelayMs Number of milliseconds to wait before write
768     * @return int
769     * @throws TimedOutException
770     * @throws \Exception
771     */
772    public function async_request(array $params, $stdin, int $writeDelayMs = 0)
773    {
774        $this->connect();
775
776        // Pick random number between 1 and max 16 bit unsigned int 65535
777        $id = mt_rand(1, (1 << 16) - 1);
778
779        // Using persistent sockets implies you want them kept alive by server!
780        $keepAlive = intval($this->_keepAlive);
781
782        $request = $this->buildPacket(
783            self::BEGIN_REQUEST,
784            chr(0) . chr(self::RESPONDER) . chr($keepAlive)
785            . str_repeat(chr(0), 5),
786            $id
787        );
788
789        $paramsRequest = '';
790        foreach ($params as $key => $value) {
791            $paramsRequest .= $this->buildNvpair($key, $value, $id);
792        }
793        if ($paramsRequest) {
794            $request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id);
795        }
796        $request .= $this->buildPacket(self::PARAMS, '', $id);
797
798        if ($stdin) {
799            $request .= $this->buildPacket(self::STDIN, $stdin, $id);
800        }
801        $request .= $this->buildPacket(self::STDIN, '', $id);
802
803        if ($writeDelayMs > 0) {
804            usleep($writeDelayMs * 1000);
805        }
806
807        if ($this->transport->write($request) === false || $this->transport->flush() === false) {
808
809            $info = $this->transport->getMetaData();
810
811            if (!empty($info) && $info['timed_out']) {
812                throw new TimedOutException('Write timed out');
813            }
814
815            // Broken pipe, tear down so future requests might succeed
816            $this->transport->close();
817            throw new \Exception('Failed to write request to socket');
818        }
819
820        $this->_requests[$id] = array(
821            'state' => self::REQ_STATE_WRITTEN,
822            'response' => null,
823            'err_response' => null,
824            'out_response' => null,
825        );
826
827        return $id;
828    }
829
830    /**
831     * Append response data.
832     *
833     * @param $resp Response
834     * @param $type Either err or our
835     *
836     * @throws \Exception
837     */
838    private function fcgi_stream_append($resp, $type) {
839        if (isset($this->_requests[$resp['requestId']][$type . '_finished'])) {
840            throw new \Exception('FCGI_STD' . strtoupper($type) . ' stream already finished by empty record');
841        }
842        if ($resp['content'] === '') {
843            $this->_requests[$resp['requestId']][$type . '_finished'] = true;
844        } else {
845            $this->_requests[$resp['requestId']][$type . '_response'] .= $resp['content'];
846        }
847    }
848
849    /**
850     * Blocking call that waits for response data of the specific request
851     *
852     * @param int $requestId
853     * @param int $timeoutMs [optional] the number of milliseconds to wait.
854     * @param int $readLimit [optional] the number of bytes to accept in a single packet or -1 if unlimited
855     * @return array response data
856     * @throws ForbiddenException
857     * @throws TimedOutException
858     * @throws \Exception
859     */
860    public function wait_for_response_data($requestId, $timeoutMs = 0, $readLimit = -1)
861    {
862        if (!isset($this->_requests[$requestId])) {
863            throw new \Exception('Invalid request id given');
864        }
865
866        // If we already read the response during an earlier call for different id, just return it
867        if ($this->_requests[$requestId]['state'] == self::REQ_STATE_OK
868            || $this->_requests[$requestId]['state'] == self::REQ_STATE_ERR
869            ) {
870            return $this->_requests[$requestId]['response'];
871        }
872
873        if ($timeoutMs > 0) {
874            // Reset timeout on socket for now
875            $this->transport->setDataTimeout($timeoutMs);
876        } else {
877            $timeoutMs = $this->_readWriteTimeout;
878        }
879
880        // Need to manually check since we might do several reads none of which timeout themselves
881        // but still not get the response requested
882        $startTime = microtime(true);
883
884        while ($resp = $this->readPacket($readLimit)) {
885            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
886                if ($resp['type'] == self::STDERR) {
887                    $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR;
888                    $this->fcgi_stream_append($resp, 'err');
889                } else {
890                    $this->fcgi_stream_append($resp, 'out');
891                }
892                $this->_requests[$resp['requestId']]['response'] .= $resp['content'];
893            } elseif ($resp['type'] == self::END_REQUEST) {
894                $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK;
895                if ($resp['requestId'] == $requestId) {
896                    break;
897                }
898            }
899            if (microtime(true) - $startTime >= ($timeoutMs * 1000)) {
900                // Reset
901                $this->transport->setDataTimeout($this->_readWriteTimeout);
902                throw new \Exception('Timed out');
903            }
904        }
905
906        if (!is_array($resp)) {
907            $info = $this->transport->getMetaData();
908
909            // We must reset timeout but it must be AFTER we get info
910            $this->transport->setDataTimeout($this->_readWriteTimeout);
911
912            if (!empty($info)) {
913                if ($info['timed_out']) {
914                    throw new TimedOutException( 'Read timed out' );
915                }
916
917                if ($info['unread_bytes'] == 0 && $info['blocked'] && $info['eof']) {
918                    throw new ForbiddenException( 'Not in white list. Check listen.allowed_clients.' );
919                }
920            }
921
922            throw new \Exception('Read failed');
923        }
924
925        // Reset timeout
926        $this->transport->setDataTimeout($this->_readWriteTimeout);
927
928        switch (ord($resp['content'][4])) {
929            case self::CANT_MPX_CONN:
930                throw new \Exception('This app can\'t multiplex [CANT_MPX_CONN]');
931                break;
932            case self::OVERLOADED:
933                throw new \Exception('New request rejected; too busy [OVERLOADED]');
934                break;
935            case self::UNKNOWN_ROLE:
936                throw new \Exception('Role value not known [UNKNOWN_ROLE]');
937                break;
938            case self::REQUEST_COMPLETE:
939                return $this->_requests[$requestId];
940        }
941    }
942
943    /**
944     * Blocking call that waits for response to specific request
945     *
946     * @param int $requestId
947     * @param int $timeoutMs [optional] the number of milliseconds to wait.
948     * @return string The response content.
949     * @throws ForbiddenException
950     * @throws TimedOutException
951     * @throws \Exception
952     */
953    public function wait_for_response($requestId, $timeoutMs = 0)
954    {
955        return $this->wait_for_response_data($requestId, $timeoutMs)['response'];
956    }
957}
958