123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- <?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 {}
- class ReadLimitExceeded 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 int
- */
- private $_port = null;
- /**
- * Keep Alive
- * @var bool
- */
- 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 bool
- */
- private $_persistentSocket = false;
- /**
- * Connect timeout in milliseconds
- * @var int
- */
- private $_connectTimeout = 5000;
- /**
- * Read/Write timeout in milliseconds
- * @var int
- */
- private $_readWriteTimeout = 5000;
- /**
- * Constructor
- *
- * @param string $host Host of the FastCGI application
- * @param int $port Port of the FastCGI application
- */
- public function __construct($host, $port)
- {
- $this->_host = $host;
- $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 bool $b true if the connection should stay alive, false otherwise
- */
- public function setKeepAlive($b)
- {
- $this->_keepAlive = (bool)$b;
- if (!$this->_keepAlive && $this->_sock) {
- fclose($this->_sock);
- }
- }
- /**
- * Get the keep alive status
- *
- * @return bool 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 bool $b true if persistent socket should be used, false otherwise
- */
- public function setPersistentSocket($b)
- {
- $was_persistent = ($this->_sock && $this->_persistentSocket);
- $this->_persistentSocket = (bool)$b;
- if (!$this->_persistentSocket && $was_persistent) {
- fclose($this->_sock);
- }
- }
- /**
- * Get the pesistent socket status
- *
- * @return bool true if the socket should be persistent, false otherwise
- */
- public function getPersistentSocket()
- {
- return $this->_persistentSocket;
- }
- /**
- * Set the connect timeout
- *
- * @param int number of milliseconds before connect will timeout
- */
- public function setConnectTimeout($timeoutMs)
- {
- $this->_connectTimeout = $timeoutMs;
- }
- /**
- * Get the connect timeout
- *
- * @return int number of milliseconds before connect will timeout
- */
- public function getConnectTimeout()
- {
- return $this->_connectTimeout;
- }
- /**
- * Set the read/write timeout
- *
- * @param int 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 int 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 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
- );
- }
- /**
- * 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 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)
- {
- $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
- *
- * @param int $readLimit max content size
- * @return array
- * @throws ReadLimitExceeded
- */
- private function readPacket($readLimit = -1)
- {
- if ($packet = fread($this->_sock, self::HEADER_LEN)) {
- $resp = $this->decodePacketHeader($packet);
- $resp['content'] = '';
- if ($resp['contentLength']) {
- $len = $resp['contentLength'];
- if ($readLimit >= 0 && $len > $readLimit) {
- // close connection so it can be re-set reset and throw an error
- fclose($this->_sock);
- $this->_sock = null;
- throw new ReadLimitExceeded("Content has $len bytes but the limit is $readLimit bytes");
- }
- 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 Information on the FastCGI application
- *
- * @param array $requestedInfo information to retrieve
- * @return array
- * @throws \Exception
- */
- 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 and return response body
- *
- * @param array $params Array of parameters
- * @param string $stdin Content
- * @return string
- * @throws ForbiddenException
- * @throws TimedOutException
- * @throws \Exception
- */
- 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 and return request data
- *
- * @param array $params Array of parameters
- * @param string $stdin Content
- * @param int $readLimit [optional] the number of bytes to accept in a single packet or -1 if unlimited
- * @return array
- * @throws ForbiddenException
- * @throws TimedOutException
- * @throws \Exception
- */
- public function request_data(array $params, $stdin, $readLimit = -1)
- {
- $id = $this->async_request($params, $stdin);
- return $this->wait_for_response_data($id, 0, $readLimit);
- }
- /**
- * Execute a request to the FastCGI application asynchronously
- *
- * 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 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 int
- * @throws TimedOutException
- * @throws \Exception
- */
- 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 kept 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,
- 'err_response' => null,
- 'out_response' => null,
- );
- return $id;
- }
- /**
- * Append response data.
- *
- * @param $resp Response
- * @param $type Either err or our
- *
- * @throws \Exception
- */
- private function fcgi_stream_append($resp, $type) {
- if (isset($this->_requests[$resp['requestId']][$type . '_finished'])) {
- throw new \Exception('FCGI_STD' . strtoupper($type) . ' stream already finished by empty record');
- }
- if ($resp['content'] === '') {
- $this->_requests[$resp['requestId']][$type . '_finished'] = true;
- } else {
- $this->_requests[$resp['requestId']][$type . '_response'] .= $resp['content'];
- }
- }
- /**
- * Blocking call that waits for response data of the specific request
- *
- * @param int $requestId
- * @param int $timeoutMs [optional] the number of milliseconds to wait.
- * @param int $readLimit [optional] the number of bytes to accept in a single packet or -1 if unlimited
- * @return array response data
- * @throws ForbiddenException
- * @throws TimedOutException
- * @throws \Exception
- */
- public function wait_for_response_data($requestId, $timeoutMs = 0, $readLimit = -1)
- {
- 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);
- while ($resp = $this->readPacket($readLimit)) {
- if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
- if ($resp['type'] == self::STDERR) {
- $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR;
- $this->fcgi_stream_append($resp, 'err');
- } else {
- $this->fcgi_stream_append($resp, 'out');
- }
- $this->_requests[$resp['requestId']]['response'] .= $resp['content'];
- } elseif ($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');
- }
- }
- 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];
- }
- }
- /**
- * 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'];
- }
- }
|