Your IP : 18.117.85.73


Current Path : /var/www/axolotl/data/www/novosibirsk.axolotls.ru/bitrix/modules/main/lib/web/
Upload File :
Current File : /var/www/axolotl/data/www/novosibirsk.axolotls.ru/bitrix/modules/main/lib/web/httpclient.php

<?php
/**
 * Bitrix Framework
 * @package bitrix
 * @subpackage main
 * @copyright 2001-2014 Bitrix
 */
namespace Bitrix\Main\Web;

use Bitrix\Main\Text\BinaryString;
use Bitrix\Main\IO;
use Bitrix\Main\Config\Configuration;

class HttpClient
{
	const HTTP_1_0 = "1.0";
	const HTTP_1_1 = "1.1";
	const HTTP_GET = "GET";
	const HTTP_POST = "POST";
	const HTTP_PUT = "PUT";
	const HTTP_HEAD = "HEAD";
	const HTTP_PATCH = "PATCH";
	const HTTP_DELETE = "DELETE";

	const DEFAULT_SOCKET_TIMEOUT = 30;
	const DEFAULT_STREAM_TIMEOUT = 60;
	const DEFAULT_STREAM_TIMEOUT_NO_WAIT = 1;

	const BUF_READ_LEN = 16384;
	const BUF_POST_LEN = 131072;

	protected $proxyHost;
	protected $proxyPort;
	protected $proxyUser;
	protected $proxyPassword;

	protected $resource;
	protected $socketTimeout = self::DEFAULT_SOCKET_TIMEOUT;
	protected $streamTimeout = self::DEFAULT_STREAM_TIMEOUT;
	protected $error = array();
	protected $peerSocketName;

	/** @var HttpHeaders */
	protected $requestHeaders;
	/** @var HttpCookies  */
	protected $requestCookies;
	protected $waitResponse = true;
	protected $redirect = true;
	protected $redirectMax = 5;
	protected $redirectCount = 0;
	protected $compress = false;
	protected $version = self::HTTP_1_0;
	protected $requestCharset = '';
	protected $sslVerify = true;
	protected $bodyLengthMax = 0;
	protected $privateIp = true;

	protected $status = 0;
	/** @var HttpHeaders */
	protected $responseHeaders;
	/** @var HttpCookies  */
	protected $responseCookies;
	protected $result = '';
	protected $outputStream;

	/** @var IpAddress */
	protected $effectiveIp;
	protected $effectiveUrl;
	protected $receivedBytesLength = 0;

	protected $contextOptions = [];

	/**
	 * @param array $options Optional array with options:
	 *		"redirect" bool Follow redirects (default true)
	 *		"redirectMax" int Maximum number of redirects (default 5)
	 *		"waitResponse" bool Read the body or disconnect just after reading headers (default true)
	 *		"socketTimeout" int Connection timeout in seconds (default 30)
	 *		"streamTimeout" int Stream reading timeout in seconds (default 60 for waitResponse == true and 1 for waitResponse == false)
	 *		"version" string HTTP version (HttpClient::HTTP_1_0, HttpClient::HTTP_1_1) (default "1.0")
	 *		"proxyHost" string Proxy host name/address
	 *		"proxyPort" int Proxy port number
	 *		"proxyUser" string Proxy username
	 *		"proxyPassword" string Proxy password
	 *		"compress" bool Accept gzip encoding (default false)
	 *		"charset" string Charset for body in POST and PUT
	 *		"disableSslVerification" bool Pass true to disable ssl check
	 *      "bodyLengthMax" int Maximum length of the body.
	 *      "privateIp" bool Enable or disable requests to private IPs (default true).
	 * 	All the options can be set separately with setters.
	 */
	public function __construct(array $options = null)
	{
		$this->requestHeaders = new HttpHeaders();
		$this->responseHeaders = new HttpHeaders();
		$this->requestCookies = new HttpCookies();
		$this->responseCookies = new HttpCookies();

		if($options === null)
		{
			$options = array();
		}

		$defaultOptions = Configuration::getValue("http_client_options");
		if($defaultOptions !== null)
		{
			$options += $defaultOptions;
		}

		if(!empty($options))
		{
			if(isset($options["redirect"]))
			{
				$this->setRedirect($options["redirect"], $options["redirectMax"]);
			}
			if(isset($options["waitResponse"]))
			{
				$this->waitResponse($options["waitResponse"]);
			}
			if(isset($options["socketTimeout"]))
			{
				$this->setTimeout($options["socketTimeout"]);
			}
			if(isset($options["streamTimeout"]))
			{
				$this->setStreamTimeout($options["streamTimeout"]);
			}
			if(isset($options["version"]))
			{
				$this->setVersion($options["version"]);
			}
			if(isset($options["proxyHost"]))
			{
				$this->setProxy($options["proxyHost"], $options["proxyPort"], $options["proxyUser"], $options["proxyPassword"]);
			}
			if(isset($options["compress"]))
			{
				$this->setCompress($options["compress"]);
			}
			if(isset($options["charset"]))
			{
				$this->setCharset($options["charset"]);
			}
			if(isset($options["disableSslVerification"]) && $options["disableSslVerification"] === true)
			{
				$this->disableSslVerification();
			}
			if(isset($options["bodyLengthMax"]))
			{
				$this->setBodyLengthMax($options["bodyLengthMax"]);
			}
			if(isset($options["privateIp"]))
			{
				$this->setPrivateIp($options["privateIp"]);
			}
		}
	}

