3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
10 namespace Zend\Http\Client\Adapter
;
13 use Zend\Http\Client\Adapter\Exception
as AdapterException
;
14 use Zend\Http\Response
;
15 use Zend\Stdlib\ErrorHandler
;
18 * HTTP Proxy-supporting Zend\Http\Client adapter class, based on the default
19 * socket based adapter.
21 * Should be used if proxy HTTP access is required. If no proxy is set, will
22 * fall back to Zend\Http\Client\Adapter\Socket behavior. Just like the
23 * default Socket adapter, this adapter does not require any special extensions
26 class Proxy
extends Socket
33 protected $config = array(
34 'ssltransport' => 'ssl',
36 'sslpassphrase' => null,
37 'sslverifypeer' => true,
39 'sslallowselfsigned' => false,
40 'sslusecontext' => false,
45 'proxy_auth' => Client
::AUTH_BASIC
,
50 * Whether HTTPS CONNECT was already negotiated with the proxy or not
54 protected $negotiated = false;
57 * Set the configuration array for the adapter
59 * @param array $options
61 public function setOptions($options = array())
63 //enforcing that the proxy keys are set in the form proxy_*
64 foreach ($options as $k => $v) {
65 if (preg_match("/^proxy[a-z]+/", $k)) {
66 $options['proxy_' . substr($k, 5, strlen($k))] = $v;
71 parent
::setOptions($options);
75 * Connect to the remote server
77 * Will try to connect to the proxy server. If no proxy was set, will
78 * fall back to the target server (behave like regular Socket adapter)
83 * @throws AdapterException\RuntimeException
85 public function connect($host, $port = 80, $secure = false)
87 // If no proxy is set, fall back to Socket adapter
88 if (! $this->config
['proxy_host']) {
89 parent
::connect($host, $port, $secure);
93 /* Url might require stream context even if proxy connection doesn't */
95 $this->config
['sslusecontext'] = true;
98 // Connect (a non-secure connection) to the proxy server
100 $this->config
['proxy_host'],
101 $this->config
['proxy_port'],
107 * Send request to the proxy server
109 * @param string $method
110 * @param \Zend\Uri\Uri $uri
111 * @param string $httpVer
112 * @param array $headers
113 * @param string $body
114 * @throws AdapterException\RuntimeException
115 * @return string Request as string
117 public function write($method, $uri, $httpVer = '1.1', $headers = array(), $body = '')
119 // If no proxy is set, fall back to default Socket adapter
120 if (! $this->config
['proxy_host']) return parent
::write($method, $uri, $httpVer, $headers, $body);
122 // Make sure we're properly connected
123 if (! $this->socket
) {
124 throw new AdapterException\
RuntimeException("Trying to write but we are not connected");
127 $host = $this->config
['proxy_host'];
128 $port = $this->config
['proxy_port'];
130 if ($this->connectedTo
[0] != "tcp://$host" ||
$this->connectedTo
[1] != $port) {
131 throw new AdapterException\
RuntimeException("Trying to write but we are connected to the wrong proxy server");
134 // Add Proxy-Authorization header
135 if ($this->config
['proxy_user'] && ! isset($headers['proxy-authorization'])) {
136 $headers['proxy-authorization'] = Client
::encodeAuthHeader(
137 $this->config
['proxy_user'], $this->config
['proxy_pass'], $this->config
['proxy_auth']
141 // if we are proxying HTTPS, preform CONNECT handshake with the proxy
142 if ($uri->getScheme() == 'https' && (! $this->negotiated
)) {
143 $this->connectHandshake($uri->getHost(), $uri->getPort(), $httpVer, $headers);
144 $this->negotiated
= true;
147 // Save request method for later
148 $this->method
= $method;
150 // Build request headers
151 if ($this->negotiated
) {
152 $path = $uri->getPath();
153 if ($uri->getQuery()) {
154 $path .= '?' . $uri->getQuery();
156 $request = "$method $path HTTP/$httpVer\r\n";
158 $request = "$method $uri HTTP/$httpVer\r\n";
161 // Add all headers to the request string
162 foreach ($headers as $k => $v) {
163 if (is_string($k)) $v = "$k: $v";
164 $request .= "$v\r\n";
167 if (is_resource($body)) {
170 // Add the request body
171 $request .= "\r\n" . $body;
175 ErrorHandler
::start();
176 $test = fwrite($this->socket
, $request);
177 $error = ErrorHandler
::stop();
179 throw new AdapterException\
RuntimeException("Error writing request to proxy server", 0, $error);
182 if (is_resource($body)) {
183 if (stream_copy_to_stream($body, $this->socket
) == 0) {
184 throw new AdapterException\
RuntimeException('Error writing request to server');
192 * Preform handshaking with HTTPS proxy using CONNECT method
194 * @param string $host
196 * @param string $httpVer
197 * @param array $headers
198 * @throws AdapterException\RuntimeException
200 protected function connectHandshake($host, $port = 443, $httpVer = '1.1', array &$headers = array())
202 $request = "CONNECT $host:$port HTTP/$httpVer\r\n" .
203 "Host: " . $this->config
['proxy_host'] . "\r\n";
205 // Add the user-agent header
206 if (isset($this->config
['useragent'])) {
207 $request .= "User-agent: " . $this->config
['useragent'] . "\r\n";
210 // If the proxy-authorization header is set, send it to proxy but remove
211 // it from headers sent to target host
212 if (isset($headers['proxy-authorization'])) {
213 $request .= "Proxy-authorization: " . $headers['proxy-authorization'] . "\r\n";
214 unset($headers['proxy-authorization']);
220 ErrorHandler
::start();
221 $test = fwrite($this->socket
, $request);
222 $error = ErrorHandler
::stop();
224 throw new AdapterException\
RuntimeException("Error writing request to proxy server", 0, $error);
227 // Read response headers only
230 ErrorHandler
::start();
231 while ($line = fgets($this->socket
)) {
232 $gotStatus = $gotStatus ||
(strpos($line, 'HTTP') !== false);
235 if (!rtrim($line)) break;
238 ErrorHandler
::stop();
240 // Check that the response from the proxy is 200
241 if (Response
::extractCode($response) != 200) {
242 throw new AdapterException\
RuntimeException("Unable to connect to HTTPS proxy. Server response: " . $response);
245 // If all is good, switch socket to secure mode. We have to fall back
246 // through the different modes
248 STREAM_CRYPTO_METHOD_TLS_CLIENT
,
249 STREAM_CRYPTO_METHOD_SSLv3_CLIENT
,
250 STREAM_CRYPTO_METHOD_SSLv23_CLIENT
,
251 STREAM_CRYPTO_METHOD_SSLv2_CLIENT
255 foreach ($modes as $mode) {
256 $success = stream_socket_enable_crypto($this->socket
, true, $mode);
261 throw new AdapterException\
RuntimeException("Unable to connect to" .
262 " HTTPS server through proxy: could not negotiate secure connection.");
267 * Close the connection to the server
270 public function close()
273 $this->negotiated
= false;
277 * Destructor: make sure the socket is disconnected
280 public function __destruct()
282 if ($this->socket
) $this->close();