|
1 <?php |
|
2 |
|
3 /** |
|
4 * Zend Framework |
|
5 * |
|
6 * LICENSE |
|
7 * |
|
8 * This source file is subject to the new BSD license that is bundled |
|
9 * with this package in the file LICENSE.txt. |
|
10 * It is also available through the world-wide-web at this URL: |
|
11 * http://framework.zend.com/license/new-bsd |
|
12 * If you did not receive a copy of the license and are unable to |
|
13 * obtain it through the world-wide-web, please send an email |
|
14 * to license@zend.com so we can send you a copy immediately. |
|
15 * |
|
16 * @category Zend |
|
17 * @package Zend_Http |
|
18 * @subpackage Client_Adapter |
|
19 * @version $Id: Socket.php 22576 2010-07-16 15:49:24Z dragonbe $ |
|
20 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
21 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
22 */ |
|
23 |
|
24 /** |
|
25 * @see Zend_Uri_Http |
|
26 */ |
|
27 require_once 'Zend/Uri/Http.php'; |
|
28 /** |
|
29 * @see Zend_Http_Client_Adapter_Interface |
|
30 */ |
|
31 require_once 'Zend/Http/Client/Adapter/Interface.php'; |
|
32 /** |
|
33 * @see Zend_Http_Client_Adapter_Stream |
|
34 */ |
|
35 require_once 'Zend/Http/Client/Adapter/Stream.php'; |
|
36 |
|
37 /** |
|
38 * A sockets based (stream_socket_client) adapter class for Zend_Http_Client. Can be used |
|
39 * on almost every PHP environment, and does not require any special extensions. |
|
40 * |
|
41 * @category Zend |
|
42 * @package Zend_Http |
|
43 * @subpackage Client_Adapter |
|
44 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
45 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
46 */ |
|
47 class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream |
|
48 { |
|
49 /** |
|
50 * The socket for server connection |
|
51 * |
|
52 * @var resource|null |
|
53 */ |
|
54 protected $socket = null; |
|
55 |
|
56 /** |
|
57 * What host/port are we connected to? |
|
58 * |
|
59 * @var array |
|
60 */ |
|
61 protected $connected_to = array(null, null); |
|
62 |
|
63 /** |
|
64 * Stream for storing output |
|
65 * |
|
66 * @var resource |
|
67 */ |
|
68 protected $out_stream = null; |
|
69 |
|
70 /** |
|
71 * Parameters array |
|
72 * |
|
73 * @var array |
|
74 */ |
|
75 protected $config = array( |
|
76 'persistent' => false, |
|
77 'ssltransport' => 'ssl', |
|
78 'sslcert' => null, |
|
79 'sslpassphrase' => null, |
|
80 'sslusecontext' => false |
|
81 ); |
|
82 |
|
83 /** |
|
84 * Request method - will be set by write() and might be used by read() |
|
85 * |
|
86 * @var string |
|
87 */ |
|
88 protected $method = null; |
|
89 |
|
90 /** |
|
91 * Stream context |
|
92 * |
|
93 * @var resource |
|
94 */ |
|
95 protected $_context = null; |
|
96 |
|
97 /** |
|
98 * Adapter constructor, currently empty. Config is set using setConfig() |
|
99 * |
|
100 */ |
|
101 public function __construct() |
|
102 { |
|
103 } |
|
104 |
|
105 /** |
|
106 * Set the configuration array for the adapter |
|
107 * |
|
108 * @param Zend_Config | array $config |
|
109 */ |
|
110 public function setConfig($config = array()) |
|
111 { |
|
112 if ($config instanceof Zend_Config) { |
|
113 $config = $config->toArray(); |
|
114 |
|
115 } elseif (! is_array($config)) { |
|
116 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
117 throw new Zend_Http_Client_Adapter_Exception( |
|
118 'Array or Zend_Config object expected, got ' . gettype($config) |
|
119 ); |
|
120 } |
|
121 |
|
122 foreach ($config as $k => $v) { |
|
123 $this->config[strtolower($k)] = $v; |
|
124 } |
|
125 } |
|
126 |
|
127 /** |
|
128 * Retrieve the array of all configuration options |
|
129 * |
|
130 * @return array |
|
131 */ |
|
132 public function getConfig() |
|
133 { |
|
134 return $this->config; |
|
135 } |
|
136 |
|
137 /** |
|
138 * Set the stream context for the TCP connection to the server |
|
139 * |
|
140 * Can accept either a pre-existing stream context resource, or an array |
|
141 * of stream options, similar to the options array passed to the |
|
142 * stream_context_create() PHP function. In such case a new stream context |
|
143 * will be created using the passed options. |
|
144 * |
|
145 * @since Zend Framework 1.9 |
|
146 * |
|
147 * @param mixed $context Stream context or array of context options |
|
148 * @return Zend_Http_Client_Adapter_Socket |
|
149 */ |
|
150 public function setStreamContext($context) |
|
151 { |
|
152 if (is_resource($context) && get_resource_type($context) == 'stream-context') { |
|
153 $this->_context = $context; |
|
154 |
|
155 } elseif (is_array($context)) { |
|
156 $this->_context = stream_context_create($context); |
|
157 |
|
158 } else { |
|
159 // Invalid parameter |
|
160 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
161 throw new Zend_Http_Client_Adapter_Exception( |
|
162 "Expecting either a stream context resource or array, got " . gettype($context) |
|
163 ); |
|
164 } |
|
165 |
|
166 return $this; |
|
167 } |
|
168 |
|
169 /** |
|
170 * Get the stream context for the TCP connection to the server. |
|
171 * |
|
172 * If no stream context is set, will create a default one. |
|
173 * |
|
174 * @return resource |
|
175 */ |
|
176 public function getStreamContext() |
|
177 { |
|
178 if (! $this->_context) { |
|
179 $this->_context = stream_context_create(); |
|
180 } |
|
181 |
|
182 return $this->_context; |
|
183 } |
|
184 |
|
185 /** |
|
186 * Connect to the remote server |
|
187 * |
|
188 * @param string $host |
|
189 * @param int $port |
|
190 * @param boolean $secure |
|
191 */ |
|
192 public function connect($host, $port = 80, $secure = false) |
|
193 { |
|
194 // If the URI should be accessed via SSL, prepend the Hostname with ssl:// |
|
195 $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host; |
|
196 |
|
197 // If we are connected to the wrong host, disconnect first |
|
198 if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) { |
|
199 if (is_resource($this->socket)) $this->close(); |
|
200 } |
|
201 |
|
202 // Now, if we are not connected, connect |
|
203 if (! is_resource($this->socket) || ! $this->config['keepalive']) { |
|
204 $context = $this->getStreamContext(); |
|
205 if ($secure || $this->config['sslusecontext']) { |
|
206 if ($this->config['sslcert'] !== null) { |
|
207 if (! stream_context_set_option($context, 'ssl', 'local_cert', |
|
208 $this->config['sslcert'])) { |
|
209 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
210 throw new Zend_Http_Client_Adapter_Exception('Unable to set sslcert option'); |
|
211 } |
|
212 } |
|
213 if ($this->config['sslpassphrase'] !== null) { |
|
214 if (! stream_context_set_option($context, 'ssl', 'passphrase', |
|
215 $this->config['sslpassphrase'])) { |
|
216 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
217 throw new Zend_Http_Client_Adapter_Exception('Unable to set sslpassphrase option'); |
|
218 } |
|
219 } |
|
220 } |
|
221 |
|
222 $flags = STREAM_CLIENT_CONNECT; |
|
223 if ($this->config['persistent']) $flags |= STREAM_CLIENT_PERSISTENT; |
|
224 |
|
225 $this->socket = @stream_socket_client($host . ':' . $port, |
|
226 $errno, |
|
227 $errstr, |
|
228 (int) $this->config['timeout'], |
|
229 $flags, |
|
230 $context); |
|
231 |
|
232 if (! $this->socket) { |
|
233 $this->close(); |
|
234 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
235 throw new Zend_Http_Client_Adapter_Exception( |
|
236 'Unable to Connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr); |
|
237 } |
|
238 |
|
239 // Set the stream timeout |
|
240 if (! stream_set_timeout($this->socket, (int) $this->config['timeout'])) { |
|
241 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
242 throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout'); |
|
243 } |
|
244 |
|
245 // Update connected_to |
|
246 $this->connected_to = array($host, $port); |
|
247 } |
|
248 } |
|
249 |
|
250 /** |
|
251 * Send request to the remote server |
|
252 * |
|
253 * @param string $method |
|
254 * @param Zend_Uri_Http $uri |
|
255 * @param string $http_ver |
|
256 * @param array $headers |
|
257 * @param string $body |
|
258 * @return string Request as string |
|
259 */ |
|
260 public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') |
|
261 { |
|
262 // Make sure we're properly connected |
|
263 if (! $this->socket) { |
|
264 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
265 throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected'); |
|
266 } |
|
267 |
|
268 $host = $uri->getHost(); |
|
269 $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host; |
|
270 if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) { |
|
271 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
272 throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host'); |
|
273 } |
|
274 |
|
275 // Save request method for later |
|
276 $this->method = $method; |
|
277 |
|
278 // Build request headers |
|
279 $path = $uri->getPath(); |
|
280 if ($uri->getQuery()) $path .= '?' . $uri->getQuery(); |
|
281 $request = "{$method} {$path} HTTP/{$http_ver}\r\n"; |
|
282 foreach ($headers as $k => $v) { |
|
283 if (is_string($k)) $v = ucfirst($k) . ": $v"; |
|
284 $request .= "$v\r\n"; |
|
285 } |
|
286 |
|
287 if(is_resource($body)) { |
|
288 $request .= "\r\n"; |
|
289 } else { |
|
290 // Add the request body |
|
291 $request .= "\r\n" . $body; |
|
292 } |
|
293 |
|
294 // Send the request |
|
295 if (! @fwrite($this->socket, $request)) { |
|
296 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
297 throw new Zend_Http_Client_Adapter_Exception('Error writing request to server'); |
|
298 } |
|
299 |
|
300 if(is_resource($body)) { |
|
301 if(stream_copy_to_stream($body, $this->socket) == 0) { |
|
302 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
303 throw new Zend_Http_Client_Adapter_Exception('Error writing request to server'); |
|
304 } |
|
305 } |
|
306 |
|
307 return $request; |
|
308 } |
|
309 |
|
310 /** |
|
311 * Read response from server |
|
312 * |
|
313 * @return string |
|
314 */ |
|
315 public function read() |
|
316 { |
|
317 // First, read headers only |
|
318 $response = ''; |
|
319 $gotStatus = false; |
|
320 $stream = !empty($this->config['stream']); |
|
321 |
|
322 while (($line = @fgets($this->socket)) !== false) { |
|
323 $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false); |
|
324 if ($gotStatus) { |
|
325 $response .= $line; |
|
326 if (rtrim($line) === '') break; |
|
327 } |
|
328 } |
|
329 |
|
330 $this->_checkSocketReadTimeout(); |
|
331 |
|
332 $statusCode = Zend_Http_Response::extractCode($response); |
|
333 |
|
334 // Handle 100 and 101 responses internally by restarting the read again |
|
335 if ($statusCode == 100 || $statusCode == 101) return $this->read(); |
|
336 |
|
337 // Check headers to see what kind of connection / transfer encoding we have |
|
338 $headers = Zend_Http_Response::extractHeaders($response); |
|
339 |
|
340 /** |
|
341 * Responses to HEAD requests and 204 or 304 responses are not expected |
|
342 * to have a body - stop reading here |
|
343 */ |
|
344 if ($statusCode == 304 || $statusCode == 204 || |
|
345 $this->method == Zend_Http_Client::HEAD) { |
|
346 |
|
347 // Close the connection if requested to do so by the server |
|
348 if (isset($headers['connection']) && $headers['connection'] == 'close') { |
|
349 $this->close(); |
|
350 } |
|
351 return $response; |
|
352 } |
|
353 |
|
354 // If we got a 'transfer-encoding: chunked' header |
|
355 if (isset($headers['transfer-encoding'])) { |
|
356 |
|
357 if (strtolower($headers['transfer-encoding']) == 'chunked') { |
|
358 |
|
359 do { |
|
360 $line = @fgets($this->socket); |
|
361 $this->_checkSocketReadTimeout(); |
|
362 |
|
363 $chunk = $line; |
|
364 |
|
365 // Figure out the next chunk size |
|
366 $chunksize = trim($line); |
|
367 if (! ctype_xdigit($chunksize)) { |
|
368 $this->close(); |
|
369 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
370 throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' . |
|
371 $chunksize . '" unable to read chunked body'); |
|
372 } |
|
373 |
|
374 // Convert the hexadecimal value to plain integer |
|
375 $chunksize = hexdec($chunksize); |
|
376 |
|
377 // Read next chunk |
|
378 $read_to = ftell($this->socket) + $chunksize; |
|
379 |
|
380 do { |
|
381 $current_pos = ftell($this->socket); |
|
382 if ($current_pos >= $read_to) break; |
|
383 |
|
384 if($this->out_stream) { |
|
385 if(stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) { |
|
386 $this->_checkSocketReadTimeout(); |
|
387 break; |
|
388 } |
|
389 } else { |
|
390 $line = @fread($this->socket, $read_to - $current_pos); |
|
391 if ($line === false || strlen($line) === 0) { |
|
392 $this->_checkSocketReadTimeout(); |
|
393 break; |
|
394 } |
|
395 $chunk .= $line; |
|
396 } |
|
397 } while (! feof($this->socket)); |
|
398 |
|
399 $chunk .= @fgets($this->socket); |
|
400 $this->_checkSocketReadTimeout(); |
|
401 |
|
402 if(!$this->out_stream) { |
|
403 $response .= $chunk; |
|
404 } |
|
405 } while ($chunksize > 0); |
|
406 } else { |
|
407 $this->close(); |
|
408 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
409 throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' . |
|
410 $headers['transfer-encoding'] . '" transfer encoding'); |
|
411 } |
|
412 |
|
413 // We automatically decode chunked-messages when writing to a stream |
|
414 // this means we have to disallow the Zend_Http_Response to do it again |
|
415 if ($this->out_stream) { |
|
416 $response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $response); |
|
417 } |
|
418 // Else, if we got the content-length header, read this number of bytes |
|
419 } elseif (isset($headers['content-length'])) { |
|
420 |
|
421 // If we got more than one Content-Length header (see ZF-9404) use |
|
422 // the last value sent |
|
423 if (is_array($headers['content-length'])) { |
|
424 $contentLength = $headers['content-length'][count($headers['content-length']) - 1]; |
|
425 } else { |
|
426 $contentLength = $headers['content-length']; |
|
427 } |
|
428 |
|
429 $current_pos = ftell($this->socket); |
|
430 $chunk = ''; |
|
431 |
|
432 for ($read_to = $current_pos + $contentLength; |
|
433 $read_to > $current_pos; |
|
434 $current_pos = ftell($this->socket)) { |
|
435 |
|
436 if($this->out_stream) { |
|
437 if(@stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) { |
|
438 $this->_checkSocketReadTimeout(); |
|
439 break; |
|
440 } |
|
441 } else { |
|
442 $chunk = @fread($this->socket, $read_to - $current_pos); |
|
443 if ($chunk === false || strlen($chunk) === 0) { |
|
444 $this->_checkSocketReadTimeout(); |
|
445 break; |
|
446 } |
|
447 |
|
448 $response .= $chunk; |
|
449 } |
|
450 |
|
451 // Break if the connection ended prematurely |
|
452 if (feof($this->socket)) break; |
|
453 } |
|
454 |
|
455 // Fallback: just read the response until EOF |
|
456 } else { |
|
457 |
|
458 do { |
|
459 if($this->out_stream) { |
|
460 if(@stream_copy_to_stream($this->socket, $this->out_stream) == 0) { |
|
461 $this->_checkSocketReadTimeout(); |
|
462 break; |
|
463 } |
|
464 } else { |
|
465 $buff = @fread($this->socket, 8192); |
|
466 if ($buff === false || strlen($buff) === 0) { |
|
467 $this->_checkSocketReadTimeout(); |
|
468 break; |
|
469 } else { |
|
470 $response .= $buff; |
|
471 } |
|
472 } |
|
473 |
|
474 } while (feof($this->socket) === false); |
|
475 |
|
476 $this->close(); |
|
477 } |
|
478 |
|
479 // Close the connection if requested to do so by the server |
|
480 if (isset($headers['connection']) && $headers['connection'] == 'close') { |
|
481 $this->close(); |
|
482 } |
|
483 |
|
484 return $response; |
|
485 } |
|
486 |
|
487 /** |
|
488 * Close the connection to the server |
|
489 * |
|
490 */ |
|
491 public function close() |
|
492 { |
|
493 if (is_resource($this->socket)) @fclose($this->socket); |
|
494 $this->socket = null; |
|
495 $this->connected_to = array(null, null); |
|
496 } |
|
497 |
|
498 /** |
|
499 * Check if the socket has timed out - if so close connection and throw |
|
500 * an exception |
|
501 * |
|
502 * @throws Zend_Http_Client_Adapter_Exception with READ_TIMEOUT code |
|
503 */ |
|
504 protected function _checkSocketReadTimeout() |
|
505 { |
|
506 if ($this->socket) { |
|
507 $info = stream_get_meta_data($this->socket); |
|
508 $timedout = $info['timed_out']; |
|
509 if ($timedout) { |
|
510 $this->close(); |
|
511 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
512 throw new Zend_Http_Client_Adapter_Exception( |
|
513 "Read timed out after {$this->config['timeout']} seconds", |
|
514 Zend_Http_Client_Adapter_Exception::READ_TIMEOUT |
|
515 ); |
|
516 } |
|
517 } |
|
518 } |
|
519 |
|
520 /** |
|
521 * Set output stream for the response |
|
522 * |
|
523 * @param resource $stream |
|
524 * @return Zend_Http_Client_Adapter_Socket |
|
525 */ |
|
526 public function setOutputStream($stream) |
|
527 { |
|
528 $this->out_stream = $stream; |
|
529 return $this; |
|
530 } |
|
531 |
|
532 /** |
|
533 * Destructor: make sure the socket is disconnected |
|
534 * |
|
535 * If we are in persistent TCP mode, will not close the connection |
|
536 * |
|
537 */ |
|
538 public function __destruct() |
|
539 { |
|
540 if (! $this->config['persistent']) { |
|
541 if ($this->socket) $this->close(); |
|
542 } |
|
543 } |
|
544 } |