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 5
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: */
41: abstract class CAS_ProxiedService_Http_Abstract extends
42: CAS_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 string $_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 or 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: