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