	/**
	 * Closes the connection on the object destruction.
	 */
	public function __destruct()
	{
		$this->disconnect();
	}

	/**
	 * Performs GET request.
	 *
	 * @param string $url Absolute URI eg. "http://user:pass @ host:port/path/?query".
	 * @return string|bool Response entity string or false on error. Note, it's empty string if outputStream is set.
	 */
	public function get($url)
	{
		if($this->query(self::HTTP_GET, $url))
		{
			return $this->getResult();
		}
		return false;
	}

	/**
	 * Performs HEAD request.
	 *
	 * @param string $url Absolute URI eg. "http://user:pass @ host:port/path/?query"
	 * @return HttpHeaders|bool Response headers or false on error.
	 */
	public function head($url)
	{
		if($this->query(self::HTTP_HEAD, $url))
		{
			return $this->getHeaders();
		}
		return false;
	}

	/**
	 * Performs POST request.
	 *
	 * @param string $url Absolute URI eg. "http://user:pass @ host:port/path/?query".
	 * @param array|string|resource $postData Entity of POST/PUT request. If it's resource handler then data will be read directly from the stream.
	 * @param boolean $multipart Whether or not to use multipart/form-data encoding. If true, method accepts file as a resource or as an array with keys 'resource' (or 'content') and optionally 'filename' and 'contentType'
	 * @return string|bool Response entity string or false on error. Note, it's empty string if outputStream is set.
	 */
	public function post($url, $postData = null, $multipart = false)
	{
		if ($multipart)
		{
			$postData = $this->prepareMultipart($postData);
			if($postData === false)
			{
				return false;
			}
		}

		if($this->query(self::HTTP_POST, $url, $postData))
		{
			return $this->getResult();
		}
		return false;
	}

	/**
	 * Performs multipart/form-data encoding.
	 * Accepts file as a resource or as an array with keys 'resource' (or 'content') and optionally 'filename' and 'contentType'
	 *
	 * @param array|string|resource $postData Entity of POST/PUT request
	 * @return string|bool False on error
	 */
	protected function prepareMultipart($postData)
	{
		if (is_array($postData))
		{
			$boundary = 'BXC'.md5(rand().time());

			$data = '';

			foreach ($postData as $k => $v)
			{
				$data .= '--'.$boundary."\r\n";

				if ((is_resource($v) && get_resource_type($v) === 'stream') || is_array($v))
				{
					$filename = $k;
					$contentType = 'application/octet-stream';

					if (is_array($v))
					{
						if (isset($v['resource']) && is_resource($v['resource']) && get_resource_type($v['resource']) === 'stream')
						{
							$resource = $v['resource'];
							$content = stream_get_contents($resource);
						}
						else
						{
							if (isset($v['content']))
							{
								$content = $v['content'];
							}
							else
							{
								$this->error["MULTIPART"] = "File `{$k}` not found for multipart upload";
								trigger_error($this->error["MULTIPART"], E_USER_WARNING);
								return false;
							}
						}

						if (isset($v['filename']))
						{
							$filename = $v['filename'];
						}

						if (isset($v['contentType']))
						{
							$contentType = $v['contentType'];
						}
					}
					else
					{
						$content = stream_get_contents($v);
					}

					$data .= 'Content-Disposition: form-data; name="'.$k.'"; filename="'.$filename.'"'."\r\n";
					$data .= 'Content-Type: '.$contentType."\r\n\r\n";
					$data .= $content."\r\n";
				}
				else
				{
					$data .= 'Content-Disposition: form-data; name="'.$k.'"'."\r\n\r\n";
					$data .= $v."\r\n";
				}
			}

			$data .= '--'.$boundary."--\r\n";
			$postData = $data;

			$this->setHeader('Content-type', 'multipart/form-data; boundary='.$boundary);
		}

		return $postData;
	}

