fcgi.inc 20 KB

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