MediaWiki
REL1_24
|
00001 <?php 00032 class Http { 00033 static public $httpEngine = false; 00034 00060 public static function request( $method, $url, $options = array() ) { 00061 wfDebug( "HTTP: $method: $url\n" ); 00062 wfProfileIn( __METHOD__ . "-$method" ); 00063 00064 $options['method'] = strtoupper( $method ); 00065 00066 if ( !isset( $options['timeout'] ) ) { 00067 $options['timeout'] = 'default'; 00068 } 00069 if ( !isset( $options['connectTimeout'] ) ) { 00070 $options['connectTimeout'] = 'default'; 00071 } 00072 00073 $req = MWHttpRequest::factory( $url, $options ); 00074 $status = $req->execute(); 00075 00076 $content = false; 00077 if ( $status->isOK() ) { 00078 $content = $req->getContent(); 00079 } 00080 wfProfileOut( __METHOD__ . "-$method" ); 00081 return $content; 00082 } 00083 00093 public static function get( $url, $timeout = 'default', $options = array() ) { 00094 $options['timeout'] = $timeout; 00095 return Http::request( 'GET', $url, $options ); 00096 } 00097 00106 public static function post( $url, $options = array() ) { 00107 return Http::request( 'POST', $url, $options ); 00108 } 00109 00116 public static function isLocalURL( $url ) { 00117 global $wgCommandLineMode, $wgConf; 00118 00119 if ( $wgCommandLineMode ) { 00120 return false; 00121 } 00122 00123 // Extract host part 00124 $matches = array(); 00125 if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) { 00126 $host = $matches[1]; 00127 // Split up dotwise 00128 $domainParts = explode( '.', $host ); 00129 // Check if this domain or any superdomain is listed in $wgConf as a local virtual host 00130 $domainParts = array_reverse( $domainParts ); 00131 00132 $domain = ''; 00133 $countParts = count( $domainParts ); 00134 for ( $i = 0; $i < $countParts; $i++ ) { 00135 $domainPart = $domainParts[$i]; 00136 if ( $i == 0 ) { 00137 $domain = $domainPart; 00138 } else { 00139 $domain = $domainPart . '.' . $domain; 00140 } 00141 00142 if ( $wgConf->isLocalVHost( $domain ) ) { 00143 return true; 00144 } 00145 } 00146 } 00147 00148 return false; 00149 } 00150 00155 public static function userAgent() { 00156 global $wgVersion; 00157 return "MediaWiki/$wgVersion"; 00158 } 00159 00172 public static function isValidURI( $uri ) { 00173 return preg_match( 00174 '/^https?:\/\/[^\/\s]\S*$/D', 00175 $uri 00176 ); 00177 } 00178 } 00179 00187 class MWHttpRequest { 00188 const SUPPORTS_FILE_POSTS = false; 00189 00190 protected $content; 00191 protected $timeout = 'default'; 00192 protected $headersOnly = null; 00193 protected $postData = null; 00194 protected $proxy = null; 00195 protected $noProxy = false; 00196 protected $sslVerifyHost = true; 00197 protected $sslVerifyCert = true; 00198 protected $caInfo = null; 00199 protected $method = "GET"; 00200 protected $reqHeaders = array(); 00201 protected $url; 00202 protected $parsedUrl; 00203 protected $callback; 00204 protected $maxRedirects = 5; 00205 protected $followRedirects = false; 00206 00210 protected $cookieJar; 00211 00212 protected $headerList = array(); 00213 protected $respVersion = "0.9"; 00214 protected $respStatus = "200 Ok"; 00215 protected $respHeaders = array(); 00216 00217 public $status; 00218 00223 protected function __construct( $url, $options = array() ) { 00224 global $wgHTTPTimeout, $wgHTTPConnectTimeout; 00225 00226 $this->url = wfExpandUrl( $url, PROTO_HTTP ); 00227 $this->parsedUrl = wfParseUrl( $this->url ); 00228 00229 if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { 00230 $this->status = Status::newFatal( 'http-invalid-url' ); 00231 } else { 00232 $this->status = Status::newGood( 100 ); // continue 00233 } 00234 00235 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) { 00236 $this->timeout = $options['timeout']; 00237 } else { 00238 $this->timeout = $wgHTTPTimeout; 00239 } 00240 if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) { 00241 $this->connectTimeout = $options['connectTimeout']; 00242 } else { 00243 $this->connectTimeout = $wgHTTPConnectTimeout; 00244 } 00245 if ( isset( $options['userAgent'] ) ) { 00246 $this->setUserAgent( $options['userAgent'] ); 00247 } 00248 00249 $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", 00250 "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ); 00251 00252 foreach ( $members as $o ) { 00253 if ( isset( $options[$o] ) ) { 00254 // ensure that MWHttpRequest::method is always 00255 // uppercased. Bug 36137 00256 if ( $o == 'method' ) { 00257 $options[$o] = strtoupper( $options[$o] ); 00258 } 00259 $this->$o = $options[$o]; 00260 } 00261 } 00262 00263 if ( $this->noProxy ) { 00264 $this->proxy = ''; // noProxy takes precedence 00265 } 00266 } 00267 00273 public static function canMakeRequests() { 00274 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' ); 00275 } 00276 00285 public static function factory( $url, $options = null ) { 00286 if ( !Http::$httpEngine ) { 00287 Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; 00288 } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { 00289 throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . 00290 ' Http::$httpEngine is set to "curl"' ); 00291 } 00292 00293 switch ( Http::$httpEngine ) { 00294 case 'curl': 00295 return new CurlHttpRequest( $url, $options ); 00296 case 'php': 00297 if ( !wfIniGetBool( 'allow_url_fopen' ) ) { 00298 throw new MWException( __METHOD__ . ': allow_url_fopen ' . 00299 'needs to be enabled for pure PHP http requests to ' . 00300 'work. If possible, curl should be used instead. See ' . 00301 'http://php.net/curl.' 00302 ); 00303 } 00304 return new PhpHttpRequest( $url, $options ); 00305 default: 00306 throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); 00307 } 00308 } 00309 00315 public function getContent() { 00316 return $this->content; 00317 } 00318 00325 public function setData( $args ) { 00326 $this->postData = $args; 00327 } 00328 00334 public function proxySetup() { 00335 global $wgHTTPProxy; 00336 00337 // If there is an explicit proxy set and proxies are not disabled, then use it 00338 if ( $this->proxy && !$this->noProxy ) { 00339 return; 00340 } 00341 00342 // Otherwise, fallback to $wgHTTPProxy/http_proxy (when set) if this is not a machine 00343 // local URL and proxies are not disabled 00344 if ( Http::isLocalURL( $this->url ) || $this->noProxy ) { 00345 $this->proxy = ''; 00346 } elseif ( $wgHTTPProxy ) { 00347 $this->proxy = $wgHTTPProxy; 00348 } elseif ( getenv( "http_proxy" ) ) { 00349 $this->proxy = getenv( "http_proxy" ); 00350 } 00351 } 00352 00357 public function setUserAgent( $UA ) { 00358 $this->setHeader( 'User-Agent', $UA ); 00359 } 00360 00366 public function setHeader( $name, $value ) { 00367 // I feel like I should normalize the case here... 00368 $this->reqHeaders[$name] = $value; 00369 } 00370 00375 public function getHeaderList() { 00376 $list = array(); 00377 00378 if ( $this->cookieJar ) { 00379 $this->reqHeaders['Cookie'] = 00380 $this->cookieJar->serializeToHttpRequest( 00381 $this->parsedUrl['path'], 00382 $this->parsedUrl['host'] 00383 ); 00384 } 00385 00386 foreach ( $this->reqHeaders as $name => $value ) { 00387 $list[] = "$name: $value"; 00388 } 00389 00390 return $list; 00391 } 00392 00411 public function setCallback( $callback ) { 00412 if ( !is_callable( $callback ) ) { 00413 throw new MWException( 'Invalid MwHttpRequest callback' ); 00414 } 00415 $this->callback = $callback; 00416 } 00417 00426 public function read( $fh, $content ) { 00427 $this->content .= $content; 00428 return strlen( $content ); 00429 } 00430 00436 public function execute() { 00437 wfProfileIn( __METHOD__ ); 00438 00439 $this->content = ""; 00440 00441 if ( strtoupper( $this->method ) == "HEAD" ) { 00442 $this->headersOnly = true; 00443 } 00444 00445 $this->proxySetup(); // set up any proxy as needed 00446 00447 if ( !$this->callback ) { 00448 $this->setCallback( array( $this, 'read' ) ); 00449 } 00450 00451 if ( !isset( $this->reqHeaders['User-Agent'] ) ) { 00452 $this->setUserAgent( Http::userAgent() ); 00453 } 00454 00455 wfProfileOut( __METHOD__ ); 00456 } 00457 00463 protected function parseHeader() { 00464 wfProfileIn( __METHOD__ ); 00465 00466 $lastname = ""; 00467 00468 foreach ( $this->headerList as $header ) { 00469 if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) { 00470 $this->respVersion = $match[1]; 00471 $this->respStatus = $match[2]; 00472 } elseif ( preg_match( "#^[ \t]#", $header ) ) { 00473 $last = count( $this->respHeaders[$lastname] ) - 1; 00474 $this->respHeaders[$lastname][$last] .= "\r\n$header"; 00475 } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) { 00476 $this->respHeaders[strtolower( $match[1] )][] = $match[2]; 00477 $lastname = strtolower( $match[1] ); 00478 } 00479 } 00480 00481 $this->parseCookies(); 00482 00483 wfProfileOut( __METHOD__ ); 00484 } 00485 00494 protected function setStatus() { 00495 if ( !$this->respHeaders ) { 00496 $this->parseHeader(); 00497 } 00498 00499 if ( (int)$this->respStatus > 399 ) { 00500 list( $code, $message ) = explode( " ", $this->respStatus, 2 ); 00501 $this->status->fatal( "http-bad-status", $code, $message ); 00502 } 00503 } 00504 00512 public function getStatus() { 00513 if ( !$this->respHeaders ) { 00514 $this->parseHeader(); 00515 } 00516 00517 return (int)$this->respStatus; 00518 } 00519 00525 public function isRedirect() { 00526 if ( !$this->respHeaders ) { 00527 $this->parseHeader(); 00528 } 00529 00530 $status = (int)$this->respStatus; 00531 00532 if ( $status >= 300 && $status <= 303 ) { 00533 return true; 00534 } 00535 00536 return false; 00537 } 00538 00547 public function getResponseHeaders() { 00548 if ( !$this->respHeaders ) { 00549 $this->parseHeader(); 00550 } 00551 00552 return $this->respHeaders; 00553 } 00554 00561 public function getResponseHeader( $header ) { 00562 if ( !$this->respHeaders ) { 00563 $this->parseHeader(); 00564 } 00565 00566 if ( isset( $this->respHeaders[strtolower( $header )] ) ) { 00567 $v = $this->respHeaders[strtolower( $header )]; 00568 return $v[count( $v ) - 1]; 00569 } 00570 00571 return null; 00572 } 00573 00579 public function setCookieJar( $jar ) { 00580 $this->cookieJar = $jar; 00581 } 00582 00588 public function getCookieJar() { 00589 if ( !$this->respHeaders ) { 00590 $this->parseHeader(); 00591 } 00592 00593 return $this->cookieJar; 00594 } 00595 00605 public function setCookie( $name, $value = null, $attr = null ) { 00606 if ( !$this->cookieJar ) { 00607 $this->cookieJar = new CookieJar; 00608 } 00609 00610 $this->cookieJar->setCookie( $name, $value, $attr ); 00611 } 00612 00616 protected function parseCookies() { 00617 wfProfileIn( __METHOD__ ); 00618 00619 if ( !$this->cookieJar ) { 00620 $this->cookieJar = new CookieJar; 00621 } 00622 00623 if ( isset( $this->respHeaders['set-cookie'] ) ) { 00624 $url = parse_url( $this->getFinalUrl() ); 00625 foreach ( $this->respHeaders['set-cookie'] as $cookie ) { 00626 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] ); 00627 } 00628 } 00629 00630 wfProfileOut( __METHOD__ ); 00631 } 00632 00649 public function getFinalUrl() { 00650 $headers = $this->getResponseHeaders(); 00651 00652 //return full url (fix for incorrect but handled relative location) 00653 if ( isset( $headers['location'] ) ) { 00654 $locations = $headers['location']; 00655 $domain = ''; 00656 $foundRelativeURI = false; 00657 $countLocations = count( $locations ); 00658 00659 for ( $i = $countLocations - 1; $i >= 0; $i-- ) { 00660 $url = parse_url( $locations[$i] ); 00661 00662 if ( isset( $url['host'] ) ) { 00663 $domain = $url['scheme'] . '://' . $url['host']; 00664 break; //found correct URI (with host) 00665 } else { 00666 $foundRelativeURI = true; 00667 } 00668 } 00669 00670 if ( $foundRelativeURI ) { 00671 if ( $domain ) { 00672 return $domain . $locations[$countLocations - 1]; 00673 } else { 00674 $url = parse_url( $this->url ); 00675 if ( isset( $url['host'] ) ) { 00676 return $url['scheme'] . '://' . $url['host'] . 00677 $locations[$countLocations - 1]; 00678 } 00679 } 00680 } else { 00681 return $locations[$countLocations - 1]; 00682 } 00683 } 00684 00685 return $this->url; 00686 } 00687 00693 public function canFollowRedirects() { 00694 return true; 00695 } 00696 } 00697 00701 class CurlHttpRequest extends MWHttpRequest { 00702 const SUPPORTS_FILE_POSTS = true; 00703 00704 protected $curlOptions = array(); 00705 protected $headerText = ""; 00706 00712 protected function readHeader( $fh, $content ) { 00713 $this->headerText .= $content; 00714 return strlen( $content ); 00715 } 00716 00717 public function execute() { 00718 wfProfileIn( __METHOD__ ); 00719 00720 parent::execute(); 00721 00722 if ( !$this->status->isOK() ) { 00723 wfProfileOut( __METHOD__ ); 00724 return $this->status; 00725 } 00726 00727 $this->curlOptions[CURLOPT_PROXY] = $this->proxy; 00728 $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout; 00729 00730 // Only supported in curl >= 7.16.2 00731 if ( defined( 'CURLOPT_CONNECTTIMEOUT_MS' ) ) { 00732 $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000; 00733 } 00734 00735 $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; 00736 $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback; 00737 $this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" ); 00738 $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects; 00739 $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression 00740 00741 $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent']; 00742 00743 $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0; 00744 $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert; 00745 00746 if ( $this->caInfo ) { 00747 $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo; 00748 } 00749 00750 if ( $this->headersOnly ) { 00751 $this->curlOptions[CURLOPT_NOBODY] = true; 00752 $this->curlOptions[CURLOPT_HEADER] = true; 00753 } elseif ( $this->method == 'POST' ) { 00754 $this->curlOptions[CURLOPT_POST] = true; 00755 $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; 00756 // Suppress 'Expect: 100-continue' header, as some servers 00757 // will reject it with a 417 and Curl won't auto retry 00758 // with HTTP 1.0 fallback 00759 $this->reqHeaders['Expect'] = ''; 00760 } else { 00761 $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method; 00762 } 00763 00764 $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList(); 00765 00766 $curlHandle = curl_init( $this->url ); 00767 00768 if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { 00769 wfProfileOut( __METHOD__ ); 00770 throw new MWException( "Error setting curl options." ); 00771 } 00772 00773 if ( $this->followRedirects && $this->canFollowRedirects() ) { 00774 wfSuppressWarnings(); 00775 if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { 00776 wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . 00777 "Probably safe_mode or open_basedir is set.\n" ); 00778 // Continue the processing. If it were in curl_setopt_array, 00779 // processing would have halted on its entry 00780 } 00781 wfRestoreWarnings(); 00782 } 00783 00784 $curlRes = curl_exec( $curlHandle ); 00785 if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) { 00786 $this->status->fatal( 'http-timed-out', $this->url ); 00787 } elseif ( $curlRes === false ) { 00788 $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) ); 00789 } else { 00790 $this->headerList = explode( "\r\n", $this->headerText ); 00791 } 00792 00793 curl_close( $curlHandle ); 00794 00795 $this->parseHeader(); 00796 $this->setStatus(); 00797 00798 wfProfileOut( __METHOD__ ); 00799 00800 return $this->status; 00801 } 00802 00806 public function canFollowRedirects() { 00807 if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) { 00808 wfDebug( "Cannot follow redirects in safe mode\n" ); 00809 return false; 00810 } 00811 00812 $curlVersionInfo = curl_version(); 00813 if ( $curlVersionInfo['version_number'] < 0x071304 ) { 00814 wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" ); 00815 return false; 00816 } 00817 00818 return true; 00819 } 00820 } 00821 00822 class PhpHttpRequest extends MWHttpRequest { 00823 00828 protected function urlToTcp( $url ) { 00829 $parsedUrl = parse_url( $url ); 00830 00831 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port']; 00832 } 00833 00834 public function execute() { 00835 wfProfileIn( __METHOD__ ); 00836 00837 parent::execute(); 00838 00839 if ( is_array( $this->postData ) ) { 00840 $this->postData = wfArrayToCgi( $this->postData ); 00841 } 00842 00843 if ( $this->parsedUrl['scheme'] != 'http' 00844 && $this->parsedUrl['scheme'] != 'https' ) { 00845 $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] ); 00846 } 00847 00848 $this->reqHeaders['Accept'] = "*/*"; 00849 $this->reqHeaders['Connection'] = 'Close'; 00850 if ( $this->method == 'POST' ) { 00851 // Required for HTTP 1.0 POSTs 00852 $this->reqHeaders['Content-Length'] = strlen( $this->postData ); 00853 if ( !isset( $this->reqHeaders['Content-Type'] ) ) { 00854 $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded"; 00855 } 00856 } 00857 00858 // Set up PHP stream context 00859 $options = array( 00860 'http' => array( 00861 'method' => $this->method, 00862 'header' => implode( "\r\n", $this->getHeaderList() ), 00863 'protocol_version' => '1.1', 00864 'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0, 00865 'ignore_errors' => true, 00866 'timeout' => $this->timeout, 00867 // Curl options in case curlwrappers are installed 00868 'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0, 00869 'curl_verify_ssl_peer' => $this->sslVerifyCert, 00870 ), 00871 'ssl' => array( 00872 'verify_peer' => $this->sslVerifyCert, 00873 'SNI_enabled' => true, 00874 ), 00875 ); 00876 00877 if ( $this->proxy ) { 00878 $options['http']['proxy'] = $this->urlToTCP( $this->proxy ); 00879 $options['http']['request_fulluri'] = true; 00880 } 00881 00882 if ( $this->postData ) { 00883 $options['http']['content'] = $this->postData; 00884 } 00885 00886 if ( $this->sslVerifyHost ) { 00887 $options['ssl']['CN_match'] = $this->parsedUrl['host']; 00888 } 00889 00890 if ( is_dir( $this->caInfo ) ) { 00891 $options['ssl']['capath'] = $this->caInfo; 00892 } elseif ( is_file( $this->caInfo ) ) { 00893 $options['ssl']['cafile'] = $this->caInfo; 00894 } elseif ( $this->caInfo ) { 00895 throw new MWException( "Invalid CA info passed: {$this->caInfo}" ); 00896 } 00897 00898 $context = stream_context_create( $options ); 00899 00900 $this->headerList = array(); 00901 $reqCount = 0; 00902 $url = $this->url; 00903 00904 $result = array(); 00905 00906 do { 00907 $reqCount++; 00908 wfSuppressWarnings(); 00909 $fh = fopen( $url, "r", false, $context ); 00910 wfRestoreWarnings(); 00911 00912 if ( !$fh ) { 00913 break; 00914 } 00915 00916 $result = stream_get_meta_data( $fh ); 00917 $this->headerList = $result['wrapper_data']; 00918 $this->parseHeader(); 00919 00920 if ( !$this->followRedirects ) { 00921 break; 00922 } 00923 00924 # Handle manual redirection 00925 if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) { 00926 break; 00927 } 00928 # Check security of URL 00929 $url = $this->getResponseHeader( "Location" ); 00930 00931 if ( !Http::isValidURI( $url ) ) { 00932 wfDebug( __METHOD__ . ": insecure redirection\n" ); 00933 break; 00934 } 00935 } while ( true ); 00936 00937 $this->setStatus(); 00938 00939 if ( $fh === false ) { 00940 $this->status->fatal( 'http-request-error' ); 00941 wfProfileOut( __METHOD__ ); 00942 return $this->status; 00943 } 00944 00945 if ( $result['timed_out'] ) { 00946 $this->status->fatal( 'http-timed-out', $this->url ); 00947 wfProfileOut( __METHOD__ ); 00948 return $this->status; 00949 } 00950 00951 // If everything went OK, or we received some error code 00952 // get the response body content. 00953 if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) { 00954 while ( !feof( $fh ) ) { 00955 $buf = fread( $fh, 8192 ); 00956 00957 if ( $buf === false ) { 00958 $this->status->fatal( 'http-read-error' ); 00959 break; 00960 } 00961 00962 if ( strlen( $buf ) ) { 00963 call_user_func( $this->callback, $fh, $buf ); 00964 } 00965 } 00966 } 00967 fclose( $fh ); 00968 00969 wfProfileOut( __METHOD__ ); 00970 00971 return $this->status; 00972 } 00973 }