[ Index ] |
PHP Cross Reference of YOURLS |
[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 100 $host = $url_parts['host']; 101 $context = stream_context_create(); 102 $verifyname = false; 103 $case_insensitive_headers = new CaseInsensitiveDictionary($headers); 104 105 // HTTPS support 106 if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { 107 $remote_socket = 'ssl://' . $host; 108 if (!isset($url_parts['port'])) { 109 $url_parts['port'] = Port::HTTPS; 110 } 111 112 $context_options = [ 113 'verify_peer' => true, 114 'capture_peer_cert' => true, 115 ]; 116 $verifyname = true; 117 118 // SNI, if enabled (OpenSSL >=0.9.8j) 119 // phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound 120 if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) { 121 $context_options['SNI_enabled'] = true; 122 if (isset($options['verifyname']) && $options['verifyname'] === false) { 123 $context_options['SNI_enabled'] = false; 124 } 125 } 126 127 if (isset($options['verify'])) { 128 if ($options['verify'] === false) { 129 $context_options['verify_peer'] = false; 130 $context_options['verify_peer_name'] = false; 131 $verifyname = false; 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 } else { 144 $remote_socket = 'tcp://' . $host; 145 } 146 147 $this->max_bytes = $options['max_bytes']; 148 149 if (!isset($url_parts['port'])) { 150 $url_parts['port'] = Port::HTTP; 151 } 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 } else { 183 $path = self::format_get($url_parts, []); 184 } 185 186 $options['hooks']->dispatch('fsockopen.remote_host_path', [&$path, $url]); 187 188 $request_body = ''; 189 $out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']); 190 191 if ($options['type'] !== Requests::TRACE) { 192 if (is_array($data)) { 193 $request_body = http_build_query($data, '', '&'); 194 } else { 195 $request_body = $data; 196 } 197 198 // Always include Content-length on POST requests to prevent 199 // 411 errors from some servers when the body is empty. 200 if (!empty($data) || $options['type'] === Requests::POST) { 201 if (!isset($case_insensitive_headers['Content-Length'])) { 202 $headers['Content-Length'] = strlen($request_body); 203 } 204 205 if (!isset($case_insensitive_headers['Content-Type'])) { 206 $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 207 } 208 } 209 } 210 211 if (!isset($case_insensitive_headers['Host'])) { 212 $out .= sprintf('Host: %s', $url_parts['host']); 213 $scheme_lower = strtolower($url_parts['scheme']); 214 215 if (($scheme_lower === 'http' && $url_parts['port'] !== Port::HTTP) || ($scheme_lower === 'https' && $url_parts['port'] !== Port::HTTPS)) { 216 $out .= ':' . $url_parts['port']; 217 } 218 219 $out .= "\r\n"; 220 } 221 222 if (!isset($case_insensitive_headers['User-Agent'])) { 223 $out .= sprintf("User-Agent: %s\r\n", $options['useragent']); 224 } 225 226 $accept_encoding = $this->accept_encoding(); 227 if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) { 228 $out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding); 229 } 230 231 $headers = Requests::flatten($headers); 232 233 if (!empty($headers)) { 234 $out .= implode("\r\n", $headers) . "\r\n"; 235 } 236 237 $options['hooks']->dispatch('fsockopen.after_headers', [&$out]); 238 239 if (substr($out, -2) !== "\r\n") { 240 $out .= "\r\n"; 241 } 242 243 if (!isset($case_insensitive_headers['Connection'])) { 244 $out .= "Connection: Close\r\n"; 245 } 246 247 $out .= "\r\n" . $request_body; 248 249 $options['hooks']->dispatch('fsockopen.before_send', [&$out]); 250 251 fwrite($socket, $out); 252 $options['hooks']->dispatch('fsockopen.after_send', [$out]); 253 254 if (!$options['blocking']) { 255 fclose($socket); 256 $fake_headers = ''; 257 $options['hooks']->dispatch('fsockopen.after_request', [&$fake_headers]); 258 return ''; 259 } 260 261 $timeout_sec = (int) floor($options['timeout']); 262 if ($timeout_sec === $options['timeout']) { 263 $timeout_msec = 0; 264 } else { 265 $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; 266 } 267 268 stream_set_timeout($socket, $timeout_sec, $timeout_msec); 269 270 $response = ''; 271 $body = ''; 272 $headers = ''; 273 $this->info = stream_get_meta_data($socket); 274 $size = 0; 275 $doingbody = false; 276 $download = false; 277 if ($options['filename']) { 278 // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception. 279 $download = @fopen($options['filename'], 'wb'); 280 if ($download === false) { 281 $error = error_get_last(); 282 throw new Exception($error['message'], 'fopen'); 283 } 284 } 285 286 while (!feof($socket)) { 287 $this->info = stream_get_meta_data($socket); 288 if ($this->info['timed_out']) { 289 throw new Exception('fsocket timed out', 'timeout'); 290 } 291 292 $block = fread($socket, Requests::BUFFER_SIZE); 293 if (!$doingbody) { 294 $response .= $block; 295 if (strpos($response, "\r\n\r\n")) { 296 list($headers, $block) = explode("\r\n\r\n", $response, 2); 297 $doingbody = true; 298 } 299 } 300 301 // Are we in body mode now? 302 if ($doingbody) { 303 $options['hooks']->dispatch('request.progress', [$block, $size, $this->max_bytes]); 304 $data_length = strlen($block); 305 if ($this->max_bytes) { 306 // Have we already hit a limit? 307 if ($size === $this->max_bytes) { 308 continue; 309 } 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 } else { 322 $body .= $block; 323 } 324 } 325 } 326 327 $this->headers = $headers; 328 329 if ($download) { 330 fclose($download); 331 } else { 332 $this->headers .= "\r\n\r\n" . $body; 333 } 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 } catch (Exception $e) { 374 $responses[$id] = $e; 375 } 376 377 if (!is_string($responses[$id])) { 378 $request['options']['hooks']->dispatch('multiple.request.complete', [&$responses[$id], $id]); 379 } 380 } 381 382 return $responses; 383 } 384 385 /** 386 * Retrieve the encodings we can accept 387 * 388 * @return string Accept-Encoding header value 389 */ 390 private static function accept_encoding() { 391 $type = []; 392 if (function_exists('gzinflate')) { 393 $type[] = 'deflate;q=1.0'; 394 } 395 396 if (function_exists('gzuncompress')) { 397 $type[] = 'compress;q=0.5'; 398 } 399 400 $type[] = 'gzip;q=0.5'; 401 402 return implode(', ', $type); 403 } 404 405 /** 406 * Format a URL given GET data 407 * 408 * @param array $url_parts 409 * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query} 410 * @return string URL with data 411 */ 412 private static function format_get($url_parts, $data) { 413 if (!empty($data)) { 414 if (empty($url_parts['query'])) { 415 $url_parts['query'] = ''; 416 } 417 418 $url_parts['query'] .= '&' . http_build_query($data, '', '&'); 419 $url_parts['query'] = trim($url_parts['query'], '&'); 420 } 421 422 if (isset($url_parts['path'])) { 423 if (isset($url_parts['query'])) { 424 $get = $url_parts['path'] . '?' . $url_parts['query']; 425 } else { 426 $get = $url_parts['path']; 427 } 428 } else { 429 $get = '/'; 430 } 431 432 return $get; 433 } 434 435 /** 436 * Error handler for stream_socket_client() 437 * 438 * @param int $errno Error number (e.g. E_WARNING) 439 * @param string $errstr Error message 440 */ 441 public function connect_error_handler($errno, $errstr) { 442 // Double-check we can handle it 443 if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) { 444 // Return false to indicate the default error handler should engage 445 return false; 446 } 447 448 $this->connect_error .= $errstr . "\n"; 449 return true; 450 } 451 452 /** 453 * Verify the certificate against common name and subject alternative names 454 * 455 * Unfortunately, PHP doesn't check the certificate against the alternative 456 * names, leading things like 'https://www.github.com/' to be invalid. 457 * Instead 458 * 459 * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 460 * 461 * @param string $host Host name to verify against 462 * @param resource $context Stream context 463 * @return bool 464 * 465 * @throws \WpOrg\Requests\Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`) 466 * @throws \WpOrg\Requests\Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) 467 */ 468 public function verify_certificate_from_context($host, $context) { 469 $meta = stream_context_get_options($context); 470 471 // If we don't have SSL options, then we couldn't make the connection at 472 // all 473 if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) { 474 throw new Exception(rtrim($this->connect_error), 'ssl.connect_error'); 475 } 476 477 $cert = openssl_x509_parse($meta['ssl']['peer_certificate']); 478 479 return Ssl::verify_certificate($host, $cert); 480 } 481 482 /** 483 * Self-test whether the transport can be used. 484 * 485 * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. 486 * 487 * @codeCoverageIgnore 488 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. 489 * @return bool Whether the transport can be used. 490 */ 491 public static function test($capabilities = []) { 492 if (!function_exists('fsockopen')) { 493 return false; 494 } 495 496 // If needed, check that streams support SSL 497 if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { 498 if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) { 499 return false; 500 } 501 } 502 503 return true; 504 } 505 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Tue Jan 21 05:10:11 2025 | Cross-referenced by PHPXref 0.7.1 |