xref: /PHP-7.0/sapi/fpm/tests/fcgi.inc (revision a740f9e7)
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 Adoy\FastCGI;
26
27class TimedOutException extends \Exception {}
28class ForbiddenException extends \Exception {}
29
30/**
31 * Handles communication with a FastCGI application
32 *
33 * @author      Pierrick Charron <pierrick@adoy.net>
34 * @version     1.0
35 */
36class Client
37{
38    const VERSION_1            = 1;
39
40    const BEGIN_REQUEST        = 1;
41    const ABORT_REQUEST        = 2;
42    const END_REQUEST          = 3;
43    const PARAMS               = 4;
44    const STDIN                = 5;
45    const STDOUT               = 6;
46    const STDERR               = 7;
47    const DATA                 = 8;
48    const GET_VALUES           = 9;
49    const GET_VALUES_RESULT    = 10;
50    const UNKNOWN_TYPE         = 11;
51    const MAXTYPE              = self::UNKNOWN_TYPE;
52
53    const RESPONDER            = 1;
54    const AUTHORIZER           = 2;
55    const FILTER               = 3;
56
57    const REQUEST_COMPLETE     = 0;
58    const CANT_MPX_CONN        = 1;
59    const OVERLOADED           = 2;
60    const UNKNOWN_ROLE         = 3;
61
62    const MAX_CONNS            = 'MAX_CONNS';
63    const MAX_REQS             = 'MAX_REQS';
64    const MPXS_CONNS           = 'MPXS_CONNS';
65
66    const HEADER_LEN           = 8;
67
68    const REQ_STATE_WRITTEN    = 1;
69    const REQ_STATE_OK         = 2;
70    const REQ_STATE_ERR        = 3;
71    const REQ_STATE_TIMED_OUT  = 4;
72
73    /**
74     * Socket
75     * @var Resource
76     */
77    private $_sock = null;
78
79    /**
80     * Host
81     * @var String
82     */
83    private $_host = null;
84
85    /**
86     * Port
87     * @var Integer
88     */
89    private $_port = null;
90
91    /**
92     * Keep Alive
93     * @var Boolean
94     */
95    private $_keepAlive = false;
96
97    /**
98     * Outstanding request statuses keyed by request id
99     *
100     * Each request is an array with following form:
101     *
102     *  array(
103     *    'state' => REQ_STATE_*
104     *    'response' => null | string
105     *  )
106     *
107     * @var array
108     */
109    private $_requests = array();
110
111    /**
112     * Use persistent sockets to connect to backend
113     * @var Boolean
114     */
115    private $_persistentSocket = false;
116
117    /**
118     * Connect timeout in milliseconds
119     * @var Integer
120     */
121    private $_connectTimeout = 5000;
122
123    /**
124     * Read/Write timeout in milliseconds
125     * @var Integer
126     */
127    private $_readWriteTimeout = 5000;
128
129    /**
130     * Constructor
131     *
132     * @param String $host Host of the FastCGI application
133     * @param Integer $port Port of the FastCGI application
134     */
135    public function __construct($host, $port)
136    {
137        $this->_host = $host;
138        $this->_port = $port;
139    }
140
141    /**
142     * Define whether or not the FastCGI application should keep the connection
143     * alive at the end of a request
144     *
145     * @param Boolean $b true if the connection should stay alive, false otherwise
146     */
147    public function setKeepAlive($b)
148    {
149        $this->_keepAlive = (boolean)$b;
150        if (!$this->_keepAlive && $this->_sock) {
151            fclose($this->_sock);
152        }
153    }
154
155    /**
156     * Get the keep alive status
157     *
158     * @return Boolean true if the connection should stay alive, false otherwise
159     */
160    public function getKeepAlive()
161    {
162        return $this->_keepAlive;
163    }
164
165    /**
166     * Define whether or not PHP should attempt to re-use sockets opened by previous
167     * request for efficiency
168     *
169     * @param Boolean $b true if persistent socket should be used, false otherwise
170     */
171    public function setPersistentSocket($b)
172    {
173        $was_persistent = ($this->_sock && $this->_persistentSocket);
174        $this->_persistentSocket = (boolean)$b;
175        if (!$this->_persistentSocket && $was_persistent) {
176            fclose($this->_sock);
177        }
178    }
179
180    /**
181     * Get the pesistent socket status
182     *
183     * @return Boolean true if the socket should be persistent, false otherwise
184     */
185    public function getPersistentSocket()
186    {
187        return $this->_persistentSocket;
188    }
189
190
191    /**
192     * Set the connect timeout
193     *
194     * @param Integer  number of milliseconds before connect will timeout
195     */
196    public function setConnectTimeout($timeoutMs)
197    {
198        $this->_connectTimeout = $timeoutMs;
199    }
200
201    /**
202     * Get the connect timeout
203     *
204     * @return Integer  number of milliseconds before connect will timeout
205     */
206    public function getConnectTimeout()
207    {
208        return $this->_connectTimeout;
209    }
210
211    /**
212     * Set the read/write timeout
213     *
214     * @param Integer  number of milliseconds before read or write call will timeout
215     */
216    public function setReadWriteTimeout($timeoutMs)
217    {
218        $this->_readWriteTimeout = $timeoutMs;
219        $this->set_ms_timeout($this->_readWriteTimeout);
220    }
221
222    /**
223     * Get the read timeout
224     *
225     * @return Integer  number of milliseconds before read will timeout
226     */
227    public function getReadWriteTimeout()
228    {
229        return $this->_readWriteTimeout;
230    }
231
232    /**
233     * Helper to avoid duplicating milliseconds to secs/usecs in a few places
234     *
235     * @param Integer millisecond timeout
236     * @return Boolean
237     */
238    private function set_ms_timeout($timeoutMs) {
239        if (!$this->_sock) {
240            return false;
241        }
242        return stream_set_timeout($this->_sock, floor($timeoutMs / 1000), ($timeoutMs % 1000) * 1000);
243    }
244
245
246    /**
247     * Create a connection to the FastCGI application
248     */
249    private function connect()
250    {
251        if (!$this->_sock) {
252            if ($this->_persistentSocket) {
253                $this->_sock = pfsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000);
254            } else {
255                $this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000);
256            }
257
258            if (!$this->_sock) {
259                throw new \Exception('Unable to connect to FastCGI application: ' . $errstr);
260            }
261
262            if (!$this->set_ms_timeout($this->_readWriteTimeout)) {
263                throw new \Exception('Unable to set timeout on socket');
264            }
265        }
266    }
267
268    /**
269     * Build a FastCGI packet
270     *
271     * @param Integer $type Type of the packet
272     * @param String $content Content of the packet
273     * @param Integer $requestId RequestId
274     */
275    private function buildPacket($type, $content, $requestId = 1)
276    {
277        $clen = strlen($content);
278        return chr(self::VERSION_1)         /* version */
279            . chr($type)                    /* type */
280            . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
281            . chr($requestId & 0xFF)        /* requestIdB0 */
282            . chr(($clen >> 8 ) & 0xFF)     /* contentLengthB1 */
283            . chr($clen & 0xFF)             /* contentLengthB0 */
284            . chr(0)                        /* paddingLength */
285            . chr(0)                        /* reserved */
286            . $content;                     /* content */
287    }
288
289    /**
290     * Build an FastCGI Name value pair
291     *
292     * @param String $name Name
293     * @param String $value Value
294     * @return String FastCGI Name value pair
295     */
296    private function buildNvpair($name, $value)
297    {
298        $nlen = strlen($name);
299        $vlen = strlen($value);
300        if ($nlen < 128) {
301            /* nameLengthB0 */
302            $nvpair = chr($nlen);
303        } else {
304            /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
305            $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
306        }
307        if ($vlen < 128) {
308            /* valueLengthB0 */
309            $nvpair .= chr($vlen);
310        } else {
311            /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
312            $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
313        }
314        /* nameData & valueData */
315        return $nvpair . $name . $value;
316    }
317
318    /**
319     * Read a set of FastCGI Name value pairs
320     *
321     * @param String $data Data containing the set of FastCGI NVPair
322     * @return array of NVPair
323     */
324    private function readNvpair($data, $length = null)
325    {
326        $array = array();
327
328        if ($length === null) {
329            $length = strlen($data);
330        }
331
332        $p = 0;
333
334        while ($p != $length) {
335
336            $nlen = ord($data{$p++});
337            if ($nlen >= 128) {
338                $nlen = ($nlen & 0x7F << 24);
339                $nlen |= (ord($data{$p++}) << 16);
340                $nlen |= (ord($data{$p++}) << 8);
341                $nlen |= (ord($data{$p++}));
342            }
343            $vlen = ord($data{$p++});
344            if ($vlen >= 128) {
345                $vlen = ($nlen & 0x7F << 24);
346                $vlen |= (ord($data{$p++}) << 16);
347                $vlen |= (ord($data{$p++}) << 8);
348                $vlen |= (ord($data{$p++}));
349            }
350            $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
351            $p += ($nlen + $vlen);
352        }
353
354        return $array;
355    }
356
357    /**
358     * Decode a FastCGI Packet
359     *
360     * @param String $data String containing all the packet
361     * @return array
362     */
363    private function decodePacketHeader($data)
364    {
365        $ret = array();
366        $ret['version']       = ord($data{0});
367        $ret['type']          = ord($data{1});
368        $ret['requestId']     = (ord($data{2}) << 8) + ord($data{3});
369        $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
370        $ret['paddingLength'] = ord($data{6});
371        $ret['reserved']      = ord($data{7});
372        return $ret;
373    }
374
375    /**
376     * Read a FastCGI Packet
377     *
378     * @return array
379     */
380    private function readPacket()
381    {
382        if ($packet = fread($this->_sock, self::HEADER_LEN)) {
383            $resp = $this->decodePacketHeader($packet);
384            $resp['content'] = '';
385            if ($resp['contentLength']) {
386                $len  = $resp['contentLength'];
387                while ($len && $buf=fread($this->_sock, $len)) {
388                    $len -= strlen($buf);
389                    $resp['content'] .= $buf;
390                }
391            }
392            if ($resp['paddingLength']) {
393                $buf = fread($this->_sock, $resp['paddingLength']);
394            }
395            return $resp;
396        } else {
397            return false;
398        }
399    }
400
401    /**
402     * Get Informations on the FastCGI application
403     *
404     * @param array $requestedInfo information to retrieve
405     * @return array
406     */
407    public function getValues(array $requestedInfo)
408    {
409        $this->connect();
410
411        $request = '';
412        foreach ($requestedInfo as $info) {
413            $request .= $this->buildNvpair($info, '');
414        }
415        fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
416
417        $resp = $this->readPacket();
418        if ($resp['type'] == self::GET_VALUES_RESULT) {
419            return $this->readNvpair($resp['content'], $resp['length']);
420        } else {
421            throw new \Exception('Unexpected response type, expecting GET_VALUES_RESULT');
422        }
423    }
424
425    /**
426     * Execute a request to the FastCGI application
427     *
428     * @param array $params Array of parameters
429     * @param String $stdin Content
430     * @return String
431     */
432    public function request(array $params, $stdin)
433    {
434        $id = $this->async_request($params, $stdin);
435        return $this->wait_for_response($id);
436    }
437
438    /**
439     * Execute a request to the FastCGI application asyncronously
440     *
441     * This sends request to application and returns the assigned ID for that request.
442     *
443     * You should keep this id for later use with wait_for_response(). Ids are chosen randomly
444     * rather than seqentially to guard against false-positives when using persistent sockets.
445     * In that case it is possible that a delayed response to a request made by a previous script
446     * invocation comes back on this socket and is mistaken for response to request made with same ID
447     * during this request.
448     *
449     * @param array $params Array of parameters
450     * @param String $stdin Content
451     * @return Integer
452     */
453    public function async_request(array $params, $stdin)
454    {
455        $this->connect();
456
457        // Pick random number between 1 and max 16 bit unsigned int 65535
458        $id = mt_rand(1, (1 << 16) - 1);
459
460        // Using persistent sockets implies you want them keept alive by server!
461        $keepAlive = intval($this->_keepAlive || $this->_persistentSocket);
462
463        $request = $this->buildPacket(self::BEGIN_REQUEST
464                                     ,chr(0) . chr(self::RESPONDER) . chr($keepAlive) . str_repeat(chr(0), 5)
465                                     ,$id
466                                     );
467
468        $paramsRequest = '';
469        foreach ($params as $key => $value) {
470            $paramsRequest .= $this->buildNvpair($key, $value, $id);
471        }
472        if ($paramsRequest) {
473            $request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id);
474        }
475        $request .= $this->buildPacket(self::PARAMS, '', $id);
476
477        if ($stdin) {
478            $request .= $this->buildPacket(self::STDIN, $stdin, $id);
479        }
480        $request .= $this->buildPacket(self::STDIN, '', $id);
481
482        if (fwrite($this->_sock, $request) === false || fflush($this->_sock) === false) {
483
484            $info = stream_get_meta_data($this->_sock);
485
486            if ($info['timed_out']) {
487                throw new TimedOutException('Write timed out');
488            }
489
490            // Broken pipe, tear down so future requests might succeed
491            fclose($this->_sock);
492            throw new \Exception('Failed to write request to socket');
493        }
494
495        $this->_requests[$id] = array(
496            'state' => self::REQ_STATE_WRITTEN,
497            'response' => null
498        );
499
500        return $id;
501    }
502
503    /**
504     * Blocking call that waits for response to specific request
505     *
506     * @param Integer $requestId
507     * @param Integer $timeoutMs [optional] the number of milliseconds to wait. Defaults to the ReadWriteTimeout value set.
508     * @return string  response body
509     */
510    public function wait_for_response($requestId, $timeoutMs = 0) {
511
512        if (!isset($this->_requests[$requestId])) {
513            throw new \Exception('Invalid request id given');
514        }
515
516        // If we already read the response during an earlier call for different id, just return it
517        if ($this->_requests[$requestId]['state'] == self::REQ_STATE_OK
518            || $this->_requests[$requestId]['state'] == self::REQ_STATE_ERR
519            ) {
520            return $this->_requests[$requestId]['response'];
521        }
522
523        if ($timeoutMs > 0) {
524            // Reset timeout on socket for now
525            $this->set_ms_timeout($timeoutMs);
526        } else {
527            $timeoutMs = $this->_readWriteTimeout;
528        }
529
530        // Need to manually check since we might do several reads none of which timeout themselves
531        // but still not get the response requested
532        $startTime = microtime(true);
533
534        do {
535            $resp = $this->readPacket();
536
537            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
538                if ($resp['type'] == self::STDERR) {
539                    $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR;
540                }
541                $this->_requests[$resp['requestId']]['response'] .= $resp['content'];
542            }
543            if ($resp['type'] == self::END_REQUEST) {
544                $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK;
545                if ($resp['requestId'] == $requestId) {
546                    break;
547                }
548            }
549            if (microtime(true) - $startTime >= ($timeoutMs * 1000)) {
550                // Reset
551                $this->set_ms_timeout($this->_readWriteTimeout);
552                throw new \Exception('Timed out');
553            }
554        } while ($resp);
555
556        if (!is_array($resp)) {
557            $info = stream_get_meta_data($this->_sock);
558
559            // We must reset timeout but it must be AFTER we get info
560            $this->set_ms_timeout($this->_readWriteTimeout);
561
562            if ($info['timed_out']) {
563                throw new TimedOutException('Read timed out');
564            }
565
566            if ($info['unread_bytes'] == 0
567                    && $info['blocked']
568                    && $info['eof']) {
569                throw new ForbiddenException('Not in white list. Check listen.allowed_clients.');
570            }
571
572            throw new \Exception('Read failed');
573        }
574
575        // Reset timeout
576        $this->set_ms_timeout($this->_readWriteTimeout);
577
578        switch (ord($resp['content']{4})) {
579            case self::CANT_MPX_CONN:
580                throw new \Exception('This app can\'t multiplex [CANT_MPX_CONN]');
581                break;
582            case self::OVERLOADED:
583                throw new \Exception('New request rejected; too busy [OVERLOADED]');
584                break;
585            case self::UNKNOWN_ROLE:
586                throw new \Exception('Role value not known [UNKNOWN_ROLE]');
587                break;
588            case self::REQUEST_COMPLETE:
589                return $this->_requests[$requestId]['response'];
590        }
591    }
592}
593