HttpSocketResponse.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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. * HTTP Response from HttpSocket.
  20. *
  21. * @package Cake.Network.Http
  22. */
  23. class HttpSocketResponse implements ArrayAccess {
  24. /**
  25. * Body content
  26. *
  27. * @var string
  28. */
  29. public $body = '';
  30. /**
  31. * Headers
  32. *
  33. * @var array
  34. */
  35. public $headers = array();
  36. /**
  37. * Cookies
  38. *
  39. * @var array
  40. */
  41. public $cookies = array();
  42. /**
  43. * HTTP version
  44. *
  45. * @var string
  46. */
  47. public $httpVersion = 'HTTP/1.1';
  48. /**
  49. * Response code
  50. *
  51. * @var integer
  52. */
  53. public $code = 0;
  54. /**
  55. * Reason phrase
  56. *
  57. * @var string
  58. */
  59. public $reasonPhrase = '';
  60. /**
  61. * Pure raw content
  62. *
  63. * @var string
  64. */
  65. public $raw = '';
  66. /**
  67. * Context data in the response.
  68. * Contains SSL certificates for example.
  69. *
  70. * @var array
  71. */
  72. public $context = array();
  73. /**
  74. * Constructor
  75. *
  76. * @param string $message
  77. */
  78. public function __construct($message = null) {
  79. if ($message !== null) {
  80. $this->parseResponse($message);
  81. }
  82. }
  83. /**
  84. * Body content
  85. *
  86. * @return string
  87. */
  88. public function body() {
  89. return (string)$this->body;
  90. }
  91. /**
  92. * Get header in case insensitive
  93. *
  94. * @param string $name Header name
  95. * @param array $headers
  96. * @return mixed String if header exists or null
  97. */
  98. public function getHeader($name, $headers = null) {
  99. if (!is_array($headers)) {
  100. $headers =& $this->headers;
  101. }
  102. if (isset($headers[$name])) {
  103. return $headers[$name];
  104. }
  105. foreach ($headers as $key => $value) {
  106. if (strcasecmp($key, $name) === 0) {
  107. return $value;
  108. }
  109. }
  110. return null;
  111. }
  112. /**
  113. * If return is 200 (OK)
  114. *
  115. * @return boolean
  116. */
  117. public function isOk() {
  118. return $this->code == 200;
  119. }
  120. /**
  121. * If return is a valid 3xx (Redirection)
  122. *
  123. * @return boolean
  124. */
  125. public function isRedirect() {
  126. return in_array($this->code, array(301, 302, 303, 307)) && !is_null($this->getHeader('Location'));
  127. }
  128. /**
  129. * Parses the given message and breaks it down in parts.
  130. *
  131. * @param string $message Message to parse
  132. * @return void
  133. * @throws SocketException
  134. */
  135. public function parseResponse($message) {
  136. if (!is_string($message)) {
  137. throw new SocketException(__d('cake_dev', 'Invalid response.'));
  138. }
  139. if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
  140. throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
  141. }
  142. list(, $statusLine, $header) = $match;
  143. $this->raw = $message;
  144. $this->body = (string)substr($message, strlen($match[0]));
  145. if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
  146. $this->httpVersion = $match[1];
  147. $this->code = $match[2];
  148. $this->reasonPhrase = $match[3];
  149. }
  150. $this->headers = $this->_parseHeader($header);
  151. $transferEncoding = $this->getHeader('Transfer-Encoding');
  152. $decoded = $this->_decodeBody($this->body, $transferEncoding);
  153. $this->body = $decoded['body'];
  154. if (!empty($decoded['header'])) {
  155. $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
  156. }
  157. if (!empty($this->headers)) {
  158. $this->cookies = $this->parseCookies($this->headers);
  159. }
  160. }
  161. /**
  162. * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
  163. * 'body' and 'header' or false on failure.
  164. *
  165. * @param string $body A string containing the body to decode.
  166. * @param string|boolean $encoding Can be false in case no encoding is being used, or a string representing the encoding.
  167. * @return mixed Array of response headers and body or false.
  168. */
  169. protected function _decodeBody($body, $encoding = 'chunked') {
  170. if (!is_string($body)) {
  171. return false;
  172. }
  173. if (empty($encoding)) {
  174. return array('body' => $body, 'header' => false);
  175. }
  176. $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
  177. if (!is_callable(array(&$this, $decodeMethod))) {
  178. return array('body' => $body, 'header' => false);
  179. }
  180. return $this->{$decodeMethod}($body);
  181. }
  182. /**
  183. * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
  184. * a result.
  185. *
  186. * @param string $body A string containing the chunked body to decode.
  187. * @return mixed Array of response headers and body or false.
  188. * @throws SocketException
  189. */
  190. protected function _decodeChunkedBody($body) {
  191. if (!is_string($body)) {
  192. return false;
  193. }
  194. $decodedBody = null;
  195. $chunkLength = null;
  196. while ($chunkLength !== 0) {
  197. if (!preg_match('/^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
  198. throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
  199. }
  200. $chunkSize = 0;
  201. $hexLength = 0;
  202. $chunkExtensionName = '';
  203. $chunkExtensionValue = '';
  204. if (isset($match[0])) {
  205. $chunkSize = $match[0];
  206. }
  207. if (isset($match[1])) {
  208. $hexLength = $match[1];
  209. }
  210. if (isset($match[2])) {
  211. $chunkExtensionName = $match[2];
  212. }
  213. if (isset($match[3])) {
  214. $chunkExtensionValue = $match[3];
  215. }
  216. $body = substr($body, strlen($chunkSize));
  217. $chunkLength = hexdec($hexLength);
  218. $chunk = substr($body, 0, $chunkLength);
  219. if (!empty($chunkExtensionName)) {
  220. // @todo See if there are popular chunk extensions we should implement
  221. }
  222. $decodedBody .= $chunk;
  223. if ($chunkLength !== 0) {
  224. $body = substr($body, $chunkLength + strlen("\r\n"));
  225. }
  226. }
  227. $entityHeader = false;
  228. if (!empty($body)) {
  229. $entityHeader = $this->_parseHeader($body);
  230. }
  231. return array('body' => $decodedBody, 'header' => $entityHeader);
  232. }
  233. /**
  234. * Parses an array based header.
  235. *
  236. * @param array $header Header as an indexed array (field => value)
  237. * @return array Parsed header
  238. */
  239. protected function _parseHeader($header) {
  240. if (is_array($header)) {
  241. return $header;
  242. } elseif (!is_string($header)) {
  243. return false;
  244. }
  245. preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
  246. $header = array();
  247. foreach ($matches as $match) {
  248. list(, $field, $value) = $match;
  249. $value = trim($value);
  250. $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
  251. $field = $this->_unescapeToken($field);
  252. if (!isset($header[$field])) {
  253. $header[$field] = $value;
  254. } else {
  255. $header[$field] = array_merge((array)$header[$field], (array)$value);
  256. }
  257. }
  258. return $header;
  259. }
  260. /**
  261. * Parses cookies in response headers.
  262. *
  263. * @param array $header Header array containing one ore more 'Set-Cookie' headers.
  264. * @return mixed Either false on no cookies, or an array of cookies received.
  265. * @todo Make this 100% RFC 2965 confirm
  266. */
  267. public function parseCookies($header) {
  268. $cookieHeader = $this->getHeader('Set-Cookie', $header);
  269. if (!$cookieHeader) {
  270. return false;
  271. }
  272. $cookies = array();
  273. foreach ((array)$cookieHeader as $cookie) {
  274. if (strpos($cookie, '";"') !== false) {
  275. $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
  276. $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
  277. } else {
  278. $parts = preg_split('/\;[ \t]*/', $cookie);
  279. }
  280. list($name, $value) = explode('=', array_shift($parts), 2);
  281. $cookies[$name] = compact('value');
  282. foreach ($parts as $part) {
  283. if (strpos($part, '=') !== false) {
  284. list($key, $value) = explode('=', $part);
  285. } else {
  286. $key = $part;
  287. $value = true;
  288. }
  289. $key = strtolower($key);
  290. if (!isset($cookies[$name][$key])) {
  291. $cookies[$name][$key] = $value;
  292. }
  293. }
  294. }
  295. return $cookies;
  296. }
  297. /**
  298. * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
  299. *
  300. * @param string $token Token to unescape
  301. * @param array $chars
  302. * @return string Unescaped token
  303. * @todo Test $chars parameter
  304. */
  305. protected function _unescapeToken($token, $chars = null) {
  306. $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
  307. $token = preg_replace($regex, '\\1', $token);
  308. return $token;
  309. }
  310. /**
  311. * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
  312. *
  313. * @param boolean $hex true to get them as HEX values, false otherwise
  314. * @param array $chars
  315. * @return array Escape chars
  316. * @todo Test $chars parameter
  317. */
  318. protected function _tokenEscapeChars($hex = true, $chars = null) {
  319. if (!empty($chars)) {
  320. $escape = $chars;
  321. } else {
  322. $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
  323. for ($i = 0; $i <= 31; $i++) {
  324. $escape[] = chr($i);
  325. }
  326. $escape[] = chr(127);
  327. }
  328. if (!$hex) {
  329. return $escape;
  330. }
  331. foreach ($escape as $key => $char) {
  332. $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
  333. }
  334. return $escape;
  335. }
  336. /**
  337. * ArrayAccess - Offset Exists
  338. *
  339. * @param string $offset
  340. * @return boolean
  341. */
  342. public function offsetExists($offset) {
  343. return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
  344. }
  345. /**
  346. * ArrayAccess - Offset Get
  347. *
  348. * @param string $offset
  349. * @return mixed
  350. */
  351. public function offsetGet($offset) {
  352. switch ($offset) {
  353. case 'raw':
  354. $firstLineLength = strpos($this->raw, "\r\n") + 2;
  355. if ($this->raw[$firstLineLength] === "\r") {
  356. $header = null;
  357. } else {
  358. $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
  359. }
  360. return array(
  361. 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
  362. 'header' => $header,
  363. 'body' => $this->body,
  364. 'response' => $this->raw
  365. );
  366. case 'status':
  367. return array(
  368. 'http-version' => $this->httpVersion,
  369. 'code' => $this->code,
  370. 'reason-phrase' => $this->reasonPhrase
  371. );
  372. case 'header':
  373. return $this->headers;
  374. case 'body':
  375. return $this->body;
  376. case 'cookies':
  377. return $this->cookies;
  378. }
  379. return null;
  380. }
  381. /**
  382. * ArrayAccess - Offset Set
  383. *
  384. * @param string $offset
  385. * @param mixed $value
  386. * @return void
  387. */
  388. public function offsetSet($offset, $value) {
  389. }
  390. /**
  391. * ArrayAccess - Offset Unset
  392. *
  393. * @param string $offset
  394. * @return void
  395. */
  396. public function offsetUnset($offset) {
  397. }
  398. /**
  399. * Instance as string
  400. *
  401. * @return string
  402. */
  403. public function __toString() {
  404. return $this->body();
  405. }
  406. }