Changeset View
Changeset View
Standalone View
Standalone View
src/future/http/HTTPSFuture.php
Show All 16 Lines | final class HTTPSFuture extends BaseHTTPFuture { | ||||
private $responseBuffer = ''; | private $responseBuffer = ''; | ||||
private $responseBufferPos; | private $responseBufferPos; | ||||
private $files = array(); | private $files = array(); | ||||
private $temporaryFiles = array(); | private $temporaryFiles = array(); | ||||
private $rawBody; | private $rawBody; | ||||
private $rawBodyPos = 0; | private $rawBodyPos = 0; | ||||
private $fileHandle; | private $fileHandle; | ||||
private $downloadPath; | |||||
private $downloadHandle; | |||||
private $parser; | |||||
/** | /** | ||||
* Create a temp file containing an SSL cert, and use it for this session. | * Create a temp file containing an SSL cert, and use it for this session. | ||||
* | * | ||||
* This allows us to do host-specific SSL certificates in whatever client | * This allows us to do host-specific SSL certificates in whatever client | ||||
* is using libphutil. e.g. in Arcanist, you could add an "ssl_cert" key | * is using libphutil. e.g. in Arcanist, you could add an "ssl_cert" key | ||||
* to a specific host in ~/.arcrc and use that. | * to a specific host in ~/.arcrc and use that. | ||||
* | * | ||||
* cURL needs this to be a file, it doesn't seem to be able to handle a string | * cURL needs this to be a file, it doesn't seem to be able to handle a string | ||||
▲ Show 20 Lines • Show All 99 Lines • ▼ Show 20 Lines | public static function loadContent($uri, $timeout = null) { | ||||
try { | try { | ||||
list($body) = $future->resolvex(); | list($body) = $future->resolvex(); | ||||
return $body; | return $body; | ||||
} catch (HTTPFutureResponseStatus $ex) { | } catch (HTTPFutureResponseStatus $ex) { | ||||
return false; | return false; | ||||
} | } | ||||
} | } | ||||
public function setDownloadPath($download_path) { | |||||
$this->downloadPath = $download_path; | |||||
if (Filesystem::pathExists($download_path)) { | |||||
throw new Exception( | |||||
pht( | |||||
'Specified download path "%s" already exists, refusing to '. | |||||
'overwrite.')); | |||||
} | |||||
return $this; | |||||
} | |||||
/** | /** | ||||
* Attach a file to the request. | * Attach a file to the request. | ||||
* | * | ||||
* @param string HTTP parameter name. | * @param string HTTP parameter name. | ||||
* @param string File content. | * @param string File content. | ||||
* @param string File name. | * @param string File name. | ||||
* @param string File mime type. | * @param string File mime type. | ||||
* @return this | * @return this | ||||
Show All 21 Lines | final class HTTPSFuture extends BaseHTTPFuture { | ||||
public function isReady() { | public function isReady() { | ||||
if (isset($this->result)) { | if (isset($this->result)) { | ||||
return true; | return true; | ||||
} | } | ||||
$uri = $this->getURI(); | $uri = $this->getURI(); | ||||
$domain = id(new PhutilURI($uri))->getDomain(); | $domain = id(new PhutilURI($uri))->getDomain(); | ||||
$is_download = $this->isDownload(); | |||||
// See T13396. For now, use the streaming response parser only if we're | |||||
// downloading the response to disk. | |||||
$use_streaming_parser = (bool)$is_download; | |||||
if (!$this->handle) { | if (!$this->handle) { | ||||
$uri_object = new PhutilURI($uri); | $uri_object = new PhutilURI($uri); | ||||
$proxy = PhutilHTTPEngineExtension::buildHTTPProxyURI($uri_object); | $proxy = PhutilHTTPEngineExtension::buildHTTPProxyURI($uri_object); | ||||
$profiler = PhutilServiceProfiler::getInstance(); | $profiler = PhutilServiceProfiler::getInstance(); | ||||
$this->profilerCallID = $profiler->beginServiceCall( | $this->profilerCallID = $profiler->beginServiceCall( | ||||
array( | array( | ||||
'type' => 'http', | 'type' => 'http', | ||||
▲ Show 20 Lines • Show All 174 Lines • ▼ Show 20 Lines | if (!$this->handle) { | ||||
// See T13391. Recent versions of cURL default to "HTTP/2" on some | // See T13391. Recent versions of cURL default to "HTTP/2" on some | ||||
// connections, but do not support HTTP/2 proxies. Until HTTP/2 | // connections, but do not support HTTP/2 proxies. Until HTTP/2 | ||||
// stabilizes, force HTTP/1.1 explicitly. | // stabilizes, force HTTP/1.1 explicitly. | ||||
curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); | curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); | ||||
if ($proxy) { | if ($proxy) { | ||||
curl_setopt($curl, CURLOPT_PROXY, (string)$proxy); | curl_setopt($curl, CURLOPT_PROXY, (string)$proxy); | ||||
} | } | ||||
if ($is_download) { | |||||
$this->downloadHandle = @fopen($this->downloadPath, 'wb+'); | |||||
if (!$this->downloadHandle) { | |||||
throw new Exception( | |||||
pht( | |||||
'Failed to open filesystem path "%s" for writing.', | |||||
$this->downloadPath)); | |||||
} | |||||
} | |||||
if ($use_streaming_parser) { | |||||
$streaming_parser = id(new PhutilHTTPResponseParser()) | |||||
->setFollowLocationHeaders($this->getFollowLocation()); | |||||
if ($this->downloadHandle) { | |||||
$streaming_parser->setWriteHandle($this->downloadHandle); | |||||
} | |||||
$this->parser = $streaming_parser; | |||||
} | |||||
} else { | } else { | ||||
$curl = $this->handle; | $curl = $this->handle; | ||||
if (!self::$results) { | if (!self::$results) { | ||||
// NOTE: In curl_multi_select(), PHP calls curl_multi_fdset() but does | // NOTE: In curl_multi_select(), PHP calls curl_multi_fdset() but does | ||||
// not check the return value of &maxfd for -1 until recent versions | // not check the return value of &maxfd for -1 until recent versions | ||||
// of PHP (5.4.8 and newer). cURL may return -1 as maxfd in some unusual | // of PHP (5.4.8 and newer). cURL may return -1 as maxfd in some unusual | ||||
// situations; if it does, PHP enters select() with nfds=0, which blocks | // situations; if it does, PHP enters select() with nfds=0, which blocks | ||||
Show All 36 Lines | if ($err_code) { | ||||
$uri); | $uri); | ||||
} else { | } else { | ||||
$status = new HTTPFutureCURLResponseStatus($err_code, $uri); | $status = new HTTPFutureCURLResponseStatus($err_code, $uri); | ||||
} | } | ||||
$body = null; | $body = null; | ||||
$headers = array(); | $headers = array(); | ||||
$this->result = array($status, $body, $headers); | $this->result = array($status, $body, $headers); | ||||
} else if ($this->parser) { | |||||
$streaming_parser = $this->parser; | |||||
try { | |||||
$responses = $streaming_parser->getResponses(); | |||||
$final_response = last($responses); | |||||
$result = array( | |||||
$final_response->getStatus(), | |||||
$final_response->getBody(), | |||||
$final_response->getHeaders(), | |||||
); | |||||
} catch (HTTPFutureParseResponseStatus $ex) { | |||||
$result = array($ex, null, array()); | |||||
} | |||||
$this->result = $result; | |||||
} else { | } else { | ||||
// cURL returns headers of all redirects, we strip all but the final one. | // cURL returns headers of all redirects, we strip all but the final one. | ||||
$redirects = curl_getinfo($curl, CURLINFO_REDIRECT_COUNT); | $redirects = curl_getinfo($curl, CURLINFO_REDIRECT_COUNT); | ||||
$result = preg_replace('/^(.*\r\n\r\n){'.$redirects.'}/sU', '', $result); | $result = preg_replace('/^(.*\r\n\r\n){'.$redirects.'}/sU', '', $result); | ||||
$this->result = $this->parseRawHTTPResponse($result); | $this->result = $this->parseRawHTTPResponse($result); | ||||
} | } | ||||
curl_multi_remove_handle(self::$multi, $curl); | curl_multi_remove_handle(self::$multi, $curl); | ||||
unset(self::$results[(int)$curl]); | unset(self::$results[(int)$curl]); | ||||
// NOTE: We want to use keepalive if possible. Return the handle to a | // NOTE: We want to use keepalive if possible. Return the handle to a | ||||
// pool for the domain; don't close it. | // pool for the domain; don't close it. | ||||
if ($this->shouldReuseHandles()) { | if ($this->shouldReuseHandles()) { | ||||
self::$pool[$domain][] = $curl; | self::$pool[$domain][] = $curl; | ||||
} | } | ||||
if ($is_download) { | |||||
if ($this->downloadHandle) { | |||||
fflush($this->downloadHandle); | |||||
fclose($this->downloadHandle); | |||||
$this->downloadHandle = null; | |||||
} | |||||
} | |||||
$profiler = PhutilServiceProfiler::getInstance(); | $profiler = PhutilServiceProfiler::getInstance(); | ||||
$profiler->endServiceCall($this->profilerCallID, array()); | $profiler->endServiceCall($this->profilerCallID, array()); | ||||
return true; | return true; | ||||
} | } | ||||
/** | /** | ||||
* Callback invoked by cURL as it reads HTTP data from the response. We save | * Callback invoked by cURL as it reads HTTP data from the response. We save | ||||
* the data to a buffer. | * the data to a buffer. | ||||
*/ | */ | ||||
public function didReceiveDataCallback($handle, $data) { | public function didReceiveDataCallback($handle, $data) { | ||||
if ($this->parser) { | |||||
$this->parser->readBytes($data); | |||||
} else { | |||||
$this->responseBuffer .= $data; | $this->responseBuffer .= $data; | ||||
} | |||||
return strlen($data); | return strlen($data); | ||||
} | } | ||||
/** | /** | ||||
* Read data from the response buffer. | * Read data from the response buffer. | ||||
* | * | ||||
* NOTE: Like @{class:ExecFuture}, this method advances a read cursor but | * NOTE: Like @{class:ExecFuture}, this method advances a read cursor but | ||||
* does not discard the data. The data will still be buffered, and it will | * does not discard the data. The data will still be buffered, and it will | ||||
* all be returned when the future resolves. To discard the data after | * all be returned when the future resolves. To discard the data after | ||||
* reading it, call @{method:discardBuffers}. | * reading it, call @{method:discardBuffers}. | ||||
* | * | ||||
* @return string Response data, if available. | * @return string Response data, if available. | ||||
*/ | */ | ||||
public function read() { | public function read() { | ||||
if ($this->isDownload()) { | |||||
throw new Exception( | |||||
pht( | |||||
'You can not read the result buffer while streaming results '. | |||||
'to disk: there is no in-memory buffer to read.')); | |||||
} | |||||
if ($this->parser) { | |||||
throw new Exception( | |||||
pht( | |||||
'Streaming reads are not currently supported by the streaming '. | |||||
'parser.')); | |||||
} | |||||
$result = substr($this->responseBuffer, $this->responseBufferPos); | $result = substr($this->responseBuffer, $this->responseBufferPos); | ||||
$this->responseBufferPos = strlen($this->responseBuffer); | $this->responseBufferPos = strlen($this->responseBuffer); | ||||
return $result; | return $result; | ||||
} | } | ||||
/** | /** | ||||
* Discard any buffered data. Normally, you call this after reading the | * Discard any buffered data. Normally, you call this after reading the | ||||
* data with @{method:read}. | * data with @{method:read}. | ||||
* | * | ||||
* @return this | * @return this | ||||
*/ | */ | ||||
public function discardBuffers() { | public function discardBuffers() { | ||||
if ($this->isDownload()) { | |||||
throw new Exception( | |||||
pht( | |||||
'You can not discard the result buffer while streaming results '. | |||||
'to disk: there is no in-memory buffer to discard.')); | |||||
} | |||||
if ($this->parser) { | |||||
throw new Exception( | |||||
pht( | |||||
'Buffer discards are not currently supported by the streaming '. | |||||
'parser.')); | |||||
} | |||||
$this->responseBuffer = ''; | $this->responseBuffer = ''; | ||||
$this->responseBufferPos = 0; | $this->responseBufferPos = 0; | ||||
return $this; | return $this; | ||||
} | } | ||||
/** | /** | ||||
* Produces a value safe to pass to `CURLOPT_POSTFIELDS`. | * Produces a value safe to pass to `CURLOPT_POSTFIELDS`. | ||||
▲ Show 20 Lines • Show All 201 Lines • ▼ Show 20 Lines | private function shouldReuseHandles() { | ||||
// handle reuse and accept a small performance penalty. See T8654. | // handle reuse and accept a small performance penalty. See T8654. | ||||
if ($version == '7.43.0') { | if ($version == '7.43.0') { | ||||
return false; | return false; | ||||
} | } | ||||
return true; | return true; | ||||
} | } | ||||
private function isDownload() { | |||||
return ($this->downloadPath !== null); | |||||
} | |||||
} | } |