	/**
	 * Perfoms HTTP request.
	 *
	 * @param string $method HTTP method (GET, POST, etc.). Note, it must be in UPPERCASE.
	 * @param string $url Absolute URI eg. "http://user:pass @ host:port/path/?query".
	 * @param array|string|resource $entityBody Entity body of the request. If it's resource handler then data will be read directly from the stream.
	 * @return bool Query result (true or false). Response entity string can be get via getResult() method. Note, it's empty string if outputStream is set.
	 */
	public function query($method, $url, $entityBody = null)
	{
		$queryMethod = $method;
		$this->effectiveUrl = $url;
		$this->effectiveIp = null;
		$this->error = [];

		if(is_array($entityBody))
		{
			$entityBody = http_build_query($entityBody, "", "&");
		}

		$this->redirectCount = 0;

		while(true)
		{
			//Only absoluteURI is accepted
			//Location response-header field must be absoluteURI either
			$parsedUrl = new Uri($this->effectiveUrl);
			if($parsedUrl->getHost() == '')
			{
				$this->error["URI"] = "Incorrect URI: ".$this->effectiveUrl;
				return false;
			}

			$error = $parsedUrl->convertToPunycode();
			if($error instanceof \Bitrix\Main\Error)
			{
				$this->error["URI"] = "Error converting hostname to punycode: ".$error->getMessage();
				return false;
			}

			if($this->privateIp == false)
			{
				$ip = IpAddress::createByUri($parsedUrl);
				if($ip->isPrivate())
				{
					$this->error["PRIVATE_IP"] = "Resolved IP is incorrect or private: ".$ip->get();
					return false;
				}
				$this->effectiveIp = $ip;
			}

			//just in case of serial queries
			$this->disconnect();

			if($this->connect($parsedUrl) === false)
			{
				return false;
			}

			$this->sendRequest($queryMethod, $parsedUrl, $entityBody);

			if(!$this->readHeaders())
			{
				$this->disconnect();
				return false;
			}

			if(!$this->waitResponse)
			{
				$this->disconnect();
				return true;
			}

			if($this->redirect && ($location = $this->responseHeaders->get("Location")) !== null && $location <> '')
			{
				//we don't need a body on redirect
				$this->disconnect();

				if($this->redirectCount < $this->redirectMax)
				{
					$this->effectiveUrl = $location;
					if($this->status == 302 || $this->status == 303)
					{
						$queryMethod = self::HTTP_GET;
					}
					$this->redirectCount++;
				}
				else
				{
					$this->error["REDIRECT"] = "Maximum number of redirects (".$this->redirectMax.") has been reached at URL ".$url;
					trigger_error($this->error["REDIRECT"], E_USER_WARNING);
					return false;
				}
			}
			else
			{
				//the connection is still active to read the response body
				break;
			}
		}
		return true;
	}

	/**
	 * Sets an HTTP request header field.
	 *
	 * @param string $name Name of the header field.
	 * @param string $value Value of the field.
	 * @param bool $replace Replace existing header field with the same name or add one more.
	 * @return $this
	 */
	public function setHeader($name, $value, $replace = true)
	{
		if($replace == true || $this->requestHeaders->get($name) === null)
		{
			$this->requestHeaders->set($name, $value);
		}
		return $this;
	}

	/**
	 * Clears all HTTP request header fields.
	 */
	public function clearHeaders()
	{
		$this->requestHeaders->clear();
	}

