Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/Rationalize transports (v2) #188

Merged
merged 13 commits into from
Oct 26, 2015
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# Ignore coverage report
tests/coverage/*

# Ignore composer related files
/composer.lock
/vendor
46 changes: 44 additions & 2 deletions library/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ class Requests {
*/
const DELETE = 'DELETE';

/**
* OPTIONS method
*
* @var string
*/
const OPTIONS = 'OPTIONS';

/**
* TRACE method
*
* @var string
*/
const TRACE = 'TRACE';

/**
* PATCH method
*
Expand Down Expand Up @@ -220,6 +234,13 @@ public static function head($url, $headers = array(), $options = array()) {
public static function delete($url, $headers = array(), $options = array()) {
return self::request($url, $headers, null, self::DELETE, $options);
}

/**
* Send a TRACE request
*/
public static function trace($url, $headers = array(), $options = array()) {
return self::request($url, $headers, null, self::TRACE, $options);
}
/**#@-*/

/**#@+
Expand All @@ -243,6 +264,13 @@ public static function put($url, $headers = array(), $data = array(), $options =
return self::request($url, $headers, $data, self::PUT, $options);
}

/**
* Send an OPTIONS request
*/
public static function options($url, $headers = array(), $data = array(), $options = array()) {
return self::request($url, $headers, $data, self::OPTIONS, $options);
}

/**
* Send a PATCH request
*
Expand Down Expand Up @@ -301,6 +329,9 @@ public static function patch($url, $headers, $data = array(), $options = array()
* (string|boolean, default: library/Requests/Transport/cacert.pem)
* - `verifyname`: Should we verify the common name in the SSL certificate?
* (boolean: default, true)
* - `data_format`: How should we send the `$data` parameter?
* (string, one of 'query' or 'body', default: 'query' for
* HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH)
*
* @throws Requests_Exception On invalid URLs (`nonhttp`)
*
Expand Down Expand Up @@ -461,6 +492,7 @@ protected static function get_default_options($multirequest = false) {
'timeout' => 10,
'connect_timeout' => 10,
'useragent' => 'php-requests/' . self::VERSION,
'protocol_version' => 1.1,
'redirected' => 0,
'redirects' => 10,
'follow_redirects' => true,
Expand Down Expand Up @@ -531,6 +563,15 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio
$iri->host = Requests_IDNAEncoder::encode($iri->ihost);
$url = $iri->uri;
}

if (!isset($options['data_format'])) {
if (in_array($type, array(self::HEAD, self::GET, self::DELETE))) {
$options['data_format'] = 'query';
}
else {
$options['data_format'] = 'body';
}
}
}

/**
Expand Down Expand Up @@ -573,11 +614,12 @@ protected static function parse_response($headers, $url, $req_headers, $req_data
// Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
$headers = preg_replace('/\n[ \t]/', ' ', $headers);
$headers = explode("\n", $headers);
preg_match('#^HTTP/1\.\d[ \t]+(\d+)#i', array_shift($headers), $matches);
preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
if (empty($matches)) {
throw new Requests_Exception('Response could not be parsed', 'noversion', $headers);
}
$return->status_code = (int) $matches[1];
$return->protocol_version = (float) $matches[1];
$return->status_code = (int) $matches[2];
if ($return->status_code >= 200 && $return->status_code < 300) {
$return->success = true;
}
Expand Down
8 changes: 7 additions & 1 deletion library/Requests/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public function __construct() {
*/
public $status_code = false;

/**
* Protocol version, false if non-blocking
* @var float|boolean
*/
public $protocol_version = false;

/**
* Whether the request succeeded or not
*
Expand Down Expand Up @@ -112,4 +118,4 @@ public function throw_for_status($allow_redirects = true) {
throw new $exception(null, $this);
}
}
}
}
43 changes: 34 additions & 9 deletions library/Requests/Transport/cURL.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ public function __construct() {
}
}

/**
* Destructor
*/
public function __destruct() {
if (is_resource($this->handle)) {
curl_close($this->handle);
}
}

