xref: /plugin/authssocas/vendor/apereo/phpcas/source/CAS/CookieJar.php (revision d10b5556242e78d8a430c323b91984ec16415a46)
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