Skip to content
This repository
Browse code

Merge pull request #947 from cakephp/2.3-http-socket

2.3 HttpSocket enhancements
  • Loading branch information...
commit bfbd05576be9e6b79b44cd39050595b6f575d82c 2 parents 9ef9b8b + 12e2e13
Mark Story markstory authored
3,920 lib/Cake/Config/cacert.pem
3,920 additions, 0 deletions not shown
62 lib/Cake/Network/CakeSocket.php
@@ -102,6 +102,14 @@ class CakeSocket {
102 102 );
103 103
104 104 /**
  105 + * Used to capture connection warnings which can happen when there are
  106 + * SSL errors for example.
  107 + *
  108 + * @var array
  109 + */
  110 + protected $_connectionErrors = array();
  111 +
  112 +/**
105 113 * Constructor.
106 114 *
107 115 * @param array $config Socket configuration, which will be merged with the base configuration
@@ -126,21 +134,42 @@ public function connect() {
126 134 }
127 135
128 136 $scheme = null;
129   - if (isset($this->config['request']) && $this->config['request']['uri']['scheme'] == 'https') {
  137 + if (isset($this->config['request']['uri']) && $this->config['request']['uri']['scheme'] == 'https') {
130 138 $scheme = 'ssl://';
131 139 }
132 140
133   - if ($this->config['persistent']) {
134   - $this->connection = @pfsockopen($scheme . $this->config['host'], $this->config['port'], $errNum, $errStr, $this->config['timeout']);
  141 + if (!empty($this->config['context'])) {
  142 + $context = stream_context_create($this->config['context']);
135 143 } else {
136   - $this->connection = @fsockopen($scheme . $this->config['host'], $this->config['port'], $errNum, $errStr, $this->config['timeout']);
  144 + $context = stream_context_create();
137 145 }
138 146
  147 + $connectAs = STREAM_CLIENT_CONNECT;
  148 + if ($this->config['persistent']) {
  149 + $connectAs |= STREAM_CLIENT_PERSISTENT;
  150 + }
  151 +
  152 + set_error_handler(array($this, '_connectionErrorHandler'));
  153 + $this->connection = stream_socket_client(
  154 + $scheme . $this->config['host'] . ':' . $this->config['port'],
  155 + $errNum,
  156 + $errStr,
  157 + $this->config['timeout'],
  158 + $connectAs,
  159 + $context
  160 + );
  161 + restore_error_handler();
  162 +
139 163 if (!empty($errNum) || !empty($errStr)) {
140 164 $this->setLastError($errNum, $errStr);
141 165 throw new SocketException($errStr, $errNum);
142 166 }
143 167
  168 + if (!$this->connection && $this->_connectionErrors) {
  169 + $message = implode("\n", $this->_connectionErrors);
  170 + throw new SocketException($message, E_WARNING);
  171 + }
  172 +
144 173 $this->connected = is_resource($this->connection);
145 174 if ($this->connected) {
146 175 stream_set_timeout($this->connection, $this->config['timeout']);
@@ -149,6 +178,31 @@ public function connect() {
149 178 }
150 179
151 180 /**
  181 + * socket_stream_client() does not populate errNum, or $errStr when there are
  182 + * connection errors, as in the case of SSL verification failure.
  183 + *
  184 + * Instead we need to handle those errors manually.
  185 + *
  186 + * @param int $code
  187 + * @param string $message
  188 + */
  189 + protected function _connectionErrorHandler($code, $message) {
  190 + $this->_connectionErrors[] = $message;
  191 + }
  192 +
  193 +/**
  194 + * Get the connection context.
  195 + *
  196 + * @return null|array Null when there is no connnection, an array when there is.
  197 + */
  198 + public function context() {
  199 + if (!$this->connection) {
  200 + return;
  201 + }
  202 + return stream_context_get_options($this->connection);
  203 + }
  204 +
  205 +/**
152 206 * Get the host name of the current connection.
153 207 *
154 208 * @return string Host name
433 lib/Cake/Network/Http/HttpResponse.php
@@ -12,437 +12,24 @@
12 12 *
13 13 * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
14 14 * @link http://cakephp.org CakePHP(tm) Project
15   - * @package Cake.Network.Http
16 15 * @since CakePHP(tm) v 2.0.0
17 16 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
18 17 */
  18 +App::uses('HttpSocketResponse', 'Network/Http');
  19 +
  20 +if (class_exists('HttpResponse')) {
  21 + trigger_error(__d(
  22 + 'cake_dev',
  23 + "HttpResponse is deprecated due to naming conflicts. Use HttpSocketResponse instead."
  24 + ), E_USER_ERROR);
  25 +}
