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