/**
* Perform a request
*
Expand Down Expand Up @@ -166,7 +175,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar
}

$this->process_response($response, $options);
curl_close($this->handle);

return $this->headers;
}

Expand Down Expand Up @@ -282,11 +291,17 @@ protected function setup_handle($url, $headers, $data, $options) {
$options['hooks']->dispatch('curl.before_request', array(&$this->handle));

$headers = Requests::flatten($headers);
if (in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)) & !empty($data)) {
$url = self::format_get($url, $data);
}
elseif (!empty($data) && !is_string($data)) {
$data = http_build_query($data, null, '&');

if (!empty($data)) {
$data_format = $options['data_format'];

if ($data_format === 'query') {
$url = self::format_get($url, $data);
$data = '';
}
elseif (!is_string($data)) {
$data = http_build_query($data, null, '&');
}
}

switch ($options['type']) {
Expand All @@ -296,15 +311,18 @@ protected function setup_handle($url, $headers, $data, $options) {
break;
case Requests::PATCH:
case Requests::PUT:
case Requests::DELETE:
case Requests::OPTIONS:
curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
break;
case Requests::DELETE:
curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case Requests::HEAD:
curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
curl_setopt($this->handle, CURLOPT_NOBODY, true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@egeloen Looks like CURLOPT_POSTFIELDS doesn't get set for HEAD requests; was this intentional?

break;
case Requests::TRACE:
curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
break;
}

if (is_int($options['timeout']) || $this->version < self::CURL_7_16_2) {
Expand All @@ -325,6 +343,13 @@ protected function setup_handle($url, $headers, $data, $options) {
curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);

if ($options['protocol_version'] === 1.1) {
curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
}
else {
curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
}

if (true === $options['blocking']) {
curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
Expand Down
93 changes: 47 additions & 46 deletions library/Requests/Transport/fsockopen.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar
$host = $url_parts['host'];
$context = stream_context_create();
$verifyname = false;
$case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers);

// HTTPS support
if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
Expand Down Expand Up @@ -120,72 +121,68 @@ public function request($url, $headers = array(), $data = array(), $options = ar

restore_error_handler();

if ($verifyname) {
if (!$this->verify_certificate_from_context($host, $context)) {
throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
}
if ($verifyname && !$this->verify_certificate_from_context($host, $context)) {
throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
}

if (!$socket) {
if ($errno === 0) {
// Connection issue
throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
}
else {
throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno);
}

throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno);
}

$data_format = $options['data_format'];

if ($data_format === 'query') {
$path = self::format_get($url_parts, $data);
$data = '';
}
else {
$path = self::format_get($url_parts, array());
}

$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));

$request_body = '';
$out = '';
switch ($options['type']) {
case Requests::POST:
case Requests::PUT:
case Requests::PATCH:
if (isset($url_parts['path'])) {
$path = $url_parts['path'];
if (isset($url_parts['query'])) {
$path .= '?' . $url_parts['query'];
}
}
else {
$path = '/';
}
$out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']);

$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
$out = sprintf("%s %s HTTP/1.0\r\n", $options['type'], $path);
if ($options['type'] !== Requests::TRACE) {
if (is_array($data)) {
$request_body = http_build_query($data, null, '&');
}
else {
$request_body = $data;
}

if (is_array($data)) {
$request_body = http_build_query($data, null, '&');
}
else {
$request_body = $data;
}
if (empty($headers['Content-Length'])) {
if (!empty($data)) {
if (!isset($case_insensitive_headers['Content-Length'])) {
$headers['Content-Length'] = strlen($request_body);
}
if (empty($headers['Content-Type'])) {

if (!isset($case_insensitive_headers['Content-Type'])) {
$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
}
break;
case Requests::HEAD:
case Requests::GET:
case Requests::DELETE:
$path = self::format_get($url_parts, $data);
$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
$out = sprintf("%s %s HTTP/1.0\r\n", $options['type'], $path);
break;
}
}
$out .= sprintf('Host: %s', $url_parts['host']);

if ($url_parts['port'] !== 80) {
$out .= ':' . $url_parts['port'];
if (!isset($case_insensitive_headers['Host'])) {
$out .= sprintf('Host: %s', $url_parts['host']);

if ($url_parts['port'] !== 80) {
$out .= ':' . $url_parts['port'];
}
$out .= "\r\n";
}

if (!isset($case_insensitive_headers['User-Agent'])) {
$out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
}
$out .= "\r\n";

$out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
$accept_encoding = $this->accept_encoding();
if (!empty($accept_encoding)) {
if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) {
$out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding);
}

Expand All @@ -201,7 +198,11 @@ public function request($url, $headers = array(), $data = array(), $options = ar
$out .= "\r\n";
}

$out .= "Connection: Close\r\n\r\n" . $request_body;
if (!isset($case_insensitive_headers['Connection'])) {
$out .= "Connection: Close\r\n";
}

$out .= "\r\n" . $request_body;

$options['hooks']->dispatch('fsockopen.before_send', array(&$out));

Expand Down
11 changes: 11 additions & 0 deletions library/Requests/Utility/CaseInsensitiveDictionary.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ class Requests_Utility_CaseInsensitiveDictionary implements ArrayAccess, Iterato
*/
protected $data = array();

/**
* Creates a case insensitive dictionary.
*
* @param array $data Dictionary/map to convert to case-insensitive
*/
public function __construct(array $data = array()) {
foreach ($data as $key => $value) {
$this->offsetSet($key, $value);
}
}

/**
* Check if the given item exists
*
Expand Down
16 changes: 15 additions & 1 deletion tests/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ public function testHeaderParsing() {
}
}

public function testProtocolVersionParsing() {
$transport = new RawTransport();
$transport->data =
"HTTP/1.0 200 OK\r\n".
"Host: localhost\r\n\r\n";

$options = array(
'transport' => $transport
);

$response = Requests::get('http://example.com/', array(), $options);
$this->assertEquals(1.0, $response->protocol_version);
}

public function testRawAccess() {
$transport = new RawTransport();
$transport->data =
Expand Down Expand Up @@ -145,4 +159,4 @@ public function testTimeoutException() {
$options = array('timeout' => 0.5);
$response = Requests::get(httpbin('/delay/3'), array(), $options);
}
}
}
Loading