	/**
	 * Sets an array of cookies for HTTP request.
	 *
	 * @param array $cookies Array of cookie_name => value pairs.
	 * @return $this
	 */
	public function setCookies(array $cookies)
	{
		$this->requestCookies->set($cookies);
		return $this;
	}

	/**
	 * Sets Basic Authorization request header field.
	 *
	 * @param string $user Username.
	 * @param string $pass Password.
	 * @return $this
	 */
	public function setAuthorization($user, $pass)
	{
		$this->setHeader("Authorization", "Basic ".base64_encode($user.":".$pass));
		return $this;
	}

	/**
	 * Sets redirect options.
	 *
	 * @param bool $value If true, do redirect (default true).
	 * @param null|int $max Maximum allowed redirect count.
	 * @return $this
	 */
	public function setRedirect($value, $max = null)
	{
		$this->redirect = ($value? true : false);
		if($max !== null)
		{
			$this->redirectMax = intval($max);
		}
		return $this;
	}

	/**
	 * Sets response body waiting option.
	 *
	 * @param bool $value If true, wait for response body. If false, disconnect just after reading headers (default true).
	 * @return $this
	 */
	public function waitResponse($value)
	{
		$this->waitResponse = (bool)$value;
		if(!$this->waitResponse)
		{
			$this->setStreamTimeout(self::DEFAULT_STREAM_TIMEOUT_NO_WAIT);
		}

		return $this;
	}

	/**
	 * Sets connection timeout.
	 *
	 * @param int $value Connection timeout in seconds (default 30).
	 * @return $this
	 */
	public function setTimeout($value)
	{
		$this->socketTimeout = intval($value);
		return $this;
	}

	/**
	 * Sets socket stream reading timeout.
	 *
	 * @param int $value Stream reading timeout in seconds; "0" means no timeout (default 60).
	 * @return $this
	 */
	public function setStreamTimeout($value)
	{
		$this->streamTimeout = intval($value);
		return $this;
	}

	/**
	 * Sets HTTP protocol version. In version 1.1 chunked response is possible.
	 *
	 * @param string $value Version "1.0" or "1.1" (default "1.0").
	 * @return $this
	 */
	public function setVersion($value)
	{
		$this->version = $value;
		return $this;
	}

	/**
	 * Sets compression option.
	 * Consider not to use the "compress" option with the output stream if a content can be large.
	 * Note, that compressed response is processed anyway if Content-Encoding response header field is set
	 *
	 * @param bool $value If true, "Accept-Encoding: gzip" will be sent.
	 * @return $this
	 */
	public function setCompress($value)
	{
		$this->compress = (bool)$value;
		return $this;
	}

	/**
	 * Sets charset for entity-body (used in the Content-Type request header field for POST and PUT)
	 *
	 * @param string $value Charset.
	 * @return $this
	 */
	public function setCharset($value)
	{
		$this->requestCharset = $value;
		return $this;
	}

	/**
	 * Disables ssl certificate verification.
	 *
	 * @return $this
	 */
	public function disableSslVerification()
	{
		$this->sslVerify = false;
		return $this;
	}

	/**
	 * Enables or disables requests to private IPs.
	 *
	 * @param bool $value
	 * @return $this
	 */
	public function setPrivateIp($value)
	{
		$this->privateIp = (bool)$value;
		return $this;
	}

	/**
	 * Sets HTTP proxy for request.
	 *
	 * @param string $proxyHost Proxy host name or address (without "http://").
	 * @param null|int $proxyPort Proxy port number.
	 * @param null|string $proxyUser Proxy username.
	 * @param null|string $proxyPassword Proxy password.
	 * @return $this
	 */
	public function setProxy($proxyHost, $proxyPort = null, $proxyUser = null, $proxyPassword = null)
	{
		$this->proxyHost = $proxyHost;
		$this->proxyPort = intval($proxyPort);
		if($this->proxyPort <= 0)
		{
			$this->proxyPort = 80;
		}
		$this->proxyUser = $proxyUser;
		$this->proxyPassword = $proxyPassword;

		return $this;
	}

	/**
	 * Sets the response output to the stream instead of the string result. Useful for large responses.
	 * Note, the stream must be readable/writable to support a compressed response.
	 * Note, in this mode the result string is empty.
	 *
	 * @param resource $handler File or stream handler.
	 * @return $this
	 */
	public function setOutputStream($handler)
	{
		$this->outputStream = $handler;
		return $this;
	}

