forked from GNUsocial/gnu-social
		
	
		
			
	
	
		
			385 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
		
		
			
		
	
	
			385 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
|   | <?php | ||
|  | 
 | ||
|  | /** | ||
|  |  * Licensed to Jasig under one or more contributor license | ||
|  |  * agreements. See the NOTICE file distributed with this work for | ||
|  |  * additional information regarding copyright ownership. | ||
|  |  * | ||
|  |  * Jasig licenses this file to you under the Apache License, | ||
|  |  * Version 2.0 (the "License"); you may not use this file except in | ||
|  |  * compliance with the License. You may obtain a copy of the License at: | ||
|  |  * | ||
|  |  * http://www.apache.org/licenses/LICENSE-2.0 | ||
|  |  * | ||
|  |  * Unless required by applicable law or agreed to in writing, software | ||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
|  |  * See the License for the specific language governing permissions and | ||
|  |  * limitations under the License. | ||
|  |  * | ||
|  |  * PHP Version 5 | ||
|  |  * | ||
|  |  * @file     CAS/CookieJar.php | ||
|  |  * @category Authentication | ||
|  |  * @package  PhpCAS | ||
|  |  * @author   Adam Franco <afranco@middlebury.edu> | ||
|  |  * @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 <afranco@middlebury.edu> | ||
|  |  * @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); | ||
|  | 
 | ||
|  |         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|false $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; | ||
|  |     } | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | ?>
 |