PhpHttpRequest.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. */
  20. class PhpHttpRequest extends MWHttpRequest {
  21. private $fopenErrors = [];
  22. public function __construct() {
  23. if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
  24. throw new RuntimeException( __METHOD__ . ': allow_url_fopen needs to be enabled for ' .
  25. 'pure PHP http requests to work. If possible, curl should be used instead. See ' .
  26. 'https://www.php.net/curl.'
  27. );
  28. }
  29. parent::__construct( ...func_get_args() );
  30. }
  31. /**
  32. * @param string $url
  33. * @return string
  34. */
  35. protected function urlToTcp( $url ) {
  36. $parsedUrl = parse_url( $url );
  37. return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
  38. }
  39. /**
  40. * Returns an array with a 'capath' or 'cafile' key
  41. * that is suitable to be merged into the 'ssl' sub-array of
  42. * a stream context options array.
  43. * Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
  44. * default CA bundle if PHP supports that, or searches a few standard locations.
  45. * @return array
  46. * @throws DomainException
  47. */
  48. protected function getCertOptions() {
  49. $certOptions = [];
  50. $certLocations = [];
  51. if ( $this->caInfo ) {
  52. $certLocations = [ 'manual' => $this->caInfo ];
  53. }
  54. foreach ( $certLocations as $key => $cert ) {
  55. if ( is_dir( $cert ) ) {
  56. $certOptions['capath'] = $cert;
  57. break;
  58. } elseif ( is_file( $cert ) ) {
  59. $certOptions['cafile'] = $cert;
  60. break;
  61. } elseif ( $key === 'manual' ) {
  62. // fail more loudly if a cert path was manually configured and it is not valid
  63. throw new DomainException( "Invalid CA info passed: $cert" );
  64. }
  65. }
  66. return $certOptions;
  67. }
  68. /**
  69. * Custom error handler for dealing with fopen() errors.
  70. * fopen() tends to fire multiple errors in succession, and the last one
  71. * is completely useless (something like "fopen: failed to open stream")
  72. * so normal methods of handling errors programmatically
  73. * like get_last_error() don't work.
  74. * @internal
  75. * @param int $errno
  76. * @param string $errstr
  77. */
  78. public function errorHandler( $errno, $errstr ) {
  79. $n = count( $this->fopenErrors ) + 1;
  80. $this->fopenErrors += [ "errno$n" => $errno, "errstr$n" => $errstr ];
  81. }
  82. /**
  83. * @see MWHttpRequest::execute
  84. *
  85. * @return Status
  86. */
  87. public function execute() {
  88. $this->prepare();
  89. if ( is_array( $this->postData ) ) {
  90. $this->postData = wfArrayToCgi( $this->postData );
  91. }
  92. if ( $this->parsedUrl['scheme'] != 'http'
  93. && $this->parsedUrl['scheme'] != 'https' ) {
  94. $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
  95. }
  96. $this->reqHeaders['Accept'] = "*/*";
  97. $this->reqHeaders['Connection'] = 'Close';
  98. if ( $this->method == 'POST' ) {
  99. // Required for HTTP 1.0 POSTs
  100. $this->reqHeaders['Content-Length'] = strlen( $this->postData );
  101. if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
  102. $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
  103. }
  104. }
  105. // Set up PHP stream context
  106. $options = [
  107. 'http' => [
  108. 'method' => $this->method,
  109. 'header' => implode( "\r\n", $this->getHeaderList() ),
  110. 'protocol_version' => '1.1',
  111. 'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
  112. 'ignore_errors' => true,
  113. 'timeout' => $this->timeout,
  114. // Curl options in case curlwrappers are installed
  115. 'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
  116. 'curl_verify_ssl_peer' => $this->sslVerifyCert,
  117. ],
  118. 'ssl' => [
  119. 'verify_peer' => $this->sslVerifyCert,
  120. 'SNI_enabled' => true,
  121. 'ciphers' => 'HIGH:!SSLv2:!SSLv3:-ADH:-kDH:-kECDH:-DSS',
  122. 'disable_compression' => true,
  123. ],
  124. ];
  125. if ( $this->proxy ) {
  126. $options['http']['proxy'] = $this->urlToTcp( $this->proxy );
  127. $options['http']['request_fulluri'] = true;
  128. }
  129. if ( $this->postData ) {
  130. $options['http']['content'] = $this->postData;
  131. }
  132. if ( $this->sslVerifyHost ) {
  133. $options['ssl']['peer_name'] = $this->parsedUrl['host'];
  134. }
  135. $options['ssl'] += $this->getCertOptions();
  136. $context = stream_context_create( $options );
  137. $this->headerList = [];
  138. $reqCount = 0;
  139. $url = $this->url;
  140. $result = [];
  141. if ( $this->profiler ) {
  142. $profileSection = $this->profiler->scopedProfileIn(
  143. __METHOD__ . '-' . $this->profileName
  144. );
  145. }
  146. do {
  147. $reqCount++;
  148. $this->fopenErrors = [];
  149. set_error_handler( [ $this, 'errorHandler' ] );
  150. $fh = fopen( $url, "r", false, $context );
  151. restore_error_handler();
  152. if ( !$fh ) {
  153. break;
  154. }
  155. $result = stream_get_meta_data( $fh );
  156. $this->headerList = $result['wrapper_data'];
  157. $this->parseHeader();
  158. if ( !$this->followRedirects ) {
  159. break;
  160. }
  161. # Handle manual redirection
  162. if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
  163. break;
  164. }
  165. # Check security of URL
  166. $url = $this->getResponseHeader( "Location" );
  167. if ( !Http::isValidURI( $url ) ) {
  168. $this->logger->debug( __METHOD__ . ": insecure redirection\n" );
  169. break;
  170. }
  171. } while ( true );
  172. if ( $this->profiler ) {
  173. $this->profiler->scopedProfileOut( $profileSection );
  174. }
  175. $this->setStatus();
  176. if ( $fh === false ) {
  177. if ( $this->fopenErrors ) {
  178. $this->logger->warning( __CLASS__
  179. . ': error opening connection: {errstr1}', $this->fopenErrors );
  180. }
  181. $this->status->fatal( 'http-request-error' );
  182. return Status::wrap( $this->status ); // TODO B/C; move this to callers
  183. }
  184. if ( $result['timed_out'] ) {
  185. $this->status->fatal( 'http-timed-out', $this->url );
  186. return Status::wrap( $this->status ); // TODO B/C; move this to callers
  187. }
  188. // If everything went OK, or we received some error code
  189. // get the response body content.
  190. if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
  191. while ( !feof( $fh ) ) {
  192. $buf = fread( $fh, 8192 );
  193. if ( $buf === false ) {
  194. $this->status->fatal( 'http-read-error' );
  195. break;
  196. }
  197. if ( $buf !== '' ) {
  198. call_user_func( $this->callback, $fh, $buf );
  199. }
  200. }
  201. }
  202. fclose( $fh );
  203. return Status::wrap( $this->status ); // TODO B/C; move this to callers
  204. }
  205. }