	/**
	 * Sets the maximum body length that will be received in $this->readBody().
	 *
	 * @param int $bodyLengthMax
	 * @return $this
	 */
	public function setBodyLengthMax($bodyLengthMax)
	{
		$this->bodyLengthMax = intval($bodyLengthMax);
		return $this;
	}

	/**
	 * Downloads and saves a file.
	 *
	 * @param string $url URI to download.
	 * @param string $filePath Absolute file path.
	 * @return bool
	 */
	public function download($url, $filePath)
	{
		$dir = IO\Path::getDirectory($filePath);
		IO\Directory::createDirectory($dir);

		$file = new IO\File($filePath);
		$handler = $file->open("w+");
		if($handler !== false)
		{
			$this->setOutputStream($handler);
			$res = $this->query(self::HTTP_GET, $url);
			if($res)
			{
				$res = $this->readBody();
			}
			$this->disconnect();

			fclose($handler);
			return $res;
		}

		return false;
	}

	/**
	 * Returns URL of the last redirect if request was redirected, or initial URL if request was not redirected.
	 * @return string
	 */
	public function getEffectiveUrl()
	{
		return $this->effectiveUrl;
	}

	/**
	 * Sets context options and parameters.
	 *
	 * @param array $options Context options and parameters
	 * @return $this
	 */
	public function setContextOptions(array $options)
	{
		$this->contextOptions = array_replace_recursive($this->contextOptions, $options);
		return $this;
	}

	protected function connect(Uri $url)
	{
		if($this->proxyHost <> '')
		{
			$proto = "";
			$host = $this->proxyHost;
			$port = $this->proxyPort;
		}
		else
		{
			$proto = ($url->getScheme() == "https"? "ssl://" : "");
			$host = $url->getHost();
			$port = $url->getPort();

			if($this->effectiveIp !== null)
			{
				//set original host to match a sertificate
				$this->setContextOptions(["ssl" => ["peer_name" => $host]]);

				//resolved in query() if private IPs were disabled
				$host = $this->effectiveIp->get();
			}
		}

		$context = $this->createContext();

		//$context can be FALSE
		if($context)
		{
			$res = stream_socket_client($proto.$host.":".$port, $errno, $errstr, $this->socketTimeout, STREAM_CLIENT_CONNECT, $context);
		}
		else
		{
			$res = stream_socket_client($proto.$host.":".$port, $errno, $errstr, $this->socketTimeout);
		}

		if(is_resource($res))
		{
			$this->resource = $res;
			$this->peerSocketName = stream_socket_get_name($this->resource, true);

			if($this->streamTimeout > 0)
			{
				stream_set_timeout($this->resource, $this->streamTimeout);
			}

			return true;
		}

		if(intval($errno) > 0)
		{
			$this->error["CONNECTION"] = "[".$errno."] ".$errstr;
		}
		else
		{
			$this->error["SOCKET"] = "Socket connection error.";
		}

		return false;
	}

	protected function createContext()
	{
		if ($this->sslVerify === false)
		{
			$this->contextOptions["ssl"]["verify_peer_name"] = false;
			$this->contextOptions["ssl"]["verify_peer"] = false;
			$this->contextOptions["ssl"]["allow_self_signed"] = true;
		}

		return stream_context_create($this->contextOptions);
	}

	protected function disconnect()
	{
		if($this->resource)
		{
			fclose($this->resource);
			$this->resource = null;
		}
	}

	protected function send($data)
	{
		return fwrite($this->resource, $data);
	}

	protected function receive($bufLength = null)
	{
		if($bufLength === null)
		{
			$bufLength = self::BUF_READ_LEN;
		}

		$buf = stream_get_contents($this->resource, $bufLength);
		if($buf !== false)
		{
			if(is_resource($this->outputStream))
			{
				//we can write response directly to stream (file, etc.) to minimize memory usage
				fwrite($this->outputStream, $buf);
				fflush($this->outputStream);
			}
			else
			{
				$this->result .= $buf;
			}
		}

		return $buf;
	}

