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