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