diff options
-rw-r--r-- | sapi/fpm/tests/002.phpt | 5 | ||||
-rw-r--r-- | sapi/fpm/tests/003.phpt | 5 | ||||
-rw-r--r-- | sapi/fpm/tests/005.phpt | 58 | ||||
-rw-r--r-- | sapi/fpm/tests/006.phpt | 58 | ||||
-rw-r--r-- | sapi/fpm/tests/007.phpt | 69 | ||||
-rw-r--r-- | sapi/fpm/tests/008.phpt | 85 | ||||
-rw-r--r-- | sapi/fpm/tests/009.phpt | 54 | ||||
-rw-r--r-- | sapi/fpm/tests/010.phpt | 85 | ||||
-rw-r--r-- | sapi/fpm/tests/011.phpt | 54 | ||||
-rw-r--r-- | sapi/fpm/tests/fcgi.inc | 592 | ||||
-rw-r--r-- | sapi/fpm/tests/include.inc | 24 |
11 files changed, 1082 insertions, 7 deletions
diff --git a/sapi/fpm/tests/002.phpt b/sapi/fpm/tests/002.phpt index ce878661ea..2ef6cedc38 100644 --- a/sapi/fpm/tests/002.phpt +++ b/sapi/fpm/tests/002.phpt @@ -8,13 +8,12 @@ FPM: Startup and connect include "include.inc"; $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; -$port = 9000+PHP_INT_SIZE; $cfg = <<<EOT [global] error_log = $logfile [unconfined] -listen = 127.0.0.1:$port +listen = 127.0.0.1:9000 pm = dynamic pm.max_children = 5 pm.start_servers = 2 @@ -27,7 +26,7 @@ if (is_resource($fpm)) { var_dump(fgets($tail)); var_dump(fgets($tail)); $i = 0; - while (($i++ < 30) && !($fp = @fsockopen('127.0.0.1', $port))) { + while (($i++ < 30) && !($fp = @fsockopen('127.0.0.1', 9000))) { usleep(10000); } if ($fp) { diff --git a/sapi/fpm/tests/003.phpt b/sapi/fpm/tests/003.phpt index 0ce1bd2828..389cb2401e 100644 --- a/sapi/fpm/tests/003.phpt +++ b/sapi/fpm/tests/003.phpt @@ -8,13 +8,12 @@ FPM: Test IPv6 support include "include.inc"; $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; -$port = 9000+PHP_INT_SIZE; $cfg = <<<EOT [global] error_log = $logfile [unconfined] -listen = [::1]:$port +listen = [::1]:9000 pm = dynamic pm.max_children = 5 pm.start_servers = 2 @@ -27,7 +26,7 @@ if (is_resource($fpm)) { var_dump(fgets($tail)); var_dump(fgets($tail)); $i = 0; - while (($i++ < 30) && !($fp = fsockopen('[::1]', $port))) { + while (($i++ < 30) && !($fp = fsockopen('[::1]', 9000))) { usleep(10000); } if ($fp) { diff --git a/sapi/fpm/tests/005.phpt b/sapi/fpm/tests/005.phpt new file mode 100644 index 0000000000..992b01f08e --- /dev/null +++ b/sapi/fpm/tests/005.phpt @@ -0,0 +1,58 @@ +--TEST-- +FPM: Test IPv4 allowed clients +--SKIPIF-- +<?php include "skipif.inc"; ?> +--FILE-- +<?php + +include "include.inc"; + +$logfile = dirname(__FILE__).'/php-fpm.log.tmp'; +$port = 9000+PHP_INT_SIZE; + +$cfg = <<<EOT +[global] +error_log = $logfile +[unconfined] +listen = [::]:$port +listen.allowed_clients = 127.0.0.1 +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +EOT; + +$fpm = run_fpm($cfg, $tail); +if (is_resource($fpm)) { + echo fgets($tail); + echo fgets($tail); + try { + run_request('127.0.0.1', $port); + echo "IPv4 ok\n"; + } catch (Exception $e) { + echo "IPv4 error\n"; + } + try { + run_request('[::1]', $port); + echo "IPv6 ok\n"; + } catch (Exception $e) { + echo "IPv6 error\n"; + } + proc_terminate($fpm); + stream_get_contents($tail); + fclose($tail); + proc_close($fpm); +} + +?> +--EXPECTF-- +[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d +[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections +IPv4 ok +IPv6 error +--CLEAN-- +<?php + $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; + @unlink($logfile); +?> diff --git a/sapi/fpm/tests/006.phpt b/sapi/fpm/tests/006.phpt new file mode 100644 index 0000000000..d8b47ee533 --- /dev/null +++ b/sapi/fpm/tests/006.phpt @@ -0,0 +1,58 @@ +--TEST-- +FPM: Test IPv6 allowed clients (bug #68428) +--SKIPIF-- +<?php include "skipif.inc"; ?> +--FILE-- +<?php + +include "include.inc"; + +$logfile = dirname(__FILE__).'/php-fpm.log.tmp'; +$port = 9000+PHP_INT_SIZE; + +$cfg = <<<EOT +[global] +error_log = $logfile +[unconfined] +listen = [::]:$port +listen.allowed_clients = ::1 +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +EOT; + +$fpm = run_fpm($cfg, $tail); +if (is_resource($fpm)) { + echo fgets($tail); + echo fgets($tail); + try { + run_request('127.0.0.1', $port); + echo "IPv4 ok\n"; + } catch (Exception $e) { + echo "IPv4 error\n"; + } + try { + run_request('[::1]', $port); + echo "IPv6 ok\n"; + } catch (Exception $e) { + echo "IPv6 error\n"; + } + proc_terminate($fpm); + stream_get_contents($tail); + fclose($tail); + proc_close($fpm); +} + +?> +--EXPECTF-- +[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d +[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections +IPv4 error +IPv6 ok +--CLEAN-- +<?php + $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; + @unlink($logfile); +?> diff --git a/sapi/fpm/tests/007.phpt b/sapi/fpm/tests/007.phpt new file mode 100644 index 0000000000..18893819e2 --- /dev/null +++ b/sapi/fpm/tests/007.phpt @@ -0,0 +1,69 @@ +--TEST-- +FPM: Test IPv6 all addresses and access_log (bug #68421) +--SKIPIF-- +<?php include "skipif.inc"; ?> +--FILE-- +<?php + +include "include.inc"; + +$logfile = dirname(__FILE__).'/php-fpm.log.tmp'; +$accfile = dirname(__FILE__).'/php-fpm.acc.tmp'; +$port = 9000+PHP_INT_SIZE; + +$cfg = <<<EOT +[global] +error_log = $logfile +[unconfined] +listen = [::]:$port +access.log = $accfile +ping.path = /ping +ping.response = pong +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +EOT; + +$fpm = run_fpm($cfg, $tail); +if (is_resource($fpm)) { + echo fgets($tail); + echo fgets($tail); + try { + var_dump(strpos(run_request('127.0.0.1', $port), 'pong')); + echo "IPv4 ok\n"; + } catch (Exception $e) { + echo "IPv4 error\n"; + } + try { + var_dump(strpos(run_request('[::1]', $port), 'pong')); + echo "IPv6 ok\n"; + } catch (Exception $e) { + echo "IPv6 error\n"; + } + proc_terminate($fpm); + stream_get_contents($tail); + fclose($tail); + proc_close($fpm); + + echo file_get_contents($accfile); +} + +?> +--EXPECTF-- +[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d +[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections +int(%d) +IPv4 ok +int(%d) +IPv6 ok +127.0.0.1 %s "GET /ping" 200 +::1 %s "GET /ping" 200 +--CLEAN-- +<?php + $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; + @unlink($logfile); + $accfile = dirname(__FILE__).'/php-fpm.acc.tmp'; + @unlink($accfile); +?> diff --git a/sapi/fpm/tests/008.phpt b/sapi/fpm/tests/008.phpt new file mode 100644 index 0000000000..aaa773093c --- /dev/null +++ b/sapi/fpm/tests/008.phpt @@ -0,0 +1,85 @@ +--TEST-- +FPM: Test multi pool (dynamic + ondemand + static) (bug #68423) +--SKIPIF-- +<?php include "skipif.inc"; ?> +--FILE-- +<?php + +include "include.inc"; + +$logfile = dirname(__FILE__).'/php-fpm.log.tmp'; +$port1 = 9000+PHP_INT_SIZE; +$port2 = 9001+PHP_INT_SIZE; +$port3 = 9002+PHP_INT_SIZE; + +$cfg = <<<EOT +[global] +error_log = $logfile +[pool_dynamic] +listen = 127.0.0.1:$port1 +ping.path = /ping +ping.response = pong-dynamic +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +[poold_ondemand] +listen = 127.0.0.1:$port2 +ping.path = /ping +ping.response = pong-on-demand +pm = ondemand +pm.max_children = 2 +pm.process_idle_timeout = 10 +[pool_static] +listen = 127.0.0.1:$port3 +ping.path = /ping +ping.response = pong-static +pm = static +pm.max_children = 2 +EOT; + +$fpm = run_fpm($cfg, $tail); +if (is_resource($fpm)) { + echo fgets($tail); + echo fgets($tail); + try { + var_dump(strpos(run_request('127.0.0.1', $port1), 'pong-dynamic')); + echo "Dynamic ok\n"; + } catch (Exception $e) { + echo "Dynamic error\n"; + } + try { + var_dump(strpos(run_request('127.0.0.1', $port2), 'pong-on-demand')); + echo "OnDemand ok\n"; + } catch (Exception $e) { + echo "OnDemand error\n"; + } + try { + var_dump(strpos(run_request('127.0.0.1', $port3), 'pong-static')); + echo "Static ok\n"; + } catch (Exception $e) { + echo "Static error\n"; + } + + proc_terminate($fpm); + stream_get_contents($tail); + fclose($tail); + proc_close($fpm); +} + +?> +--EXPECTF-- +[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d +[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections +int(%d) +Dynamic ok +int(%d) +OnDemand ok +int(%d) +Static ok +--CLEAN-- +<?php + $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; + @unlink($logfile); +?> diff --git a/sapi/fpm/tests/009.phpt b/sapi/fpm/tests/009.phpt new file mode 100644 index 0000000000..836fb19323 --- /dev/null +++ b/sapi/fpm/tests/009.phpt @@ -0,0 +1,54 @@ +--TEST-- +FPM: Test Unix Domain Socket +--SKIPIF-- +<?php include "skipif.inc"; ?> +--FILE-- +<?php + +include "include.inc"; + +$logfile = dirname(__FILE__).'/php-fpm.log.tmp'; +$socket = dirname(__FILE__).'/php-fpm.sock'; + +$cfg = <<<EOT +[global] +error_log = $logfile +[unconfined] +listen = $socket +ping.path = /ping +ping.response = pong +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +EOT; + +$fpm = run_fpm($cfg, $tail); +if (is_resource($fpm)) { + echo fgets($tail); + echo fgets($tail); + try { + var_dump(strpos(run_request('unix://'.$socket, -1), 'pong')); + echo "UDS ok\n"; + } catch (Exception $e) { + echo "UDS error\n"; + } + + proc_terminate($fpm); + stream_get_contents($tail); + fclose($tail); + proc_close($fpm); +} + +?> +--EXPECTF-- +[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d +[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections +int(%d) +UDS ok +--CLEAN-- +<?php + $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; + @unlink($logfile); +?> diff --git a/sapi/fpm/tests/010.phpt b/sapi/fpm/tests/010.phpt new file mode 100644 index 0000000000..dc8d5bb949 --- /dev/null +++ b/sapi/fpm/tests/010.phpt @@ -0,0 +1,85 @@ +--TEST-- +FPM: Test status page +--SKIPIF-- +<?php include "skipif.inc"; ?> +--FILE-- +<?php + +include "include.inc"; + +$logfile = dirname(__FILE__).'/php-fpm.log.tmp'; +$port = 9000+PHP_INT_SIZE; + +$cfg = <<<EOT +[global] +error_log = $logfile +[unconfined] +listen = 127.0.0.1:$port +pm.status_path = /status +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +EOT; + +$fpm = run_fpm($cfg, $tail); +if (is_resource($fpm)) { + echo fgets($tail); + echo fgets($tail); + try { + echo run_request('127.0.0.1', $port, '/status'); + + $html = run_request('127.0.0.1', $port, '/status', 'html'); + var_dump(strpos($html, 'text/html') && strpos($html, 'DOCTYPE') && strpos($html, 'PHP-FPM Status Page')); + + $json = run_request('127.0.0.1', $port, '/status', 'json'); + var_dump(strpos($json, 'application/json') && strpos($json, '"pool":"unconfined"')); + + $xml = run_request('127.0.0.1', $port, '/status', 'xml'); + var_dump(strpos($xml, 'text/xml') && strpos($xml, '<?xml')); + + echo "IPv4 ok\n"; + } catch (Exception $e) { + echo "IPv4 error\n"; + } + + proc_terminate($fpm); + stream_get_contents($tail); + fclose($tail); + proc_close($fpm); +} + +?> +--EXPECTF-- +[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d +[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections +X-Powered-By: PHP/%s +Expires: %s +Cache-Control: %s +Content-type: text/plain%s + +pool: unconfined +process manager: dynamic +start time: %s +start since: %d +accepted conn: 1 +listen queue: 0 +max listen queue: 0 +listen queue len: %d +idle processes: 1 +active processes: 1 +total processes: 2 +max active processes: 1 +max children reached: 0 +slow requests: 0 + +bool(true) +bool(true) +bool(true) +IPv4 ok +--CLEAN-- +<?php + $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; + @unlink($logfile); +?> diff --git a/sapi/fpm/tests/011.phpt b/sapi/fpm/tests/011.phpt new file mode 100644 index 0000000000..ddcf67f6d3 --- /dev/null +++ b/sapi/fpm/tests/011.phpt @@ -0,0 +1,54 @@ +--TEST-- +FPM: Test IPv4 all addresses (bug #68420) +--SKIPIF-- +<?php include "skipif.inc"; ?> +--FILE-- +<?php + +include "include.inc"; + +$logfile = dirname(__FILE__).'/php-fpm.log.tmp'; +$port = 9000+PHP_INT_SIZE; + +$cfg = <<<EOT +[global] +error_log = $logfile +[unconfined] +listen = $port +ping.path = /ping +ping.response = pong +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +EOT; + +$fpm = run_fpm($cfg, $tail); +if (is_resource($fpm)) { + echo fgets($tail); + echo fgets($tail); + try { + var_dump(strpos(run_request('127.0.0.1', $port), 'pong')); + echo "IPv4 ok\n"; + } catch (Exception $e) { + echo "IPv4 error\n"; + } + + proc_terminate($fpm); + stream_get_contents($tail); + fclose($tail); + proc_close($fpm); +} + +?> +--EXPECTF-- +[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d +[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections +int(%d) +IPv4 ok +--CLEAN-- +<?php + $logfile = dirname(__FILE__).'/php-fpm.log.tmp'; + @unlink($logfile); +?> diff --git a/sapi/fpm/tests/fcgi.inc b/sapi/fpm/tests/fcgi.inc new file mode 100644 index 0000000000..b31676260d --- /dev/null +++ b/sapi/fpm/tests/fcgi.inc @@ -0,0 +1,592 @@ +<?php +/* + * This file is part of PHP-FastCGI-Client. + * + * (c) Pierrick Charron <pierrick@adoy.net> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +namespace Adoy\FastCGI; + +class TimedOutException extends \Exception {} +class ForbiddenException extends \Exception {} + +/** + * Handles communication with a FastCGI application + * + * @author Pierrick Charron <pierrick@adoy.net> + * @version 1.0 + */ +class Client +{ + const VERSION_1 = 1; + + const BEGIN_REQUEST = 1; + const ABORT_REQUEST = 2; + const END_REQUEST = 3; + const PARAMS = 4; + const STDIN = 5; + const STDOUT = 6; + const STDERR = 7; + const DATA = 8; + const GET_VALUES = 9; + const GET_VALUES_RESULT = 10; + const UNKNOWN_TYPE = 11; + const MAXTYPE = self::UNKNOWN_TYPE; + + const RESPONDER = 1; + const AUTHORIZER = 2; + const FILTER = 3; + + const REQUEST_COMPLETE = 0; + const CANT_MPX_CONN = 1; + const OVERLOADED = 2; + const UNKNOWN_ROLE = 3; + + const MAX_CONNS = 'MAX_CONNS'; + const MAX_REQS = 'MAX_REQS'; + const MPXS_CONNS = 'MPXS_CONNS'; + + const HEADER_LEN = 8; + + const REQ_STATE_WRITTEN = 1; + const REQ_STATE_OK = 2; + const REQ_STATE_ERR = 3; + const REQ_STATE_TIMED_OUT = 4; + + /** + * Socket + * @var Resource + */ + private $_sock = null; + + /** + * Host + * @var String + */ + private $_host = null; + + /** + * Port + * @var Integer + */ + private $_port = null; + + /** + * Keep Alive + * @var Boolean + */ + private $_keepAlive = false; + + /** + * Outstanding request statuses keyed by request id + * + * Each request is an array with following form: + * + * array( + * 'state' => REQ_STATE_* + * 'response' => null | string + * ) + * + * @var array + */ + private $_requests = array(); + + /** + * Use persistent sockets to connect to backend + * @var Boolean + */ + private $_persistentSocket = false; + + /** + * Connect timeout in milliseconds + * @var Integer + */ + private $_connectTimeout = 5000; + + /** + * Read/Write timeout in milliseconds + * @var Integer + */ + private $_readWriteTimeout = 5000; + + /** + * Constructor + * + * @param String $host Host of the FastCGI application + * @param Integer $port Port of the FastCGI application + */ + public function __construct($host, $port) + { + $this->_host = $host; + $this->_port = $port; + } + + /** + * Define whether or not the FastCGI application should keep the connection + * alive at the end of a request + * + * @param Boolean $b true if the connection should stay alive, false otherwise + */ + public function setKeepAlive($b) + { + $this->_keepAlive = (boolean)$b; + if (!$this->_keepAlive && $this->_sock) { + fclose($this->_sock); + } + } + + /** + * Get the keep alive status + * + * @return Boolean true if the connection should stay alive, false otherwise + */ + public function getKeepAlive() + { + return $this->_keepAlive; + } + + /** + * Define whether or not PHP should attempt to re-use sockets opened by previous + * request for efficiency + * + * @param Boolean $b true if persistent socket should be used, false otherwise + */ + public function setPersistentSocket($b) + { + $was_persistent = ($this->_sock && $this->_persistentSocket); + $this->_persistentSocket = (boolean)$b; + if (!$this->_persistentSocket && $was_persistent) { + fclose($this->_sock); + } + } + + /** + * Get the pesistent socket status + * + * @return Boolean true if the socket should be persistent, false otherwise + */ + public function getPersistentSocket() + { + return $this->_persistentSocket; + } + + + /** + * Set the connect timeout + * + * @param Integer number of milliseconds before connect will timeout + */ + public function setConnectTimeout($timeoutMs) + { + $this->_connectTimeout = $timeoutMs; + } + + /** + * Get the connect timeout + * + * @return Integer number of milliseconds before connect will timeout + */ + public function getConnectTimeout() + { + return $this->_connectTimeout; + } + + /** + * Set the read/write timeout + * + * @param Integer number of milliseconds before read or write call will timeout + */ + public function setReadWriteTimeout($timeoutMs) + { + $this->_readWriteTimeout = $timeoutMs; + $this->set_ms_timeout($this->_readWriteTimeout); + } + + /** + * Get the read timeout + * + * @return Integer number of milliseconds before read will timeout + */ + public function getReadWriteTimeout() + { + return $this->_readWriteTimeout; + } + + /** + * Helper to avoid duplicating milliseconds to secs/usecs in a few places + * + * @param Integer millisecond timeout + * @return Boolean + */ + private function set_ms_timeout($timeoutMs) { + if (!$this->_sock) { + return false; + } + return stream_set_timeout($this->_sock, floor($timeoutMs / 1000), ($timeoutMs % 1000) * 1000); + } + + + /** + * Create a connection to the FastCGI application + */ + private function connect() + { + if (!$this->_sock) { + if ($this->_persistentSocket) { + $this->_sock = pfsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000); + } else { + $this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000); + } + + if (!$this->_sock) { + throw new \Exception('Unable to connect to FastCGI application: ' . $errstr); + } + + if (!$this->set_ms_timeout($this->_readWriteTimeout)) { + throw new \Exception('Unable to set timeout on socket'); + } + } + } + + /** + * Build a FastCGI packet + * + * @param Integer $type Type of the packet + * @param String $content Content of the packet + * @param Integer $requestId RequestId + */ + private function buildPacket($type, $content, $requestId = 1) + { + $clen = strlen($content); + return chr(self::VERSION_1) /* version */ + . chr($type) /* type */ + . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */ + . chr($requestId & 0xFF) /* requestIdB0 */ + . chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */ + . chr($clen & 0xFF) /* contentLengthB0 */ + . chr(0) /* paddingLength */ + . chr(0) /* reserved */ + . $content; /* content */ + } + + /** + * Build an FastCGI Name value pair + * + * @param String $name Name + * @param String $value Value + * @return String FastCGI Name value pair + */ + private function buildNvpair($name, $value) + { + $nlen = strlen($name); + $vlen = strlen($value); + if ($nlen < 128) { + /* nameLengthB0 */ + $nvpair = chr($nlen); + } else { + /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ + $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); + } + if ($vlen < 128) { + /* valueLengthB0 */ + $nvpair .= chr($vlen); + } else { + /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */ + $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); + } + /* nameData & valueData */ + return $nvpair . $name . $value; + } + + /** + * Read a set of FastCGI Name value pairs + * + * @param String $data Data containing the set of FastCGI NVPair + * @return array of NVPair + */ + private function readNvpair($data, $length = null) + { + $array = array(); + + if ($length === null) { + $length = strlen($data); + } + + $p = 0; + + while ($p != $length) { + + $nlen = ord($data{$p++}); + if ($nlen >= 128) { + $nlen = ($nlen & 0x7F << 24); + $nlen |= (ord($data{$p++}) << 16); + $nlen |= (ord($data{$p++}) << 8); + $nlen |= (ord($data{$p++})); + } + $vlen = ord($data{$p++}); + if ($vlen >= 128) { + $vlen = ($nlen & 0x7F << 24); + $vlen |= (ord($data{$p++}) << 16); + $vlen |= (ord($data{$p++}) << 8); + $vlen |= (ord($data{$p++})); + } + $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); + $p += ($nlen + $vlen); + } + + return $array; + } + + /** + * Decode a FastCGI Packet + * + * @param String $data String containing all the packet + * @return array + */ + private function decodePacketHeader($data) + { + $ret = array(); + $ret['version'] = ord($data{0}); + $ret['type'] = ord($data{1}); + $ret['requestId'] = (ord($data{2}) << 8) + ord($data{3}); + $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5}); + $ret['paddingLength'] = ord($data{6}); + $ret['reserved'] = ord($data{7}); + return $ret; + } + + /** + * Read a FastCGI Packet + * + * @return array + */ + private function readPacket() + { + if ($packet = fread($this->_sock, self::HEADER_LEN)) { + $resp = $this->decodePacketHeader($packet); + $resp['content'] = ''; + if ($resp['contentLength']) { + $len = $resp['contentLength']; + while ($len && $buf=fread($this->_sock, $len)) { + $len -= strlen($buf); + $resp['content'] .= $buf; + } + } + if ($resp['paddingLength']) { + $buf = fread($this->_sock, $resp['paddingLength']); + } + return $resp; + } else { + return false; + } + } + + /** + * Get Informations on the FastCGI application + * + * @param array $requestedInfo information to retrieve + * @return array + */ + public function getValues(array $requestedInfo) + { + $this->connect(); + + $request = ''; + foreach ($requestedInfo as $info) { + $request .= $this->buildNvpair($info, ''); + } + fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0)); + + $resp = $this->readPacket(); + if ($resp['type'] == self::GET_VALUES_RESULT) { + return $this->readNvpair($resp['content'], $resp['length']); + } else { + throw new \Exception('Unexpected response type, expecting GET_VALUES_RESULT'); + } + } + + /** + * Execute a request to the FastCGI application + * + * @param array $params Array of parameters + * @param String $stdin Content + * @return String + */ + public function request(array $params, $stdin) + { + $id = $this->async_request($params, $stdin); + return $this->wait_for_response($id); + } + + /** + * Execute a request to the FastCGI application asyncronously + * + * This sends request to application and returns the assigned ID for that request. + * + * You should keep this id for later use with wait_for_response(). Ids are chosen randomly + * rather than seqentially to guard against false-positives when using persistent sockets. + * In that case it is possible that a delayed response to a request made by a previous script + * invocation comes back on this socket and is mistaken for response to request made with same ID + * during this request. + * + * @param array $params Array of parameters + * @param String $stdin Content + * @return Integer + */ + public function async_request(array $params, $stdin) + { + $this->connect(); + + // Pick random number between 1 and max 16 bit unsigned int 65535 + $id = mt_rand(1, (1 << 16) - 1); + + // Using persistent sockets implies you want them keept alive by server! + $keepAlive = intval($this->_keepAlive || $this->_persistentSocket); + + $request = $this->buildPacket(self::BEGIN_REQUEST + ,chr(0) . chr(self::RESPONDER) . chr($keepAlive) . str_repeat(chr(0), 5) + ,$id + ); + + $paramsRequest = ''; + foreach ($params as $key => $value) { + $paramsRequest .= $this->buildNvpair($key, $value, $id); + } + if ($paramsRequest) { + $request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id); + } + $request .= $this->buildPacket(self::PARAMS, '', $id); + + if ($stdin) { + $request .= $this->buildPacket(self::STDIN, $stdin, $id); + } + $request .= $this->buildPacket(self::STDIN, '', $id); + + if (fwrite($this->_sock, $request) === false || fflush($this->_sock) === false) { + + $info = stream_get_meta_data($this->_sock); + + if ($info['timed_out']) { + throw new TimedOutException('Write timed out'); + } + + // Broken pipe, tear down so future requests might succeed + fclose($this->_sock); + throw new \Exception('Failed to write request to socket'); + } + + $this->_requests[$id] = array( + 'state' => self::REQ_STATE_WRITTEN, + 'response' => null + ); + + return $id; + } + + /** + * Blocking call that waits for response to specific request + * + * @param Integer $requestId + * @param Integer $timeoutMs [optional] the number of milliseconds to wait. Defaults to the ReadWriteTimeout value set. + * @return string response body + */ + public function wait_for_response($requestId, $timeoutMs = 0) { + + if (!isset($this->_requests[$requestId])) { + throw new \Exception('Invalid request id given'); + } + + // If we already read the response during an earlier call for different id, just return it + if ($this->_requests[$requestId]['state'] == self::REQ_STATE_OK + || $this->_requests[$requestId]['state'] == self::REQ_STATE_ERR + ) { + return $this->_requests[$requestId]['response']; + } + + if ($timeoutMs > 0) { + // Reset timeout on socket for now + $this->set_ms_timeout($timeoutMs); + } else { + $timeoutMs = $this->_readWriteTimeout; + } + + // Need to manually check since we might do several reads none of which timeout themselves + // but still not get the response requested + $startTime = microtime(true); + + do { + $resp = $this->readPacket(); + + if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) { + if ($resp['type'] == self::STDERR) { + $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR; + } + $this->_requests[$resp['requestId']]['response'] .= $resp['content']; + } + if ($resp['type'] == self::END_REQUEST) { + $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK; + if ($resp['requestId'] == $requestId) { + break; + } + } + if (microtime(true) - $startTime >= ($timeoutMs * 1000)) { + // Reset + $this->set_ms_timeout($this->_readWriteTimeout); + throw new \Exception('Timed out'); + } + } while ($resp); + + if (!is_array($resp)) { + $info = stream_get_meta_data($this->_sock); + + // We must reset timeout but it must be AFTER we get info + $this->set_ms_timeout($this->_readWriteTimeout); + + if ($info['timed_out']) { + throw new TimedOutException('Read timed out'); + } + + if ($info['unread_bytes'] == 0 + && $info['blocked'] + && $info['eof']) { + throw new ForbiddenException('Not in white list. Check listen.allowed_clients.'); + } + + throw new \Exception('Read failed'); + } + + // Reset timeout + $this->set_ms_timeout($this->_readWriteTimeout); + + switch (ord($resp['content']{4})) { + case self::CANT_MPX_CONN: + throw new \Exception('This app can\'t multiplex [CANT_MPX_CONN]'); + break; + case self::OVERLOADED: + throw new \Exception('New request rejected; too busy [OVERLOADED]'); + break; + case self::UNKNOWN_ROLE: + throw new \Exception('Role value not known [UNKNOWN_ROLE]'); + break; + case self::REQUEST_COMPLETE: + return $this->_requests[$requestId]['response']; + } + } +} diff --git a/sapi/fpm/tests/include.inc b/sapi/fpm/tests/include.inc index 983cbd3454..5d01ae693d 100644 --- a/sapi/fpm/tests/include.inc +++ b/sapi/fpm/tests/include.inc @@ -76,4 +76,26 @@ function run_fpm_till($needle, $config, $max = 10) /* {{{ */ } /* }}} */ -?> +function run_request($host, $port, $uri='/ping', $query='') { + require_once 'fcgi.inc'; + $client = new Adoy\FastCGI\Client($host, $port); + $params = array( + 'GATEWAY_INTERFACE' => 'FastCGI/1.0', + 'REQUEST_METHOD' => 'GET', + 'SCRIPT_FILENAME' => $uri, + 'SCRIPT_NAME' => $uri, + 'QUERY_STRING' => $query, + 'REQUEST_URI' => $uri . ($query ? '?'.$query : ""), + 'DOCUMENT_URI' => $uri, + 'SERVER_SOFTWARE' => 'php/fcgiclient', + 'REMOTE_ADDR' => '127.0.0.1', + 'REMOTE_PORT' => '9985', + 'SERVER_ADDR' => '127.0.0.1', + 'SERVER_PORT' => '80', + 'SERVER_NAME' => php_uname('n'), + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'CONTENT_TYPE' => '', + 'CONTENT_LENGTH' => 0 + ); + return $client->request($params, false)."\n"; +} |