fcgi.inc 21 KB

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