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/ProxiedService/Http/Abstract.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 implements common methods for ProxiedService implementations included
32 * with phpCAS.
33 *
34 * @class    CAS_ProxiedService_Http_Abstract
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 */
41abstract class CAS_ProxiedService_Http_Abstract extends
42CAS_ProxiedService_Abstract implements CAS_ProxiedService_Http
43{
44    /**
45     * The HTTP request mechanism talking to the target service.
46     *
47     * @var CAS_Request_RequestInterface $requestHandler
48     */
49    protected $requestHandler;
50
51    /**
52     * The storage mechanism for cookies set by the target service.
53     *
54     * @var CAS_CookieJar $_cookieJar
55     */
56    private $_cookieJar;
57
58    /**
59     * Constructor.
60     *
61     * @param CAS_Request_RequestInterface $requestHandler request handler object
62     * @param CAS_CookieJar                $cookieJar      cookieJar object
63     *
64     * @return void
65     */
66    public function __construct(CAS_Request_RequestInterface $requestHandler,
67        CAS_CookieJar $cookieJar
68    ) {
69        $this->requestHandler = $requestHandler;
70        $this->_cookieJar = $cookieJar;
71    }
72
73    /**
74     * The target service url.
75     * @var string $_url;
76     */
77    private $_url;
78
79    /**
80     * Answer a service identifier (URL) for whom we should fetch a proxy ticket.
81     *
82     * @return string
83     * @throws Exception If no service url is available.
84     */
85    public function getServiceUrl()
86    {
87        if (empty($this->_url)) {
88            throw new CAS_ProxiedService_Exception(
89                'No URL set via ' . get_class($this) . '->setUrl($url).'
90            );
91        }
92
93        return $this->_url;
94    }
95
96    /*********************************************************
97     * Configure the Request
98     *********************************************************/
99
100    /**
101     * Set the URL of the Request
102     *
103     * @param string $url url to set
104     *
105     * @return void
106     * @throws CAS_OutOfSequenceException If called after the Request has been sent.
107     */
108    public function setUrl($url)
109    {
110        if ($this->hasBeenSent()) {
111            throw new CAS_OutOfSequenceException(
112                'Cannot set the URL, request already sent.'
113            );
114        }
115        if (!is_string($url)) {
116            throw new CAS_InvalidArgumentException('$url must be a string.');
117        }
118
119        $this->_url = $url;
120    }
121
122    /*********************************************************
123     * 2. Send the Request
124     *********************************************************/
125
126    /**
127     * Perform the request.
128     *
129     * @return void
130     * @throws CAS_OutOfSequenceException If called multiple times.
131     * @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
132     *		The code of the Exception will be one of:
133     *			PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
134     *			PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
135     *			PHPCAS_SERVICE_PT_FAILURE
136     * @throws CAS_ProxiedService_Exception If there is a failure sending the
137     * request to the target service.
138     */
139    public function send()
140    {
141        if ($this->hasBeenSent()) {
142            throw new CAS_OutOfSequenceException(
143                'Cannot send, request already sent.'
144            );
145        }
146
147        phpCAS::traceBegin();
148
149        // Get our proxy ticket and append it to our URL.
150        $this->initializeProxyTicket();
151        $url = $this->getServiceUrl();
152        if (strstr($url, '?') === false) {
153            $url = $url . '?ticket=' . $this->getProxyTicket();
154        } else {
155            $url = $url . '&ticket=' . $this->getProxyTicket();
156        }
157
158        try {
159            $this->makeRequest($url);
160        } catch (Exception $e) {
161            phpCAS::traceEnd();
162            throw $e;
163        }
164    }
165
166    /**
167     * Indicator of the number of requests (including redirects performed.
168     *
169     * @var int $_numRequests;
170     */
171    private $_numRequests = 0;
172
173    /**
174     * The response headers.
175     *
176     * @var array $_responseHeaders;
177     */
178    private $_responseHeaders = array();
179
180    /**
181     * The response status code.
182     *
183     * @var int $_responseStatusCode;
184     */
185    private $_responseStatusCode = '';
186
187    /**
188     * The response headers.
189     *
190     * @var string $_responseBody;
191     */
192    private $_responseBody = '';
193
194    /**
195     * Build and perform a request, following redirects
196     *
197     * @param string $url url for the request
198     *
199     * @return void
200     * @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
201     *		The code of the Exception will be one of:
202     *			PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
203     *			PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
204     *			PHPCAS_SERVICE_PT_FAILURE
205     * @throws CAS_ProxiedService_Exception If there is a failure sending the
206     * request to the target service.
207     */
208    protected function makeRequest($url)
209    {
210        // Verify that we are not in a redirect loop
211        $this->_numRequests++;
212        if ($this->_numRequests > 4) {
213            $message = 'Exceeded the maximum number of redirects (3) in proxied service request.';
214            phpCAS::trace($message);
215            throw new CAS_ProxiedService_Exception($message);
216        }
217
218        // Create a new request.
219        $request = clone $this->requestHandler;
220        $request->setUrl($url);
221
222        // Add any cookies to the request.
223        $request->addCookies($this->_cookieJar->getCookies($url));
224
225        // Add any other parts of the request needed by concrete classes
226        $this->populateRequest($request);
227
228        // Perform the request.
229        phpCAS::trace('Performing proxied service request to \'' . $url . '\'');
230        if (!$request->send()) {
231            $message = 'Could not perform proxied service request to URL`'
232            . $url . '\'. ' . $request->getErrorMessage();
233            phpCAS::trace($message);
234            throw new CAS_ProxiedService_Exception($message);
235        }
236
237        // Store any cookies from the response;
238        $this->_cookieJar->storeCookies($url, $request->getResponseHeaders());
239
240        // Follow any redirects
241        if ($redirectUrl = $this->getRedirectUrl($request->getResponseHeaders())
242        ) {
243            phpCAS::trace('Found redirect:' . $redirectUrl);
244            $this->makeRequest($redirectUrl);
245        } else {
246
247            $this->_responseHeaders = $request->getResponseHeaders();
248            $this->_responseBody = $request->getResponseBody();
249            $this->_responseStatusCode = $request->getResponseStatusCode();
250        }
251    }
252
253    /**
254     * Add any other parts of the request needed by concrete classes
255     *
256     * @param CAS_Request_RequestInterface $request request interface object
257     *
258     * @return void
259     */
260    abstract protected function populateRequest(
261        CAS_Request_RequestInterface $request
262    );
263
264    /**
265     * Answer a redirect URL if a redirect header is found, otherwise null.
266     *
267     * @param array $responseHeaders response header to extract a redirect from
268     *
269     * @return string|null
270     */
271    protected function getRedirectUrl(array $responseHeaders)
272    {
273        // Check for the redirect after authentication
274        foreach ($responseHeaders as $header) {
275            if ( preg_match('/^(Location:|URI:)\s*([^\s]+.*)$/', $header, $matches)
276            ) {
277                return trim(array_pop($matches));
278            }
279        }
280        return null;
281    }
282
283    /*********************************************************
284     * 3. Access the response
285     *********************************************************/
286
287    /**
288     * Answer true if our request has been sent yet.
289     *
290     * @return bool
291     */
292    protected function hasBeenSent()
293    {
294        return ($this->_numRequests > 0);
295    }
296
297    /**
298     * Answer the headers of the response.
299     *
300     * @return array An array of header strings.
301     * @throws CAS_OutOfSequenceException If called before the Request has been sent.
302     */
303    public function getResponseHeaders()
304    {
305        if (!$this->hasBeenSent()) {
306            throw new CAS_OutOfSequenceException(
307                'Cannot access response, request not sent yet.'
308            );
309        }
310
311        return $this->_responseHeaders;
312    }
313
314    /**
315     * Answer HTTP status code of the response
316     *
317     * @return int
318     * @throws CAS_OutOfSequenceException If called before the Request has been sent.
319     */
320    public function getResponseStatusCode()
321    {
322        if (!$this->hasBeenSent()) {
323            throw new CAS_OutOfSequenceException(
324                'Cannot access response, request not sent yet.'
325            );
326        }
327
328        return $this->_responseStatusCode;
329    }
330
331    /**
332     * Answer the body of response.
333     *
334     * @return string
335     * @throws CAS_OutOfSequenceException If called before the Request has been sent.
336     */
337    public function getResponseBody()
338    {
339        if (!$this->hasBeenSent()) {
340            throw new CAS_OutOfSequenceException(
341                'Cannot access response, request not sent yet.'
342            );
343        }
344
345        return $this->_responseBody;
346    }
347
348    /**
349     * Answer the cookies from the response. This may include cookies set during
350     * redirect responses.
351     *
352     * @return array An array containing cookies. E.g. array('name' => 'val');
353     */
354    public function getCookies()
355    {
356        return $this->_cookieJar->getCookies($this->getServiceUrl());
357    }
358
359}
360?>
361