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