CurlHttpRequest.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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. /**
  21. * MWHttpRequest implemented using internal curl compiled into PHP
  22. */
  23. class CurlHttpRequest extends MWHttpRequest {
  24. const SUPPORTS_FILE_POSTS = true;
  25. protected $curlOptions = [];
  26. protected $headerText = "";
  27. /**
  28. * @throws RuntimeException
  29. */
  30. public function __construct() {
  31. if ( !function_exists( 'curl_init' ) ) {
  32. throw new RuntimeException(
  33. __METHOD__ . ': curl (https://www.php.net/curl) is not installed' );
  34. }
  35. parent::__construct( ...func_get_args() );
  36. }
  37. /**
  38. * @param resource $fh
  39. * @param string $content
  40. * @return int
  41. */
  42. protected function readHeader( $fh, $content ) {
  43. $this->headerText .= $content;
  44. return strlen( $content );
  45. }
  46. /**
  47. * @see MWHttpRequest::execute
  48. *
  49. * @throws MWException
  50. * @return Status
  51. */
  52. public function execute() {
  53. $this->prepare();
  54. if ( !$this->status->isOK() ) {
  55. return Status::wrap( $this->status ); // TODO B/C; move this to callers
  56. }
  57. $this->curlOptions[CURLOPT_PROXY] = $this->proxy;
  58. $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
  59. $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000;
  60. $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
  61. $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
  62. $this->curlOptions[CURLOPT_HEADERFUNCTION] = [ $this, "readHeader" ];
  63. $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
  64. $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
  65. $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
  66. $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0;
  67. $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
  68. if ( $this->caInfo ) {
  69. $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
  70. }
  71. if ( $this->headersOnly ) {
  72. $this->curlOptions[CURLOPT_NOBODY] = true;
  73. $this->curlOptions[CURLOPT_HEADER] = true;
  74. } elseif ( $this->method == 'POST' ) {
  75. $this->curlOptions[CURLOPT_POST] = true;
  76. $postData = $this->postData;
  77. // Don't interpret POST parameters starting with '@' as file uploads, because this
  78. // makes it impossible to POST plain values starting with '@' (and causes security
  79. // issues potentially exposing the contents of local files).
  80. $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
  81. $this->curlOptions[CURLOPT_POSTFIELDS] = $postData;
  82. // Suppress 'Expect: 100-continue' header, as some servers
  83. // will reject it with a 417 and Curl won't auto retry
  84. // with HTTP 1.0 fallback
  85. $this->reqHeaders['Expect'] = '';
  86. } else {
  87. $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
  88. }
  89. $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
  90. $curlHandle = curl_init( $this->url );
  91. if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
  92. $this->status->fatal( 'http-internal-error' );
  93. throw new InvalidArgumentException( "Error setting curl options." );
  94. }
  95. if ( $this->followRedirects && $this->canFollowRedirects() ) {
  96. Wikimedia\suppressWarnings();
  97. if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
  98. $this->logger->debug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
  99. "Probably open_basedir is set.\n" );
  100. // Continue the processing. If it were in curl_setopt_array,
  101. // processing would have halted on its entry
  102. }
  103. Wikimedia\restoreWarnings();
  104. }
  105. if ( $this->profiler ) {
  106. $profileSection = $this->profiler->scopedProfileIn(
  107. __METHOD__ . '-' . $this->profileName
  108. );
  109. }
  110. $curlRes = curl_exec( $curlHandle );
  111. if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) {
  112. $this->status->fatal( 'http-timed-out', $this->url );
  113. } elseif ( $curlRes === false ) {
  114. $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
  115. } else {
  116. $this->headerList = explode( "\r\n", $this->headerText );
  117. }
  118. curl_close( $curlHandle );
  119. if ( $this->profiler ) {
  120. $this->profiler->scopedProfileOut( $profileSection );
  121. }
  122. $this->parseHeader();
  123. $this->setStatus();
  124. return Status::wrap( $this->status ); // TODO B/C; move this to callers
  125. }
  126. /**
  127. * @return bool
  128. */
  129. public function canFollowRedirects() {
  130. $curlVersionInfo = curl_version();
  131. if ( $curlVersionInfo['version_number'] < 0x071304 ) {
  132. $this->logger->debug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
  133. return false;
  134. }
  135. return true;
  136. }
  137. }