19 26
20 27 /**
21 28 * HTTP Response from HttpSocket.
22 29 *
23 30 * @package Cake.Network.Http
  31 + * @deprecated This class is deprecated as it has naming conflicts with pecl/http
24 32 */
25   -class HttpResponse implements ArrayAccess {
26   -
27   -/**
28   - * Body content
29   - *
30   - * @var string
31   - */
32   - public $body = '';
33   -
34   -/**
35   - * Headers
36   - *
37   - * @var array
38   - */
39   - public $headers = array();
40   -
41   -/**
42   - * Cookies
43   - *
44   - * @var array
45   - */
46   - public $cookies = array();
47   -
48   -/**
49   - * HTTP version
50   - *
51   - * @var string
52   - */
53   - public $httpVersion = 'HTTP/1.1';
54   -
55   -/**
56   - * Response code
57   - *
58   - * @var integer
59   - */
60   - public $code = 0;
61   -
62   -/**
63   - * Reason phrase
64   - *
65   - * @var string
66   - */
67   - public $reasonPhrase = '';
68   -
69   -/**
70   - * Pure raw content
71   - *
72   - * @var string
73   - */
74   - public $raw = '';
75   -
76   -/**
77   - * Constructor
78   - *
79   - * @param string $message
80   - */
81   - public function __construct($message = null) {
82   - if ($message !== null) {
83   - $this->parseResponse($message);
84   - }
85   - }
86   -
87   -/**
88   - * Body content
89   - *
90   - * @return string
91   - */
92   - public function body() {
93   - return (string)$this->body;
94   - }
95   -
96   -/**
97   - * Get header in case insensitive
98   - *
99   - * @param string $name Header name
100   - * @param array $headers
101   - * @return mixed String if header exists or null
102   - */
103   - public function getHeader($name, $headers = null) {
104   - if (!is_array($headers)) {
105   - $headers =& $this->headers;
106   - }
107   - if (isset($headers[$name])) {
108   - return $headers[$name];
109   - }
110   - foreach ($headers as $key => $value) {
111   - if (strcasecmp($key, $name) === 0) {
112   - return $value;
113   - }
114   - }
115   - return null;
116   - }
117   -
118   -/**
119   - * If return is 200 (OK)
120   - *
121   - * @return boolean
122   - */
123   - public function isOk() {
124   - return $this->code == 200;
125   - }
126   -
127   -/**
128   - * If return is a valid 3xx (Redirection)
129   - *
130   - * @return boolean
131   - */
132   - public function isRedirect() {
133   - return in_array($this->code, array(301, 302, 303, 307)) && !is_null($this->getHeader('Location'));
134   - }
135   -
136   -/**
137   - * Parses the given message and breaks it down in parts.
138   - *
139   - * @param string $message Message to parse
140   - * @return void
141   - * @throws SocketException
142   - */
143   - public function parseResponse($message) {
144   - if (!is_string($message)) {
145   - throw new SocketException(__d('cake_dev', 'Invalid response.'));
146   - }
147   -
148   - if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
149   - throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
150   - }
151   -
152   - list(, $statusLine, $header) = $match;
153   - $this->raw = $message;
154   - $this->body = (string)substr($message, strlen($match[0]));
155   -
156   - if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
157   - $this->httpVersion = $match[1];
158   - $this->code = $match[2];
159   - $this->reasonPhrase = $match[3];
160   - }
161   -
162   - $this->headers = $this->_parseHeader($header);
163   - $transferEncoding = $this->getHeader('Transfer-Encoding');
164   - $decoded = $this->_decodeBody($this->body, $transferEncoding);
165   - $this->body = $decoded['body'];
166   -
167   - if (!empty($decoded['header'])) {
168   - $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
169   - }
170   -
171   - if (!empty($this->headers)) {
172   - $this->cookies = $this->parseCookies($this->headers);
173   - }
174   - }
175   -
176   -/**
177   - * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
178   - * 'body' and 'header' or false on failure.
179   - *
180   - * @param string $body A string containing the body to decode.
181   - * @param string|boolean $encoding Can be false in case no encoding is being used, or a string representing the encoding.
182   - * @return mixed Array of response headers and body or false.
183   - */
184   - protected function _decodeBody($body, $encoding = 'chunked') {
185   - if (!is_string($body)) {
186   - return false;
187   - }
188   - if (empty($encoding)) {
189   - return array('body' => $body, 'header' => false);
190   - }
191   - $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
192   -
193   - if (!is_callable(array(&$this, $decodeMethod))) {
194   - return array('body' => $body, 'header' => false);
195   - }
196   - return $this->{$decodeMethod}($body);
197   - }
198   -
199   -/**
200   - * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
201   - * a result.
202   - *
203   - * @param string $body A string containing the chunked body to decode.
204   - * @return mixed Array of response headers and body or false.
205   - * @throws SocketException
206   - */
207   - protected function _decodeChunkedBody($body) {
208   - if (!is_string($body)) {
209   - return false;
210   - }
211   -
212   - $decodedBody = null;
213   - $chunkLength = null;
214   -
215   - while ($chunkLength !== 0) {
216   - if (!preg_match('/^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
217   - throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
218   - }
219   -
220   - $chunkSize = 0;
221   - $hexLength = 0;
222   - $chunkExtensionName = '';
223   - $chunkExtensionValue = '';
224   - if (isset($match[0])) {
225   - $chunkSize = $match[0];
226   - }
227   - if (isset($match[1])) {
228   - $hexLength = $match[1];
229   - }
230   - if (isset($match[2])) {
231   - $chunkExtensionName = $match[2];
232   - }
233   - if (isset($match[3])) {
234   - $chunkExtensionValue = $match[3];
235   - }
236   -
237   - $body = substr($body, strlen($chunkSize));
238   - $chunkLength = hexdec($hexLength);
239   - $chunk = substr($body, 0, $chunkLength);
240   - if (!empty($chunkExtensionName)) {
241   - // @todo See if there are popular chunk extensions we should implement
242   - }
243   - $decodedBody .= $chunk;
244   - if ($chunkLength !== 0) {
245   - $body = substr($body, $chunkLength + strlen("\r\n"));
246   - }
247   - }
248   -
249   - $entityHeader = false;
250   - if (!empty($body)) {
251   - $entityHeader = $this->_parseHeader($body);
252   - }
253   - return array('body' => $decodedBody, 'header' => $entityHeader);
254   - }
255   -
256   -/**
257   - * Parses an array based header.
258   - *
259   - * @param array $header Header as an indexed array (field => value)
260   - * @return array Parsed header
261   - */
262   - protected function _parseHeader($header) {
263   - if (is_array($header)) {
264   - return $header;
265   - } elseif (!is_string($header)) {
266   - return false;
267   - }
268   -
269   - preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
270   -
271   - $header = array();
272   - foreach ($matches as $match) {
273   - list(, $field, $value) = $match;
274   -
275   - $value = trim($value);
276   - $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
277   -
278   - $field = $this->_unescapeToken($field);
279   -
280   - if (!isset($header[$field])) {
281   - $header[$field] = $value;
282   - } else {
283   - $header[$field] = array_merge((array)$header[$field], (array)$value);
284   - }
285   - }
286   - return $header;
287   - }
288   -
289   -/**
290   - * Parses cookies in response headers.
291   - *
292   - * @param array $header Header array containing one ore more 'Set-Cookie' headers.
293   - * @return mixed Either false on no cookies, or an array of cookies received.
294   - * @todo Make this 100% RFC 2965 confirm
295   - */
296   - public function parseCookies($header) {
297   - $cookieHeader = $this->getHeader('Set-Cookie', $header);
298   - if (!$cookieHeader) {
299   - return false;
300   - }
301   -
302   - $cookies = array();
303   - foreach ((array)$cookieHeader as $cookie) {
304   - if (strpos($cookie, '";"') !== false) {
305   - $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
306   - $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
307   - } else {
308   - $parts = preg_split('/\;[ \t]*/', $cookie);
309   - }
310   -
311   - list($name, $value) = explode('=', array_shift($parts), 2);
312   - $cookies[$name] = compact('value');
313   -
314   - foreach ($parts as $part) {
315   - if (strpos($part, '=') !== false) {
316   - list($key, $value) = explode('=', $part);
317   - } else {
318   - $key = $part;
319   - $value = true;
320   - }
321   -
322   - $key = strtolower($key);
323   - if (!isset($cookies[$name][$key])) {
324   - $cookies[$name][$key] = $value;
325   - }
326   - }
327   - }
328   - return $cookies;
329   - }
330   -
331   -/**
332   - * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
333   - *
334   - * @param string $token Token to unescape
335   - * @param array $chars
336   - * @return string Unescaped token
337   - * @todo Test $chars parameter
338   - */
339   - protected function _unescapeToken($token, $chars = null) {
340   - $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
341   - $token = preg_replace($regex, '\\1', $token);
342   - return $token;
343   - }
344   -
345   -/**
346   - * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
347   - *
348   - * @param boolean $hex true to get them as HEX values, false otherwise
349   - * @param array $chars
350   - * @return array Escape chars
351   - * @todo Test $chars parameter
352   - */
353   - protected function _tokenEscapeChars($hex = true, $chars = null) {
354   - if (!empty($chars)) {
355   - $escape = $chars;
356   - } else {
357   - $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
358   - for ($i = 0; $i <= 31; $i++) {
359   - $escape[] = chr($i);
360   - }
361   - $escape[] = chr(127);
362   - }
363   -
364   - if (!$hex) {
365   - return $escape;
366   - }
367   - foreach ($escape as $key => $char) {
368   - $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
369   - }
370   - return $escape;
371   - }
372   -
373   -/**
374   - * ArrayAccess - Offset Exists
375   - *
376   - * @param string $offset
377   - * @return boolean
378   - */
379   - public function offsetExists($offset) {
380   - return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
381   - }
382   -
383   -/**
384   - * ArrayAccess - Offset Get
385   - *
386   - * @param string $offset
387   - * @return mixed
388   - */
389   - public function offsetGet($offset) {
390   - switch ($offset) {
391   - case 'raw':
392   - $firstLineLength = strpos($this->raw, "\r\n") + 2;
393   - if ($this->raw[$firstLineLength] === "\r") {
394   - $header = null;
395   - } else {
396   - $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
397   - }
398   - return array(
399   - 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
400   - 'header' => $header,
401   - 'body' => $this->body,
402   - 'response' => $this->raw
403   - );
404   - case 'status':
405   - return array(
406   - 'http-version' => $this->httpVersion,
407   - 'code' => $this->code,
408   - 'reason-phrase' => $this->reasonPhrase
409   - );
410   - case 'header':
411   - return $this->headers;
412   - case 'body':
413   - return $this->body;
414   - case 'cookies':
415   - return $this->cookies;
416   - }
417   - return null;
418   - }
419   -
420   -/**
421   - * ArrayAccess - Offset Set
422   - *
423   - * @param string $offset
424   - * @param mixed $value
425   - * @return void
426   - */
427   - public function offsetSet($offset, $value) {
428   - }
429   -
430   -/**
431   - * ArrayAccess - Offset Unset
432   - *
433   - * @param string $offset
434   - * @return void
435   - */
436   - public function offsetUnset($offset) {
437   - }
438   -
439   -/**
440   - * Instance as string
441   - *
442   - * @return string
443   - */
444   - public function __toString() {
445   - return $this->body();
446   - }
  33 +class HttpResponse extends HttpSocketResponse {
447 34
448 35 }
42 lib/Cake/Network/Http/HttpSocket.php
@@ -18,6 +18,7 @@
18 18 */
19 19 App::uses('CakeSocket', 'Network');
20 20 App::uses('Router', 'Routing');
  21 +App::uses('Hash', 'Utility');
21 22
22 23 /**
23 24 * Cake network socket connection class.
@@ -64,7 +65,7 @@ class HttpSocket extends CakeSocket {
64 65 ),
65 66 'raw' => null,
66 67 'redirect' => false,
67   - 'cookies' => array()
  68 + 'cookies' => array(),
68 69 );
69 70
70 71 /**
@@ -79,7 +80,7 @@ class HttpSocket extends CakeSocket {
79 80 *
80 81 * @var string
81 82 */
82   - public $responseClass = 'HttpResponse';
  83 + public $responseClass = 'HttpSocketResponse';
83 84
84 85 /**
85 86 * Configuration settings for the HttpSocket and the requests
@@ -92,6 +93,9 @@ class HttpSocket extends CakeSocket {
92 93 'protocol' => 'tcp',
93 94 'port' => 80,
94 95 'timeout' => 30,
  96 + 'ssl_verify_peer' => true,
  97 + 'ssl_verify_depth' => 5,
  98 + 'ssl_verify_host' => true,
95 99 'request' => array(
96 100 'uri' => array(
97 101 'scheme' => array('http', 'https'),
@@ -99,7 +103,7 @@ class HttpSocket extends CakeSocket {
99 103 'port' => array(80, 443)
100 104 ),
101 105 'redirect' => false,
102   - 'cookies' => array()
  106 + 'cookies' => array(),
103 107 )
104 108 );
105 109
@@ -246,7 +250,7 @@ public function setContentResource($resource) {
246 250 * method and provide a more granular interface.
247 251 *
248 252 * @param string|array $request Either an URI string, or an array defining host/uri
249   - * @return mixed false on error, HttpResponse on success
  253 + * @return mixed false on error, HttpSocketResponse on success
250 254 * @throws SocketException
251 255 */
252 256 public function request($request = array()) {
@@ -348,6 +352,8 @@ public function request($request = array()) {
348 352 return false;
349 353 }
350 354
  355 + $this->_configContext($this->request['uri']['host']);
  356 +
351 357 $this->request['raw'] = '';
352 358 if ($this->request['line'] !== false) {
353 359 $this->request['raw'] = $this->request['line'];
@@ -395,6 +401,7 @@ public function request($request = array()) {
395 401 throw new SocketException(__d('cake_dev', 'Class %s not found.', $this->responseClass));
396 402 }
397 403 $this->response = new $responseClass($response);
  404 +
398 405 if (!empty($this->response->cookies)) {
399 406 if (!isset($this->config['request']['cookies'][$Host])) {
400 407 $this->config['request']['cookies'][$Host] = array();
@@ -644,6 +651,33 @@ protected function _configUri($uri = null) {
644 651 }
645 652
646 653 /**
  654 + * Configure the socket's context. Adds in configuration
  655 + * that can not be declared in the class definition.
  656 + *
  657 + * @param string $host The host you're connecting to.
  658 + * @return void
  659 + */
  660 + protected function _configContext($host) {
  661 + foreach ($this->config as $key => $value) {
  662 + if (substr($key, 0, 4) !== 'ssl_') {
  663 + continue;
  664 + }
  665 + $contextKey = substr($key, 4);
  666 + if (empty($this->config['context']['ssl'][$contextKey])) {
  667 + $this->config['context']['ssl'][$contextKey] = $value;
  668 + }
  669 + unset($this->config[$key]);
  670 + }
  671 + if (empty($this->_context['ssl']['cafile'])) {
  672 + $this->config['context']['ssl']['cafile'] = CAKE . 'Config' . DS . 'cacert.pem';
  673 + }
  674 + if (!empty($this->config['context']['ssl']['verify_host'])) {
  675 + $this->config['context']['ssl']['CN_match'] = $host;
  676 + unset($this->config['context']['ssl']['verify_host']);
  677 + }
  678 + }
  679 +
  680 +/**
647 681 * Takes a $uri array and turns it into a fully qualified URL string
648 682 *
649 683 * @param string|array $uri Either A $uri array, or a request string. Will use $this->config if left empty.
455 lib/Cake/Network/Http/HttpSocketResponse.php
... ... @@ -0,0 +1,455 @@
  1 +<?php
  2 +/**
  3 + * HTTP Response from HttpSocket.
  4 + *
  5 + * PHP 5
  6 + *
  7 + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8 + * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9 + *
  10 + * Licensed under The MIT License
  11 + * Redistributions of files must retain the above copyright notice.
  12 + *
  13 + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14 + * @link http://cakephp.org CakePHP(tm) Project
  15 + * @since CakePHP(tm) v 2.0.0
  16 + * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  17 + */
  18 +
  19 +/**
  20 + * HTTP Response from HttpSocket.
  21 + *
  22 + * @package Cake.Network.Http
  23 + */
  24 +class HttpSocketResponse implements ArrayAccess {
  25 +
  26 +/**
  27 + * Body content
  28 + *
  29 + * @var string
  30 + */
  31 + public $body = '';
  32 +
  33 +/**
  34 + * Headers
  35 + *
  36 + * @var array
  37 + */
  38 + public $headers = array();
  39 +
  40 +/**
  41 + * Cookies
  42 + *
  43 + * @var array
  44 + */
  45 + public $cookies = array();
  46 +
  47 +/**
  48 + * HTTP version
  49 + *
  50 + * @var string
  51 + */
  52 + public $httpVersion = 'HTTP/1.1';
  53 +
  54 +/**
  55 + * Response code
  56 + *
  57 + * @var integer
  58 + */
  59 + public $code = 0;
  60 +
  61 +/**
  62 + * Reason phrase
  63 + *
  64 + * @var string
  65 + */
  66 + public $reasonPhrase = '';
  67 +
  68 +/**
  69 + * Pure raw content
  70 + *
  71 + * @var string
  72 + */
  73 + public $raw = '';
  74 +
  75 +/**
  76 + * Context data in the response.
  77 + * Contains SSL certificates for example.
  78 + *
  79 + * @var array
  80 + */
  81 + public $context = array();
  82 +
  83 +/**
  84 + * Constructor
  85 + *
  86 + * @param string $message
  87 + */
  88 + public function __construct($message = null) {
  89 + if ($message !== null) {
  90 + $this->parseResponse($message);
  91 + }
  92 + }
  93 +
  94 +/**
  95 + * Body content
  96 + *
  97 + * @return string
  98 + */
  99 + public function body() {
  100 + return (string)$this->body;
  101 + }
  102 +
  103 +/**
  104 + * Get header in case insensitive
  105 + *
  106 + * @param string $name Header name
  107 + * @param array $headers
  108 + * @return mixed String if header exists or null
  109 + */
  110 + public function getHeader($name, $headers = null) {
  111 + if (!is_array($headers)) {
  112 + $headers =& $this->headers;
  113 + }
  114 + if (isset($headers[$name])) {
  115 + return $headers[$name];
  116 + }
  117 + foreach ($headers as $key => $value) {
  118 + if (strcasecmp($key, $name) === 0) {
  119 + return $value;
  120 + }
  121 + }
  122 + return null;
  123 + }
  124 +
  125 +/**
  126 + * If return is 200 (OK)
  127 + *
  128 + * @return boolean
  129 + */
  130 + public function isOk() {
  131 + return $this->code == 200;
  132 + }
  133 +
  134 +/**
  135 + * If return is a valid 3xx (Redirection)
  136 + *
  137 + * @return boolean
  138 + */
  139 + public function isRedirect() {
  140 + return in_array($this->code, array(301, 302, 303, 307)) && !is_null($this->getHeader('Location'));
  141 + }
  142 +
  143 +/**
  144 + * Parses the given message and breaks it down in parts.
  145 + *
  146 + * @param string $message Message to parse
  147 + * @return void
  148 + * @throws SocketException
  149 + */
  150 + public function parseResponse($message) {
  151 + if (!is_string($message)) {
  152 + throw new SocketException(__d('cake_dev', 'Invalid response.'));
  153 + }
  154 +
  155 + if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
  156 + throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
  157 + }
  158 +
  159 + list(, $statusLine, $header) = $match;
  160 + $this->raw = $message;
  161 + $this->body = (string)substr($message, strlen($match[0]));
  162 +
  163 + if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
  164 + $this->httpVersion = $match[1];
  165 + $this->code = $match[2];
  166 + $this->reasonPhrase = $match[3];
  167 + }
  168 +
  169 + $this->headers = $this->_parseHeader($header);
  170 + $transferEncoding = $this->getHeader('Transfer-Encoding');
  171 + $decoded = $this->_decodeBody($this->body, $transferEncoding);
  172 + $this->body = $decoded['body'];
  173 +
  174 + if (!empty($decoded['header'])) {
  175 + $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
  176 + }
  177 +
  178 + if (!empty($this->headers)) {
  179 + $this->cookies = $this->parseCookies($this->headers);
  180 + }
  181 + }
  182 +
  183 +/**
  184 + * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
  185 + * 'body' and 'header' or false on failure.
  186 + *
  187 + * @param string $body A string containing the body to decode.
  188 + * @param string|boolean $encoding Can be false in case no encoding is being used, or a string representing the encoding.
  189 + * @return mixed Array of response headers and body or false.
  190 + */
  191 + protected function _decodeBody($body, $encoding = 'chunked') {
  192 + if (!is_string($body)) {
  193 + return false;
  194 + }
  195 + if (empty($encoding)) {
  196 + return array('body' => $body, 'header' => false);
  197 + }
  198 + $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
  199 +
  200 + if (!is_callable(array(&$this, $decodeMethod))) {
  201 + return array('body' => $body, 'header' => false);
  202 + }
  203 + return $this->{$decodeMethod}($body);
  204 + }
  205 +
  206 +/**
  207 + * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
  208 + * a result.
  209 + *
  210 + * @param string $body A string containing the chunked body to decode.
  211 + * @return mixed Array of response headers and body or false.
  212 + * @throws SocketException
  213 + */
  214 + protected function _decodeChunkedBody($body) {
  215 + if (!is_string($body)) {
  216 + return false;
  217 + }
  218 +
  219 + $decodedBody = null;
  220 + $chunkLength = null;
  221 +
  222 + while ($chunkLength !== 0) {
  223 + if (!preg_match('/^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
  224 + throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
  225 + }
  226 +
  227 + $chunkSize = 0;
  228 + $hexLength = 0;
  229 + $chunkExtensionName = '';
  230 + $chunkExtensionValue = '';
  231 + if (isset($match[0])) {
  232 + $chunkSize = $match[0];
  233 + }
  234 + if (isset($match[1])) {
  235 + $hexLength = $match[1];
  236 + }
  237 + if (isset($match[2])) {
  238 + $chunkExtensionName = $match[2];
  239 + }
  240 + if (isset($match[3])) {
  241 + $chunkExtensionValue = $match[3];
  242 + }
  243 +
  244 + $body = substr($body, strlen($chunkSize));
  245 + $chunkLength = hexdec($hexLength);
  246 + $chunk = substr($body, 0, $chunkLength);
  247 + if (!empty($chunkExtensionName)) {
  248 + // @todo See if there are popular chunk extensions we should implement
  249 + }
  250 + $decodedBody .= $chunk;
  251 + if ($chunkLength !== 0) {
  252 + $body = substr($body, $chunkLength + strlen("\r\n"));
  253 + }
  254 + }
  255 +
  256 + $entityHeader = false;
  257 + if (!empty($body)) {
  258 + $entityHeader = $this->_parseHeader($body);
  259 + }
  260 + return array('body' => $decodedBody, 'header' => $entityHeader);
  261 + }
  262 +
  263 +/**
  264 + * Parses an array based header.
  265 + *
  266 + * @param array $header Header as an indexed array (field => value)
  267 + * @return array Parsed header
  268 + */
  269 + protected function _parseHeader($header) {
  270 + if (is_array($header)) {
  271 + return $header;
  272 + } elseif (!is_string($header)) {
  273 + return false;
  274 + }
  275 +
  276 + preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
  277 +
  278 + $header = array();
  279 + foreach ($matches as $match) {
  280 + list(, $field, $value) = $match;
  281 +
  282 + $value = trim($value);
  283 + $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
  284 +
  285 + $field = $this->_unescapeToken($field);
  286 +
  287 + if (!isset($header[$field])) {
  288 + $header[$field] = $value;
  289 + } else {
  290 + $header[$field] = array_merge((array)$header[$field], (array)$value);
  291 + }
  292 + }
  293 + return $header;
  294 + }
  295 +
  296 +/**
  297 + * Parses cookies in response headers.
  298 + *
  299 + * @param array $header Header array containing one ore more 'Set-Cookie' headers.
  300 + * @return mixed Either false on no cookies, or an array of cookies received.
  301 + * @todo Make this 100% RFC 2965 confirm
  302 + */
  303 + public function parseCookies($header) {
  304 + $cookieHeader = $this->getHeader('Set-Cookie', $header);
  305 + if (!$cookieHeader) {
  306 + return false;
  307 + }
  308 +
  309 + $cookies = array();
  310 + foreach ((array)$cookieHeader as $cookie) {
  311 + if (strpos($cookie, '";"') !== false) {
  312 + $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
  313 + $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
  314 + } else {
  315 + $parts = preg_split('/\;[ \t]*/', $cookie);
  316 + }
  317 +
  318 + list($name, $value) = explode('=', array_shift($parts), 2);
  319 + $cookies[$name] = compact('value');
  320 +
  321 + foreach ($parts as $part) {
  322 + if (strpos($part, '=') !== false) {
  323 + list($key, $value) = explode('=', $part);
  324 + } else {
  325 + $key = $part;
  326 + $value = true;
  327 + }
  328 +
  329 + $key = strtolower($key);
  330 + if (!isset($cookies[$name][$key])) {
  331 + $cookies[$name][$key] = $value;
  332 + }
  333 + }
  334 + }
  335 + return $cookies;
  336 + }
  337 +
  338 +/**
  339 + * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
  340 + *
  341 + * @param string $token Token to unescape
  342 + * @param array $chars
  343 + * @return string Unescaped token
  344 + * @todo Test $chars parameter
  345 + */
  346 + protected function _unescapeToken($token, $chars = null) {
  347 + $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
  348 + $token = preg_replace($regex, '\\1', $token);
  349 + return $token;
  350 + }
  351 +
  352 +/**
  353 + * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
  354 + *
  355 + * @param boolean $hex true to get them as HEX values, false otherwise
  356 + * @param array $chars
  357 + * @return array Escape chars
  358 + * @todo Test $chars parameter
  359 + */
  360 + protected function _tokenEscapeChars($hex = true, $chars = null) {
  361 + if (!empty($chars)) {
  362 + $escape = $chars;
  363 + } else {
  364 + $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
  365 + for ($i = 0; $i <= 31; $i++) {
  366 + $escape[] = chr($i);
  367 + }
  368 + $escape[] = chr(127);
  369 + }
  370 +
  371 + if (!$hex) {
  372 + return $escape;
  373 + }
  374 + foreach ($escape as $key => $char) {
  375 + $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
  376 + }
  377 + return $escape;
  378 + }
  379 +
  380 +/**
  381 + * ArrayAccess - Offset Exists
  382 + *
  383 + * @param string $offset
  384 + * @return boolean
  385 + */
  386 + public function offsetExists($offset) {
  387 + return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
  388 + }
  389 +
  390 +/**
  391 + * ArrayAccess - Offset Get
  392 + *
  393 + * @param string $offset
  394 + * @return mixed
  395 + */
  396 + public function offsetGet($offset) {
  397 + switch ($offset) {
  398 + case 'raw':
  399 + $firstLineLength = strpos($this->raw, "\r\n") + 2;
  400 + if ($this->raw[$firstLineLength] === "\r") {
  401 + $header = null;
  402 + } else {
  403 + $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
  404 + }
  405 + return array(
  406 + 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
  407 + 'header' => $header,
  408 + 'body' => $this->body,
  409 + 'response' => $this->raw
  410 + );
  411 + case 'status':
  412 + return array(
  413 + 'http-version' => $this->httpVersion,
  414 + 'code' => $this->code,
  415 + 'reason-phrase' => $this->reasonPhrase
  416 + );
  417 + case 'header':
  418 + return $this->headers;
  419 + case 'body':
  420 + return $this->body;
  421 + case 'cookies':
  422 + return $this->cookies;
  423 + }
  424 + return null;
  425 + }
  426 +
  427 +/**
  428 + * ArrayAccess - Offset Set
  429 + *
  430 + * @param string $offset
  431 + * @param mixed $value
  432 + * @return void
  433 + */
  434 + public function offsetSet($offset, $value) {
  435 + }
  436 +
  437 +/**
  438 + * ArrayAccess - Offset Unset
  439 + *
  440 + * @param string $offset
  441 + * @return void
  442 + */
  443 + public function offsetUnset($offset) {
  444 + }
  445 +
  446 +/**
  447 + * Instance as string
  448 + *
  449 + * @return string
  450 + */
  451 + public function __toString() {
  452 + return $this->body();
  453 + }
  454 +
  455 +}
21 lib/Cake/Test/Case/Network/CakeSocketTest.php
@@ -326,4 +326,25 @@ public function testEnableCryptoEnableStatus() {
326 326 $this->assertTrue($this->Socket->encrypted);
327 327 }
328 328
  329 +/**
  330 + * test getting the context for a socket.
  331 + *
  332 + * @return void
  333 + */
  334 + public function testGetContext() {
  335 + $this->skipIf(!extension_loaded('openssl'), 'OpenSSL is not enabled cannot test SSL.');
  336 + $config = array(
  337 + 'host' => 'smtp.gmail.com',
  338 + 'port' => 465,
  339 + 'timeout' => 5,
  340 + 'context' => array(
  341 + 'ssl' => array('capture_peer' => true)
  342 + )
  343 + );
  344 + $this->Socket = new CakeSocket($config);
  345 + $this->Socket->connect();
  346 + $result = $this->Socket->context();
  347 + $this->assertEquals($config['context'], $result);
  348 + }
  349 +
329 350 }
54 lib/Cake/Test/Case/Network/Http/HttpSocketTest.php
@@ -253,6 +253,9 @@ public function testConfigUri() {
253 253 'protocol' => 'tcp',
254 254 'port' => 23,
255 255 'timeout' => 30,
  256 + 'ssl_verify_peer' => true,
  257 + 'ssl_verify_depth' => 5,
  258 + 'ssl_verify_host' => true,
256 259 'request' => array(
257