Page MenuHomePhabricator

HTTPFuture.php
No OneTemporary

HTTPFuture.php

<?php
/**
* Socket-based HTTP future, for making HTTP requests using future semantics.
* This is an alternative to @{class:CURLFuture} which has better resolution
* behavior (select()-based wait instead of busy wait) but fewer features. You
* should prefer this class to @{class:CURLFuture} unless you need its advanced
* features (like HTTP/1.1, chunked transfer encoding, gzip, etc.).
*
* Example Usage
*
* $future = new HTTPFuture('http://www.example.com/');
* list($response_body, $headers) = $future->resolvex();
*
* Or
*
* $future = new HTTPFuture('http://www.example.com/');
* list($http_response_status_object,
* $response_body,
* $headers) = $future->resolve();
*
* Prefer @{method:resolvex} to @{method:resolve} as the former throws
* @{class:HTTPFutureHTTPResponseStatus} on failures, which includes an
* informative exception message.
*/
final class HTTPFuture extends BaseHTTPFuture {
private $host;
private $port = 80;
private $fullRequestPath;
private $socket;
private $writeBuffer;
private $response;
private $stateConnected = false;
private $stateWriteComplete = false;
private $stateReady = false;
private $stateStartTime;
private $profilerCallID;
public function setURI($uri) {
$parts = parse_url($uri);
if (!$parts) {
throw new Exception(pht("Could not parse URI '%s'.", $uri));
}
if (empty($parts['scheme']) || $parts['scheme'] !== 'http') {
throw new Exception(
pht(
"URI '%s' must be fully qualified with '%s' scheme.",
$uri,
'http://'));
}
if (!isset($parts['host'])) {
throw new Exception(
pht("URI '%s' must be fully qualified and include host name.", $uri));
}
$this->host = $parts['host'];
if (!empty($parts['port'])) {
$this->port = $parts['port'];
}
if (isset($parts['user']) || isset($parts['pass'])) {
throw new Exception(
pht('HTTP Basic Auth is not supported by %s.', __CLASS__));
}
if (isset($parts['path'])) {
$this->fullRequestPath = $parts['path'];
} else {
$this->fullRequestPath = '/';
}
if (isset($parts['query'])) {
$this->fullRequestPath .= '?'.$parts['query'];
}
return parent::setURI($uri);
}
public function __destruct() {
if ($this->socket) {
@fclose($this->socket);
$this->socket = null;
}
}
public function getReadSockets() {
if ($this->socket) {
return array($this->socket);
}
return array();
}
public function getWriteSockets() {
if (strlen($this->writeBuffer)) {
return array($this->socket);
}
return array();
}
public function isWriteComplete() {
return $this->stateWriteComplete;
}
private function getDefaultUserAgent() {
return __CLASS__.'/1.0';
}
public function isReady() {
if ($this->stateReady) {
return true;
}
if (!$this->socket) {
$this->stateStartTime = microtime(true);
$this->socket = $this->buildSocket();
if (!$this->socket) {
return $this->stateReady;
}
$profiler = PhutilServiceProfiler::getInstance();
$this->profilerCallID = $profiler->beginServiceCall(
array(
'type' => 'http',
'uri' => $this->getURI(),
));
}
if (!$this->stateConnected) {
$read = array();
$write = array($this->socket);
$except = array();
$select = stream_select($read, $write, $except, $tv_sec = 0);
if ($write) {
$this->stateConnected = true;
}
}
if ($this->stateConnected) {
if (strlen($this->writeBuffer)) {
$bytes = @fwrite($this->socket, $this->writeBuffer);
if ($bytes === false) {
throw new Exception(pht('Failed to write to buffer.'));
} else if ($bytes) {
$this->writeBuffer = substr($this->writeBuffer, $bytes);
}
}
if (!strlen($this->writeBuffer)) {
$this->stateWriteComplete = true;
}
while (($data = fread($this->socket, 32768)) || strlen($data)) {
$this->response .= $data;
}
if ($data === false) {
throw new Exception(pht('Failed to read socket.'));
}
}
return $this->checkSocket();
}
private function buildSocket() {
$errno = null;
$errstr = null;
$socket = @stream_socket_client(
'tcp://'.$this->host.':'.$this->port,
$errno,
$errstr,
$ignored_connection_timeout = 1.0,
STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
if (!$socket) {
$this->stateReady = true;
$this->result = $this->buildErrorResult(
HTTPFutureTransportResponseStatus::ERROR_CONNECTION_FAILED);
return null;
}
$ok = stream_set_blocking($socket, 0);
if (!$ok) {
throw new Exception(pht('Failed to set stream nonblocking.'));
}
$this->writeBuffer = $this->buildHTTPRequest();
return $socket;
}
private function checkSocket() {
$timeout = false;
$now = microtime(true);
if (($now - $this->stateStartTime) > $this->getTimeout()) {
$timeout = true;
}
if (!feof($this->socket) && !$timeout) {
return false;
}
$this->stateReady = true;
if ($timeout) {
$this->result = $this->buildErrorResult(
HTTPFutureTransportResponseStatus::ERROR_TIMEOUT);
} else if (!$this->stateConnected) {
$this->result = $this->buildErrorResult(
HTTPFutureTransportResponseStatus::ERROR_CONNECTION_REFUSED);
} else if (!$this->stateWriteComplete) {
$this->result = $this->buildErrorResult(
HTTPFutureTransportResponseStatus::ERROR_CONNECTION_FAILED);
} else {
$this->result = $this->parseRawHTTPResponse($this->response);
}
$profiler = PhutilServiceProfiler::getInstance();
$profiler->endServiceCall($this->profilerCallID, array());
return true;
}
private function buildErrorResult($error) {
return array(
$status = new HTTPFutureTransportResponseStatus($error, $this->getURI()),
$body = null,
$headers = array(),
);
}
private function buildHTTPRequest() {
$data = $this->getData();
$method = $this->getMethod();
$uri = $this->fullRequestPath;
$add_headers = array();
if ($this->getMethod() == 'GET') {
if (is_array($data)) {
$data = http_build_query($data, '', '&');
if (strpos($uri, '?') !== false) {
$uri .= '&'.$data;
} else {
$uri .= '?'.$data;
}
$data = '';
}
} else {
if (is_array($data)) {
$data = http_build_query($data, '', '&')."\r\n";
$add_headers[] = array(
'Content-Type',
'application/x-www-form-urlencoded',
);
}
}
$length = strlen($data);
$add_headers[] = array(
'Content-Length',
$length,
);
if (!$this->getHeaders('User-Agent')) {
$add_headers[] = array(
'User-Agent',
$this->getDefaultUserAgent(),
);
}
if (!$this->getHeaders('Host')) {
$add_headers[] = array(
'Host',
$this->host,
);
}
$headers = array_merge($this->getHeaders(), $add_headers);
foreach ($headers as $key => $header) {
list($name, $value) = $header;
if (strlen($value)) {
$value = ': '.$value;
}
$headers[$key] = $name.$value."\r\n";
}
return
"{$method} {$uri} HTTP/1.0\r\n".
implode('', $headers).
"\r\n".
$data;
}
}

File Metadata

Mime Type
text/x-php
Expires
Tue, Dec 24, 5:59 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6903239
Default Alt Text
HTTPFuture.php (7 KB)

Event Timeline