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