diff --git a/sapi/fpm/tests/002.phpt b/sapi/fpm/tests/002.phpt deleted file mode 100644 index 5ad9e4bd5cb..00000000000 --- a/sapi/fpm/tests/002.phpt +++ /dev/null @@ -1,51 +0,0 @@ ---TEST-- -FPM: Startup and connect ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d -[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/003.phpt b/sapi/fpm/tests/003.phpt deleted file mode 100644 index 8accbd0d101..00000000000 --- a/sapi/fpm/tests/003.phpt +++ /dev/null @@ -1,54 +0,0 @@ ---TEST-- -FPM: Test IPv6 support ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d -[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/004.phpt b/sapi/fpm/tests/004.phpt deleted file mode 100644 index 4375f8d0d03..00000000000 --- a/sapi/fpm/tests/004.phpt +++ /dev/null @@ -1,62 +0,0 @@ ---TEST-- -FPM: Test IPv4/IPv6 support ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -[%d-%s-%d %d:%d:%d] NOTICE: fpm is running, pid %d -[%d-%s-%d %d:%d:%d] NOTICE: ready to handle connections -Done IPv4 -Done IPv6 ---CLEAN-- - diff --git a/sapi/fpm/tests/005.phpt b/sapi/fpm/tests/005.phpt deleted file mode 100644 index 6c8210ec8e7..00000000000 --- a/sapi/fpm/tests/005.phpt +++ /dev/null @@ -1,60 +0,0 @@ ---TEST-- -FPM: Test IPv4 allowed clients ---SKIPIF-- - ---FILE-- - ---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-- - diff --git a/sapi/fpm/tests/006.phpt b/sapi/fpm/tests/006.phpt deleted file mode 100644 index e5520873359..00000000000 --- a/sapi/fpm/tests/006.phpt +++ /dev/null @@ -1,60 +0,0 @@ ---TEST-- -FPM: Test IPv6 allowed clients (bug #68428) ---SKIPIF-- - ---FILE-- - ---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-- - diff --git a/sapi/fpm/tests/007.phpt b/sapi/fpm/tests/007.phpt deleted file mode 100644 index 6329af209ad..00000000000 --- a/sapi/fpm/tests/007.phpt +++ /dev/null @@ -1,71 +0,0 @@ ---TEST-- -FPM: Test IPv6 all addresses and access_log (bug #68421) ---SKIPIF-- - ---FILE-- - ---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-- - diff --git a/sapi/fpm/tests/008.phpt b/sapi/fpm/tests/008.phpt deleted file mode 100644 index d5fe05ba761..00000000000 --- a/sapi/fpm/tests/008.phpt +++ /dev/null @@ -1,100 +0,0 @@ ---TEST-- -FPM: Test multi pool (dynamic + ondemand + static) (bug #68423) ---SKIPIF-- - ---FILE-- - ---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-- - diff --git a/sapi/fpm/tests/009.phpt b/sapi/fpm/tests/009.phpt deleted file mode 100644 index 34cdffcf833..00000000000 --- a/sapi/fpm/tests/009.phpt +++ /dev/null @@ -1,53 +0,0 @@ ---TEST-- -FPM: Test Unix Domain Socket ---SKIPIF-- - ---FILE-- - ---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-- - diff --git a/sapi/fpm/tests/010.phpt b/sapi/fpm/tests/010.phpt deleted file mode 100644 index 49e1a079230..00000000000 --- a/sapi/fpm/tests/010.phpt +++ /dev/null @@ -1,87 +0,0 @@ ---TEST-- -FPM: Test status page ---SKIPIF-- - ---XFAIL-- -randomly intermittently failing all the time in CI, with diff: -017+ active processes: 0 -018+ total processes: 1 -017- active processes: 1 -018- total processes: 2 ---FILE-- - ---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: static -start time: %s -start since: %d -accepted conn: 1 -listen queue: 0 -max listen queue: 0 -listen queue len: %d -idle processes: 0 -active processes: 1 -total processes: 1 -max active processes: 1 -max children reached: 0 -slow requests: 0 - -bool(true) -bool(true) -bool(true) -IPv4 ok ---CLEAN-- - diff --git a/sapi/fpm/tests/011.phpt b/sapi/fpm/tests/011.phpt deleted file mode 100644 index 0b849f873ba..00000000000 --- a/sapi/fpm/tests/011.phpt +++ /dev/null @@ -1,53 +0,0 @@ ---TEST-- -FPM: Test IPv4 all addresses (bug #68420) ---SKIPIF-- - ---FILE-- - ---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-- - diff --git a/sapi/fpm/tests/012.phpt b/sapi/fpm/tests/012.phpt deleted file mode 100644 index d96c53081c2..00000000000 --- a/sapi/fpm/tests/012.phpt +++ /dev/null @@ -1,79 +0,0 @@ ---TEST-- -FPM: Test reload configuration (bug #68442) ---SKIPIF-- - ---FILE-- - ---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 -[%d-%s-%d %d:%d:%d] NOTICE: Reloading in progress ... -[%d-%s-%d %d:%d:%d] NOTICE: reloading: %s -[%d-%s-%d %d:%d:%d] NOTICE: using inherited socket fd=%d, "127.0.0.1:%d" -[%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-- - diff --git a/sapi/fpm/tests/013.phpt b/sapi/fpm/tests/013.phpt deleted file mode 100644 index d28f3e4e009..00000000000 --- a/sapi/fpm/tests/013.phpt +++ /dev/null @@ -1,53 +0,0 @@ ---TEST-- -FPM: Test for log_level in fpm_unix_init_main #68381 ---SKIPIF-- - ---FILE-- - -Done ---EXPECTF-- -Started -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/014.phpt b/sapi/fpm/tests/014.phpt deleted file mode 100644 index 56e24715a8f..00000000000 --- a/sapi/fpm/tests/014.phpt +++ /dev/null @@ -1,53 +0,0 @@ ---TEST-- -FPM: Test for pm.start_servers default calculation message being a notice and not a warning #68458 ---SKIPIF-- - ---FILE-- - -Done ---EXPECTF-- -Started -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/015.phpt b/sapi/fpm/tests/015.phpt deleted file mode 100644 index a3c7ad3eee8..00000000000 --- a/sapi/fpm/tests/015.phpt +++ /dev/null @@ -1,89 +0,0 @@ ---TEST-- -FPM: Test various messages on start, from master and childs ---SKIPIF-- - ---XFAIL-- -randomly intermittently failing all the time in CI, -ERROR: unable to read what child say: Bad file descriptor (9) -catch_workers_output = yes seems not reliable ---FILE-- - -Done ---EXPECTF-- -Started -Error 2 -[%s] NOTICE: [pool pool1] pm.start_servers is not set. It's been set to 2. -[%s] NOTICE: [pool pool1] 'user' directive is ignored when FPM is not running as root -[%s] NOTICE: fpm is running, pid %d -[%s] NOTICE: ready to handle connections -[%s] WARNING: [pool pool2] child %d said into stderr: "ERROR: Wrong IP address 'xxx' in listen.allowed_clients" -[%s] WARNING: [pool pool2] child %d said into stderr: "ERROR: There are no allowed %s" -[%s] WARNING: [pool pool2] child %d said into stderr: "ERROR: Connection disallowed: IP address '127.0.0.1' has been dropped." -[%s] NOTICE: Terminating ... -[%s] NOTICE: exiting, bye-bye! -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/016.phpt b/sapi/fpm/tests/016.phpt deleted file mode 100644 index 04ba4b5f694..00000000000 --- a/sapi/fpm/tests/016.phpt +++ /dev/null @@ -1,101 +0,0 @@ ---TEST-- -FPM: Test splited configuration and load order #68391 ---SKIPIF-- - ---FILE-- - -Done ---EXPECTF-- -[%s] NOTICE: [pool aaaa] 'user' directive is ignored when FPM is not running as root -[%s] NOTICE: [pool bbbb] 'user' directive is ignored when FPM is not running as root -[%s] NOTICE: [pool cccc] 'user' directive is ignored when FPM is not running as root -[%s] NOTICE: [pool dddd] 'user' directive is ignored when FPM is not running as root -[%s] NOTICE: [pool eeee] 'user' directive is ignored when FPM is not running as root -[%s] NOTICE: fpm is running, pid %d -[%s] NOTICE: ready to handle connections -OK cccc -OK aaaa -OK eeee -OK dddd -OK bbbb -[%s] NOTICE: Terminating ... -[%s] NOTICE: exiting, bye-bye! -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/017.phpt b/sapi/fpm/tests/017.phpt deleted file mode 100644 index 46e5efd3f75..00000000000 --- a/sapi/fpm/tests/017.phpt +++ /dev/null @@ -1,67 +0,0 @@ ---TEST-- -FPM: Test fastcgi_finish_request function ---SKIPIF-- - ---FILE-- - -Done ---EXPECTF-- -[%s] NOTICE: fpm is running, pid %d -[%s] NOTICE: ready to handle connections -Test Start - -Request ok -[%s] NOTICE: Terminating ... -[%s] NOTICE: exiting, bye-bye! -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/019.phpt b/sapi/fpm/tests/019.phpt deleted file mode 100644 index 7bed9e2436e..00000000000 --- a/sapi/fpm/tests/019.phpt +++ /dev/null @@ -1,79 +0,0 @@ ---TEST-- -FPM: Test global prefix ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -[%s] NOTICE: fpm is running, pid %d -[%s] NOTICE: ready to handle connections -Ping ok -File php-fpm.log.tmp exists -File php-fpm.acc.tmp exists -File php-fpm.slw.tmp exists -File php-fpm.pid.tmp exists -[%s] NOTICE: Terminating ... -[%s] NOTICE: exiting, bye-bye! -File php-fpm.pid.tmp removed -127.0.0.1 - %s "GET /ping" 200 ---CLEAN-- - diff --git a/sapi/fpm/tests/020.phpt b/sapi/fpm/tests/020.phpt deleted file mode 100644 index 81317500152..00000000000 --- a/sapi/fpm/tests/020.phpt +++ /dev/null @@ -1,75 +0,0 @@ ---TEST-- -FPM: Test pool prefix ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -[%s] NOTICE: fpm is running, pid %d -[%s] NOTICE: ready to handle connections -Ping ok -File php-fpm.acc.tmp exists -File php-fpm.slw.tmp exists -[%s] NOTICE: Terminating ... -[%s] NOTICE: exiting, bye-bye! -127.0.0.1 - %s "GET /ping" 200 ---CLEAN-- - diff --git a/sapi/fpm/tests/021-uds-acl.phpt b/sapi/fpm/tests/021-uds-acl.phpt deleted file mode 100644 index 6e9ec08d8b2..00000000000 --- a/sapi/fpm/tests/021-uds-acl.phpt +++ /dev/null @@ -1,102 +0,0 @@ ---TEST-- -FPM: Test Unix Domain Socket with Posix ACL ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -[%s] NOTICE: fpm is running, pid %d -[%s] NOTICE: ready to handle connections -int(%d) -UDS ok -user::rw- -user:%s:rw- -user:%s:rw- -user:%s:rw- -group::--- -group:%s:rw- -group:%s:rw- -mask::rw- -other::--- - -[%s] NOTICE: Terminating ... -[%s] NOTICE: exiting, bye-bye! ---CLEAN-- - diff --git a/sapi/fpm/tests/022-cve-2016-5385.phpt b/sapi/fpm/tests/022-cve-2016-5385.phpt deleted file mode 100644 index 0bdf238f30e..00000000000 --- a/sapi/fpm/tests/022-cve-2016-5385.phpt +++ /dev/null @@ -1,81 +0,0 @@ ---TEST-- -FPM: HTTP_PROXY - CVE-2016-5385 ---SKIPIF-- - ---FILE-- - 'BAR', - 'HTTP_PROXY' => 'BADPROXY', - ]; - $req = run_request('127.0.0.1', $port, $srcfile, '', $headers); - echo strstr($req, "Test Start"); - echo "Request ok\n"; - } catch (Exception $e) { - echo "Request error\n"; - } - proc_terminate($fpm); - fpm_display_log($tail, -1); - fclose($tail); - proc_close($fpm); -} - -?> -Done ---EXPECTF-- -[%s] NOTICE: fpm is running, pid %d -[%s] NOTICE: ready to handle connections -Test Start -NULL -string(3) "BAR" -bool(false) -string(3) "BAR" -Test End - -Request ok -[%s] NOTICE: Terminating ... -[%s] NOTICE: exiting, bye-bye! -Done ---CLEAN-- - diff --git a/sapi/fpm/tests/bug68381-log-level-warning.phpt b/sapi/fpm/tests/bug68381-log-level-warning.phpt new file mode 100644 index 00000000000..8d4a9af4cc5 --- /dev/null +++ b/sapi/fpm/tests/bug68381-log-level-warning.phpt @@ -0,0 +1,40 @@ +--TEST-- +FPM: bug68381 - Log messages with warning level only +--SKIPIF-- + +--FILE-- +start(); +$tester->checkConnection(); +$tester->terminate(); +$tester->expectNoLogMessages(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug68391-conf-include-order.phpt b/sapi/fpm/tests/bug68391-conf-include-order.phpt new file mode 100644 index 00000000000..012a978f298 --- /dev/null +++ b/sapi/fpm/tests/bug68391-conf-include-order.phpt @@ -0,0 +1,53 @@ +--TEST-- +FPM: bug68391 - Configuration inclusion in alphabetical order +--SKIPIF-- + +--FILE-- +start(); +$userMessage = "'user' directive is ignored when FPM is not running as root"; +$tester->expectLogNotice($userMessage, 'aaaa'); +$tester->expectLogNotice($userMessage, 'bbbb'); +$tester->expectLogNotice($userMessage, 'cccc'); +$tester->expectLogNotice($userMessage, 'dddd'); +$tester->expectLogNotice($userMessage, 'eeee'); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug68420-ipv4-all-addresses.phpt b/sapi/fpm/tests/bug68420-ipv4-all-addresses.phpt new file mode 100644 index 00000000000..9a4692d17c1 --- /dev/null +++ b/sapi/fpm/tests/bug68420-ipv4-all-addresses.phpt @@ -0,0 +1,42 @@ +--TEST-- +FPM: bug68420 - IPv4 all addresses +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->ping('127.0.0.1'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug68421-ipv6-access-log.phpt b/sapi/fpm/tests/bug68421-ipv6-access-log.phpt new file mode 100644 index 00000000000..80c115c17f9 --- /dev/null +++ b/sapi/fpm/tests/bug68421-ipv6-access-log.phpt @@ -0,0 +1,47 @@ +--TEST-- +FPM: bug68421 - IPv6 all addresses and access_log +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->ping('127.0.0.1'); +$tester->ping('[::1]'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +$tester->printAccessLog(); +?> +Done +--EXPECTF-- +127.0.0.1 %s "GET /ping" 200 +::1 %s "GET /ping" 200 +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug68423-multi-pool-all-pms.phpt b/sapi/fpm/tests/bug68423-multi-pool-all-pms.phpt new file mode 100644 index 00000000000..ae6b48351a0 --- /dev/null +++ b/sapi/fpm/tests/bug68423-multi-pool-all-pms.phpt @@ -0,0 +1,55 @@ +--TEST-- +FPM: bug68423 - Multiple pools with different PMs (dynamic + ondemand + static) +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->ping('{{ADDR[dynamic]}}', 'pong-dynamic'); +$tester->ping('{{ADDR[ondemand]}}', 'pong-on-demand'); +$tester->ping('{{ADDR[static]}}', 'pong-static'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug68428-ipv6-allowed-clients.phpt b/sapi/fpm/tests/bug68428-ipv6-allowed-clients.phpt new file mode 100644 index 00000000000..0998cf0acaf --- /dev/null +++ b/sapi/fpm/tests/bug68428-ipv6-allowed-clients.phpt @@ -0,0 +1,45 @@ +--TEST-- +FPM: bug68428 - IPv6 allowed client only +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkRequest('127.0.0.1', 'IPv4: ok', 'IPv4: error'); +$tester->checkRequest('[::1]', 'IPv6: ok', 'IPv6: error'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +IPv4: error +IPv6: ok +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug68442-signal-reload.phpt b/sapi/fpm/tests/bug68442-signal-reload.phpt new file mode 100644 index 00000000000..d15c8e14e76 --- /dev/null +++ b/sapi/fpm/tests/bug68442-signal-reload.phpt @@ -0,0 +1,47 @@ +--TEST-- +FPM: bug68442 - Signal reload +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->ping('{{ADDR}}'); +$tester->signal('USR2'); +$tester->expectLogNotice('Reloading in progress ...'); +$tester->expectLogNotice('reloading: .*'); +$tester->expectLogNotice('using inherited socket fd=\d+, "127.0.0.1:\d+"'); +$tester->expectLogStartNotices(); +$tester->ping('{{ADDR}}'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug68458-pm-no-start-server.phpt b/sapi/fpm/tests/bug68458-pm-no-start-server.phpt new file mode 100644 index 00000000000..c0c69b64b99 --- /dev/null +++ b/sapi/fpm/tests/bug68458-pm-no-start-server.phpt @@ -0,0 +1,42 @@ +--TEST-- +FPM: bug68458 - Missing pm.start_servers should emit notice instead of warning +--SKIPIF-- + +--FILE-- +start(); +$tester->checkConnection(); +$tester->terminate(); +$tester->expectNoLogMessages(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/bug72573-http-proxy.phpt b/sapi/fpm/tests/bug72573-http-proxy.phpt new file mode 100644 index 00000000000..ffa60d97134 --- /dev/null +++ b/sapi/fpm/tests/bug72573-http-proxy.phpt @@ -0,0 +1,66 @@ +--TEST-- +FPM: bug72573 - HTTP_PROXY - CVE-2016-5385 +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester + ->request( + '', + [ + 'HTTP_FOO' => 'BAR', + 'HTTP_PROXY' => 'BADPROXY', + ] + ) + ->expectBody( + [ + 'Test Start', + 'NULL', + 'string(3) "BAR"', + 'bool(false)', + 'string(3) "BAR"', + 'Test End' + ] + ); +$tester->terminate(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/fastcgi_finish_request_basic.phpt b/sapi/fpm/tests/fastcgi_finish_request_basic.phpt new file mode 100644 index 00000000000..939782fa312 --- /dev/null +++ b/sapi/fpm/tests/fastcgi_finish_request_basic.phpt @@ -0,0 +1,45 @@ +--TEST-- +FPM: Function fastcgi_finish_request basic test +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->request()->expectBody("Test Start"); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/fcgi.inc b/sapi/fpm/tests/fcgi.inc index b31676260da..71bdad17b95 100644 --- a/sapi/fpm/tests/fcgi.inc +++ b/sapi/fpm/tests/fcgi.inc @@ -72,25 +72,25 @@ class Client /** * Socket - * @var Resource + * @var resource */ private $_sock = null; /** * Host - * @var String + * @var string */ private $_host = null; /** * Port - * @var Integer + * @var int */ private $_port = null; /** * Keep Alive - * @var Boolean + * @var bool */ private $_keepAlive = false; @@ -110,27 +110,27 @@ class Client /** * Use persistent sockets to connect to backend - * @var Boolean + * @var bool */ private $_persistentSocket = false; /** * Connect timeout in milliseconds - * @var Integer + * @var int */ private $_connectTimeout = 5000; /** * Read/Write timeout in milliseconds - * @var Integer + * @var int */ private $_readWriteTimeout = 5000; /** * Constructor * - * @param String $host Host of the FastCGI application - * @param Integer $port Port of the FastCGI application + * @param string $host Host of the FastCGI application + * @param int $port Port of the FastCGI application */ public function __construct($host, $port) { @@ -138,15 +138,25 @@ class Client $this->_port = $port; } + /** + * Get host. + * + * @return string + */ + public function getHost() + { + return $this->_host; + } + /** * 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 + * @param bool $b true if the connection should stay alive, false otherwise */ public function setKeepAlive($b) { - $this->_keepAlive = (boolean)$b; + $this->_keepAlive = (bool)$b; if (!$this->_keepAlive && $this->_sock) { fclose($this->_sock); } @@ -155,7 +165,7 @@ class Client /** * Get the keep alive status * - * @return Boolean true if the connection should stay alive, false otherwise + * @return bool true if the connection should stay alive, false otherwise */ public function getKeepAlive() { @@ -166,12 +176,12 @@ class Client * 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 + * @param bool $b true if persistent socket should be used, false otherwise */ public function setPersistentSocket($b) { $was_persistent = ($this->_sock && $this->_persistentSocket); - $this->_persistentSocket = (boolean)$b; + $this->_persistentSocket = (bool)$b; if (!$this->_persistentSocket && $was_persistent) { fclose($this->_sock); } @@ -180,7 +190,7 @@ class Client /** * Get the pesistent socket status * - * @return Boolean true if the socket should be persistent, false otherwise + * @return bool true if the socket should be persistent, false otherwise */ public function getPersistentSocket() { @@ -191,7 +201,7 @@ class Client /** * Set the connect timeout * - * @param Integer number of milliseconds before connect will timeout + * @param int number of milliseconds before connect will timeout */ public function setConnectTimeout($timeoutMs) { @@ -201,7 +211,7 @@ class Client /** * Get the connect timeout * - * @return Integer number of milliseconds before connect will timeout + * @return int number of milliseconds before connect will timeout */ public function getConnectTimeout() { @@ -211,7 +221,7 @@ class Client /** * Set the read/write timeout * - * @param Integer number of milliseconds before read or write call will timeout + * @param int number of milliseconds before read or write call will timeout */ public function setReadWriteTimeout($timeoutMs) { @@ -222,7 +232,7 @@ class Client /** * Get the read timeout * - * @return Integer number of milliseconds before read will timeout + * @return int number of milliseconds before read will timeout */ public function getReadWriteTimeout() { @@ -232,14 +242,18 @@ class Client /** * Helper to avoid duplicating milliseconds to secs/usecs in a few places * - * @param Integer millisecond timeout - * @return Boolean + * @param int millisecond timeout + * @return bool */ private function set_ms_timeout($timeoutMs) { if (!$this->_sock) { return false; } - return stream_set_timeout($this->_sock, floor($timeoutMs / 1000), ($timeoutMs % 1000) * 1000); + return stream_set_timeout( + $this->_sock, + floor($timeoutMs / 1000), + ($timeoutMs % 1000) * 1000 + ); } @@ -250,9 +264,21 @@ class Client { if (!$this->_sock) { if ($this->_persistentSocket) { - $this->_sock = pfsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000); + $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); + $this->_sock = fsockopen( + $this->_host, + $this->_port, + $errno, + $errstr, + $this->_connectTimeout/1000 + ); } if (!$this->_sock) { @@ -268,9 +294,10 @@ class Client /** * Build a FastCGI packet * - * @param Integer $type Type of the packet - * @param String $content Content of the packet - * @param Integer $requestId RequestId + * @param int $type Type of the packet + * @param string $content Content of the packet + * @param int $requestId RequestId + * @return string */ private function buildPacket($type, $content, $requestId = 1) { @@ -289,9 +316,9 @@ class Client /** * Build an FastCGI Name value pair * - * @param String $name Name - * @param String $value Value - * @return String FastCGI Name value pair + * @param string $name Name + * @param string $value Value + * @return string FastCGI Name value pair */ private function buildNvpair($name, $value) { @@ -302,14 +329,16 @@ class Client $nvpair = chr($nlen); } else { /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ - $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); + $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); + $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) + . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); } /* nameData & valueData */ return $nvpair . $name . $value; @@ -318,7 +347,7 @@ class Client /** * Read a set of FastCGI Name value pairs * - * @param String $data Data containing the set of FastCGI NVPair + * @param string $data Data containing the set of FastCGI NVPair * @return array of NVPair */ private function readNvpair($data, $length = null) @@ -357,7 +386,7 @@ class Client /** * Decode a FastCGI Packet * - * @param String $data String containing all the packet + * @param string $data string containing all the packet * @return array */ private function decodePacketHeader($data) @@ -403,6 +432,7 @@ class Client * * @param array $requestedInfo information to retrieve * @return array + * @throws \Exception */ public function getValues(array $requestedInfo) { @@ -423,11 +453,14 @@ class Client } /** - * Execute a request to the FastCGI application + * Execute a request to the FastCGI application and return response body * * @param array $params Array of parameters - * @param String $stdin Content - * @return String + * @param string $stdin Content + * @return string + * @throws ForbiddenException + * @throws TimedOutException + * @throws \Exception */ public function request(array $params, $stdin) { @@ -435,20 +468,38 @@ class Client return $this->wait_for_response($id); } + /** + * Execute a request to the FastCGI application and return request data + * + * @param array $params Array of parameters + * @param string $stdin Content + * @return array + * @throws ForbiddenException + * @throws TimedOutException + * @throws \Exception + */ + public function request_data(array $params, $stdin) + { + $id = $this->async_request($params, $stdin); + return $this->wait_for_response_data($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. + * rather than sequentially 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 + * @param string $stdin Content + * @return int + * @throws TimedOutException + * @throws \Exception */ public function async_request(array $params, $stdin) { @@ -460,10 +511,12 @@ class Client // 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 - ); + $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) { @@ -494,21 +547,26 @@ class Client $this->_requests[$id] = array( 'state' => self::REQ_STATE_WRITTEN, - 'response' => null + 'response' => null, + 'err_response' => null, + 'out_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 + * Blocking call that waits for response data of the specific request + * + * @param int $requestId + * @param int $timeoutMs [optional] the number of milliseconds to wait. + * @return array response data + * @throws ForbiddenException + * @throws TimedOutException + * @throws \Exception */ - public function wait_for_response($requestId, $timeoutMs = 0) { - + public function wait_for_response_data($requestId, $timeoutMs = 0) + { if (!isset($this->_requests[$requestId])) { throw new \Exception('Invalid request id given'); } @@ -537,6 +595,9 @@ class Client 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']]['err_response'] .= $resp['content']; + } else { + $this->_requests[$resp['requestId']]['out_response'] .= $resp['content']; } $this->_requests[$resp['requestId']]['response'] .= $resp['content']; } @@ -586,7 +647,22 @@ class Client throw new \Exception('Role value not known [UNKNOWN_ROLE]'); break; case self::REQUEST_COMPLETE: - return $this->_requests[$requestId]['response']; + return $this->_requests[$requestId]; } } + + /** + * Blocking call that waits for response to specific request + * + * @param int $requestId + * @param int $timeoutMs [optional] the number of milliseconds to wait. + * @return string The response content. + * @throws ForbiddenException + * @throws TimedOutException + * @throws \Exception + */ + public function wait_for_response($requestId, $timeoutMs = 0) + { + return $this->wait_for_response_data($requestId, $timeoutMs)['response']; + } } diff --git a/sapi/fpm/tests/include.inc b/sapi/fpm/tests/include.inc deleted file mode 100644 index dd2e69b9280..00000000000 --- a/sapi/fpm/tests/include.inc +++ /dev/null @@ -1,135 +0,0 @@ - array('pipe', 'w')]; - } - /* Since it's not possible to spawn a process under linux without using a - * shell in php (why?!?) we need a little shell trickery, so that we can - * actually kill php-fpm */ - $asroot = getenv('TEST_FPM_RUN_AS_ROOT') ? '--allow-to-run-as-root' : ''; - $cmd = get_fpm_path()." $asroot -F -O -y $cfg $extra_args"; - $fpm = proc_open("killit () { kill \$child; }; trap killit TERM; $cmd 2>&1 & child=\$!; wait", - $desc, $pipes); - register_shutdown_function( - function($fpm) use($cfg) { - @unlink($cfg); - if (is_resource($fpm)) { - @proc_terminate($fpm); - while (proc_get_status($fpm)['running']) { - usleep(10000); - } - } - }, - $fpm - ); - if ($out !== false) { - $out = $pipes[1]; - } - return $fpm; -} -/* }}} */ - -function test_fpm_conf($config, &$msg = NULL) { /* {{{ */ - $cfg = dirname(__FILE__).'/test-fpm-config.tmp'; - file_put_contents($cfg, $config); - exec(get_fpm_path() . ' -t -y ' . $cfg . ' 2>&1', $output, $code); - if ($code) { - $msg = preg_replace("/\[.+?\]/", "", $output[0]); - return false; - } - return true; -} -/* }}} */ - -function run_fpm_till($needle, $config, $max = 10) /* {{{ */ -{ - $i = 0; - $fpm = run_fpm($config, $tail); - if (is_resource($fpm)) { - while($i < $max) { - $i++; - $line = fgets($tail); - if(preg_match($needle, $line) === 1) { - break; - } - } - if ($i >= $max) { - $line = false; - } - proc_terminate($fpm); - stream_get_contents($tail); - fclose($tail); - proc_close($fpm); - } - return $line; -} -/* }}} */ - -function fpm_display_log($tail, $n=1, $ignore='systemd') { /* {{{ */ - /* Read $n lines or until EOF */ - while ($n>0 || ($n<0 && !feof($tail))) { - $a = fgets($tail); - if (empty($ignore) || !strpos($a, $ignore)) { - echo $a; - $n--; - } - } -} /* }}} */ - -function run_request($host, $port, $uri='/ping', $query='', $headers=array()) { /* {{{ */ - require_once 'fcgi.inc'; - $client = new Adoy\FastCGI\Client($host, $port); - $params = array_merge(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' => '', - 'DOCUMENT_ROOT' => __DIR__, - 'CONTENT_LENGTH' => 0 - ), $headers); - return $client->request($params, false)."\n"; -} -/* }}} */ diff --git a/sapi/fpm/tests/logtool.inc b/sapi/fpm/tests/logtool.inc new file mode 100644 index 00000000000..219c6fedbb8 --- /dev/null +++ b/sapi/fpm/tests/logtool.inc @@ -0,0 +1,476 @@ +message = ($repeat > 0) ? str_repeat($message, $repeat) : $message; + $this->limit = $limit; + $this->position = 0; + } + + /** + * @param string $level + * @return int + */ + public function setExpectedLevel(string $level) + { + return $this->level = $level; + } + + /** + * @return string + */ + public function getExpectedLevel(): string + { + return $this->level ?: 'WARNING'; + } + + /** + * @param string $line + * @return bool + */ + public function checkTruncatedMessage(string $line) + { + if ($this->message === null) { + throw new \LogicException('The message has not been set'); + } + $lineLen = strlen($line); + if (!$this->checkLineLength($line)) { + return false; + } + $this->pattern = '/^PHP message: (.*?)(\.\.\.)?$/'; + if (preg_match($this->pattern, $line, $matches) === 0) { + return $this->error("Unexpected truncated message: {$line}"); + } + + if ($lineLen === $this->limit) { + if (!isset($matches[2])) { + return $this->error("The truncated line is not ended with '...'"); + } + if (!$this->checkMessage($matches[1])) { + return false; + } + } else { + if (isset($matches[2])) { + // this is expecting that the expected message does not end with '...' + // which should not be an issue for the test purpose. + return $this->error("The line is complete and should not end with '...'"); + } + if (!$this->checkMessage($matches[1], -1)) { + return false; + } + } + + return true; + } + + /** + * @param array $lines + * @param bool $terminated + * @param bool $decorated + * @return bool + */ + public function checkWrappedMessage(array $lines, bool $terminated = true, bool $decorated = true) + { + if ($this->message === null) { + throw new \LogicException('The message has not been set'); + } + if ($decorated) { + $this->pattern = sprintf( + '/^(%s %s: %s)"([^"]*)"(.*)?$/', + self::P_TIME, + $this->getExpectedLevel(), + self::P_PREFIX + ); + } else { + $this->pattern = null; + } + + $idx = 0; + foreach ($lines as $idx => $line) { + if (!$this->checkLine($line)) { + break; + } + } + + if ($this->suffixPosition > 0) { + $suffixPattern = sprintf( + '/^%s %s: %s(.*)$/', + self::P_TIME, $this->getExpectedLevel(), + self::P_PREFIX + ); + $line = $lines[++$idx]; + if (preg_match($suffixPattern, $line, $matches) === 0) { + return $this->error("Unexpected line: $line"); + } + if ($matches[1] !== substr(self::FINAL_SUFFIX, $this->suffixPosition)) { + return $this->error( + "The suffix has not been finished from position $this->suffixPosition in line: $line" + ); + } + } + + if ($terminated) { + return $this->expectTerminatorLines($lines, $idx); + } + + return true; + } + + /** + * @param string $line + * @return bool + */ + private function checkLine(string $line) + { + if ($this->pattern === null) { + // plain (not decorated) output + $out = rtrim($line); + $finalSuffix = null; + } elseif (($res = preg_match($this->pattern, $line, $matches)) > 0) { + $out = $matches[2]; + $finalSuffix = $matches[3] ?? false; + } else { + return $this->error("Unexpected line: $line"); + } + + $rem = strlen($this->message) - $this->position; + $lineLen = strlen($line); + if (!$this->checkLineLength($line, $lineLen)) { + return false; + } + if (!$this->checkMessage($out, $this->position)) { + return false; + } + $outLen = strlen($out); + if ($rem > $outLen) { // continuous line + if ($lineLen !== $this->limit) { + if ($lineLen + ($rem - $outLen) < $this->limit) { + return $this->error("Printed less than the message len"); + } + return $this->error( + "The continuous line length is $lineLen but it should equal to limit $this->limit" + ); + } + $this->position += $outLen; + return true; + } + if ($rem !== $outLen) { + return $this->error("Printed more than the message len"); + } + if ($finalSuffix === null || $finalSuffix === "") { + return false; + } + if ($finalSuffix === false) { + return $this->error("No final suffix"); + } + if (strpos(self::FINAL_SUFFIX, $finalSuffix) === false) { + return $this->error("The final suffix has to be equal to ', pipe is closed'"); + } + if (self::FINAL_SUFFIX !== $finalSuffix) { + $this->suffixPosition = strlen($finalSuffix); + } + // complete final suffix printed + return false; + } + + /** + * @param string $line + * @param int $lineLen + * @return bool + */ + private function checkLineLength(string $line, $lineLen = null) { + $lineLen = $lineLen ?: strlen($line); + if ($lineLen > $this->limit) { + return $this->error( + "The line length is $lineLen which is higher than limit $this->limit" + ); + } + + return true; + } + + /** + * @param string $matchedMessage + * @param int $expectedMessageStart + * @return bool + */ + private function checkMessage(string $matchedMessage, int $expectedMessageStart = 0) + { + if ($expectedMessageStart < 0) { + $expectedMessage = $this->message; + } else { + $expectedMessage = substr($this->message, $expectedMessageStart, strlen($matchedMessage)); + } + if ($expectedMessage !== $matchedMessage) { + return $this->error( + sprintf( + "The actual string(%d) does not match expected string(%d):\n", + strlen($matchedMessage), + strlen($expectedMessage) + ) . + "- EXPECT: '$expectedMessage'\n" . + "- ACTUAL: '$matchedMessage'" + ); + } + + return true; + } + + /** + * @param array $lines + * @return bool + */ + public function expectStartingLines(array $lines) + { + if ($this->getError()) { + return false; + } + + if (count($lines) < 2) { + return $this->error("No starting lines"); + } + + return ( + $this->expectNotice($lines[0], 'fpm is running, pid \d+') && + $this->expectNotice($lines[1], 'ready to handle connections') + ); + } + + /** + * @param array $lines + * @param int $idx + * @return bool + */ + public function expectTerminatorLines(array $lines, int $idx = -1) + { + if ($this->getError()) { + return false; + } + + if (count($lines) - $idx < 3) { + return $this->error("No terminating lines"); + } + + return ( + $this->expectNotice($lines[++$idx], 'Terminating ...') && + $this->expectNotice($lines[++$idx], 'exiting, bye-bye!') + ); + } + + /** + * @param string $type + * @param string $line + * @param string $expectedMessage + * @param string|null $pool + * @return bool + */ + public function expectEntry(string $type, string $line, string $expectedMessage, $pool = null) + { + if ($this->getError()) { + return false; + } + if ($pool !== null) { + $expectedMessage = '\[pool ' . $pool . '\] ' . $expectedMessage; + } + + $line = rtrim($line); + $pattern = sprintf('/^%s %s: %s$/', self::P_TIME, $type, $expectedMessage); + + if (preg_match($pattern, $line, $matches) === 0) { + return $this->error( + "The $type does not match expected message:\n" . + "- PATTERN: $pattern\n" . + "- MESSAGE: $line\n" . + "- EXPECT: '$expectedMessage'\n" . + "- ACTUAL: '" . substr($line, strpos($line, $type) + strlen($type) + 2) . "'" + ); + } + + return true; + } + + /** + * @param string $line + * @param string $expectedMessage + * @param string|null $pool + * @return bool + */ + public function expectDebug(string $line, string $expectedMessage, $pool = null) + { + return $this->expectEntry('DEBUG', $line, $expectedMessage, $pool); + } + + /** + * @param string $line + * @param string $expectedMessage + * @param string|null $pool + * @return bool + */ + public function expectNotice(string $line, string $expectedMessage, $pool = null) + { + return $this->expectEntry('NOTICE', $line, $expectedMessage, $pool); + } + + /** + * @param string $line + * @param string $expectedMessage + * @param string|null $pool + * @return bool + */ + public function expectWarning(string $line, string $expectedMessage, $pool = null) + { + return $this->expectEntry('WARNING', $line, $expectedMessage, $pool); + } + + /** + * @param string $line + * @param string $expectedMessage + * @param string|null $pool + * @return bool + */ + public function expectError(string $line, string $expectedMessage, $pool = null) + { + return $this->expectEntry('ERROR', $line, $expectedMessage, $pool); + } + + /** + * @param string $line + * @param string $expectedMessage + * @param string|null $pool + * @return bool + */ + public function expectAlert(string $line, string $expectedMessage, $pool = null) + { + return $this->expectEntry('ALERT', $line, $expectedMessage, $pool); + } + + + /** + * @param string $msg + * @return bool + */ + private function error(string $msg) + { + $this->error = $msg; + echo "ERROR: $msg\n"; + return false; + } + + /** + * @return string + */ + public function getError() + { + return $this->error; + } +} + +if (isset($argv[1]) && $argv[1] === 'logtool-selftest') { + $cases = [ + [ + 'limit' => 1050, + 'lines' => [ + '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . + str_repeat('a', 968) . '"', + '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . + str_repeat('a', 968) . '"', + '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . + str_repeat('a', 112) . '", pipe is closed', + '[08-Oct-2017 19:53:55] NOTICE: Terminating ...', + '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!', + ], + 'message' => str_repeat('a', 2048), + 'type' => 'stdio', + ], + [ + 'limit' => 1050, + 'lines' => [ + '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . + str_repeat('a', 968) . '"', + '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . + str_repeat('a', 968) . '"', + '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . + str_repeat('a', 964) . '", pi', + '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: pe is closed', + '[08-Oct-2017 19:53:55] NOTICE: Terminating ...', + '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!', + ], + 'message' => str_repeat('a', 2900), + 'type' => 'stdio', + ], + [ + 'limit' => 1024, + 'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',989) . '...', + 'message' => str_repeat('a', 2900), + 'type' => 'message', + ], + [ + 'limit' => 1024, + 'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',20), + 'message' => str_repeat('a', 20), + 'type' => 'message', + ], + ]; + foreach ($cases as $case) { + printf("Test message with len %d and limit %d: ", strlen($case['message']), $case['limit']); + $logTool = new LogTool(); + $logTool->setExpectedMessage($case['message'], $case['limit']); + if ($case['type'] === 'stdio') { + $logTool->checkWrappedMessage($case['lines']); + } else { + $logTool->checkTruncatedMessage($case['line']); + } + if (!$logTool->getError()) { + echo "OK\n"; + } + } + echo "Done\n"; +} diff --git a/sapi/fpm/tests/main-global-prefix.phpt b/sapi/fpm/tests/main-global-prefix.phpt new file mode 100644 index 00000000000..710e688c408 --- /dev/null +++ b/sapi/fpm/tests/main-global-prefix.phpt @@ -0,0 +1,50 @@ +--TEST-- +FPM: Main invocation with prefix +--SKIPIF-- + +--FILE-- +start('--prefix ' . $prefix); +$tester->expectLogStartNotices(); +$tester->expectFile(FPM\Tester::FILE_EXT_LOG_ACC, $prefix); +$tester->expectFile(FPM\Tester::FILE_EXT_LOG_ERR, $prefix); +$tester->expectFile(FPM\Tester::FILE_EXT_LOG_SLOW, $prefix); +$tester->expectFile(FPM\Tester::FILE_EXT_PID, $prefix); +$tester->ping(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +$tester->expectNoFile(FPM\Tester::FILE_EXT_PID, $prefix); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/001.phpt b/sapi/fpm/tests/main-version.phpt similarity index 81% rename from sapi/fpm/tests/001.phpt rename to sapi/fpm/tests/main-version.phpt index b721bfa9254..6e42aae48f7 100644 --- a/sapi/fpm/tests/001.phpt +++ b/sapi/fpm/tests/main-version.phpt @@ -5,9 +5,9 @@ FPM: version string --FILE-- --FILE-- runTill( + '/(SIGSEGV|failed to query apparmor confinement|' . + 'failed to change to new confinement|exited with code 70)/' +); ?> ---EXPECTF-- -string(%d) "%s -" +Done +--EXPECT-- +Done --CLEAN-- +require_once "tester.inc"; +FPM\Tester::clean(); +?> \ No newline at end of file diff --git a/sapi/fpm/tests/pool-prefix.phpt b/sapi/fpm/tests/pool-prefix.phpt new file mode 100644 index 00000000000..b32b37e13a6 --- /dev/null +++ b/sapi/fpm/tests/pool-prefix.phpt @@ -0,0 +1,54 @@ +--TEST-- +FPM: Pool prefix +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->ping(); +$tester->expectFile(FPM\Tester::FILE_EXT_LOG_ACC, $prefix); +$tester->expectFile(FPM\Tester::FILE_EXT_LOG_ERR); +$tester->expectFile(FPM\Tester::FILE_EXT_LOG_SLOW, $prefix); +$tester->expectFile(FPM\Tester::FILE_EXT_PID); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +$tester->expectNoFile(FPM\Tester::FILE_EXT_PID); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + \ No newline at end of file diff --git a/sapi/fpm/tests/proc-no-start-server.phpt b/sapi/fpm/tests/proc-no-start-server.phpt new file mode 100644 index 00000000000..82f10727b68 --- /dev/null +++ b/sapi/fpm/tests/proc-no-start-server.phpt @@ -0,0 +1,45 @@ +--TEST-- +FPM: Process manager config option pm.start_servers missing +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogNotice( + "pm.start_servers is not set. It's been set to 2.", + 'unconfined' +); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/proc-user-ignored.phpt b/sapi/fpm/tests/proc-user-ignored.phpt new file mode 100644 index 00000000000..42a7dc2392b --- /dev/null +++ b/sapi/fpm/tests/proc-user-ignored.phpt @@ -0,0 +1,46 @@ +--TEST-- +FPM: Process user setting ignored when FPM is not running as root +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogNotice( + "'user' directive is ignored when FPM is not running as root", + 'unconfined' +); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + \ No newline at end of file diff --git a/sapi/fpm/tests/response.inc b/sapi/fpm/tests/response.inc new file mode 100644 index 00000000000..9888ec1b83f --- /dev/null +++ b/sapi/fpm/tests/response.inc @@ -0,0 +1,281 @@ + $data, + 'err_response' => null, + 'out_response' => $data, + ]; + } + + $this->data = $data; + $this->expectInvalid = $expectInvalid; + } + + /** + * @param mixed $body + * @param string $contentType + * @return Response + */ + public function expectBody($body, $contentType = 'text/html') + { + if ($multiLine = is_array($body)) { + $body = implode("\n", $body); + } + + if ( + $this->checkIfValid() && + $this->checkDefaultHeaders($contentType) && + $body !== $this->rawBody + ) { + if ($multiLine) { + $this->error( + "==> The expected body:\n$body\n" . + "==> does not match the actual body:\n$this->rawBody" + ); + } else { + $this->error( + "The expected body '$body' does not match actual body '$this->rawBody'" + ); + } + } + + return $this; + } + + /** + * @return Response + */ + public function expectEmptyBody() + { + return $this->expectBody(''); + } + + /** + * @param string $contentType + * @return string|null + */ + public function getBody($contentType = 'text/html') + { + if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) { + return $this->rawBody; + } + + return null; + } + + /** + * Print raw body + */ + public function dumpBody() + { + var_dump($this->getBody()); + } + + /** + * Print raw body + */ + public function printBody() + { + echo $this->getBody(); + } + + /** + * Debug response output + */ + public function debugOutput() + { + echo "-------------- RESPONSE: --------------\n"; + echo "OUT:\n"; + echo $this->data['out_response']; + echo "ERR:\n"; + echo $this->data['err_response']; + echo "---------------------------------------\n\n"; + } + + /** + * @return string|null + */ + public function getErrorData() + { + return $this->data['err_response']; + } + + /** + * Check if the response is valid and if not emit error message + * + * @return bool + */ + private function checkIfValid() + { + if ($this->isValid()) { + return true; + } + + if (!$this->expectInvalid) { + $this->error("The response is invalid: $this->rawData"); + } + + return false; + } + + /** + * @param string $contentType + * @return bool + */ + private function checkDefaultHeaders($contentType) + { + // check default headers + return ( + $this->checkHeader('X-Powered-By', '|^PHP/7|', true) && + $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true) + ); + } + + /** + * @param string $name + * @param string $value + * @param bool $useRegex + * @return bool + */ + private function checkHeader(string $name, string $value, $useRegex = false) + { + $lcName = strtolower($name); + $headers = $this->getHeaders(); + if (!isset($headers[$lcName])) { + return $this->error("The header $name is not present"); + } + $header = $headers[$lcName]; + + if (!$useRegex) { + if ($header === $value) { + return true; + } + return $this->error("The header $name value '$header' is not the same as '$value'"); + } + + if (!preg_match($value, $header)) { + return $this->error("The header $name value '$header' does not match RegExp '$value'"); + } + + return true; + } + + /** + * @return array|null + */ + private function getHeaders() + { + if (!$this->isValid()) { + return null; + } + + if (is_array($this->headers)) { + return $this->headers; + } + + $headerRows = explode("\r\n", $this->rawHeaders); + $headers = []; + foreach ($headerRows as $headerRow) { + $colonPosition = strpos($headerRow, ':'); + if ($colonPosition === false) { + $this->error("Invalid header row (no colon): $headerRow"); + } + $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim( + substr($headerRow, $colonPosition + 1) + ); + } + + return ($this->headers = $headers); + } + + /** + * @return bool + */ + private function isValid() + { + if ($this->valid === null) { + $this->processData(); + } + + return $this->valid; + } + + /** + * Process data and set validity and raw data + */ + private function processData() + { + $this->rawData = $this->data['out_response']; + $this->valid = ( + !is_null($this->rawData) && + strpos($this->rawData, self::HEADER_SEPARATOR) + ); + if ($this->valid) { + list ($this->rawHeaders, $this->rawBody) = array_map( + 'trim', + explode(self::HEADER_SEPARATOR, $this->rawData) + ); + } + } + + /** + * Emit error message + * + * @param string $message + * @return bool + */ + private function error($message) + { + echo "ERROR: $message\n"; + + return false; + } +} \ No newline at end of file diff --git a/sapi/fpm/tests/skipapparmor.inc b/sapi/fpm/tests/skipapparmor.inc deleted file mode 100644 index b286d0361dc..00000000000 --- a/sapi/fpm/tests/skipapparmor.inc +++ /dev/null @@ -1,30 +0,0 @@ - diff --git a/sapi/fpm/tests/skipif.inc b/sapi/fpm/tests/skipif.inc index 08c6bbff69d..25910a8e05e 100644 --- a/sapi/fpm/tests/skipif.inc +++ b/sapi/fpm/tests/skipif.inc @@ -1,17 +1,15 @@ +if (!FPM\Tester::findExecutable()) { + die("skip php-fpm binary not found"); +} \ No newline at end of file diff --git a/sapi/fpm/tests/socket-invalid-allowed-clients.phpt b/sapi/fpm/tests/socket-invalid-allowed-clients.phpt new file mode 100644 index 00000000000..b2240687fb7 --- /dev/null +++ b/sapi/fpm/tests/socket-invalid-allowed-clients.phpt @@ -0,0 +1,48 @@ +--TEST-- +FPM: Socket for invalid allowed client only +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkRequest('127.0.0.1', 'Req: ok', 'Req: error'); +$tester->terminate(); +// this is from child when starting +$tester->expectLogLine("ERROR: Wrong IP address 'xxx' in listen.allowed_clients"); +$tester->expectLogLine("ERROR: There are no allowed addresses"); +// this is from the request +$tester->expectLogLine("ERROR: Connection disallowed: IP address '127.0.0.1' has been dropped."); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Req: error +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/socket-ipv4-allowed-clients.phpt b/sapi/fpm/tests/socket-ipv4-allowed-clients.phpt new file mode 100644 index 00000000000..3de7c0049b5 --- /dev/null +++ b/sapi/fpm/tests/socket-ipv4-allowed-clients.phpt @@ -0,0 +1,45 @@ +--TEST-- +FPM: Socket for IPv4 allowed client only +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkRequest('127.0.0.1', 'IPv4: ok', 'IPv4: error'); +$tester->checkRequest('[::1]', 'IPv6: ok', 'IPv6: error'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +IPv4: ok +IPv6: error +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/socket-ipv4-basic.phpt b/sapi/fpm/tests/socket-ipv4-basic.phpt new file mode 100644 index 00000000000..5cce244d10d --- /dev/null +++ b/sapi/fpm/tests/socket-ipv4-basic.phpt @@ -0,0 +1,37 @@ +--TEST-- +FPM: Socket for IPv4 connection +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/socket-ipv6-any.phpt b/sapi/fpm/tests/socket-ipv6-any.phpt new file mode 100644 index 00000000000..11441acddbc --- /dev/null +++ b/sapi/fpm/tests/socket-ipv6-any.phpt @@ -0,0 +1,44 @@ +--TEST-- +FPM: Socket for IPv6 any address connection +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->checkConnection('127.0.0.1', 'IPv4: ok'); +$tester->checkConnection('[::1]', 'IPv6: ok'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +IPv4: ok +IPv6: ok +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/socket-ipv6-basic.phpt b/sapi/fpm/tests/socket-ipv6-basic.phpt new file mode 100644 index 00000000000..b91dc19718f --- /dev/null +++ b/sapi/fpm/tests/socket-ipv6-basic.phpt @@ -0,0 +1,40 @@ +--TEST-- +FPM: Socket for IPv6 connection +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/socket-uds-acl.phpt b/sapi/fpm/tests/socket-uds-acl.phpt new file mode 100644 index 00000000000..6423ae446ce --- /dev/null +++ b/sapi/fpm/tests/socket-uds-acl.phpt @@ -0,0 +1,87 @@ +--TEST-- +FPM: Unix Domain Socket with Posix ACL +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->ping('{{ADDR:UDS}}'); +passthru("/usr/bin/getfacl -cp " . $tester->getListen('{{ADDR:UDS}}')); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECTF-- +user::rw- +user:%s:rw- +user:%s:rw- +user:%s:rw- +group::--- +group:%s:rw- +group:%s:rw- +mask::rw- +other::--- + +Done +--CLEAN-- + \ No newline at end of file diff --git a/sapi/fpm/tests/socket-uds-basic.phpt b/sapi/fpm/tests/socket-uds-basic.phpt new file mode 100644 index 00000000000..b22f3384f9d --- /dev/null +++ b/sapi/fpm/tests/socket-uds-basic.phpt @@ -0,0 +1,40 @@ +--TEST-- +FPM: Unix Domain Socket connection +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->ping('{{ADDR:UDS}}'); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/status-basic.phpt b/sapi/fpm/tests/status-basic.phpt new file mode 100644 index 00000000000..323592262f1 --- /dev/null +++ b/sapi/fpm/tests/status-basic.phpt @@ -0,0 +1,50 @@ +--TEST-- +FPM: Status basic test +--SKIPIF-- + +--FILE-- + 'unconfined', + 'process manager' => 'static', + 'listen queue' => 0, + 'max listen queue' => 0, + 'idle processes' => 0, + 'active processes' => 1, + 'total processes' => 1, + 'max active processes' => 1, + 'max children reached' => 0, + 'slow requests' => 0, +]; + +$tester = new FPM\Tester($cfg); +$tester->start(); +$tester->expectLogStartNotices(); +$tester->request()->expectEmptyBody(); +$tester->status($expectedStatusData); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/status.inc b/sapi/fpm/tests/status.inc new file mode 100644 index 00000000000..5965f130eb2 --- /dev/null +++ b/sapi/fpm/tests/status.inc @@ -0,0 +1,199 @@ + 'text/plain', + 'html' => 'text/html', + 'xml' => 'text/xml', + 'json' => 'application/json', + ]; + + /** + * @var array + */ + private $defaultFields = [ + 'pool' => '\w+', + 'process manager' => '(static|dynamic|ondemand)', + 'start time' => '\d+\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2}\s[+-]\d{4}', + 'start since' => '\d+', + 'accepted conn' => '\d+', + 'listen queue' => '\d+', + 'max listen queue' => '\d+', + 'listen queue len' => '\d+', + 'idle processes' => '\d+', + 'active processes' => '\d+', + 'total processes' => '\d+', + 'max active processes' => '\d+', + 'max children reached' => '\d+', + 'slow requests' => '\d+', + ]; + + /** + * Check status page. + * + * @param Response $response + * @param array $fields + * @param string $type + * @throws \Exception + */ + public function checkStatus(Response $response, array $fields, string $type) + { + if (!isset($this->contentTypes[$type])) { + throw new \Exception('Invalid content type ' . $type); + } + + $body = $response->getBody($this->contentTypes[$type]); + if ($body === null) { + return; + } + $method = "checkStatus" . ucfirst($type); + + $this->$method($body, array_merge($this->defaultFields, $fields)); + } + + /** + * Make status check for status page. + * + * @param string $body + * @param array $fields + * @param string $rowPattern + * @param string $header + * @param string $footer + * @param null|callable $nameTransformer + * @param null|callable $valueTransformer + * @param bool $startTimeTimestamp + * @param bool $closingName + */ + private function makeStatusCheck( + string $body, + array $fields, + string $rowPattern, + string $header = '', + string $footer = '', + $nameTransformer = null, + $valueTransformer = null, + bool $startTimeTimestamp = false, + bool $closingName = false + ) { + + if ($startTimeTimestamp && $fields['start time'][0] === '\\') { + $fields['start time'] = '\d+'; + } + $pattern = '|' . $header; + foreach ($fields as $name => $value) { + if ($nameTransformer) { + $name = call_user_func($nameTransformer, $name); + } + if ($valueTransformer) { + $value = call_user_func($valueTransformer, $value); + } + if ($closingName) { + $pattern .= sprintf($rowPattern, $name, $value, $name); + } else { + $pattern .= sprintf($rowPattern, $name, $value); + } + } + $pattern = rtrim($pattern, $rowPattern[strlen($rowPattern) - 1]); + $pattern .= $footer . '|'; + + if (!preg_match($pattern, $body)) { + echo "ERROR: Expected body does not match pattern\n"; + echo "BODY:\n"; + var_dump($body); + echo "PATTERN:\n"; + var_dump($pattern); + } + } + + /** + * Check plain status page. + * + * @param string $body + * @param array $fields + */ + protected function checkStatusPlain(string $body, array $fields) + { + $this->makeStatusCheck($body, $fields, "%s:\s+%s\n"); + } + + /** + * Check html status page. + * + * @param string $body + * @param array $fields + */ + protected function checkStatusHtml(string $body, array $fields) + { + $header = "\n" . + "\n" . + "" . self::HTML_TITLE . "\n" . + "\n\n"; + $footer = "\n
\n"; + + $this->makeStatusCheck( + $body, + $fields, + "%s%s\n", + $header, + $footer + ); + } + + /** + * Check xml status page. + * + * @param string $body + * @param array $fields + */ + protected function checkStatusXml(string $body, array $fields) + { + $this->makeStatusCheck( + $body, + $fields, + "<%s>%s\n", + "<\?xml version=\"1.0\" \?>\n\n", + "\n", + function ($name) { + return str_replace(' ', '-', $name); + }, + null, + true, + true + ); + } + + /** + * Check json status page. + * + * @param string $body + * @param array $fields + */ + protected function checkStatusJson(string $body, array $fields) + { + $this->makeStatusCheck( + $body, + $fields, + '"%s":%s,', + '{', + '}', + null, + function ($value) { + if (is_numeric($value) || $value === '\d+') { + return $value; + } + + return '"' . $value . '"'; + }, + true + ); + } +} \ No newline at end of file diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc new file mode 100644 index 00000000000..c3b6c83e597 --- /dev/null +++ b/sapi/fpm/tests/tester.inc @@ -0,0 +1,1183 @@ +testConfig(); + if ($testResult !== null) { + self::clean(2); + die("skip $testResult"); + } + } + + /** + * Skip test if IPv6 is not supported. + */ + static public function skipIfIPv6IsNotSupported() + { + @stream_socket_client('tcp://[::1]:0', $errno); + if ($errno != 111) { + die('skip IPv6 is not supported.'); + } + } + + /** + * Skip if running on Travis. + * + * @param $message + */ + static public function skipIfTravis($message) + { + if (getenv("TRAVIS")) { + die('skip Travis: ' . $message); + } + } + + /** + * Tester constructor. + * + * @param string|array $configTemplate + * @param string $code + * @param array $options + * @param string $fileName + */ + public function __construct( + $configTemplate, + string $code = '', + array $options = [], + $fileName = null + ) { + $this->configTemplate = $configTemplate; + $this->code = $code; + $this->options = $options; + $this->fileName = $fileName ?: self::getCallerFileName(); + $this->logTool = new LogTool(); + $this->debug = (bool) getenv('TEST_FPM_DEBUG'); + } + + /** + * @param string $ini + */ + public function setUserIni(string $ini) + { + $iniFile = __DIR__ . '/.user.ini'; + file_put_contents($iniFile, $ini); + } + + /** + * Test configuration file. + * + * @return null|string + * @throws \Exception + */ + public function testConfig() + { + $configFile = $this->createConfig(); + $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1'; + exec($cmd, $output, $code); + if ($code) { + return preg_replace("/\[.+?\]/", "", $output[0]); + } + + return null; + } + + /** + * Start PHP-FPM master process + * + * @param string $extraArgs + * @return bool + * @throws \Exception + */ + public function start(string $extraArgs = '') + { + $configFile = $this->createConfig(); + $desc = $this->outDesc ? [] : [1 => array('pipe', 'w')]; + $asRoot = getenv('TEST_FPM_RUN_AS_ROOT') ? '--allow-to-run-as-root' : ''; + $cmd = self::findExecutable() . " $asRoot -F -O -y $configFile $extraArgs"; + /* Since it's not possible to spawn a process under linux without using a + * shell in php (why?!?) we need a little shell trickery, so that we can + * actually kill php-fpm */ + $this->masterProcess = proc_open( + "killit () { kill \$child 2> /dev/null; }; " . + "trap killit TERM; $cmd 2>&1 & child=\$!; wait", + $desc, + $pipes + ); + register_shutdown_function( + function($masterProcess) use($configFile) { + @unlink($configFile); + if (is_resource($masterProcess)) { + @proc_terminate($masterProcess); + while (proc_get_status($masterProcess)['running']) { + usleep(10000); + } + } + }, + $this->masterProcess + ); + if (!$this->outDesc !== false) { + $this->outDesc = $pipes[1]; + } + + return true; + } + + /** + * Run until needle is found in the log. + * + * @param string $needle + * @param int $max + * @return bool + * @throws \Exception + */ + public function runTill(string $needle, $max = 10) + { + $this->start(); + $found = false; + for ($i = 0; $i < $max; $i++) { + $line = $this->getLogLine(); + if (is_null($line)) { + break; + } + if (preg_match($needle, $line) === 1) { + $found = true; + break; + } + } + $this->close(true); + + if (!$found) { + return $this->error("The search pattern not found"); + } + + return true; + } + + /** + * Check if connection works. + * + * @param string $host + * @param null|string $successMessage + * @param null|string $errorMessage + * @param int $attempts + * @param int $delay + */ + public function checkConnection( + $host = '127.0.0.1', + $successMessage = null, + $errorMessage = 'Connection failed', + $attempts = 20, + $delay = 50000 + ) { + $i = 0; + do { + if ($i > 0 && $delay > 0) { + usleep($delay); + } + $fp = @fsockopen($host, $this->getPort()); + } while ((++$i < $attempts) && !$fp); + + if ($fp) { + $this->message($successMessage); + fclose($fp); + } else { + $this->message($errorMessage); + } + } + + + /** + * Execute request with parameters ordered for better checking. + * + * @param string $address + * @param string|null $successMessage + * @param string|null $errorMessage + * @param string $uri + * @param string $query + * @param array $headers + * @return Response + */ + public function checkRequest( + string $address, + string $successMessage = null, + string $errorMessage = null, + $uri = '/ping', + $query = '', + $headers = [] + ) { + return $this->request($query, $headers, $uri, $address, $successMessage, $errorMessage); + } + + /** + * Execute and check ping request. + * + * @param string $address + * @param string $pingPath + * @param string $pingResponse + */ + public function ping( + string $address = '{{ADDR}}', + string $pingResponse = 'pong', + string $pingPath = '/ping' + ) { + $response = $this->request('', [], $pingPath, $address); + $response->expectBody($pingResponse, 'text/plain'); + } + + /** + * Execute and check status request(s). + * + * @param array $expectedFields + * @param string|null $address + * @param string $statusPath + * @param mixed $formats + * @throws \Exception + */ + public function status( + array $expectedFields, + string $address = null, + string $statusPath = '/status', + $formats = ['plain', 'html', 'xml', 'json'] + ) { + if (!is_array($formats)) { + $formats = [$formats]; + } + + require_once "status.inc"; + $status = new Status(); + foreach ($formats as $format) { + $query = $format === 'plain' ? '' : $format; + $response = $this->request($query, [], $statusPath, $address); + $status->checkStatus($response, $expectedFields, $format); + } + } + + /** + * Execute request. + * + * @param string $query + * @param array $headers + * @param string|null $uri + * @param string|null $address + * @param string|null $successMessage + * @param string|null $errorMessage + * @param bool $connKeepAlive + * @return Response + */ + public function request( + string $query = '', + array $headers = [], + string $uri = null, + string $address = null, + string $successMessage = null, + string $errorMessage = null, + bool $connKeepAlive = false + ) { + if ($this->hasError()) { + return new Response(null, true); + } + if (is_null($uri)) { + $uri = $this->makeFile('src.php', $this->code); + } + + $params = array_merge( + [ + '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' => '7777', + 'SERVER_ADDR' => '127.0.0.1', + 'SERVER_PORT' => '80', + 'SERVER_NAME' => php_uname('n'), + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'DOCUMENT_ROOT' => __DIR__, + 'CONTENT_TYPE' => '', + 'CONTENT_LENGTH' => 0 + ], + $headers + ); + + try { + $this->response = new Response( + $this->getClient($address, $connKeepAlive)->request_data($params, false) + ); + $this->message($successMessage); + } catch (\Exception $exception) { + if ($errorMessage === null) { + $this->error("Request failed", $exception); + } else { + $this->message($errorMessage); + } + $this->response = new Response(); + } + if ($this->debug) { + $this->response->debugOutput(); + } + return $this->response; + } + + /** + * Get client. + * + * @param string $address + * @param bool $keepAlive + * @return Client + */ + private function getClient(string $address = null, $keepAlive = false) + { + $address = $address ? $this->processTemplate($address) : $this->getAddr(); + if ($address[0] === '/') { // uds + $host = 'unix://' . $address; + $port = -1; + } elseif ($address[0] === '[') { // ipv6 + $addressParts = explode(']:', $address); + $host = $addressParts[0]; + if (isset($addressParts[1])) { + $host .= ']'; + $port = $addressParts[1]; + } else { + $port = $this->getPort(); + } + } else { // ipv4 + $addressParts = explode(':', $address); + $host = $addressParts[0]; + $port = $addressParts[1] ?? $this->getPort(); + } + + if (!$keepAlive) { + return new Client($host, $port); + } + + if (!isset($this->clients[$host][$port])) { + $client = new Client($host, $port); + $client->setKeepAlive(true); + $this->clients[$host][$port] = $client; + } + + return $this->clients[$host][$port]; + } + + /** + * Display logs + * + * @param int $number + * @param string $ignore + */ + public function displayLog(int $number = 1, string $ignore = 'systemd') + { + /* Read $number lines or until EOF */ + while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { + $a = fgets($this->outDesc); + if (empty($ignore) || !strpos($a, $ignore)) { + echo $a; + $number--; + } + } + } + + /** + * Get a single log line + * + * @return null|string + */ + private function getLogLine() + { + $read = [$this->outDesc]; + $write = null; + $except = null; + if (stream_select($read, $write, $except, 2 )) { + return fgets($this->outDesc); + } else { + return null; + } + } + + /** + * Get log lines + * + * @param int $number + * @param bool $skipBlank + * @param string $ignore + * @return array + */ + public function getLogLines(int $number = 1, bool $skipBlank = false, string $ignore = 'systemd') + { + $lines = []; + /* Read $n lines or until EOF */ + while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { + $line = $this->getLogLine(); + if (is_null($line)) { + break; + } + if ((empty($ignore) || !strpos($line, $ignore)) && (!$skipBlank || strlen(trim($line)) > 0)) { + $lines[] = $line; + $number--; + } + } + + return $lines; + } + + /** + * @return mixed|string + */ + public function getLastLogLine() + { + $lines = $this->getLogLines(); + + return $lines[0] ?? ''; + } + + /** + * Send signal to the supplied PID or the server PID. + * + * @param string $signal + * @param int|null $pid + * @return string + */ + public function signal($signal, int $pid = null) + { + if (is_null($pid)) { + $pid = $this->getPid(); + } + + return exec("kill -$signal $pid"); + } + + /** + * Terminate master process + */ + public function terminate() + { + proc_terminate($this->masterProcess); + } + + /** + * Close all open descriptors and process resources + * + * @param bool $terminate + */ + public function close($terminate = false) + { + if ($terminate) { + $this->terminate(); + } + fclose($this->outDesc); + proc_close($this->masterProcess); + } + + /** + * Create a config file. + * + * @param string $extension + * @return string + * @throws \Exception + */ + private function createConfig($extension = 'ini') + { + if (is_array($this->configTemplate)) { + $configTemplates = $this->configTemplate; + if (!isset($configTemplates['main'])) { + throw new \Exception('The config template array has to have main config'); + } + $mainTemplate = $configTemplates['main']; + unset($configTemplates['main']); + if (!is_dir(self::CONF_DIR)) { + mkdir(self::CONF_DIR); + } + foreach ($configTemplates as $name => $configTemplate) { + $this->makeFile( + 'conf', + $this->processTemplate($configTemplate), + self::CONF_DIR, + $name + ); + } + } else { + $mainTemplate = $this->configTemplate; + } + + return $this->makeFile($extension, $this->processTemplate($mainTemplate)); + } + + /** + * Process template string. + * + * @param string $template + * @return string + */ + private function processTemplate(string $template) + { + $vars = [ + 'FILE:LOG:ACC' => ['getAbsoluteFile', self::FILE_EXT_LOG_ACC], + 'FILE:LOG:ERR' => ['getAbsoluteFile', self::FILE_EXT_LOG_ERR], + 'FILE:LOG:SLOW' => ['getAbsoluteFile', self::FILE_EXT_LOG_SLOW], + 'FILE:PID' => ['getAbsoluteFile', self::FILE_EXT_PID], + 'RFILE:LOG:ACC' => ['getRelativeFile', self::FILE_EXT_LOG_ACC], + 'RFILE:LOG:ERR' => ['getRelativeFile', self::FILE_EXT_LOG_ERR], + 'RFILE:LOG:SLOW' => ['getRelativeFile', self::FILE_EXT_LOG_SLOW], + 'RFILE:PID' => ['getRelativeFile', self::FILE_EXT_PID], + 'ADDR:IPv4' => ['getAddr', 'ipv4'], + 'ADDR:IPv4:ANY' => ['getAddr', 'ipv4-any'], + 'ADDR:IPv6' => ['getAddr', 'ipv6'], + 'ADDR:IPv6:ANY' => ['getAddr', 'ipv6-any'], + 'ADDR:UDS' => ['getAddr', 'uds'], + 'PORT' => ['getPort', 'ip'], + 'INCLUDE:CONF' => self::CONF_DIR . '/*.conf', + ]; + $aliases = [ + 'ADDR' => 'ADDR:IPv4', + 'FILE:LOG' => 'FILE:LOG:ERR', + ]; + foreach ($aliases as $aliasName => $aliasValue) { + $vars[$aliasName] = $vars[$aliasValue]; + } + + return preg_replace_callback( + '/{{([a-zA-Z0-9:]+)(\[\w+\])?}}/', + function ($matches) use ($vars) { + $varName = $matches[1]; + if (!isset($vars[$varName])) { + $this->error("Invalid config variable $varName"); + return 'INVALID'; + } + $pool = $matches[2] ?? 'default'; + $varValue = $vars[$varName]; + if (is_string($varValue)) { + return $varValue; + } + $functionName = array_shift($varValue); + $varValue[] = $pool; + return call_user_func_array([$this, $functionName], $varValue); + }, + $template + ); + } + + /** + * @param string $type + * @param string $pool + * @return string + */ + public function getAddr(string $type = 'ipv4', $pool = 'default') + { + $port = $this->getPort($type, $pool, true); + if ($type === 'uds') { + return $this->getFile($port . '.sock'); + } + + return $this->getHost($type) . ':' . $port; + } + + /** + * @param string $type + * @param string $pool + * @param bool $useAsId + * @return int + */ + public function getPort(string $type = 'ip', $pool = 'default', $useAsId = false) + { + if ($type === 'uds' && !$useAsId) { + return -1; + } + + if (isset($this->ports['values'][$pool])) { + return $this->ports['values'][$pool]; + } + $port = ($this->ports['last'] ?? 9000 + PHP_INT_SIZE - 1) + 1; + $this->ports['values'][$pool] = $this->ports['last'] = $port; + + return $port; + } + + /** + * @param string $type + * @return string + */ + public function getHost(string $type = 'ipv4') + { + switch ($type) { + case 'ipv6-any': + return '[::]'; + case 'ipv6': + return '[::1]'; + case 'ipv4-any': + return '0.0.0.0'; + default: + return '127.0.0.1'; + } + } + + /** + * Get listen address. + * + * @param string|null $template + * @return string + */ + public function getListen($template = null) + { + return $template ? $this->processTemplate($template) : $this->getAddr(); + } + + /** + * Get PID. + * + * @return int + */ + public function getPid() + { + $pidFile = $this->getFile('pid'); + if (!is_file($pidFile)) { + return (int) $this->error("PID file has not been created"); + } + $pidContent = file_get_contents($pidFile); + if (!is_numeric($pidContent)) { + return (int) $this->error("PID content '$pidContent' is not integer"); + } + + return (int) $pidContent; + } + + + /** + * @param string $extension + * @param string|null $dir + * @param string|null $name + * @return string + */ + private function getFile(string $extension, $dir = null, $name = null) + { + $fileName = (is_null($name) ? $this->fileName : $name . '.') . $extension; + + return is_null($dir) ? $fileName : $dir . '/' . $fileName; + } + + /** + * @param string $extension + * @return string + */ + private function getAbsoluteFile(string $extension) + { + return $this->getFile($extension); + } + + /** + * @param string $extension + * @return string + */ + private function getRelativeFile(string $extension) + { + $fileName = rtrim(basename($this->fileName), '.'); + + return $this->getFile($extension, null, $fileName); + } + + /** + * @param string $extension + * @param string $prefix + * @return string + */ + private function getPrefixedFile(string $extension, string $prefix = null) + { + $fileName = rtrim($this->fileName, '.'); + if (!is_null($prefix)) { + $fileName = $prefix . '/' . basename($fileName); + } + + return $this->getFile($extension, null, $fileName); + } + + /** + * @param string $extension + * @param string $content + * @param string|null $dir + * @param string|null $name + * @return string + */ + private function makeFile(string $extension, string $content = '', $dir = null, $name = null) + { + $filePath = $this->getFile($extension, $dir, $name); + file_put_contents($filePath, $content); + + return $filePath; + } + + /** + * @param string|null $msg + */ + private function message($msg) + { + if ($msg !== null) { + echo "$msg\n"; + } + } + + /** + * @param string $msg + * @param \Exception|null $exception + */ + private function error($msg, \Exception $exception = null) + { + $this->error = 'ERROR: ' . $msg; + if ($exception) { + $this->error .= '; EXCEPTION: ' . $exception->getMessage(); + } + $this->error .= "\n"; + + echo $this->error; + } + + /** + * @return bool + */ + private function hasError() + { + return !is_null($this->error) || !is_null($this->logTool->getError()); + } + + /** + * Expect file with a supplied extension to exist. + * + * @param string $extension + * @param string $prefix + * @return bool + */ + public function expectFile(string $extension, $prefix = null) + { + $filePath = $this->getPrefixedFile($extension, $prefix); + if (!file_exists($filePath)) { + return $this->error("The file $filePath does not exist"); + } + + return true; + } + + /** + * Expect file with a supplied extension to not exist. + * + * @param string $extension + * @param string $prefix + * @return bool + */ + public function expectNoFile(string $extension, $prefix = null) + { + $filePath = $this->getPrefixedFile($extension, $prefix); + if (file_exists($filePath)) { + return $this->error("The file $filePath exists"); + } + + return true; + } + + /** + * Expect message to be written to FastCGI error stream. + * + * @param string $message + * @param int $limit + * @param int $repeat + */ + public function expectFastCGIErrorMessage( + string $message, + int $limit = 1024, + int $repeat = 0 + ) { + $this->logTool->setExpectedMessage($message, $limit, $repeat); + $this->logTool->checkTruncatedMessage($this->response->getErrorData()); + } + + /** + * Expect starting lines to be logged. + */ + public function expectLogStartNotices() + { + $this->logTool->expectStartingLines($this->getLogLines(2)); + } + + /** + * Expect terminating lines to be logged. + */ + public function expectLogTerminatingNotices() + { + $this->logTool->expectTerminatorLines($this->getLogLines(-1)); + } + + /** + * Expect log message that can span multiple lines. + * + * @param string $message + * @param int $limit + * @param int $repeat + * @param bool $decorated + * @param bool $wrapped + */ + public function expectLogMessage( + string $message, + int $limit = 1024, + int $repeat = 0, + bool $decorated = true, + bool $wrapped = true + ) { + $this->logTool->setExpectedMessage($message, $limit, $repeat); + if ($wrapped) { + $logLines = $this->getLogLines(-1, true); + $this->logTool->checkWrappedMessage($logLines, true, $decorated); + } else { + $logLines = $this->getLogLines(1, true); + $this->logTool->checkTruncatedMessage($logLines[0] ?? ''); + } + if ($this->debug) { + $this->message("-------------- LOG LINES: -------------"); + var_dump($logLines); + $this->message("---------------------------------------\n"); + } + } + + /** + * Expect a single log line. + * + * @param string $message + * @return bool + */ + public function expectLogLine(string $message) + { + $messageLen = strlen($message); + $limit = $messageLen > 1024 ? $messageLen + 16 : 1024; + $this->logTool->setExpectedMessage($message, $limit); + $logLines = $this->getLogLines(1, true); + if ($this->debug) { + $this->message("LOG LINE: " . ($logLines[0] ?? '')); + } + + return $this->logTool->checkWrappedMessage($logLines, false); + } + + /** + * Expect a log debug message. + * + * @param string $message + * @param string|null $pool + * @return bool + */ + public function expectLogDebug(string $message, $pool = null) + { + return $this->logTool->expectDebug($this->getLastLogLine(), $message, $pool); + } + + /** + * Expect a log notice. + * + * @param string $message + * @param string|null $pool + * @return bool + */ + public function expectLogNotice(string $message, $pool = null) + { + return $this->logTool->expectNotice($this->getLastLogLine(), $message, $pool); + } + + /** + * Expect a log warning. + * + * @param string $message + * @param string|null $pool + * @return bool + */ + public function expectLogWarning(string $message, $pool = null) + { + return $this->logTool->expectWarning($this->getLastLogLine(), $message, $pool); + } + + /** + * Expect a log error. + * + * @param string $message + * @param string|null $pool + * @return bool + */ + public function expectLogError(string $message, $pool = null) + { + return $this->logTool->expectError($this->getLastLogLine(), $message, $pool); + } + + /** + * Expect a log alert. + * + * @param string $message + * @param string|null $pool + * @return bool + */ + public function expectLogAlert(string $message, $pool = null) + { + return $this->logTool->expectAlert($this->getLastLogLine(), $message, $pool); + } + + /** + * Expect no log lines to be logged. + * + * @return bool + */ + public function expectNoLogMessages() + { + $logLines = $this->getLogLines(-1, true); + if (!empty($logLines)) { + return $this->error( + "Expected no log lines but following lines logged:\n" . implode("\n", $logLines) + ); + } + + return true; + } + + /** + * Print content of access log. + */ + public function printAccessLog() + { + $accessLog = $this->getFile('acc.log'); + if (is_file($accessLog)) { + print file_get_contents($accessLog); + } + } +} \ No newline at end of file