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 */ 25namespace FPM\FastCGI; 26 27class TimedOutException extends \Exception {} 28class ForbiddenException extends \Exception {} 29class ReadLimitExceeded extends \Exception {} 30 31class TransportException extends \Exception {} 32 33interface Transport 34{ 35 /** 36 * Connect to the application. 37 * 38 * @param string $host Host address. 39 * @param int $port Port number. 40 * @throws TransportException 41 */ 42 public function connect(string $host, int $port, ?int $connectionTimeout): void; 43 44 /** 45 * Set keep alive. 46 * 47 * @param bool $keepAlive Whether to enable keep alive. 48 */ 49 public function setKeepAlive(bool $keepAlive): void; 50 51 /** 52 * Set data reading and writing timeout. 53 * 54 * @param int $timeoutMs 55 * @return bool 56 */ 57 public function setDataTimeout(int $timeoutMs): bool; 58 59 /** 60 * Read data. 61 * 62 * @param int $numBytes Number of bytes to read. 63 * @throws TransportException 64 * @return string 65 */ 66 public function read(int $numBytes): string; 67 68 /** 69 * Write data. 70 * 71 * @param string $bytes Bytes to write. 72 * @throws TransportException 73 * @return int Number of bytes written. 74 */ 75 public function write(string $bytes): int; 76 77 public function getMetaData(): array; 78 79 /** 80 * Flush data. 81 * 82 * @return bool 83 */ 84 public function flush(): bool; 85 86 /** 87 * Close connection. 88 * 89 * @return bool 90 */ 91 public function close(): bool; 92} 93 94/** 95 * Stream transport. 96 * 97 * Iis based on PHP streams and should be more reliable as it automatically handles timeouts and 98 * other features. 99 */ 100class StreamTransport implements Transport 101{ 102 /** 103 * @var resource|null|false 104 */ 105 private $stream = null; 106 107 /** 108 * @var bool 109 */ 110 private bool $keepAlive = false; 111 112 /** 113 * @inheritDoc 114 */ 115 public function connect(string $host, int $port, ?int $connectionTimeout = 5000): void 116 { 117 if ($this->stream) { 118 return; 119 } 120 $this->stream = fsockopen( 121 $host, 122 $port, 123 $errno, 124 $errstr, 125 $connectionTimeout / 1000 126 ); 127 128 if (!$this->stream) { 129 throw new TransportException('Unable to connect to FastCGI application: ' . $errstr); 130 } 131 132 if ($this->keepAlive) { 133 $this->setKeepAlive(true); 134 } 135 } 136 137 /** 138 * @inheritDoc 139 */ 140 public function setDataTimeout(int $timeoutMs): bool 141 { 142 if (!$this->stream) { 143 return false; 144 } 145 return stream_set_timeout( 146 $this->stream, 147 floor($timeoutMs / 1000), 148 ($timeoutMs % 1000) * 1000 149 ); 150 } 151 152 /** 153 * @inheritDoc 154 */ 155 public function setKeepAlive(bool $keepAlive): void 156 { 157 $this->keepAlive = $keepAlive; 158 if (!$this->stream) { 159 return; 160 } 161 if ($keepAlive) { 162 $socket = socket_import_stream($this->stream); 163 if ($socket) { 164 socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1); 165 } 166 } else { 167 $this->close(); 168 } 169 } 170 171 /** 172 * @inheritDoc 173 */ 174 public function read(int $numBytes): string 175 { 176 $result = fread($this->stream, $numBytes); 177 if ($result === false) { 178 throw new TransportException('Reading from the stream failed'); 179 } 180 return $result; 181 } 182 183 /** 184 * @inheritDoc 185 */ 186 public function write(string $bytes): int 187 { 188 $result = fwrite($this->stream, $bytes); 189 if ($result === false) { 190 throw new TransportException('Writing to the stream failed'); 191 } 192 return $result; 193 } 194 195 public function getMetaData(): array 196 { 197 return stream_get_meta_data($this->stream); 198 } 199 200 /** 201 * @inheritDoc 202 */ 203 public function flush(): bool 204 { 205 return fflush($this->stream); 206 } 207 208 public function close(): bool 209 { 210 if ($this->stream) { 211 $result = fclose($this->stream); 212 $this->stream = null; 213 return $result; 214 } 215 216 return false; 217 } 218} 219 220/** 221 * Socket transport. 222 * 223 * This transport is more low level than stream and supports some extra socket options like 224 * SO_KEEPALIVE. However, it is currently less robust and missing some stream features like 225 * connection timeout. It should be used only for specific use cases. 226 */ 227class SocketTransport implements Transport 228{ 229 /** 230 * @var \Socket 231 */ 232 private ?\Socket $socket = null; 233 234 /** 235 * @var int 236 */ 237 protected int $dataTimeoutMs = 5000; 238 239 /** 240 * @var bool 241 */ 242 private bool $keepAlive = false; 243 244 /** 245 * @inheritDoc 246 */ 247 public function connect(string $host, int $port, ?int $connectionTimeout = 5000): void 248 { 249 if ($this->socket) { 250 return; 251 } 252 $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 253 if (!$this->socket) { 254 throw new TransportException('Unable to create socket: ' . socket_strerror(socket_last_error())); 255 } 256 257 $ip = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host); 258 259 if (!socket_connect($this->socket, $ip, $port)) { 260 $error = socket_strerror(socket_last_error($this->socket)); 261 throw new TransportException('Unable to connect to FastCGI application: ' . $error); 262 } 263 264 if ($this->keepAlive) { 265 $this->setKeepAlive(true); 266 } 267 } 268 269 /** 270 * @inheritDoc 271 */ 272 public function setDataTimeout(int $timeoutMs): bool 273 { 274 $this->dataTimeoutMs = $timeoutMs; 275 return true; 276 } 277 278 /** 279 * @inheritDoc 280 */ 281 public function setKeepAlive(bool $keepAlive): void 282 { 283 $this->keepAlive = $keepAlive; 284 if (!$this->socket) { 285 return; 286 } 287 if ($keepAlive) { 288 socket_set_option($this->socket, SOL_SOCKET, SO_KEEPALIVE, 1); 289 } else { 290 $this->close(); 291 } 292 } 293 294 private function select(array $read, array $write = [], array $except = []): bool 295 { 296 return socket_select( 297 $read, 298 $write, 299 $except, 300 floor($this->dataTimeoutMs / 1000), 301 ($this->dataTimeoutMs % 1000) * 1000 302 ); 303 } 304 305 /** 306 * @inheritDoc 307 */ 308 public function read(int $numBytes): string 309 { 310 if ($this->select([$this->socket]) === false) { 311 throw new TimedOutException('Reading timeout'); 312 } 313 $result = socket_read($this->socket, $numBytes); 314 if ($result === false) { 315 throw new TransportException('Reading from the stream failed'); 316 } 317 return $result; 318 } 319 320 /** 321 * @inheritDoc 322 */ 323 public function write(string $bytes): int 324 { 325 if ($this->select([], [$this->socket]) === false) { 326 throw new TimedOutException('Writing timeout'); 327 } 328 $result = socket_write($this->socket, $bytes); 329 if ($result === false) { 330 throw new TransportException('Writing to the stream failed'); 331 } 332 return $result; 333 } 334 335 public function getMetaData(): array 336 { 337 return []; 338 } 339 340 /** 341 * @inheritDoc 342 */ 343 public function flush(): bool 344 { 345 return true; 346 } 347 348 public function close(): bool 349 { 350 if ($this->socket) { 351 socket_close($this->socket); 352 $this->socket = null; 353 return true; 354 } 355 356 return false; 357 } 358} 359 360/** 361 * Handles communication with a FastCGI application 362 * 363 * @author Pierrick Charron <pierrick@adoy.net>, Jakub Zelenka <bukka@php.net> 364 * @version 2.0 365 */ 366class Client 367{ 368 const VERSION_1 = 1; 369 370 const BEGIN_REQUEST = 1; 371 const ABORT_REQUEST = 2; 372 const END_REQUEST = 3; 373 const PARAMS = 4; 374 const STDIN = 5; 375 const STDOUT = 6; 376 const STDERR = 7; 377 const DATA = 8; 378 const GET_VALUES = 9; 379 const GET_VALUES_RESULT = 10; 380 const UNKNOWN_TYPE = 11; 381 const MAXTYPE = self::UNKNOWN_TYPE; 382 383 const RESPONDER = 1; 384 const AUTHORIZER = 2; 385 const FILTER = 3; 386 387 const REQUEST_COMPLETE = 0; 388 const CANT_MPX_CONN = 1; 389 const OVERLOADED = 2; 390 const UNKNOWN_ROLE = 3; 391 392 const MAX_CONNS = 'FCGI_MAX_CONNS'; 393 const MAX_REQS = 'FCGI_MAX_REQS'; 394 const MPXS_CONNS = 'FCGI_MPXS_CONNS'; 395 396 const HEADER_LEN = 8; 397 398 const REQ_STATE_WRITTEN = 1; 399 const REQ_STATE_OK = 2; 400 const REQ_STATE_ERR = 3; 401 const REQ_STATE_TIMED_OUT = 4; 402 403 /** 404 * Host 405 * @var string 406 */ 407 private $_host = null; 408 409 /** 410 * Port 411 * @var int 412 */ 413 private $_port = null; 414 415 /** 416 * Keep Alive 417 * @var bool 418 */ 419 private $_keepAlive = false; 420 421 /** 422 * Outstanding request statuses keyed by request id 423 * 424 * Each request is an array with following form: 425 * 426 * array( 427 * 'state' => REQ_STATE_* 428 * 'response' => null | string 429 * ) 430 * 431 * @var array 432 */ 433 private $_requests = array(); 434 435 /** 436 * Connect timeout in milliseconds 437 * @var int 438 */ 439 private $_connectTimeout = 5000; 440 441 /** 442 * Read/Write timeout in milliseconds 443 * @var int 444 */ 445 private $_readWriteTimeout = 5000; 446 447 /** 448 * Data transport instance 449 * @var Transport 450 */ 451 private Transport $transport; 452 453 /** 454 * Constructor 455 * 456 * @param string $host Host of the FastCGI application 457 * @param int $port Port of the FastCGI application 458 * @param Transport $transport Transport 459 */ 460 public function __construct($host, $port, Transport $transport) 461 { 462 $this->_host = $host; 463 $this->_port = $port; 464 465 $this->transport = $transport; 466 } 467 468 /** 469 * Get host. 470 * 471 * @return string 472 */ 473 public function getHost() 474 { 475 return $this->_host; 476 } 477 478 /** 479 * Define whether the FastCGI application should keep the connection 480 * alive at the end of a request and additionally set SO_KEEPALIVE or not. 481 * 482 * @param bool $connKeepAlive true if the connection should stay alive, false otherwise 483 * @param bool $socketKeepAlive true if the socket SO_KEEPALIVE should be set, false otherwise 484 */ 485 public function setKeepAlive(bool $connKeepAlive, bool $socketKeepAlive) 486 { 487 $this->_keepAlive = $connKeepAlive; 488 $this->transport->setKeepAlive($socketKeepAlive); 489 } 490 491 /** 492 * Get the keep alive status 493 * 494 * @return bool true if the connection should stay alive, false otherwise 495 */ 496 public function getKeepAlive() 497 { 498 return $this->_keepAlive; 499 } 500 501 /** 502 * Set the connect timeout 503 * 504 * @param int number of milliseconds before connect will timeout 505 */ 506 public function setConnectTimeout($timeoutMs) 507 { 508 $this->_connectTimeout = $timeoutMs; 509 } 510 511 /** 512 * Get the connect timeout 513 * 514 * @return int number of milliseconds before connect will timeout 515 */ 516 public function getConnectTimeout() 517 { 518 return $this->_connectTimeout; 519 } 520 521 /** 522 * Set the read/write timeout 523 * 524 * @param int number of milliseconds before read or write call will timeout 525 */ 526 public function setReadWriteTimeout($timeoutMs) 527 { 528 $this->_readWriteTimeout = $timeoutMs; 529 $this->transport->setDataTimeout($this->_readWriteTimeout); 530 } 531 532 /** 533 * Get the read timeout 534 * 535 * @return int number of milliseconds before read will timeout 536 */ 537 public function getReadWriteTimeout() 538 { 539 return $this->_readWriteTimeout; 540 } 541 542 /** 543 * Create a connection to the FastCGI application 544 */ 545 private function connect() 546 { 547 $this->transport->connect($this->_host, $this->_port, $this->_connectTimeout); 548 $this->transport->setDataTimeout($this->_readWriteTimeout); 549 } 550 551 /** 552 * Build a FastCGI packet 553 * 554 * @param int $type Type of the packet 555 * @param string $content Content of the packet 556 * @param int $requestId RequestId 557 * @return string 558 */ 559 private function buildPacket($type, $content, $requestId = 1) 560 { 561 $clen = strlen($content); 562 return chr(self::VERSION_1) /* version */ 563 . chr($type) /* type */ 564 . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */ 565 . chr($requestId & 0xFF) /* requestIdB0 */ 566 . chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */ 567 . chr($clen & 0xFF) /* contentLengthB0 */ 568 . chr(0) /* paddingLength */ 569 . chr(0) /* reserved */ 570 . $content; /* content */ 571 } 572 573 /** 574 * Build an FastCGI Name value pair 575 * 576 * @param string $name Name 577 * @param string $value Value 578 * @return string FastCGI Name value pair 579 */ 580 private function buildNvpair($name, $value) 581 { 582 $nlen = strlen($name); 583 $vlen = strlen($value); 584 if ($nlen < 128) { 585 /* nameLengthB0 */ 586 $nvpair = chr($nlen); 587 } else { 588 /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ 589 $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) 590 . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); 591 } 592 if ($vlen < 128) { 593 /* valueLengthB0 */ 594 $nvpair .= chr($vlen); 595 } else { 596 /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */ 597 $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) 598 . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); 599 } 600 /* nameData & valueData */ 601 return $nvpair . $name . $value; 602 } 603 604 /** 605 * Read a set of FastCGI Name value pairs 606 * 607 * @param string $data Data containing the set of FastCGI NVPair 608 * @return array of NVPair 609 */ 610 private function readNvpair($data, $length = null) 611 { 612 $array = array(); 613 614 if ($length === null) { 615 $length = strlen($data); 616 } 617 618 $p = 0; 619 620 while ($p != $length) { 621 622 $nlen = ord($data[$p++]); 623 if ($nlen >= 128) { 624 $nlen = ($nlen & 0x7F << 24); 625 $nlen |= (ord($data[$p++]) << 16); 626 $nlen |= (ord($data[$p++]) << 8); 627 $nlen |= (ord($data[$p++])); 628 } 629 $vlen = ord($data[$p++]); 630 if ($vlen >= 128) { 631 $vlen = ($nlen & 0x7F << 24); 632 $vlen |= (ord($data[$p++]) << 16); 633 $vlen |= (ord($data[$p++]) << 8); 634 $vlen |= (ord($data[$p++])); 635 } 636 $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); 637 $p += ($nlen + $vlen); 638 } 639 640 return $array; 641 } 642 643 /** 644 * Decode a FastCGI Packet 645 * 646 * @param string $data string containing all the packet 647 * @return array 648 */ 649 private function decodePacketHeader($data) 650 { 651 $ret = array(); 652 $ret['version'] = ord($data[0]); 653 $ret['type'] = ord($data[1]); 654 $ret['requestId'] = (ord($data[2]) << 8) + ord($data[3]); 655 $ret['contentLength'] = (ord($data[4]) << 8) + ord($data[5]); 656 $ret['paddingLength'] = ord($data[6]); 657 $ret['reserved'] = ord($data[7]); 658 return $ret; 659 } 660 661 /** 662 * Read a FastCGI Packet 663 * 664 * @param int $readLimit max content size 665 * @return array 666 * @throws ReadLimitExceeded 667 * @throws TransportException 668 */ 669 private function readPacket($readLimit = -1) 670 { 671 if ($packet = $this->transport->read(self::HEADER_LEN)) { 672 $resp = $this->decodePacketHeader($packet); 673 $resp['content'] = ''; 674 if ($resp['contentLength']) { 675 $len = $resp['contentLength']; 676 if ($readLimit >= 0 && $len > $readLimit) { 677 // close connection so it can be re-set reset and throw an error 678 $this->transport->close(); 679 throw new ReadLimitExceeded("Content has $len bytes but the limit is $readLimit bytes"); 680 } 681 while ($len && $buf = $this->transport->read($len)) { 682 $len -= strlen($buf); 683 $resp['content'] .= $buf; 684 } 685 } 686 if ($resp['paddingLength']) { 687 $this->transport->read($resp['paddingLength']); 688 } 689 return $resp; 690 } else { 691 return false; 692 } 693 } 694 695 /** 696 * Get Information on the FastCGI application 697 * 698 * @param array $requestedInfo information to retrieve 699 * @return array 700 * @throws \Exception 701 */ 702 public function getValues(array $requestedInfo) 703 { 704 $this->connect(); 705 706 $request = ''; 707 foreach ($requestedInfo as $info) { 708 $request .= $this->buildNvpair($info, ''); 709 } 710 $this->transport->write($this->buildPacket(self::GET_VALUES, $request, 0)); 711 712 $resp = $this->readPacket(); 713 if (isset($resp['type']) && $resp['type'] == self::GET_VALUES_RESULT) { 714 return $this->readNvpair($resp['content'], $resp['contentLength']); 715 } else { 716 throw new \Exception('Unexpected response type, expecting GET_VALUES_RESULT'); 717 } 718 } 719 720 /** 721 * Execute a request to the FastCGI application and return response body 722 * 723 * @param array $params Array of parameters 724 * @param string $stdin Content 725 * @return string 726 * @throws ForbiddenException 727 * @throws TimedOutException 728 * @throws \Exception 729 */ 730 public function request(array $params, $stdin) 731 { 732 $id = $this->async_request($params, $stdin); 733 return $this->wait_for_response($id); 734 } 735 736 /** 737 * Execute a request to the FastCGI application and return request data 738 * 739 * @param array $params Array of parameters 740 * @param string $stdin Content 741 * @param int $readLimit [optional] the number of bytes to accept in a single packet or -1 if unlimited 742 * @param int $writeDelayMs Number of milliseconds to wait before write 743 * @return array 744 * @throws ForbiddenException 745 * @throws TimedOutException 746 * @throws \Exception 747 */ 748 public function request_data(array $params, $stdin, int $readLimit = -1, int $timoutMs = 0, int $writeDelayMs = 0) 749 { 750 $id = $this->async_request($params, $stdin, $writeDelayMs); 751 return $this->wait_for_response_data($id, $timoutMs, $readLimit); 752 } 753 754 /** 755 * Execute a request to the FastCGI application asynchronously 756 * 757 * This sends request to application and returns the assigned ID for that request. 758 * 759 * You should keep this id for later use with wait_for_response(). Ids are chosen randomly 760 * rather than sequentially to guard against false-positives when using persistent sockets. 761 * In that case it is possible that a delayed response to a request made by a previous script 762 * invocation comes back on this socket and is mistaken for response to request made with same 763 * ID during this request. 764 * 765 * @param array $params Array of parameters 766 * @param string $stdin Content 767 * @param int $writeDelayMs Number of milliseconds to wait before write 768 * @return int 769 * @throws TimedOutException 770 * @throws \Exception 771 */ 772 public function async_request(array $params, $stdin, int $writeDelayMs = 0) 773 { 774 $this->connect(); 775 776 // Pick random number between 1 and max 16 bit unsigned int 65535 777 $id = mt_rand(1, (1 << 16) - 1); 778 779 // Using persistent sockets implies you want them kept alive by server! 780 $keepAlive = intval($this->_keepAlive); 781 782 $request = $this->buildPacket( 783 self::BEGIN_REQUEST, 784 chr(0) . chr(self::RESPONDER) . chr($keepAlive) 785 . str_repeat(chr(0), 5), 786 $id 787 ); 788 789 $paramsRequest = ''; 790 foreach ($params as $key => $value) { 791 $paramsRequest .= $this->buildNvpair($key, $value, $id); 792 } 793 if ($paramsRequest) { 794 $request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id); 795 } 796 $request .= $this->buildPacket(self::PARAMS, '', $id); 797 798 if ($stdin) { 799 $request .= $this->buildPacket(self::STDIN, $stdin, $id); 800 } 801 $request .= $this->buildPacket(self::STDIN, '', $id); 802 803 if ($writeDelayMs > 0) { 804 usleep($writeDelayMs * 1000); 805 } 806 807 if ($this->transport->write($request) === false || $this->transport->flush() === false) { 808 809 $info = $this->transport->getMetaData(); 810 811 if (!empty($info) && $info['timed_out']) { 812 throw new TimedOutException('Write timed out'); 813 } 814 815 // Broken pipe, tear down so future requests might succeed 816 $this->transport->close(); 817 throw new \Exception('Failed to write request to socket'); 818 } 819 820 $this->_requests[$id] = array( 821 'state' => self::REQ_STATE_WRITTEN, 822 'response' => null, 823 'err_response' => null, 824 'out_response' => null, 825 ); 826 827 return $id; 828 } 829 830 /** 831 * Append response data. 832 * 833 * @param $resp Response 834 * @param $type Either err or our 835 * 836 * @throws \Exception 837 */ 838 private function fcgi_stream_append($resp, $type) { 839 if (isset($this->_requests[$resp['requestId']][$type . '_finished'])) { 840 throw new \Exception('FCGI_STD' . strtoupper($type) . ' stream already finished by empty record'); 841 } 842 if ($resp['content'] === '') { 843 $this->_requests[$resp['requestId']][$type . '_finished'] = true; 844 } else { 845 $this->_requests[$resp['requestId']][$type . '_response'] .= $resp['content']; 846 } 847 } 848 849 /** 850 * Blocking call that waits for response data of the specific request 851 * 852 * @param int $requestId 853 * @param int $timeoutMs [optional] the number of milliseconds to wait. 854 * @param int $readLimit [optional] the number of bytes to accept in a single packet or -1 if unlimited 855 * @return array response data 856 * @throws ForbiddenException 857 * @throws TimedOutException 858 * @throws \Exception 859 */ 860 public function wait_for_response_data($requestId, $timeoutMs = 0, $readLimit = -1) 861 { 862 if (!isset($this->_requests[$requestId])) { 863 throw new \Exception('Invalid request id given'); 864 } 865 866 // If we already read the response during an earlier call for different id, just return it 867 if ($this->_requests[$requestId]['state'] == self::REQ_STATE_OK 868 || $this->_requests[$requestId]['state'] == self::REQ_STATE_ERR 869 ) { 870 return $this->_requests[$requestId]['response']; 871 } 872 873 if ($timeoutMs > 0) { 874 // Reset timeout on socket for now 875 $this->transport->setDataTimeout($timeoutMs); 876 } else { 877 $timeoutMs = $this->_readWriteTimeout; 878 } 879 880 // Need to manually check since we might do several reads none of which timeout themselves 881 // but still not get the response requested 882 $startTime = microtime(true); 883 884 while ($resp = $this->readPacket($readLimit)) { 885 if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) { 886 if ($resp['type'] == self::STDERR) { 887 $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR; 888 $this->fcgi_stream_append($resp, 'err'); 889 } else { 890 $this->fcgi_stream_append($resp, 'out'); 891 } 892 $this->_requests[$resp['requestId']]['response'] .= $resp['content']; 893 } elseif ($resp['type'] == self::END_REQUEST) { 894 $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK; 895 if ($resp['requestId'] == $requestId) { 896 break; 897 } 898 } 899 if (microtime(true) - $startTime >= ($timeoutMs * 1000)) { 900 // Reset 901 $this->transport->setDataTimeout($this->_readWriteTimeout); 902 throw new \Exception('Timed out'); 903 } 904 } 905 906 if (!is_array($resp)) { 907 $info = $this->transport->getMetaData(); 908 909 // We must reset timeout but it must be AFTER we get info 910 $this->transport->setDataTimeout($this->_readWriteTimeout); 911 912 if (!empty($info)) { 913 if ($info['timed_out']) { 914 throw new TimedOutException( 'Read timed out' ); 915 } 916 917 if ($info['unread_bytes'] == 0 && $info['blocked'] && $info['eof']) { 918 throw new ForbiddenException( 'Not in white list. Check listen.allowed_clients.' ); 919 } 920 } 921 922 throw new \Exception('Read failed'); 923 } 924 925 // Reset timeout 926 $this->transport->setDataTimeout($this->_readWriteTimeout); 927 928 switch (ord($resp['content'][4])) { 929 case self::CANT_MPX_CONN: 930 throw new \Exception('This app can\'t multiplex [CANT_MPX_CONN]'); 931 break; 932 case self::OVERLOADED: 933 throw new \Exception('New request rejected; too busy [OVERLOADED]'); 934 break; 935 case self::UNKNOWN_ROLE: 936 throw new \Exception('Role value not known [UNKNOWN_ROLE]'); 937 break; 938 case self::REQUEST_COMPLETE: 939 return $this->_requests[$requestId]; 940 } 941 } 942 943 /** 944 * Blocking call that waits for response to specific request 945 * 946 * @param int $requestId 947 * @param int $timeoutMs [optional] the number of milliseconds to wait. 948 * @return string The response content. 949 * @throws ForbiddenException 950 * @throws TimedOutException 951 * @throws \Exception 952 */ 953 public function wait_for_response($requestId, $timeoutMs = 0) 954 { 955 return $this->wait_for_response_data($requestId, $timeoutMs)['response']; 956 } 957} 958