class-wp-http-streams.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. <?php
  2. /**
  3. * HTTP API: WP_Http_Streams class
  4. *
  5. * @package WordPress
  6. * @subpackage HTTP
  7. * @since 4.4.0
  8. */
  9. /**
  10. * Core class used to integrate PHP Streams as an HTTP transport.
  11. *
  12. * @since 2.7.0
  13. * @since 3.7.0 Combined with the fsockopen transport and switched to `stream_socket_client()`.
  14. */
  15. class WP_Http_Streams {
  16. /**
  17. * Send a HTTP request to a URI using PHP Streams.
  18. *
  19. * @see WP_Http::request For default options descriptions.
  20. *
  21. * @since 2.7.0
  22. * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client().
  23. *
  24. * @param string $url The request URL.
  25. * @param string|array $args Optional. Override the defaults.
  26. * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
  27. */
  28. public function request($url, $args = array()) {
  29. $defaults = array(
  30. 'method' => 'GET', 'timeout' => 5,
  31. 'redirection' => 5, 'httpversion' => '1.0',
  32. 'blocking' => true,
  33. 'headers' => array(), 'body' => null, 'cookies' => array()
  34. );
  35. $r = wp_parse_args( $args, $defaults );
  36. if ( isset( $r['headers']['User-Agent'] ) ) {
  37. $r['user-agent'] = $r['headers']['User-Agent'];
  38. unset( $r['headers']['User-Agent'] );
  39. } elseif ( isset( $r['headers']['user-agent'] ) ) {
  40. $r['user-agent'] = $r['headers']['user-agent'];
  41. unset( $r['headers']['user-agent'] );
  42. }
  43. // Construct Cookie: header if any cookies are set.
  44. WP_Http::buildCookieHeader( $r );
  45. $arrURL = parse_url($url);
  46. $connect_host = $arrURL['host'];
  47. $secure_transport = ( $arrURL['scheme'] == 'ssl' || $arrURL['scheme'] == 'https' );
  48. if ( ! isset( $arrURL['port'] ) ) {
  49. if ( $arrURL['scheme'] == 'ssl' || $arrURL['scheme'] == 'https' ) {
  50. $arrURL['port'] = 443;
  51. $secure_transport = true;
  52. } else {
  53. $arrURL['port'] = 80;
  54. }
  55. }
  56. // Always pass a Path, defaulting to the root in cases such as http://example.com
  57. if ( ! isset( $arrURL['path'] ) ) {
  58. $arrURL['path'] = '/';
  59. }
  60. if ( isset( $r['headers']['Host'] ) || isset( $r['headers']['host'] ) ) {
  61. if ( isset( $r['headers']['Host'] ) )
  62. $arrURL['host'] = $r['headers']['Host'];
  63. else
  64. $arrURL['host'] = $r['headers']['host'];
  65. unset( $r['headers']['Host'], $r['headers']['host'] );
  66. }
  67. /*
  68. * Certain versions of PHP have issues with 'localhost' and IPv6, It attempts to connect
  69. * to ::1, which fails when the server is not set up for it. For compatibility, always
  70. * connect to the IPv4 address.
  71. */
  72. if ( 'localhost' == strtolower( $connect_host ) )
  73. $connect_host = '127.0.0.1';
  74. $connect_host = $secure_transport ? 'ssl://' . $connect_host : 'tcp://' . $connect_host;
  75. $is_local = isset( $r['local'] ) && $r['local'];
  76. $ssl_verify = isset( $r['sslverify'] ) && $r['sslverify'];
  77. if ( $is_local ) {
  78. /**
  79. * Filters whether SSL should be verified for local requests.
  80. *
  81. * @since 2.8.0
  82. *
  83. * @param bool $ssl_verify Whether to verify the SSL connection. Default true.
  84. */
  85. $ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify );
  86. } elseif ( ! $is_local ) {
  87. /**
  88. * Filters whether SSL should be verified for non-local requests.
  89. *
  90. * @since 2.8.0
  91. *
  92. * @param bool $ssl_verify Whether to verify the SSL connection. Default true.
  93. */
  94. $ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify );
  95. }
  96. $proxy = new WP_HTTP_Proxy();
  97. $context = stream_context_create( array(
  98. 'ssl' => array(
  99. 'verify_peer' => $ssl_verify,
  100. //'CN_match' => $arrURL['host'], // This is handled by self::verify_ssl_certificate()
  101. 'capture_peer_cert' => $ssl_verify,
  102. 'SNI_enabled' => true,
  103. 'cafile' => $r['sslcertificates'],
  104. 'allow_self_signed' => ! $ssl_verify,
  105. )
  106. ) );
  107. $timeout = (int) floor( $r['timeout'] );
  108. $utimeout = $timeout == $r['timeout'] ? 0 : 1000000 * $r['timeout'] % 1000000;
  109. $connect_timeout = max( $timeout, 1 );
  110. // Store error number.
  111. $connection_error = null;
  112. // Store error string.
  113. $connection_error_str = null;
  114. if ( !WP_DEBUG ) {
  115. // In the event that the SSL connection fails, silence the many PHP Warnings.
  116. if ( $secure_transport )
  117. $error_reporting = error_reporting(0);
  118. if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) )
  119. $handle = @stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
  120. else
  121. $handle = @stream_socket_client( $connect_host . ':' . $arrURL['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
  122. if ( $secure_transport )
  123. error_reporting( $error_reporting );
  124. } else {
  125. if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) )
  126. $handle = stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
  127. else
  128. $handle = stream_socket_client( $connect_host . ':' . $arrURL['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
  129. }
  130. if ( false === $handle ) {
  131. // SSL connection failed due to expired/invalid cert, or, OpenSSL configuration is broken.
  132. if ( $secure_transport && 0 === $connection_error && '' === $connection_error_str )
  133. return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) );
  134. return new WP_Error('http_request_failed', $connection_error . ': ' . $connection_error_str );
  135. }
  136. // Verify that the SSL certificate is valid for this request.
  137. if ( $secure_transport && $ssl_verify && ! $proxy->is_enabled() ) {
  138. if ( ! self::verify_ssl_certificate( $handle, $arrURL['host'] ) )
  139. return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) );
  140. }
  141. stream_set_timeout( $handle, $timeout, $utimeout );
  142. if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) //Some proxies require full URL in this field.
  143. $requestPath = $url;
  144. else
  145. $requestPath = $arrURL['path'] . ( isset($arrURL['query']) ? '?' . $arrURL['query'] : '' );
  146. $strHeaders = strtoupper($r['method']) . ' ' . $requestPath . ' HTTP/' . $r['httpversion'] . "\r\n";
  147. $include_port_in_host_header = (
  148. ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) ||
  149. ( 'http' == $arrURL['scheme'] && 80 != $arrURL['port'] ) ||
  150. ( 'https' == $arrURL['scheme'] && 443 != $arrURL['port'] )
  151. );
  152. if ( $include_port_in_host_header ) {
  153. $strHeaders .= 'Host: ' . $arrURL['host'] . ':' . $arrURL['port'] . "\r\n";
  154. } else {
  155. $strHeaders .= 'Host: ' . $arrURL['host'] . "\r\n";
  156. }
  157. if ( isset($r['user-agent']) )
  158. $strHeaders .= 'User-agent: ' . $r['user-agent'] . "\r\n";
  159. if ( is_array($r['headers']) ) {
  160. foreach ( (array) $r['headers'] as $header => $headerValue )
  161. $strHeaders .= $header . ': ' . $headerValue . "\r\n";
  162. } else {
  163. $strHeaders .= $r['headers'];
  164. }
  165. if ( $proxy->use_authentication() )
  166. $strHeaders .= $proxy->authentication_header() . "\r\n";
  167. $strHeaders .= "\r\n";
  168. if ( ! is_null($r['body']) )
  169. $strHeaders .= $r['body'];
  170. fwrite($handle, $strHeaders);
  171. if ( ! $r['blocking'] ) {
  172. stream_set_blocking( $handle, 0 );
  173. fclose( $handle );
  174. return array( 'headers' => array(), 'body' => '', 'response' => array('code' => false, 'message' => false), 'cookies' => array() );
  175. }
  176. $strResponse = '';
  177. $bodyStarted = false;
  178. $keep_reading = true;
  179. $block_size = 4096;
  180. if ( isset( $r['limit_response_size'] ) )
  181. $block_size = min( $block_size, $r['limit_response_size'] );
  182. // If streaming to a file setup the file handle.
  183. if ( $r['stream'] ) {
  184. if ( ! WP_DEBUG )
  185. $stream_handle = @fopen( $r['filename'], 'w+' );
  186. else
  187. $stream_handle = fopen( $r['filename'], 'w+' );
  188. if ( ! $stream_handle ) {
  189. return new WP_Error( 'http_request_failed', sprintf(
  190. /* translators: 1: fopen() 2: file name */
  191. __( 'Could not open handle for %1$s to %2$s.' ),
  192. 'fopen()',
  193. $r['filename']
  194. ) );
  195. }
  196. $bytes_written = 0;
  197. while ( ! feof($handle) && $keep_reading ) {
  198. $block = fread( $handle, $block_size );
  199. if ( ! $bodyStarted ) {
  200. $strResponse .= $block;
  201. if ( strpos( $strResponse, "\r\n\r\n" ) ) {
  202. $process = WP_Http::processResponse( $strResponse );
  203. $bodyStarted = true;
  204. $block = $process['body'];
  205. unset( $strResponse );
  206. $process['body'] = '';
  207. }
  208. }
  209. $this_block_size = strlen( $block );
  210. if ( isset( $r['limit_response_size'] ) && ( $bytes_written + $this_block_size ) > $r['limit_response_size'] ) {
  211. $this_block_size = ( $r['limit_response_size'] - $bytes_written );
  212. $block = substr( $block, 0, $this_block_size );
  213. }
  214. $bytes_written_to_file = fwrite( $stream_handle, $block );
  215. if ( $bytes_written_to_file != $this_block_size ) {
  216. fclose( $handle );
  217. fclose( $stream_handle );
  218. return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) );
  219. }
  220. $bytes_written += $bytes_written_to_file;
  221. $keep_reading = !isset( $r['limit_response_size'] ) || $bytes_written < $r['limit_response_size'];
  222. }
  223. fclose( $stream_handle );
  224. } else {
  225. $header_length = 0;
  226. while ( ! feof( $handle ) && $keep_reading ) {
  227. $block = fread( $handle, $block_size );
  228. $strResponse .= $block;
  229. if ( ! $bodyStarted && strpos( $strResponse, "\r\n\r\n" ) ) {
  230. $header_length = strpos( $strResponse, "\r\n\r\n" ) + 4;
  231. $bodyStarted = true;
  232. }
  233. $keep_reading = ( ! $bodyStarted || !isset( $r['limit_response_size'] ) || strlen( $strResponse ) < ( $header_length + $r['limit_response_size'] ) );
  234. }
  235. $process = WP_Http::processResponse( $strResponse );
  236. unset( $strResponse );
  237. }
  238. fclose( $handle );
  239. $arrHeaders = WP_Http::processHeaders( $process['headers'], $url );
  240. $response = array(
  241. 'headers' => $arrHeaders['headers'],
  242. // Not yet processed.
  243. 'body' => null,
  244. 'response' => $arrHeaders['response'],
  245. 'cookies' => $arrHeaders['cookies'],
  246. 'filename' => $r['filename']
  247. );
  248. // Handle redirects.
  249. if ( false !== ( $redirect_response = WP_Http::handle_redirects( $url, $r, $response ) ) )
  250. return $redirect_response;
  251. // If the body was chunk encoded, then decode it.
  252. if ( ! empty( $process['body'] ) && isset( $arrHeaders['headers']['transfer-encoding'] ) && 'chunked' == $arrHeaders['headers']['transfer-encoding'] )
  253. $process['body'] = WP_Http::chunkTransferDecode($process['body']);
  254. if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($arrHeaders['headers']) )
  255. $process['body'] = WP_Http_Encoding::decompress( $process['body'] );
  256. if ( isset( $r['limit_response_size'] ) && strlen( $process['body'] ) > $r['limit_response_size'] )
  257. $process['body'] = substr( $process['body'], 0, $r['limit_response_size'] );
  258. $response['body'] = $process['body'];
  259. return $response;
  260. }
  261. /**
  262. * Verifies the received SSL certificate against its Common Names and subjectAltName fields.
  263. *
  264. * PHP's SSL verifications only verify that it's a valid Certificate, it doesn't verify if
  265. * the certificate is valid for the hostname which was requested.
  266. * This function verifies the requested hostname against certificate's subjectAltName field,
  267. * if that is empty, or contains no DNS entries, a fallback to the Common Name field is used.
  268. *
  269. * IP Address support is included if the request is being made to an IP address.
  270. *
  271. * @since 3.7.0
  272. * @static
  273. *
  274. * @param stream $stream The PHP Stream which the SSL request is being made over
  275. * @param string $host The hostname being requested
  276. * @return bool If the cerficiate presented in $stream is valid for $host
  277. */
  278. public static function verify_ssl_certificate( $stream, $host ) {
  279. $context_options = stream_context_get_options( $stream );
  280. if ( empty( $context_options['ssl']['peer_certificate'] ) )
  281. return false;
  282. $cert = openssl_x509_parse( $context_options['ssl']['peer_certificate'] );
  283. if ( ! $cert )
  284. return false;
  285. /*
  286. * If the request is being made to an IP address, we'll validate against IP fields
  287. * in the cert (if they exist)
  288. */
  289. $host_type = ( WP_Http::is_ip_address( $host ) ? 'ip' : 'dns' );
  290. $certificate_hostnames = array();
  291. if ( ! empty( $cert['extensions']['subjectAltName'] ) ) {
  292. $match_against = preg_split( '/,\s*/', $cert['extensions']['subjectAltName'] );
  293. foreach ( $match_against as $match ) {
  294. list( $match_type, $match_host ) = explode( ':', $match );
  295. if ( $host_type == strtolower( trim( $match_type ) ) ) // IP: or DNS:
  296. $certificate_hostnames[] = strtolower( trim( $match_host ) );
  297. }
  298. } elseif ( !empty( $cert['subject']['CN'] ) ) {
  299. // Only use the CN when the certificate includes no subjectAltName extension.
  300. $certificate_hostnames[] = strtolower( $cert['subject']['CN'] );
  301. }
  302. // Exact hostname/IP matches.
  303. if ( in_array( strtolower( $host ), $certificate_hostnames ) )
  304. return true;
  305. // IP's can't be wildcards, Stop processing.
  306. if ( 'ip' == $host_type )
  307. return false;
  308. // Test to see if the domain is at least 2 deep for wildcard support.
  309. if ( substr_count( $host, '.' ) < 2 )
  310. return false;
  311. // Wildcard subdomains certs (*.example.com) are valid for a.example.com but not a.b.example.com.
  312. $wildcard_host = preg_replace( '/^[^.]+\./', '*.', $host );
  313. return in_array( strtolower( $wildcard_host ), $certificate_hostnames );
  314. }
  315. /**
  316. * Determines whether this class can be used for retrieving a URL.
  317. *
  318. * @static
  319. * @since 2.7.0
  320. * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client().
  321. *
  322. * @param array $args Optional. Array of request arguments. Default empty array.
  323. * @return bool False means this class can not be used, true means it can.
  324. */
  325. public static function test( $args = array() ) {
  326. if ( ! function_exists( 'stream_socket_client' ) )
  327. return false;
  328. $is_ssl = isset( $args['ssl'] ) && $args['ssl'];
  329. if ( $is_ssl ) {
  330. if ( ! extension_loaded( 'openssl' ) )
  331. return false;
  332. if ( ! function_exists( 'openssl_x509_parse' ) )
  333. return false;
  334. }
  335. /**
  336. * Filters whether streams can be used as a transport for retrieving a URL.
  337. *
  338. * @since 2.7.0
  339. *
  340. * @param bool $use_class Whether the class can be used. Default true.
  341. * @param array $args Request arguments.
  342. */
  343. return apply_filters( 'use_streams_transport', true, $args );
  344. }
  345. }
  346. /**
  347. * Deprecated HTTP Transport method which used fsockopen.
  348. *
  349. * This class is not used, and is included for backward compatibility only.
  350. * All code should make use of WP_Http directly through its API.
  351. *
  352. * @see WP_HTTP::request
  353. *
  354. * @since 2.7.0
  355. * @deprecated 3.7.0 Please use WP_HTTP::request() directly
  356. */
  357. class WP_HTTP_Fsockopen extends WP_HTTP_Streams {
  358. // For backward compatibility for users who are using the class directly.
  359. }