* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class provides access to service cookies and handles parsing of response * headers to pull out cookie values. * * @class CAS_CookieJar * @category Authentication * @package PhpCAS * @author Adam Franco * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_CookieJar { private $_cookies; /** * Create a new cookie jar by passing it a reference to an array in which it * should store cookies. * * @param array &$storageArray Array to store cookies * * @return void */ public function __construct (array &$storageArray) { $this->_cookies =& $storageArray; } /** * Store cookies for a web service request. * Cookie storage is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt * * @param string $request_url The URL that generated the response headers. * @param array $response_headers An array of the HTTP response header strings. * * @return void * * @access private */ public function storeCookies ($request_url, $response_headers) { $urlParts = parse_url($request_url); $defaultDomain = $urlParts['host']; $cookies = $this->parseCookieHeaders($response_headers, $defaultDomain); // var_dump($cookies); foreach ($cookies as $cookie) { // Enforce the same-origin policy by verifying that the cookie // would match the url that is setting it if (!$this->cookieMatchesTarget($cookie, $urlParts)) { continue; } // store the cookie $this->storeCookie($cookie); phpCAS::trace($cookie['name'].' -> '.$cookie['value']); } } /** * Retrieve cookies applicable for a web service request. * Cookie applicability is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt * * @param string $request_url The url that the cookies will be for. * * @return array An array containing cookies. E.g. array('name' => 'val'); * * @access private */ public function getCookies ($request_url) { if (!count($this->_cookies)) { return array(); } // If our request URL can't be parsed, no cookies apply. $target = parse_url($request_url); if ($target === false) { return array(); } $this->expireCookies(); $matching_cookies = array(); foreach ($this->_cookies as $key => $cookie) { if ($this->cookieMatchesTarget($cookie, $target)) { $matching_cookies[$cookie['name']] = $cookie['value']; } } return $matching_cookies; } /** * Parse Cookies without PECL * From the comments in http://php.net/manual/en/function.http-parse-cookie.php * * @param array $header array of header lines. * @param string $defaultDomain The domain to use if none is specified in * the cookie. * * @return array of cookies */ protected function parseCookieHeaders( $header, $defaultDomain ) { phpCAS::traceBegin(); $cookies = array(); foreach ( $header as $line ) { if ( preg_match('/^Set-Cookie2?: /i', $line)) { $cookies[] = $this->parseCookieHeader($line, $defaultDomain); } } phpCAS::traceEnd($cookies); return $cookies; } /** * Parse a single cookie header line. * * Based on RFC2965 http://www.ietf.org/rfc/rfc2965.txt * * @param string $line The header line. * @param string $defaultDomain The domain to use if none is specified in * the cookie. * * @return array */ protected function parseCookieHeader ($line, $defaultDomain) { if (!$defaultDomain) { throw new CAS_InvalidArgumentException( '$defaultDomain was not provided.' ); } // Set our default values $cookie = array( 'domain' => $defaultDomain, 'path' => '/', 'secure' => false, ); $line = preg_replace('/^Set-Cookie2?: /i', '', trim($line)); // trim any trailing semicolons. $line = trim($line, ';'); phpCAS::trace("Cookie Line: $line"); // This implementation makes the assumption that semicolons will not // be present in quoted attribute values. While attribute values that // contain semicolons are allowed by RFC2965, they are hopefully rare // enough to ignore for our purposes. Most browsers make the same // assumption. $attributeStrings = explode(';', $line); foreach ( $attributeStrings as $attributeString ) { // split on the first equals sign and use the rest as value $attributeParts = explode('=', $attributeString, 2); $attributeName = trim($attributeParts[0]); $attributeNameLC = strtolower($attributeName); if (isset($attributeParts[1])) { $attributeValue = trim($attributeParts[1]); // Values may be quoted strings. if (strpos($attributeValue, '"') === 0) { $attributeValue = trim($attributeValue, '"'); // unescape any escaped quotes: $attributeValue = str_replace('\"', '"', $attributeValue); } } else { $attributeValue = null; } switch ($attributeNameLC) { case 'expires': $cookie['expires'] = strtotime($attributeValue); break; case 'max-age': $cookie['max-age'] = (int)$attributeValue; // Set an expiry time based on the max-age if ($cookie['max-age']) { $cookie['expires'] = time() + $cookie['max-age']; } else { // If max-age is zero, then the cookie should be removed // imediately so set an expiry before now. $cookie['expires'] = time() - 1; } break; case 'secure': $cookie['secure'] = true; break; case 'domain': case 'path': case 'port': case 'version': case 'comment': case 'commenturl': case 'discard': case 'httponly': $cookie[$attributeNameLC] = $attributeValue; break; default: $cookie['name'] = $attributeName; $cookie['value'] = $attributeValue; } } return $cookie; } /** * Add, update, or remove a cookie. * * @param array $cookie A cookie array as created by parseCookieHeaders() * * @return void * * @access protected */ protected function storeCookie ($cookie) { // Discard any old versions of this cookie. $this->discardCookie($cookie); $this->_cookies[] = $cookie; } /** * Discard an existing cookie * * @param array $cookie An cookie * * @return void * * @access protected */ protected function discardCookie ($cookie) { if (!isset($cookie['domain']) || !isset($cookie['path']) || !isset($cookie['path']) ) { throw new CAS_InvalidArgumentException('Invalid Cookie array passed.'); } foreach ($this->_cookies as $key => $old_cookie) { if ( $cookie['domain'] == $old_cookie['domain'] && $cookie['path'] == $old_cookie['path'] && $cookie['name'] == $old_cookie['name'] ) { unset($this->_cookies[$key]); } } } /** * Go through our stored cookies and remove any that are expired. * * @return void * * @access protected */ protected function expireCookies () { foreach ($this->_cookies as $key => $cookie) { if (isset($cookie['expires']) && $cookie['expires'] < time()) { unset($this->_cookies[$key]); } } } /** * Answer true if cookie is applicable to a target. * * @param array $cookie An array of cookie attributes. * @param array $target An array of URL attributes as generated by parse_url(). * * @return bool * * @access private */ protected function cookieMatchesTarget ($cookie, $target) { if (!is_array($target)) { throw new CAS_InvalidArgumentException( '$target must be an array of URL attributes as generated by parse_url().' ); } if (!isset($target['host'])) { throw new CAS_InvalidArgumentException( '$target must be an array of URL attributes as generated by parse_url().' ); } // Verify that the scheme matches if ($cookie['secure'] && $target['scheme'] != 'https') { return false; } // Verify that the host matches // Match domain and mulit-host cookies if (strpos($cookie['domain'], '.') === 0) { // .host.domain.edu cookies are valid for host.domain.edu if (substr($cookie['domain'], 1) == $target['host']) { // continue with other checks } else { // non-exact host-name matches. // check that the target host a.b.c.edu is within .b.c.edu $pos = strripos($target['host'], $cookie['domain']); if (!$pos) { return false; } // verify that the cookie domain is the last part of the host. if ($pos + strlen($cookie['domain']) != strlen($target['host'])) { return false; } // verify that the host name does not contain interior dots as per // RFC 2965 section 3.3.2 Rejecting Cookies // http://www.ietf.org/rfc/rfc2965.txt $hostname = substr($target['host'], 0, $pos); if (strpos($hostname, '.') !== false) { return false; } } } else { // If the cookie host doesn't begin with '.', // the host must case-insensitive match exactly if (strcasecmp($target['host'], $cookie['domain']) !== 0) { return false; } } // Verify that the port matches if (isset($cookie['ports']) && !in_array($target['port'], $cookie['ports']) ) { return false; } // Verify that the path matches if (strpos($target['path'], $cookie['path']) !== 0) { return false; } return true; } } ?>