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 7 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 case 'samesite': 235 $cookie[$attributeNameLC] = $attributeValue; 236 break; 237 default: 238 $cookie['name'] = $attributeName; 239 $cookie['value'] = $attributeValue; 240 } 241 } 242 243 return $cookie; 244 } 245 246 /** 247 * Add, update, or remove a cookie. 248 * 249 * @param array $cookie A cookie array as created by parseCookieHeaders() 250 * 251 * @return void 252 * 253 * @access protected 254 */ 255 protected function storeCookie ($cookie) 256 { 257 // Discard any old versions of this cookie. 258 $this->discardCookie($cookie); 259 $this->_cookies[] = $cookie; 260 261 } 262 263 /** 264 * Discard an existing cookie 265 * 266 * @param array $cookie An cookie 267 * 268 * @return void 269 * 270 * @access protected 271 */ 272 protected function discardCookie ($cookie) 273 { 274 if (!isset($cookie['domain']) 275 || !isset($cookie['path']) 276 || !isset($cookie['path']) 277 ) { 278 throw new CAS_InvalidArgumentException('Invalid Cookie array passed.'); 279 } 280 281 foreach ($this->_cookies as $key => $old_cookie) { 282 if ( $cookie['domain'] == $old_cookie['domain'] 283 && $cookie['path'] == $old_cookie['path'] 284 && $cookie['name'] == $old_cookie['name'] 285 ) { 286 unset($this->_cookies[$key]); 287 } 288 } 289 } 290 291 /** 292 * Go through our stored cookies and remove any that are expired. 293 * 294 * @return void 295 * 296 * @access protected 297 */ 298 protected function expireCookies () 299 { 300 foreach ($this->_cookies as $key => $cookie) { 301 if (isset($cookie['expires']) && $cookie['expires'] < time()) { 302 unset($this->_cookies[$key]); 303 } 304 } 305 } 306 307 /** 308 * Answer true if cookie is applicable to a target. 309 * 310 * @param array $cookie An array of cookie attributes. 311 * @param array|false $target An array of URL attributes as generated by parse_url(). 312 * 313 * @return bool 314 * 315 * @access private 316 */ 317 protected function cookieMatchesTarget ($cookie, $target) 318 { 319 if (!is_array($target)) { 320 throw new CAS_InvalidArgumentException( 321 '$target must be an array of URL attributes as generated by parse_url().' 322 ); 323 } 324 if (!isset($target['host'])) { 325 throw new CAS_InvalidArgumentException( 326 '$target must be an array of URL attributes as generated by parse_url().' 327 ); 328 } 329 330 // Verify that the scheme matches 331 if ($cookie['secure'] && $target['scheme'] != 'https') { 332 return false; 333 } 334 335 // Verify that the host matches 336 // Match domain and mulit-host cookies 337 if (strpos($cookie['domain'], '.') === 0) { 338 // .host.domain.edu cookies are valid for host.domain.edu 339 if (substr($cookie['domain'], 1) == $target['host']) { 340 // continue with other checks 341 } else { 342 // non-exact host-name matches. 343 // check that the target host a.b.c.edu is within .b.c.edu 344 $pos = strripos($target['host'], $cookie['domain']); 345 if (!$pos) { 346 return false; 347 } 348 // verify that the cookie domain is the last part of the host. 349 if ($pos + strlen($cookie['domain']) != strlen($target['host'])) { 350 return false; 351 } 352 // verify that the host name does not contain interior dots as per 353 // RFC 2965 section 3.3.2 Rejecting Cookies 354 // http://www.ietf.org/rfc/rfc2965.txt 355 $hostname = substr($target['host'], 0, $pos); 356 if (strpos($hostname, '.') !== false) { 357 return false; 358 } 359 } 360 } else { 361 // If the cookie host doesn't begin with '.', 362 // the host must case-insensitive match exactly 363 if (strcasecmp($target['host'], $cookie['domain']) !== 0) { 364 return false; 365 } 366 } 367 368 // Verify that the port matches 369 if (isset($cookie['ports']) 370 && !in_array($target['port'], $cookie['ports']) 371 ) { 372 return false; 373 } 374 375 // Verify that the path matches 376 if (strpos($target['path'], $cookie['path']) !== 0) { 377 return false; 378 } 379 380 return true; 381 } 382 383} 384 385?> 386