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