	protected function sendRequest($method, Uri $url, $entityBody = null)
	{
		$this->status = 0;
		$this->result = '';
		$this->responseHeaders->clear();
		$this->responseCookies->clear();
		$this->receivedBytesLength = 0;

		if($this->proxyHost <> '')
		{
			$path = $url->getLocator();
			if($this->proxyUser <> '')
			{
				$this->setHeader("Proxy-Authorization", "Basic ".base64_encode($this->proxyUser.":".$this->proxyPassword));
			}
		}
		else
		{
			$path = $url->getPathQuery();
		}

		$request = $method." ".$path." HTTP/".$this->version."\r\n";

		$this->setHeader("Host", $url->getHost());
		$this->setHeader("Connection", "close", false);
		$this->setHeader("Accept", "*/*", false);
		$this->setHeader("Accept-Language", "en", false);

		if(($user = $url->getUser()) <> '')
		{
			$this->setAuthorization($user, $url->getPass());
		}

		$cookies = $this->requestCookies->toString();
		if($cookies <> '')
		{
			$this->setHeader("Cookie", $cookies);
		}

		if($this->compress)
		{
			$this->setHeader("Accept-Encoding", "gzip");
		}

		if(!is_resource($entityBody))
		{
			if($method == self::HTTP_POST)
			{
				//special processing for POST requests
				if($this->requestHeaders->get("Content-Type") === null)
				{
					$contentType = "application/x-www-form-urlencoded";
					if($this->requestCharset <> '')
					{
						$contentType .= "; charset=".$this->requestCharset;
					}
					$this->setHeader("Content-Type", $contentType);
				}
			}
			if($entityBody <> '' || $method == self::HTTP_POST)
			{
				//HTTP/1.0 requires Content-Length for POST
				if($this->requestHeaders->get("Content-Length") === null)
				{
					$this->setHeader("Content-Length", BinaryString::getLength($entityBody));
				}
			}
		}

		$request .= $this->requestHeaders->toString();
		$request .= "\r\n";

		$this->send($request);

		if(is_resource($entityBody))
		{
			//PUT data can be a file resource
			while(!feof($entityBody))
			{
				$this->send(fread($entityBody, self::BUF_POST_LEN));
			}
		}
		elseif($entityBody <> '')
		{
			$this->send($entityBody);
		}
	}

	protected function readHeaders()
	{
		$headers = "";
		while(!feof($this->resource))
		{
			$line = fgets($this->resource, self::BUF_READ_LEN);

			if($line == "\r\n")
			{
				break;
			}
			if(!$this->checkErrors($line))
			{
				return false;
			}

			$headers .= $line;
		}

		$this->parseHeaders($headers);

		return true;
	}

	protected function readBody()
	{
		if($this->responseHeaders->get("Transfer-Encoding") == "chunked")
		{
			while(!feof($this->resource))
			{
				/*
				chunk = chunk-size [ chunk-extension ] CRLF
						chunk-data CRLF
				chunk-size = 1*HEX
				chunk-extension = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
				*/
				$line = fgets($this->resource, self::BUF_READ_LEN);

				if($line == "\r\n")
				{
					continue;
				}
				if(($pos = strpos($line, ";")) !== false)
				{
					$line = substr($line, 0, $pos);
				}

				$length = hexdec($line);

				if(!$this->receiveBytes($length))
				{
					return false;
				}
			}
		}
		elseif(($length = $this->responseHeaders->get("Content-Length")) !== null)
		{
			//we'll read exact length of the content
			if(!$this->receiveBytes($length))
			{
				return false;
			}
		}
		else
		{
			//we don't know the length of the content - hope we'll reach the stream's end
			while(!feof($this->resource))
			{
				$buf = $this->receive();

				$this->receivedBytesLength += BinaryString::getLength($buf);

				if(!$this->checkErrors($buf))
				{
					return false;
				}
			}
		}

		if($this->responseHeaders->get("Content-Encoding") == "gzip")
		{
			$this->decompress();
		}

		return true;
	}

	protected function receiveBytes($length)
	{
		while($length > 0)
		{
			$count = ($length > self::BUF_READ_LEN? self::BUF_READ_LEN : $length);

			$buf = $this->receive($count);

			$receivedBytesLength = BinaryString::getLength($buf);
			$this->receivedBytesLength += $receivedBytesLength;

			if(!$this->checkErrors($buf))
			{
				return false;
			}

			$length -= $receivedBytesLength;
		}

		return true;
	}

