balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

fsockopen.php (13004B)


      1 <?php
      2 /**
      3  * fsockopen HTTP transport
      4  *
      5  * @package Requests
      6  * @subpackage Transport
      7  */
      8 
      9 /**
     10  * fsockopen HTTP transport
     11  *
     12  * @package Requests
     13  * @subpackage Transport
     14  */
     15 class Requests_Transport_fsockopen implements Requests_Transport {
     16 	/**
     17 	 * Second to microsecond conversion
     18 	 *
     19 	 * @var integer
     20 	 */
     21 	const SECOND_IN_MICROSECONDS = 1000000;
     22 
     23 	/**
     24 	 * Raw HTTP data
     25 	 *
     26 	 * @var string
     27 	 */
     28 	public $headers = '';
     29 
     30 	/**
     31 	 * Stream metadata
     32 	 *
     33 	 * @var array Associative array of properties, see {@see https://secure.php.net/stream_get_meta_data}
     34 	 */
     35 	public $info;
     36 
     37 	/**
     38 	 * What's the maximum number of bytes we should keep?
     39 	 *
     40 	 * @var int|bool Byte count, or false if no limit.
     41 	 */
     42 	protected $max_bytes = false;
     43 
     44 	protected $connect_error = '';
     45 
     46 	/**
     47 	 * Perform a request
     48 	 *
     49 	 * @throws Requests_Exception On failure to connect to socket (`fsockopenerror`)
     50 	 * @throws Requests_Exception On socket timeout (`timeout`)
     51 	 *
     52 	 * @param string $url URL to request
     53 	 * @param array $headers Associative array of request headers
     54 	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
     55 	 * @param array $options Request options, see {@see Requests::response()} for documentation
     56 	 * @return string Raw HTTP result
     57 	 */
     58 	public function request($url, $headers = array(), $data = array(), $options = array()) {
     59 		$options['hooks']->dispatch('fsockopen.before_request');
     60 
     61 		$url_parts = parse_url($url);
     62 		if (empty($url_parts)) {
     63 			throw new Requests_Exception('Invalid URL.', 'invalidurl', $url);
     64 		}
     65 		$host                     = $url_parts['host'];
     66 		$context                  = stream_context_create();
     67 		$verifyname               = false;
     68 		$case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers);
     69 
     70 		// HTTPS support
     71 		if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
     72 			$remote_socket = 'ssl://' . $host;
     73 			if (!isset($url_parts['port'])) {
     74 				$url_parts['port'] = 443;
     75 			}
     76 
     77 			$context_options = array(
     78 				'verify_peer'       => true,
     79 				'capture_peer_cert' => true,
     80 			);
     81 			$verifyname      = true;
     82 
     83 			// SNI, if enabled (OpenSSL >=0.9.8j)
     84 			// phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound
     85 			if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
     86 				$context_options['SNI_enabled'] = true;
     87 				if (isset($options['verifyname']) && $options['verifyname'] === false) {
     88 					$context_options['SNI_enabled'] = false;
     89 				}
     90 			}
     91 
     92 			if (isset($options['verify'])) {
     93 				if ($options['verify'] === false) {
     94 					$context_options['verify_peer']      = false;
     95 					$context_options['verify_peer_name'] = false;
     96 					$verifyname                          = false;
     97 				}
     98 				elseif (is_string($options['verify'])) {
     99 					$context_options['cafile'] = $options['verify'];
    100 				}
    101 			}
    102 
    103 			if (isset($options['verifyname']) && $options['verifyname'] === false) {
    104 				$context_options['verify_peer_name'] = false;
    105 				$verifyname                          = false;
    106 			}
    107 
    108 			stream_context_set_option($context, array('ssl' => $context_options));
    109 		}
    110 		else {
    111 			$remote_socket = 'tcp://' . $host;
    112 		}
    113 
    114 		$this->max_bytes = $options['max_bytes'];
    115 
    116 		if (!isset($url_parts['port'])) {
    117 			$url_parts['port'] = 80;
    118 		}
    119 		$remote_socket .= ':' . $url_parts['port'];
    120 
    121 		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
    122 		set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE);
    123 
    124 		$options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket));
    125 
    126 		$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
    127 
    128 		restore_error_handler();
    129 
    130 		if ($verifyname && !$this->verify_certificate_from_context($host, $context)) {
    131 			throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
    132 		}
    133 
    134 		if (!$socket) {
    135 			if ($errno === 0) {
    136 				// Connection issue
    137 				throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
    138 			}
    139 
    140 			throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno);
    141 		}
    142 
    143 		$data_format = $options['data_format'];
    144 
    145 		if ($data_format === 'query') {
    146 			$path = self::format_get($url_parts, $data);
    147 			$data = '';
    148 		}
    149 		else {
    150 			$path = self::format_get($url_parts, array());
    151 		}
    152 
    153 		$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
    154 
    155 		$request_body = '';
    156 		$out          = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']);
    157 
    158 		if ($options['type'] !== Requests::TRACE) {
    159 			if (is_array($data)) {
    160 				$request_body = http_build_query($data, '', '&');
    161 			}
    162 			else {
    163 				$request_body = $data;
    164 			}
    165 
    166 			// Always include Content-length on POST requests to prevent
    167 			// 411 errors from some servers when the body is empty.
    168 			if (!empty($data) || $options['type'] === Requests::POST) {
    169 				if (!isset($case_insensitive_headers['Content-Length'])) {
    170 					$headers['Content-Length'] = strlen($request_body);
    171 				}
    172 
    173 				if (!isset($case_insensitive_headers['Content-Type'])) {
    174 					$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
    175 				}
    176 			}
    177 		}
    178 
    179 		if (!isset($case_insensitive_headers['Host'])) {
    180 			$out .= sprintf('Host: %s', $url_parts['host']);
    181 
    182 			if ((strtolower($url_parts['scheme']) === 'http' && $url_parts['port'] !== 80) || (strtolower($url_parts['scheme']) === 'https' && $url_parts['port'] !== 443)) {
    183 				$out .= ':' . $url_parts['port'];
    184 			}
    185 			$out .= "\r\n";
    186 		}
    187 
    188 		if (!isset($case_insensitive_headers['User-Agent'])) {
    189 			$out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
    190 		}
    191 
    192 		$accept_encoding = $this->accept_encoding();
    193 		if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) {
    194 			$out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding);
    195 		}
    196 
    197 		$headers = Requests::flatten($headers);
    198 
    199 		if (!empty($headers)) {
    200 			$out .= implode("\r\n", $headers) . "\r\n";
    201 		}
    202 
    203 		$options['hooks']->dispatch('fsockopen.after_headers', array(&$out));
    204 
    205 		if (substr($out, -2) !== "\r\n") {
    206 			$out .= "\r\n";
    207 		}
    208 
    209 		if (!isset($case_insensitive_headers['Connection'])) {
    210 			$out .= "Connection: Close\r\n";
    211 		}
    212 
    213 		$out .= "\r\n" . $request_body;
    214 
    215 		$options['hooks']->dispatch('fsockopen.before_send', array(&$out));
    216 
    217 		fwrite($socket, $out);
    218 		$options['hooks']->dispatch('fsockopen.after_send', array($out));
    219 
    220 		if (!$options['blocking']) {
    221 			fclose($socket);
    222 			$fake_headers = '';
    223 			$options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers));
    224 			return '';
    225 		}
    226 
    227 		$timeout_sec = (int) floor($options['timeout']);
    228 		if ($timeout_sec === $options['timeout']) {
    229 			$timeout_msec = 0;
    230 		}
    231 		else {
    232 			$timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
    233 		}
    234 		stream_set_timeout($socket, $timeout_sec, $timeout_msec);
    235 
    236 		$response   = '';
    237 		$body       = '';
    238 		$headers    = '';
    239 		$this->info = stream_get_meta_data($socket);
    240 		$size       = 0;
    241 		$doingbody  = false;
    242 		$download   = false;
    243 		if ($options['filename']) {
    244 			$download = fopen($options['filename'], 'wb');
    245 		}
    246 
    247 		while (!feof($socket)) {
    248 			$this->info = stream_get_meta_data($socket);
    249 			if ($this->info['timed_out']) {
    250 				throw new Requests_Exception('fsocket timed out', 'timeout');
    251 			}
    252 
    253 			$block = fread($socket, Requests::BUFFER_SIZE);
    254 			if (!$doingbody) {
    255 				$response .= $block;
    256 				if (strpos($response, "\r\n\r\n")) {
    257 					list($headers, $block) = explode("\r\n\r\n", $response, 2);
    258 					$doingbody             = true;
    259 				}
    260 			}
    261 
    262 			// Are we in body mode now?
    263 			if ($doingbody) {
    264 				$options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes));
    265 				$data_length = strlen($block);
    266 				if ($this->max_bytes) {
    267 					// Have we already hit a limit?
    268 					if ($size === $this->max_bytes) {
    269 						continue;
    270 					}
    271 					if (($size + $data_length) > $this->max_bytes) {
    272 						// Limit the length
    273 						$limited_length = ($this->max_bytes - $size);
    274 						$block          = substr($block, 0, $limited_length);
    275 					}
    276 				}
    277 
    278 				$size += strlen($block);
    279 				if ($download) {
    280 					fwrite($download, $block);
    281 				}
    282 				else {
    283 					$body .= $block;
    284 				}
    285 			}
    286 		}
    287 		$this->headers = $headers;
    288 
    289 		if ($download) {
    290 			fclose($download);
    291 		}
    292 		else {
    293 			$this->headers .= "\r\n\r\n" . $body;
    294 		}
    295 		fclose($socket);
    296 
    297 		$options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info));
    298 		return $this->headers;
    299 	}
    300 
    301 	/**
    302 	 * Send multiple requests simultaneously
    303 	 *
    304 	 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request}
    305 	 * @param array $options Global options, see {@see Requests::response()} for documentation
    306 	 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
    307 	 */
    308 	public function request_multiple($requests, $options) {
    309 		$responses = array();
    310 		$class     = get_class($this);
    311 		foreach ($requests as $id => $request) {
    312 			try {
    313 				$handler        = new $class();
    314 				$responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
    315 
    316 				$request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request));
    317 			}
    318 			catch (Requests_Exception $e) {
    319 				$responses[$id] = $e;
    320 			}
    321 
    322 			if (!is_string($responses[$id])) {
    323 				$request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id));
    324 			}
    325 		}
    326 
    327 		return $responses;
    328 	}
    329 
    330 	/**
    331 	 * Retrieve the encodings we can accept
    332 	 *
    333 	 * @return string Accept-Encoding header value
    334 	 */
    335 	protected static function accept_encoding() {
    336 		$type = array();
    337 		if (function_exists('gzinflate')) {
    338 			$type[] = 'deflate;q=1.0';
    339 		}
    340 
    341 		if (function_exists('gzuncompress')) {
    342 			$type[] = 'compress;q=0.5';
    343 		}
    344 
    345 		$type[] = 'gzip;q=0.5';
    346 
    347 		return implode(', ', $type);
    348 	}
    349 
    350 	/**
    351 	 * Format a URL given GET data
    352 	 *
    353 	 * @param array $url_parts
    354 	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
    355 	 * @return string URL with data
    356 	 */
    357 	protected static function format_get($url_parts, $data) {
    358 		if (!empty($data)) {
    359 			if (empty($url_parts['query'])) {
    360 				$url_parts['query'] = '';
    361 			}
    362 
    363 			$url_parts['query'] .= '&' . http_build_query($data, '', '&');
    364 			$url_parts['query']  = trim($url_parts['query'], '&');
    365 		}
    366 		if (isset($url_parts['path'])) {
    367 			if (isset($url_parts['query'])) {
    368 				$get = $url_parts['path'] . '?' . $url_parts['query'];
    369 			}
    370 			else {
    371 				$get = $url_parts['path'];
    372 			}
    373 		}
    374 		else {
    375 			$get = '/';
    376 		}
    377 		return $get;
    378 	}
    379 
    380 	/**
    381 	 * Error handler for stream_socket_client()
    382 	 *
    383 	 * @param int $errno Error number (e.g. E_WARNING)
    384 	 * @param string $errstr Error message
    385 	 */
    386 	public function connect_error_handler($errno, $errstr) {
    387 		// Double-check we can handle it
    388 		if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
    389 			// Return false to indicate the default error handler should engage
    390 			return false;
    391 		}
    392 
    393 		$this->connect_error .= $errstr . "\n";
    394 		return true;
    395 	}
    396 
    397 	/**
    398 	 * Verify the certificate against common name and subject alternative names
    399 	 *
    400 	 * Unfortunately, PHP doesn't check the certificate against the alternative
    401 	 * names, leading things like 'https://www.github.com/' to be invalid.
    402 	 * Instead
    403 	 *
    404 	 * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
    405 	 *
    406 	 * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
    407 	 * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
    408 	 * @param string $host Host name to verify against
    409 	 * @param resource $context Stream context
    410 	 * @return bool
    411 	 */
    412 	public function verify_certificate_from_context($host, $context) {
    413 		$meta = stream_context_get_options($context);
    414 
    415 		// If we don't have SSL options, then we couldn't make the connection at
    416 		// all
    417 		if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
    418 			throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error');
    419 		}
    420 
    421 		$cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
    422 
    423 		return Requests_SSL::verify_certificate($host, $cert);
    424 	}
    425 
    426 	/**
    427 	 * Whether this transport is valid
    428 	 *
    429 	 * @codeCoverageIgnore
    430 	 * @return boolean True if the transport is valid, false otherwise.
    431 	 */
    432 	public static function test($capabilities = array()) {
    433 		if (!function_exists('fsockopen')) {
    434 			return false;
    435 		}
    436 
    437 		// If needed, check that streams support SSL
    438 		if (isset($capabilities['ssl']) && $capabilities['ssl']) {
    439 			if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) {
    440 				return false;
    441 			}
    442 
    443 			// Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156
    444 			if (defined('HHVM_VERSION')) {
    445 				return false;
    446 			}
    447 		}
    448 
    449 		return true;
    450 	}
    451 }