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