PageRenderTime 31ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/auth/cas/CAS/CAS/CookieJar.php

https://gitlab.com/unofficial-mirrors/moodle
PHP | 385 lines | 189 code | 40 blank | 156 comment | 41 complexity | e98f4b993363b6d9cd5220eefb801bc4 MD5 | raw file
  1. <?php
  2. /**
  3. * Licensed to Jasig under one or more contributor license
  4. * agreements. See the NOTICE file distributed with this work for
  5. * additional information regarding copyright ownership.
  6. *
  7. * Jasig licenses this file to you under the Apache License,
  8. * Version 2.0 (the "License"); you may not use this file except in
  9. * compliance with the License. You may obtain a copy of the License at:
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. *
  19. * PHP Version 5
  20. *
  21. * @file CAS/CookieJar.php
  22. * @category Authentication
  23. * @package PhpCAS
  24. * @author Adam Franco <afranco@middlebury.edu>
  25. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
  26. * @link https://wiki.jasig.org/display/CASC/phpCAS
  27. */
  28. /**
  29. * This class provides access to service cookies and handles parsing of response
  30. * headers to pull out cookie values.
  31. *
  32. * @class CAS_CookieJar
  33. * @category Authentication
  34. * @package PhpCAS
  35. * @author Adam Franco <afranco@middlebury.edu>
  36. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
  37. * @link https://wiki.jasig.org/display/CASC/phpCAS
  38. */
  39. class CAS_CookieJar
  40. {
  41. private $_cookies;
  42. /**
  43. * Create a new cookie jar by passing it a reference to an array in which it
  44. * should store cookies.
  45. *
  46. * @param array &$storageArray Array to store cookies
  47. *
  48. * @return void
  49. */
  50. public function __construct (array &$storageArray)
  51. {
  52. $this->_cookies =& $storageArray;
  53. }
  54. /**
  55. * Store cookies for a web service request.
  56. * Cookie storage is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
  57. *
  58. * @param string $request_url The URL that generated the response headers.
  59. * @param array $response_headers An array of the HTTP response header strings.
  60. *
  61. * @return void
  62. *
  63. * @access private
  64. */
  65. public function storeCookies ($request_url, $response_headers)
  66. {
  67. $urlParts = parse_url($request_url);
  68. $defaultDomain = $urlParts['host'];
  69. $cookies = $this->parseCookieHeaders($response_headers, $defaultDomain);
  70. // var_dump($cookies);
  71. foreach ($cookies as $cookie) {
  72. // Enforce the same-origin policy by verifying that the cookie
  73. // would match the url that is setting it
  74. if (!$this->cookieMatchesTarget($cookie, $urlParts)) {
  75. continue;
  76. }
  77. // store the cookie
  78. $this->storeCookie($cookie);
  79. phpCAS::trace($cookie['name'].' -> '.$cookie['value']);
  80. }
  81. }
  82. /**
  83. * Retrieve cookies applicable for a web service request.
  84. * Cookie applicability is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
  85. *
  86. * @param string $request_url The url that the cookies will be for.
  87. *
  88. * @return array An array containing cookies. E.g. array('name' => 'val');
  89. *
  90. * @access private
  91. */
  92. public function getCookies ($request_url)
  93. {
  94. if (!count($this->_cookies)) {
  95. return array();
  96. }
  97. // If our request URL can't be parsed, no cookies apply.
  98. $target = parse_url($request_url);
  99. if ($target === false) {
  100. return array();
  101. }
  102. $this->expireCookies();
  103. $matching_cookies = array();
  104. foreach ($this->_cookies as $key => $cookie) {
  105. if ($this->cookieMatchesTarget($cookie, $target)) {
  106. $matching_cookies[$cookie['name']] = $cookie['value'];
  107. }
  108. }
  109. return $matching_cookies;
  110. }
  111. /**
  112. * Parse Cookies without PECL
  113. * From the comments in http://php.net/manual/en/function.http-parse-cookie.php
  114. *
  115. * @param array $header array of header lines.
  116. * @param string $defaultDomain The domain to use if none is specified in
  117. * the cookie.
  118. *
  119. * @return array of cookies
  120. */
  121. protected function parseCookieHeaders( $header, $defaultDomain )
  122. {
  123. phpCAS::traceBegin();
  124. $cookies = array();
  125. foreach ( $header as $line ) {
  126. if ( preg_match('/^Set-Cookie2?: /i', $line)) {
  127. $cookies[] = $this->parseCookieHeader($line, $defaultDomain);
  128. }
  129. }
  130. phpCAS::traceEnd($cookies);
  131. return $cookies;
  132. }
  133. /**
  134. * Parse a single cookie header line.
  135. *
  136. * Based on RFC2965 http://www.ietf.org/rfc/rfc2965.txt
  137. *
  138. * @param string $line The header line.
  139. * @param string $defaultDomain The domain to use if none is specified in
  140. * the cookie.
  141. *
  142. * @return array
  143. */
  144. protected function parseCookieHeader ($line, $defaultDomain)
  145. {
  146. if (!$defaultDomain) {
  147. throw new CAS_InvalidArgumentException(
  148. '$defaultDomain was not provided.'
  149. );
  150. }
  151. // Set our default values
  152. $cookie = array(
  153. 'domain' => $defaultDomain,
  154. 'path' => '/',
  155. 'secure' => false,
  156. );
  157. $line = preg_replace('/^Set-Cookie2?: /i', '', trim($line));
  158. // trim any trailing semicolons.
  159. $line = trim($line, ';');
  160. phpCAS::trace("Cookie Line: $line");
  161. // This implementation makes the assumption that semicolons will not
  162. // be present in quoted attribute values. While attribute values that
  163. // contain semicolons are allowed by RFC2965, they are hopefully rare
  164. // enough to ignore for our purposes. Most browsers make the same
  165. // assumption.
  166. $attributeStrings = explode(';', $line);
  167. foreach ( $attributeStrings as $attributeString ) {
  168. // split on the first equals sign and use the rest as value
  169. $attributeParts = explode('=', $attributeString, 2);
  170. $attributeName = trim($attributeParts[0]);
  171. $attributeNameLC = strtolower($attributeName);
  172. if (isset($attributeParts[1])) {
  173. $attributeValue = trim($attributeParts[1]);
  174. // Values may be quoted strings.
  175. if (strpos($attributeValue, '"') === 0) {
  176. $attributeValue = trim($attributeValue, '"');
  177. // unescape any escaped quotes:
  178. $attributeValue = str_replace('\"', '"', $attributeValue);
  179. }
  180. } else {
  181. $attributeValue = null;
  182. }
  183. switch ($attributeNameLC) {
  184. case 'expires':
  185. $cookie['expires'] = strtotime($attributeValue);
  186. break;
  187. case 'max-age':
  188. $cookie['max-age'] = (int)$attributeValue;
  189. // Set an expiry time based on the max-age
  190. if ($cookie['max-age']) {
  191. $cookie['expires'] = time() + $cookie['max-age'];
  192. } else {
  193. // If max-age is zero, then the cookie should be removed
  194. // imediately so set an expiry before now.
  195. $cookie['expires'] = time() - 1;
  196. }
  197. break;
  198. case 'secure':
  199. $cookie['secure'] = true;
  200. break;
  201. case 'domain':
  202. case 'path':
  203. case 'port':
  204. case 'version':
  205. case 'comment':
  206. case 'commenturl':
  207. case 'discard':
  208. case 'httponly':
  209. $cookie[$attributeNameLC] = $attributeValue;
  210. break;
  211. default:
  212. $cookie['name'] = $attributeName;
  213. $cookie['value'] = $attributeValue;
  214. }
  215. }
  216. return $cookie;
  217. }
  218. /**
  219. * Add, update, or remove a cookie.
  220. *
  221. * @param array $cookie A cookie array as created by parseCookieHeaders()
  222. *
  223. * @return void
  224. *
  225. * @access protected
  226. */
  227. protected function storeCookie ($cookie)
  228. {
  229. // Discard any old versions of this cookie.
  230. $this->discardCookie($cookie);
  231. $this->_cookies[] = $cookie;
  232. }
  233. /**
  234. * Discard an existing cookie
  235. *
  236. * @param array $cookie An cookie
  237. *
  238. * @return void
  239. *
  240. * @access protected
  241. */
  242. protected function discardCookie ($cookie)
  243. {
  244. if (!isset($cookie['domain'])
  245. || !isset($cookie['path'])
  246. || !isset($cookie['path'])
  247. ) {
  248. throw new CAS_InvalidArgumentException('Invalid Cookie array passed.');
  249. }
  250. foreach ($this->_cookies as $key => $old_cookie) {
  251. if ( $cookie['domain'] == $old_cookie['domain']
  252. && $cookie['path'] == $old_cookie['path']
  253. && $cookie['name'] == $old_cookie['name']
  254. ) {
  255. unset($this->_cookies[$key]);
  256. }
  257. }
  258. }
  259. /**
  260. * Go through our stored cookies and remove any that are expired.
  261. *
  262. * @return void
  263. *
  264. * @access protected
  265. */
  266. protected function expireCookies ()
  267. {
  268. foreach ($this->_cookies as $key => $cookie) {
  269. if (isset($cookie['expires']) && $cookie['expires'] < time()) {
  270. unset($this->_cookies[$key]);
  271. }
  272. }
  273. }
  274. /**
  275. * Answer true if cookie is applicable to a target.
  276. *
  277. * @param array $cookie An array of cookie attributes.
  278. * @param array $target An array of URL attributes as generated by parse_url().
  279. *
  280. * @return bool
  281. *
  282. * @access private
  283. */
  284. protected function cookieMatchesTarget ($cookie, $target)
  285. {
  286. if (!is_array($target)) {
  287. throw new CAS_InvalidArgumentException(
  288. '$target must be an array of URL attributes as generated by parse_url().'
  289. );
  290. }
  291. if (!isset($target['host'])) {
  292. throw new CAS_InvalidArgumentException(
  293. '$target must be an array of URL attributes as generated by parse_url().'
  294. );
  295. }
  296. // Verify that the scheme matches
  297. if ($cookie['secure'] && $target['scheme'] != 'https') {
  298. return false;
  299. }
  300. // Verify that the host matches
  301. // Match domain and mulit-host cookies
  302. if (strpos($cookie['domain'], '.') === 0) {
  303. // .host.domain.edu cookies are valid for host.domain.edu
  304. if (substr($cookie['domain'], 1) == $target['host']) {
  305. // continue with other checks
  306. } else {
  307. // non-exact host-name matches.
  308. // check that the target host a.b.c.edu is within .b.c.edu
  309. $pos = strripos($target['host'], $cookie['domain']);
  310. if (!$pos) {
  311. return false;
  312. }
  313. // verify that the cookie domain is the last part of the host.
  314. if ($pos + strlen($cookie['domain']) != strlen($target['host'])) {
  315. return false;
  316. }
  317. // verify that the host name does not contain interior dots as per
  318. // RFC 2965 section 3.3.2 Rejecting Cookies
  319. // http://www.ietf.org/rfc/rfc2965.txt
  320. $hostname = substr($target['host'], 0, $pos);
  321. if (strpos($hostname, '.') !== false) {
  322. return false;
  323. }
  324. }
  325. } else {
  326. // If the cookie host doesn't begin with '.',
  327. // the host must case-insensitive match exactly
  328. if (strcasecmp($target['host'], $cookie['domain']) !== 0) {
  329. return false;
  330. }
  331. }
  332. // Verify that the port matches
  333. if (isset($cookie['ports'])
  334. && !in_array($target['port'], $cookie['ports'])
  335. ) {
  336. return false;
  337. }
  338. // Verify that the path matches
  339. if (strpos($target['path'], $cookie['path']) !== 0) {
  340. return false;
  341. }
  342. return true;
  343. }
  344. }
  345. ?>