	protected function checkErrors($buf)
	{
		if($this->streamTimeout > 0)
		{
			$info = stream_get_meta_data($this->resource);
			if($info['timed_out'])
			{
				$this->error['STREAM_TIMEOUT'] = "Stream reading timeout of ".$this->streamTimeout." second(s) has been reached";
				return false;
			}
		}

		if($buf === false)
		{
			$this->error['STREAM_READING'] = "Stream reading error";
			return false;
		}

		if($this->bodyLengthMax > 0 && $this->receivedBytesLength > $this->bodyLengthMax)
		{
			$this->error['STREAM_LENGTH'] = "Maximum content length has been reached. Break reading";
			return false;
		}

		return true;
	}

	protected function decompress()
	{
		if(is_resource($this->outputStream))
		{
			$compressed = stream_get_contents($this->outputStream, -1, 10);
			$compressed = BinaryString::getSubstring($compressed, 0, -8);
			if($compressed <> '')
			{
				$uncompressed = gzinflate($compressed);

				rewind($this->outputStream);
				$len = fwrite($this->outputStream, $uncompressed);
				ftruncate($this->outputStream, $len);
			}
		}
		else
		{
			$compressed = BinaryString::getSubstring($this->result, 10, -8);
			if($compressed <> '')
			{
				$this->result = gzinflate($compressed);
			}
		}
	}

	protected function parseHeaders($headers)
	{
		foreach (explode("\n", $headers) as $k => $header)
		{
			if($k == 0)
			{
				if(preg_match('#HTTP\S+ (\d+)#', $header, $find))
				{
					$this->status = intval($find[1]);
				}
			}
			elseif(strpos($header, ':') !== false)
			{
				list($headerName, $headerValue) = explode(':', $header, 2);
				if(strtolower($headerName) == 'set-cookie')
				{
					$this->responseCookies->addFromString($headerValue);
				}
				$this->responseHeaders->add($headerName, trim($headerValue));
			}
		}
	}

	/**
	 * Returns parsed HTTP response headers
	 *
	 * @return HttpHeaders
	 */
	public function getHeaders()
	{
		return $this->responseHeaders;
	}

	/**
	 * Returns parsed HTTP response cookies
	 *
	 * @return HttpCookies
	 */
	public function getCookies()
	{
		return $this->responseCookies;
	}

	/**
	 * Returns HTTP response status code
	 *
	 * @return int
	 */
	public function getStatus()
	{
		return $this->status;
	}

	/**
	 * Returns HTTP response entity string. Note, if outputStream is set, the result will be empty string.
	 *
	 * @return string
	 */
	public function getResult()
	{
		if($this->waitResponse && $this->resource)
		{
			$this->readBody();
			$this->disconnect();
		}
		return $this->result;
	}

	/**
	 * Returns array of errors on failure
	 *
	 * @return array Array with "error_code" => "error_message" pair
	 */
	public function getError()
	{
		return $this->error;
	}

	/**
	 * Returns response content type
	 *
	 * @return string
	 */
	public function getContentType()
	{
		return $this->responseHeaders->getContentType();
	}

	/**
	 * Returns response content encoding
	 *
	 * @return string
	 */
	public function getCharset()
	{
		return $this->responseHeaders->getCharset();
	}

	/**
	 * Returns remote peer socket name (usually in form ip:port)
	 *
	 * @return string
	 */
	public function getPeerSocketName()
	{
		return $this->peerSocketName ?: '';
	}

	/**
	 * Returns remote peer ip address.
	 * @return string|false
	 */
	public function getPeerAddress()
	{
		if(!preg_match('/^(\d+)\.(\d+)\.(\d+)\.(\d+):(\d+)$/', $this->peerSocketName, $matches))
			return false;

		return sprintf('%d.%d.%d.%d', $matches[1], $matches[2], $matches[3], $matches[4]);
	}

	/**
	 * Returns remote peer ip address.
	 * @return int|false
	 */
	public function getPeerPort()
	{
		if(!preg_match('/^(\d+)\.(\d+)\.(\d+)\.(\d+):(\d+)$/', $this->peerSocketName, $matches))
			return false;

		return (int)$matches[5];
	}
}