mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-27 07:00:29 +01:00
602 lines
18 KiB
PHP
602 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Sabre\HTTP;
|
|
|
|
use Sabre\Event\EventEmitter;
|
|
use Sabre\Uri;
|
|
|
|
/**
|
|
* A rudimentary HTTP client.
|
|
*
|
|
* This object wraps PHP's curl extension and provides an easy way to send it a
|
|
* Request object, and return a Response object.
|
|
*
|
|
* This is by no means intended as the next best HTTP client, but it does the
|
|
* job and provides a simple integration with the rest of sabre/http.
|
|
*
|
|
* This client emits the following events:
|
|
* beforeRequest(RequestInterface $request)
|
|
* afterRequest(RequestInterface $request, ResponseInterface $response)
|
|
* error(RequestInterface $request, ResponseInterface $response, bool &$retry, int $retryCount)
|
|
* exception(RequestInterface $request, ClientException $e, bool &$retry, int $retryCount)
|
|
*
|
|
* The beforeRequest event allows you to do some last minute changes to the
|
|
* request before it's done, such as adding authentication headers.
|
|
*
|
|
* The afterRequest event will be emitted after the request is completed
|
|
* succesfully.
|
|
*
|
|
* If a HTTP error is returned (status code higher than 399) the error event is
|
|
* triggered. It's possible using this event to retry the request, by setting
|
|
* retry to true.
|
|
*
|
|
* The amount of times a request has retried is passed as $retryCount, which
|
|
* can be used to avoid retrying indefinitely. The first time the event is
|
|
* called, this will be 0.
|
|
*
|
|
* It's also possible to intercept specific http errors, by subscribing to for
|
|
* example 'error:401'.
|
|
*
|
|
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
|
* @author Evert Pot (http://evertpot.com/)
|
|
* @license http://sabre.io/license/ Modified BSD License
|
|
*/
|
|
class Client extends EventEmitter {
|
|
|
|
/**
|
|
* List of curl settings
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $curlSettings = [];
|
|
|
|
/**
|
|
* Wether or not exceptions should be thrown when a HTTP error is returned.
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $throwExceptions = false;
|
|
|
|
/**
|
|
* The maximum number of times we'll follow a redirect.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $maxRedirects = 5;
|
|
|
|
/**
|
|
* Initializes the client.
|
|
*
|
|
* @return void
|
|
*/
|
|
function __construct() {
|
|
|
|
$this->curlSettings = [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HEADER => true,
|
|
CURLOPT_NOBODY => false,
|
|
CURLOPT_USERAGENT => 'sabre-http/' . Version::VERSION . ' (http://sabre.io/)',
|
|
];
|
|
|
|
}
|
|
|
|
/**
|
|
* Sends a request to a HTTP server, and returns a response.
|
|
*
|
|
* @param RequestInterface $request
|
|
* @return ResponseInterface
|
|
*/
|
|
function send(RequestInterface $request) {
|
|
|
|
$this->emit('beforeRequest', [$request]);
|
|
|
|
$retryCount = 0;
|
|
$redirects = 0;
|
|
|
|
do {
|
|
|
|
$doRedirect = false;
|
|
$retry = false;
|
|
|
|
try {
|
|
|
|
$response = $this->doRequest($request);
|
|
|
|
$code = (int)$response->getStatus();
|
|
|
|
// We are doing in-PHP redirects, because curl's
|
|
// FOLLOW_LOCATION throws errors when PHP is configured with
|
|
// open_basedir.
|
|
//
|
|
// https://github.com/fruux/sabre-http/issues/12
|
|
if (in_array($code, [301, 302, 307, 308]) && $redirects < $this->maxRedirects) {
|
|
|
|
$oldLocation = $request->getUrl();
|
|
|
|
// Creating a new instance of the request object.
|
|
$request = clone $request;
|
|
|
|
// Setting the new location
|
|
$request->setUrl(Uri\resolve(
|
|
$oldLocation,
|
|
$response->getHeader('Location')
|
|
));
|
|
|
|
$doRedirect = true;
|
|
$redirects++;
|
|
|
|
}
|
|
|
|
// This was a HTTP error
|
|
if ($code >= 400) {
|
|
|
|
$this->emit('error', [$request, $response, &$retry, $retryCount]);
|
|
$this->emit('error:' . $code, [$request, $response, &$retry, $retryCount]);
|
|
|
|
}
|
|
|
|
} catch (ClientException $e) {
|
|
|
|
$this->emit('exception', [$request, $e, &$retry, $retryCount]);
|
|
|
|
// If retry was still set to false, it means no event handler
|
|
// dealt with the problem. In this case we just re-throw the
|
|
// exception.
|
|
if (!$retry) {
|
|
throw $e;
|
|
}
|
|
|
|
}
|
|
|
|
if ($retry) {
|
|
$retryCount++;
|
|
}
|
|
|
|
} while ($retry || $doRedirect);
|
|
|
|
$this->emit('afterRequest', [$request, $response]);
|
|
|
|
if ($this->throwExceptions && $code >= 400) {
|
|
throw new ClientHttpException($response);
|
|
}
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
/**
|
|
* Sends a HTTP request asynchronously.
|
|
*
|
|
* Due to the nature of PHP, you must from time to time poll to see if any
|
|
* new responses came in.
|
|
*
|
|
* After calling sendAsync, you must therefore occasionally call the poll()
|
|
* method, or wait().
|
|
*
|
|
* @param RequestInterface $request
|
|
* @param callable $success
|
|
* @param callable $error
|
|
* @return void
|
|
*/
|
|
function sendAsync(RequestInterface $request, callable $success = null, callable $error = null) {
|
|
|
|
$this->emit('beforeRequest', [$request]);
|
|
$this->sendAsyncInternal($request, $success, $error);
|
|
$this->poll();
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* This method checks if any http requests have gotten results, and if so,
|
|
* call the appropriate success or error handlers.
|
|
*
|
|
* This method will return true if there are still requests waiting to
|
|
* return, and false if all the work is done.
|
|
*
|
|
* @return bool
|
|
*/
|
|
function poll() {
|
|
|
|
// nothing to do?
|
|
if (!$this->curlMultiMap) {
|
|
return false;
|
|
}
|
|
|
|
do {
|
|
$r = curl_multi_exec(
|
|
$this->curlMultiHandle,
|
|
$stillRunning
|
|
);
|
|
} while ($r === CURLM_CALL_MULTI_PERFORM);
|
|
|
|
do {
|
|
|
|
messageQueue:
|
|
|
|
$status = curl_multi_info_read(
|
|
$this->curlMultiHandle,
|
|
$messagesInQueue
|
|
);
|
|
|
|
if ($status && $status['msg'] === CURLMSG_DONE) {
|
|
|
|
$resourceId = intval($status['handle']);
|
|
list(
|
|
$request,
|
|
$successCallback,
|
|
$errorCallback,
|
|
$retryCount,
|
|
) = $this->curlMultiMap[$resourceId];
|
|
unset($this->curlMultiMap[$resourceId]);
|
|
$curlResult = $this->parseCurlResult(curl_multi_getcontent($status['handle']), $status['handle']);
|
|
$retry = false;
|
|
|
|
if ($curlResult['status'] === self::STATUS_CURLERROR) {
|
|
|
|
$e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']);
|
|
$this->emit('exception', [$request, $e, &$retry, $retryCount]);
|
|
|
|
if ($retry) {
|
|
$retryCount++;
|
|
$this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount);
|
|
goto messageQueue;
|
|
}
|
|
|
|
$curlResult['request'] = $request;
|
|
|
|
if ($errorCallback) {
|
|
$errorCallback($curlResult);
|
|
}
|
|
|
|
} elseif ($curlResult['status'] === self::STATUS_HTTPERROR) {
|
|
|
|
$this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]);
|
|
$this->emit('error:' . $curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]);
|
|
|
|
if ($retry) {
|
|
|
|
$retryCount++;
|
|
$this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount);
|
|
goto messageQueue;
|
|
|
|
}
|
|
|
|
$curlResult['request'] = $request;
|
|
|
|
if ($errorCallback) {
|
|
$errorCallback($curlResult);
|
|
}
|
|
|
|
} else {
|
|
|
|
$this->emit('afterRequest', [$request, $curlResult['response']]);
|
|
|
|
if ($successCallback) {
|
|
$successCallback($curlResult['response']);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
} while ($messagesInQueue > 0);
|
|
|
|
return count($this->curlMultiMap) > 0;
|
|
|
|
}
|
|
|
|
/**
|
|
* Processes every HTTP request in the queue, and waits till they are all
|
|
* completed.
|
|
*
|
|
* @return void
|
|
*/
|
|
function wait() {
|
|
|
|
do {
|
|
curl_multi_select($this->curlMultiHandle);
|
|
$stillRunning = $this->poll();
|
|
} while ($stillRunning);
|
|
|
|
}
|
|
|
|
/**
|
|
* If this is set to true, the Client will automatically throw exceptions
|
|
* upon HTTP errors.
|
|
*
|
|
* This means that if a response came back with a status code greater than
|
|
* or equal to 400, we will throw a ClientHttpException.
|
|
*
|
|
* This only works for the send() method. Throwing exceptions for
|
|
* sendAsync() is not supported.
|
|
*
|
|
* @param bool $throwExceptions
|
|
* @return void
|
|
*/
|
|
function setThrowExceptions($throwExceptions) {
|
|
|
|
$this->throwExceptions = $throwExceptions;
|
|
|
|
}
|
|
|
|
/**
|
|
* Adds a CURL setting.
|
|
*
|
|
* These settings will be included in every HTTP request.
|
|
*
|
|
* @param int $name
|
|
* @param mixed $value
|
|
* @return void
|
|
*/
|
|
function addCurlSetting($name, $value) {
|
|
|
|
$this->curlSettings[$name] = $value;
|
|
|
|
}
|
|
|
|
/**
|
|
* This method is responsible for performing a single request.
|
|
*
|
|
* @param RequestInterface $request
|
|
* @return ResponseInterface
|
|
*/
|
|
protected function doRequest(RequestInterface $request) {
|
|
|
|
$settings = $this->createCurlSettingsArray($request);
|
|
|
|
if (!$this->curlHandle) {
|
|
$this->curlHandle = curl_init();
|
|
}
|
|
|
|
curl_setopt_array($this->curlHandle, $settings);
|
|
$response = $this->curlExec($this->curlHandle);
|
|
$response = $this->parseCurlResult($response, $this->curlHandle);
|
|
|
|
if ($response['status'] === self::STATUS_CURLERROR) {
|
|
throw new ClientException($response['curl_errmsg'], $response['curl_errno']);
|
|
}
|
|
|
|
return $response['response'];
|
|
|
|
}
|
|
|
|
/**
|
|
* Cached curl handle.
|
|
*
|
|
* By keeping this resource around for the lifetime of this object, things
|
|
* like persistent connections are possible.
|
|
*
|
|
* @var resource
|
|
*/
|
|
private $curlHandle;
|
|
|
|
/**
|
|
* Handler for curl_multi requests.
|
|
*
|
|
* The first time sendAsync is used, this will be created.
|
|
*
|
|
* @var resource
|
|
*/
|
|
private $curlMultiHandle;
|
|
|
|
/**
|
|
* Has a list of curl handles, as well as their associated success and
|
|
* error callbacks.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $curlMultiMap = [];
|
|
|
|
/**
|
|
* Turns a RequestInterface object into an array with settings that can be
|
|
* fed to curl_setopt
|
|
*
|
|
* @param RequestInterface $request
|
|
* @return array
|
|
*/
|
|
protected function createCurlSettingsArray(RequestInterface $request) {
|
|
|
|
$settings = $this->curlSettings;
|
|
|
|
switch ($request->getMethod()) {
|
|
case 'HEAD' :
|
|
$settings[CURLOPT_NOBODY] = true;
|
|
$settings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
|
|
$settings[CURLOPT_POSTFIELDS] = '';
|
|
$settings[CURLOPT_PUT] = false;
|
|
break;
|
|
case 'GET' :
|
|
$settings[CURLOPT_CUSTOMREQUEST] = 'GET';
|
|
$settings[CURLOPT_POSTFIELDS] = '';
|
|
$settings[CURLOPT_PUT] = false;
|
|
break;
|
|
default :
|
|
$body = $request->getBody();
|
|
if (is_resource($body)) {
|
|
// This needs to be set to PUT, regardless of the actual
|
|
// method used. Without it, INFILE will be ignored for some
|
|
// reason.
|
|
$settings[CURLOPT_PUT] = true;
|
|
$settings[CURLOPT_INFILE] = $request->getBody();
|
|
} else {
|
|
// For security we cast this to a string. If somehow an array could
|
|
// be passed here, it would be possible for an attacker to use @ to
|
|
// post local files.
|
|
$settings[CURLOPT_POSTFIELDS] = (string)$body;
|
|
}
|
|
$settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
|
|
break;
|
|
|
|
}
|
|
|
|
$nHeaders = [];
|
|
foreach ($request->getHeaders() as $key => $values) {
|
|
|
|
foreach ($values as $value) {
|
|
$nHeaders[] = $key . ': ' . $value;
|
|
}
|
|
|
|
}
|
|
$settings[CURLOPT_HTTPHEADER] = $nHeaders;
|
|
$settings[CURLOPT_URL] = $request->getUrl();
|
|
// FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM
|
|
if (defined('CURLOPT_PROTOCOLS')) {
|
|
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
|
}
|
|
// FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM
|
|
if (defined('CURLOPT_REDIR_PROTOCOLS')) {
|
|
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
|
}
|
|
|
|
return $settings;
|
|
|
|
}
|
|
|
|
const STATUS_SUCCESS = 0;
|
|
const STATUS_CURLERROR = 1;
|
|
const STATUS_HTTPERROR = 2;
|
|
|
|
/**
|
|
* Parses the result of a curl call in a format that's a bit more
|
|
* convenient to work with.
|
|
*
|
|
* The method returns an array with the following elements:
|
|
* * status - one of the 3 STATUS constants.
|
|
* * curl_errno - A curl error number. Only set if status is
|
|
* STATUS_CURLERROR.
|
|
* * curl_errmsg - A current error message. Only set if status is
|
|
* STATUS_CURLERROR.
|
|
* * response - Response object. Only set if status is STATUS_SUCCESS, or
|
|
* STATUS_HTTPERROR.
|
|
* * http_code - HTTP status code, as an int. Only set if Only set if
|
|
* status is STATUS_SUCCESS, or STATUS_HTTPERROR
|
|
*
|
|
* @param string $response
|
|
* @param resource $curlHandle
|
|
* @return Response
|
|
*/
|
|
protected function parseCurlResult($response, $curlHandle) {
|
|
|
|
list(
|
|
$curlInfo,
|
|
$curlErrNo,
|
|
$curlErrMsg
|
|
) = $this->curlStuff($curlHandle);
|
|
|
|
if ($curlErrNo) {
|
|
return [
|
|
'status' => self::STATUS_CURLERROR,
|
|
'curl_errno' => $curlErrNo,
|
|
'curl_errmsg' => $curlErrMsg,
|
|
];
|
|
}
|
|
|
|
$headerBlob = substr($response, 0, $curlInfo['header_size']);
|
|
// In the case of 204 No Content, strlen($response) == $curlInfo['header_size].
|
|
// This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL
|
|
// An exception will be thrown when calling getBodyAsString then
|
|
$responseBody = substr($response, $curlInfo['header_size']) ?: null;
|
|
|
|
unset($response);
|
|
|
|
// In the case of 100 Continue, or redirects we'll have multiple lists
|
|
// of headers for each separate HTTP response. We can easily split this
|
|
// because they are separated by \r\n\r\n
|
|
$headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
|
|
|
|
// We only care about the last set of headers
|
|
$headerBlob = $headerBlob[count($headerBlob) - 1];
|
|
|
|
// Splitting headers
|
|
$headerBlob = explode("\r\n", $headerBlob);
|
|
|
|
$response = new Response();
|
|
$response->setStatus($curlInfo['http_code']);
|
|
|
|
foreach ($headerBlob as $header) {
|
|
$parts = explode(':', $header, 2);
|
|
if (count($parts) == 2) {
|
|
$response->addHeader(trim($parts[0]), trim($parts[1]));
|
|
}
|
|
}
|
|
|
|
$response->setBody($responseBody);
|
|
|
|
$httpCode = intval($response->getStatus());
|
|
|
|
return [
|
|
'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS,
|
|
'response' => $response,
|
|
'http_code' => $httpCode,
|
|
];
|
|
|
|
}
|
|
|
|
/**
|
|
* Sends an asynchronous HTTP request.
|
|
*
|
|
* We keep this in a separate method, so we can call it without triggering
|
|
* the beforeRequest event and don't do the poll().
|
|
*
|
|
* @param RequestInterface $request
|
|
* @param callable $success
|
|
* @param callable $error
|
|
* @param int $retryCount
|
|
*/
|
|
protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, $retryCount = 0) {
|
|
|
|
if (!$this->curlMultiHandle) {
|
|
$this->curlMultiHandle = curl_multi_init();
|
|
}
|
|
$curl = curl_init();
|
|
curl_setopt_array(
|
|
$curl,
|
|
$this->createCurlSettingsArray($request)
|
|
);
|
|
curl_multi_add_handle($this->curlMultiHandle, $curl);
|
|
$this->curlMultiMap[intval($curl)] = [
|
|
$request,
|
|
$success,
|
|
$error,
|
|
$retryCount
|
|
];
|
|
|
|
}
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
/**
|
|
* Calls curl_exec
|
|
*
|
|
* This method exists so it can easily be overridden and mocked.
|
|
*
|
|
* @param resource $curlHandle
|
|
* @return string
|
|
*/
|
|
protected function curlExec($curlHandle) {
|
|
|
|
return curl_exec($curlHandle);
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns a bunch of information about a curl request.
|
|
*
|
|
* This method exists so it can easily be overridden and mocked.
|
|
*
|
|
* @param resource $curlHandle
|
|
* @return array
|
|
*/
|
|
protected function curlStuff($curlHandle) {
|
|
|
|
return [
|
|
curl_getinfo($curlHandle),
|
|
curl_errno($curlHandle),
|
|
curl_error($curlHandle),
|
|
];
|
|
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
}
|