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/Client.php
23: * @category Authentication
24: * @package PhpCAS
25: * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr>
26: * @author Olivier Berger <olivier.berger@it-sudparis.eu>
27: * @author Brett Bieber <brett.bieber@gmail.com>
28: * @author Joachim Fritschi <jfritschi@freenet.de>
29: * @author Adam Franco <afranco@middlebury.edu>
30: * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
31: * @link https://wiki.jasig.org/display/CASC/phpCAS
32: */
33:
34: /**
35: * The CAS_Client class is a client interface that provides CAS authentication
36: * to PHP applications.
37: *
38: * @class CAS_Client
39: * @category Authentication
40: * @package PhpCAS
41: * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr>
42: * @author Olivier Berger <olivier.berger@it-sudparis.eu>
43: * @author Brett Bieber <brett.bieber@gmail.com>
44: * @author Joachim Fritschi <jfritschi@freenet.de>
45: * @author Adam Franco <afranco@middlebury.edu>
46: * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
47: * @link https://wiki.jasig.org/display/CASC/phpCAS
48: *
49: */
50:
51: class CAS_Client
52: {
53:
54: // ########################################################################
55: // HTML OUTPUT
56: // ########################################################################
57: /**
58: * @addtogroup internalOutput
59: * @{
60: */
61:
62: /**
63: * This method filters a string by replacing special tokens by appropriate values
64: * and prints it. The corresponding tokens are taken into account:
65: * - __CAS_VERSION__
66: * - __PHPCAS_VERSION__
67: * - __SERVER_BASE_URL__
68: *
69: * Used by CAS_Client::PrintHTMLHeader() and CAS_Client::printHTMLFooter().
70: *
71: * @param string $str the string to filter and output
72: *
73: * @return void
74: */
75: private function _htmlFilterOutput($str)
76: {
77: $str = str_replace('__CAS_VERSION__', $this->getServerVersion(), $str);
78: $str = str_replace('__PHPCAS_VERSION__', phpCAS::getVersion(), $str);
79: $str = str_replace('__SERVER_BASE_URL__', $this->_getServerBaseURL(), $str);
80: echo $str;
81: }
82:
83: /**
84: * A string used to print the header of HTML pages. Written by
85: * CAS_Client::setHTMLHeader(), read by CAS_Client::printHTMLHeader().
86: *
87: * @hideinitializer
88: * @see CAS_Client::setHTMLHeader, CAS_Client::printHTMLHeader()
89: */
90: private $_output_header = '';
91:
92: /**
93: * This method prints the header of the HTML output (after filtering). If
94: * CAS_Client::setHTMLHeader() was not used, a default header is output.
95: *
96: * @param string $title the title of the page
97: *
98: * @return void
99: * @see _htmlFilterOutput()
100: */
101: public function printHTMLHeader($title)
102: {
103: $this->_htmlFilterOutput(
104: str_replace(
105: '__TITLE__', $title,
106: (empty($this->_output_header)
107: ? '<html><head><title>__TITLE__</title></head><body><h1>__TITLE__</h1>'
108: : $this->_output_header)
109: )
110: );
111: }
112:
113: /**
114: * A string used to print the footer of HTML pages. Written by
115: * CAS_Client::setHTMLFooter(), read by printHTMLFooter().
116: *
117: * @hideinitializer
118: * @see CAS_Client::setHTMLFooter, CAS_Client::printHTMLFooter()
119: */
120: private $_output_footer = '';
121:
122: /**
123: * This method prints the footer of the HTML output (after filtering). If
124: * CAS_Client::setHTMLFooter() was not used, a default footer is output.
125: *
126: * @return void
127: * @see _htmlFilterOutput()
128: */
129: public function printHTMLFooter()
130: {
131: $lang = $this->getLangObj();
132: $this->_htmlFilterOutput(
133: empty($this->_output_footer)?
134: (phpcas::getVerbose())?
135: '<hr><address>phpCAS __PHPCAS_VERSION__ '
136: .$lang->getUsingServer()
137: .' <a href="__SERVER_BASE_URL__">__SERVER_BASE_URL__</a> (CAS __CAS_VERSION__)</a></address></body></html>'
138: :'</body></html>'
139: :$this->_output_footer
140: );
141: }
142:
143: /**
144: * This method set the HTML header used for all outputs.
145: *
146: * @param string $header the HTML header.
147: *
148: * @return void
149: */
150: public function setHTMLHeader($header)
151: {
152: // Argument Validation
153: if (gettype($header) != 'string')
154: throw new CAS_TypeMismatchException($header, '$header', 'string');
155:
156: $this->_output_header = $header;
157: }
158:
159: /**
160: * This method set the HTML footer used for all outputs.
161: *
162: * @param string $footer the HTML footer.
163: *
164: * @return void
165: */
166: public function setHTMLFooter($footer)
167: {
168: // Argument Validation
169: if (gettype($footer) != 'string')
170: throw new CAS_TypeMismatchException($footer, '$footer', 'string');
171:
172: $this->_output_footer = $footer;
173: }
174:
175:
176: /** @} */
177:
178:
179: // ########################################################################
180: // INTERNATIONALIZATION
181: // ########################################################################
182: /**
183: * @addtogroup internalLang
184: * @{
185: */
186: /**
187: * A string corresponding to the language used by phpCAS. Written by
188: * CAS_Client::setLang(), read by CAS_Client::getLang().
189:
190: * @note debugging information is always in english (debug purposes only).
191: */
192: private $_lang = PHPCAS_LANG_DEFAULT;
193:
194: /**
195: * This method is used to set the language used by phpCAS.
196: *
197: * @param string $lang representing the language.
198: *
199: * @return void
200: */
201: public function setLang($lang)
202: {
203: // Argument Validation
204: if (gettype($lang) != 'string')
205: throw new CAS_TypeMismatchException($lang, '$lang', 'string');
206:
207: phpCAS::traceBegin();
208: $obj = new $lang();
209: if (!($obj instanceof CAS_Languages_LanguageInterface)) {
210: throw new CAS_InvalidArgumentException(
211: '$className must implement the CAS_Languages_LanguageInterface'
212: );
213: }
214: $this->_lang = $lang;
215: phpCAS::traceEnd();
216: }
217: /**
218: * Create the language
219: *
220: * @return CAS_Languages_LanguageInterface object implementing the class
221: */
222: public function getLangObj()
223: {
224: $classname = $this->_lang;
225: return new $classname();
226: }
227:
228: /** @} */
229: // ########################################################################
230: // CAS SERVER CONFIG
231: // ########################################################################
232: /**
233: * @addtogroup internalConfig
234: * @{
235: */
236:
237: /**
238: * a record to store information about the CAS server.
239: * - $_server['version']: the version of the CAS server
240: * - $_server['hostname']: the hostname of the CAS server
241: * - $_server['port']: the port the CAS server is running on
242: * - $_server['uri']: the base URI the CAS server is responding on
243: * - $_server['base_url']: the base URL of the CAS server
244: * - $_server['login_url']: the login URL of the CAS server
245: * - $_server['service_validate_url']: the service validating URL of the
246: * CAS server
247: * - $_server['proxy_url']: the proxy URL of the CAS server
248: * - $_server['proxy_validate_url']: the proxy validating URL of the CAS server
249: * - $_server['logout_url']: the logout URL of the CAS server
250: *
251: * $_server['version'], $_server['hostname'], $_server['port'] and
252: * $_server['uri'] are written by CAS_Client::CAS_Client(), read by
253: * CAS_Client::getServerVersion(), CAS_Client::_getServerHostname(),
254: * CAS_Client::_getServerPort() and CAS_Client::_getServerURI().
255: *
256: * The other fields are written and read by CAS_Client::_getServerBaseURL(),
257: * CAS_Client::getServerLoginURL(), CAS_Client::getServerServiceValidateURL(),
258: * CAS_Client::getServerProxyValidateURL() and CAS_Client::getServerLogoutURL().
259: *
260: * @hideinitializer
261: */
262: private $_server = array(
263: 'version' => -1,
264: 'hostname' => 'none',
265: 'port' => -1,
266: 'uri' => 'none');
267:
268: /**
269: * This method is used to retrieve the version of the CAS server.
270: *
271: * @return string the version of the CAS server.
272: */
273: public function getServerVersion()
274: {
275: return $this->_server['version'];
276: }
277:
278: /**
279: * This method is used to retrieve the hostname of the CAS server.
280: *
281: * @return string the hostname of the CAS server.
282: */
283: private function _getServerHostname()
284: {
285: return $this->_server['hostname'];
286: }
287:
288: /**
289: * This method is used to retrieve the port of the CAS server.
290: *
291: * @return string the port of the CAS server.
292: */
293: private function _getServerPort()
294: {
295: return $this->_server['port'];
296: }
297:
298: /**
299: * This method is used to retrieve the URI of the CAS server.
300: *
301: * @return string a URI.
302: */
303: private function _getServerURI()
304: {
305: return $this->_server['uri'];
306: }
307:
308: /**
309: * This method is used to retrieve the base URL of the CAS server.
310: *
311: * @return string a URL.
312: */
313: private function _getServerBaseURL()
314: {
315: // the URL is build only when needed
316: if ( empty($this->_server['base_url']) ) {
317: $this->_server['base_url'] = 'https://' . $this->_getServerHostname();
318: if ($this->_getServerPort()!=443) {
319: $this->_server['base_url'] .= ':'
320: .$this->_getServerPort();
321: }
322: $this->_server['base_url'] .= $this->_getServerURI();
323: }
324: return $this->_server['base_url'];
325: }
326:
327: /**
328: * This method is used to retrieve the login URL of the CAS server.
329: *
330: * @param bool $gateway true to check authentication, false to force it
331: * @param bool $renew true to force the authentication with the CAS server
332: *
333: * @return a URL.
334: * @note It is recommended that CAS implementations ignore the "gateway"
335: * parameter if "renew" is set
336: */
337: public function getServerLoginURL($gateway=false,$renew=false)
338: {
339: phpCAS::traceBegin();
340: // the URL is build only when needed
341: if ( empty($this->_server['login_url']) ) {
342: $this->_server['login_url'] = $this->_buildQueryUrl($this->_getServerBaseURL().'login','service='.urlencode($this->getURL()));
343: }
344: $url = $this->_server['login_url'];
345: if ($renew) {
346: // It is recommended that when the "renew" parameter is set, its
347: // value be "true"
348: $url = $this->_buildQueryUrl($url, 'renew=true');
349: } elseif ($gateway) {
350: // It is recommended that when the "gateway" parameter is set, its
351: // value be "true"
352: $url = $this->_buildQueryUrl($url, 'gateway=true');
353: }
354: phpCAS::traceEnd($url);
355: return $url;
356: }
357:
358: /**
359: * This method sets the login URL of the CAS server.
360: *
361: * @param string $url the login URL
362: *
363: * @return string login url
364: */
365: public function setServerLoginURL($url)
366: {
367: // Argument Validation
368: if (gettype($url) != 'string')
369: throw new CAS_TypeMismatchException($url, '$url', 'string');
370:
371: return $this->_server['login_url'] = $url;
372: }
373:
374:
375: /**
376: * This method sets the serviceValidate URL of the CAS server.
377: *
378: * @param string $url the serviceValidate URL
379: *
380: * @return string serviceValidate URL
381: */
382: public function setServerServiceValidateURL($url)
383: {
384: // Argument Validation
385: if (gettype($url) != 'string')
386: throw new CAS_TypeMismatchException($url, '$url', 'string');
387:
388: return $this->_server['service_validate_url'] = $url;
389: }
390:
391:
392: /**
393: * This method sets the proxyValidate URL of the CAS server.
394: *
395: * @param string $url the proxyValidate URL
396: *
397: * @return string proxyValidate URL
398: */
399: public function setServerProxyValidateURL($url)
400: {
401: // Argument Validation
402: if (gettype($url) != 'string')
403: throw new CAS_TypeMismatchException($url, '$url', 'string');
404:
405: return $this->_server['proxy_validate_url'] = $url;
406: }
407:
408:
409: /**
410: * This method sets the samlValidate URL of the CAS server.
411: *
412: * @param string $url the samlValidate URL
413: *
414: * @return string samlValidate URL
415: */
416: public function setServerSamlValidateURL($url)
417: {
418: // Argument Validation
419: if (gettype($url) != 'string')
420: throw new CAS_TypeMismatchException($url, '$url', 'string');
421:
422: return $this->_server['saml_validate_url'] = $url;
423: }
424:
425:
426: /**
427: * This method is used to retrieve the service validating URL of the CAS server.
428: *
429: * @return string serviceValidate URL.
430: */
431: public function getServerServiceValidateURL()
432: {
433: phpCAS::traceBegin();
434: // the URL is build only when needed
435: if ( empty($this->_server['service_validate_url']) ) {
436: switch ($this->getServerVersion()) {
437: case CAS_VERSION_1_0:
438: $this->_server['service_validate_url'] = $this->_getServerBaseURL()
439: .'validate';
440: break;
441: case CAS_VERSION_2_0:
442: $this->_server['service_validate_url'] = $this->_getServerBaseURL()
443: .'serviceValidate';
444: break;
445: case CAS_VERSION_3_0:
446: $this->_server['service_validate_url'] = $this->_getServerBaseURL()
447: .'p3/serviceValidate';
448: break;
449: }
450: }
451: $url = $this->_buildQueryUrl(
452: $this->_server['service_validate_url'],
453: 'service='.urlencode($this->getURL())
454: );
455: phpCAS::traceEnd($url);
456: return $url;
457: }
458: /**
459: * This method is used to retrieve the SAML validating URL of the CAS server.
460: *
461: * @return string samlValidate URL.
462: */
463: public function getServerSamlValidateURL()
464: {
465: phpCAS::traceBegin();
466: // the URL is build only when needed
467: if ( empty($this->_server['saml_validate_url']) ) {
468: switch ($this->getServerVersion()) {
469: case SAML_VERSION_1_1:
470: $this->_server['saml_validate_url'] = $this->_getServerBaseURL().'samlValidate';
471: break;
472: }
473: }
474:
475: $url = $this->_buildQueryUrl(
476: $this->_server['saml_validate_url'],
477: 'TARGET='.urlencode($this->getURL())
478: );
479: phpCAS::traceEnd($url);
480: return $url;
481: }
482:
483: /**
484: * This method is used to retrieve the proxy validating URL of the CAS server.
485: *
486: * @return string proxyValidate URL.
487: */
488: public function getServerProxyValidateURL()
489: {
490: phpCAS::traceBegin();
491: // the URL is build only when needed
492: if ( empty($this->_server['proxy_validate_url']) ) {
493: switch ($this->getServerVersion()) {
494: case CAS_VERSION_1_0:
495: $this->_server['proxy_validate_url'] = '';
496: break;
497: case CAS_VERSION_2_0:
498: $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'proxyValidate';
499: break;
500: case CAS_VERSION_3_0:
501: $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'p3/proxyValidate';
502: break;
503: }
504: }
505: $url = $this->_buildQueryUrl(
506: $this->_server['proxy_validate_url'],
507: 'service='.urlencode($this->getURL())
508: );
509: phpCAS::traceEnd($url);
510: return $url;
511: }
512:
513:
514: /**
515: * This method is used to retrieve the proxy URL of the CAS server.
516: *
517: * @return string proxy URL.
518: */
519: public function getServerProxyURL()
520: {
521: // the URL is build only when needed
522: if ( empty($this->_server['proxy_url']) ) {
523: switch ($this->getServerVersion()) {
524: case CAS_VERSION_1_0:
525: $this->_server['proxy_url'] = '';
526: break;
527: case CAS_VERSION_2_0:
528: case CAS_VERSION_3_0:
529: $this->_server['proxy_url'] = $this->_getServerBaseURL().'proxy';
530: break;
531: }
532: }
533: return $this->_server['proxy_url'];
534: }
535:
536: /**
537: * This method is used to retrieve the logout URL of the CAS server.
538: *
539: * @return string logout URL.
540: */
541: public function getServerLogoutURL()
542: {
543: // the URL is build only when needed
544: if ( empty($this->_server['logout_url']) ) {
545: $this->_server['logout_url'] = $this->_getServerBaseURL().'logout';
546: }
547: return $this->_server['logout_url'];
548: }
549:
550: /**
551: * This method sets the logout URL of the CAS server.
552: *
553: * @param string $url the logout URL
554: *
555: * @return string logout url
556: */
557: public function setServerLogoutURL($url)
558: {
559: // Argument Validation
560: if (gettype($url) != 'string')
561: throw new CAS_TypeMismatchException($url, '$url', 'string');
562:
563: return $this->_server['logout_url'] = $url;
564: }
565:
566: /**
567: * An array to store extra curl options.
568: */
569: private $_curl_options = array();
570:
571: /**
572: * This method is used to set additional user curl options.
573: *
574: * @param string $key name of the curl option
575: * @param string $value value of the curl option
576: *
577: * @return void
578: */
579: public function setExtraCurlOption($key, $value)
580: {
581: $this->_curl_options[$key] = $value;
582: }
583:
584: /** @} */
585:
586: // ########################################################################
587: // Change the internal behaviour of phpcas
588: // ########################################################################
589:
590: /**
591: * @addtogroup internalBehave
592: * @{
593: */
594:
595: /**
596: * The class to instantiate for making web requests in readUrl().
597: * The class specified must implement the CAS_Request_RequestInterface.
598: * By default CAS_Request_CurlRequest is used, but this may be overridden to
599: * supply alternate request mechanisms for testing.
600: */
601: private $_requestImplementation = 'CAS_Request_CurlRequest';
602:
603: /**
604: * Override the default implementation used to make web requests in readUrl().
605: * This class must implement the CAS_Request_RequestInterface.
606: *
607: * @param string $className name of the RequestImplementation class
608: *
609: * @return void
610: */
611: public function setRequestImplementation ($className)
612: {
613: $obj = new $className;
614: if (!($obj instanceof CAS_Request_RequestInterface)) {
615: throw new CAS_InvalidArgumentException(
616: '$className must implement the CAS_Request_RequestInterface'
617: );
618: }
619: $this->_requestImplementation = $className;
620: }
621:
622: /**
623: * @var boolean $_clearTicketsFromUrl; If true, phpCAS will clear session
624: * tickets from the URL after a successful authentication.
625: */
626: private $_clearTicketsFromUrl = true;
627:
628: /**
629: * Configure the client to not send redirect headers and call exit() on
630: * authentication success. The normal redirect is used to remove the service
631: * ticket from the client's URL, but for running unit tests we need to
632: * continue without exiting.
633: *
634: * Needed for testing authentication
635: *
636: * @return void
637: */
638: public function setNoClearTicketsFromUrl ()
639: {
640: $this->_clearTicketsFromUrl = false;
641: }
642:
643: /**
644: * @var callback $_attributeParserCallbackFunction;
645: */
646: private $_casAttributeParserCallbackFunction = null;
647:
648: /**
649: * @var array $_attributeParserCallbackArgs;
650: */
651: private $_casAttributeParserCallbackArgs = array();
652:
653: /**
654: * Set a callback function to be run when parsing CAS attributes
655: *
656: * The callback function will be passed a XMLNode as its first parameter,
657: * followed by any $additionalArgs you pass.
658: *
659: * @param string $function callback function to call
660: * @param array $additionalArgs optional array of arguments
661: *
662: * @return void
663: */
664: public function setCasAttributeParserCallback($function, array $additionalArgs = array())
665: {
666: $this->_casAttributeParserCallbackFunction = $function;
667: $this->_casAttributeParserCallbackArgs = $additionalArgs;
668: }
669:
670: /** @var callback $_postAuthenticateCallbackFunction;
671: */
672: private $_postAuthenticateCallbackFunction = null;
673:
674: /**
675: * @var array $_postAuthenticateCallbackArgs;
676: */
677: private $_postAuthenticateCallbackArgs = array();
678:
679: /**
680: * Set a callback function to be run when a user authenticates.
681: *
682: * The callback function will be passed a $logoutTicket as its first parameter,
683: * followed by any $additionalArgs you pass. The $logoutTicket parameter is an
684: * opaque string that can be used to map a session-id to the logout request
685: * in order to support single-signout in applications that manage their own
686: * sessions (rather than letting phpCAS start the session).
687: *
688: * phpCAS::forceAuthentication() will always exit and forward client unless
689: * they are already authenticated. To perform an action at the moment the user
690: * logs in (such as registering an account, performing logging, etc), register
691: * a callback function here.
692: *
693: * @param string $function callback function to call
694: * @param array $additionalArgs optional array of arguments
695: *
696: * @return void
697: */
698: public function setPostAuthenticateCallback ($function, array $additionalArgs = array())
699: {
700: $this->_postAuthenticateCallbackFunction = $function;
701: $this->_postAuthenticateCallbackArgs = $additionalArgs;
702: }
703:
704: /**
705: * @var callback $_signoutCallbackFunction;
706: */
707: private $_signoutCallbackFunction = null;
708:
709: /**
710: * @var array $_signoutCallbackArgs;
711: */
712: private $_signoutCallbackArgs = array();
713:
714: /**
715: * Set a callback function to be run when a single-signout request is received.
716: *
717: * The callback function will be passed a $logoutTicket as its first parameter,
718: * followed by any $additionalArgs you pass. The $logoutTicket parameter is an
719: * opaque string that can be used to map a session-id to the logout request in
720: * order to support single-signout in applications that manage their own sessions
721: * (rather than letting phpCAS start and destroy the session).
722: *
723: * @param string $function callback function to call
724: * @param array $additionalArgs optional array of arguments
725: *
726: * @return void
727: */
728: public function setSingleSignoutCallback ($function, array $additionalArgs = array())
729: {
730: $this->_signoutCallbackFunction = $function;
731: $this->_signoutCallbackArgs = $additionalArgs;
732: }
733:
734: // ########################################################################
735: // Methods for supplying code-flow feedback to integrators.
736: // ########################################################################
737:
738: /**
739: * Ensure that this is actually a proxy object or fail with an exception
740: *
741: * @throws CAS_OutOfSequenceBeforeProxyException
742: *
743: * @return void
744: */
745: public function ensureIsProxy()
746: {
747: if (!$this->isProxy()) {
748: throw new CAS_OutOfSequenceBeforeProxyException();
749: }
750: }
751:
752: /**
753: * Mark the caller of authentication. This will help client integraters determine
754: * problems with their code flow if they call a function such as getUser() before
755: * authentication has occurred.
756: *
757: * @param bool $auth True if authentication was successful, false otherwise.
758: *
759: * @return null
760: */
761: public function markAuthenticationCall ($auth)
762: {
763: // store where the authentication has been checked and the result
764: $dbg = debug_backtrace();
765: $this->_authentication_caller = array (
766: 'file' => $dbg[1]['file'],
767: 'line' => $dbg[1]['line'],
768: 'method' => $dbg[1]['class'] . '::' . $dbg[1]['function'],
769: 'result' => (boolean)$auth
770: );
771: }
772: private $_authentication_caller;
773:
774: /**
775: * Answer true if authentication has been checked.
776: *
777: * @return bool
778: */
779: public function wasAuthenticationCalled ()
780: {
781: return !empty($this->_authentication_caller);
782: }
783:
784: /**
785: * Ensure that authentication was checked. Terminate with exception if no
786: * authentication was performed
787: *
788: * @throws CAS_OutOfSequenceBeforeAuthenticationCallException
789: *
790: * @return void
791: */
792: private function _ensureAuthenticationCalled()
793: {
794: if (!$this->wasAuthenticationCalled()) {
795: throw new CAS_OutOfSequenceBeforeAuthenticationCallException();
796: }
797: }
798:
799: /**
800: * Answer the result of the authentication call.
801: *
802: * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
803: * and markAuthenticationCall() didn't happen.
804: *
805: * @return bool
806: */
807: public function wasAuthenticationCallSuccessful ()
808: {
809: $this->_ensureAuthenticationCalled();
810: return $this->_authentication_caller['result'];
811: }
812:
813:
814: /**
815: * Ensure that authentication was checked. Terminate with exception if no
816: * authentication was performed
817: *
818: * @throws CAS_OutOfSequenceBeforeAuthenticationCallException
819: *
820: * @return void
821: */
822: public function ensureAuthenticationCallSuccessful()
823: {
824: $this->_ensureAuthenticationCalled();
825: if (!$this->_authentication_caller['result']) {
826: throw new CAS_OutOfSequenceException(
827: 'authentication was checked (by '
828: . $this->getAuthenticationCallerMethod()
829: . '() at ' . $this->getAuthenticationCallerFile()
830: . ':' . $this->getAuthenticationCallerLine()
831: . ') but the method returned false'
832: );
833: }
834: }
835:
836: /**
837: * Answer information about the authentication caller.
838: *
839: * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
840: * and markAuthenticationCall() didn't happen.
841: *
842: * @return array Keys are 'file', 'line', and 'method'
843: */
844: public function getAuthenticationCallerFile ()
845: {
846: $this->_ensureAuthenticationCalled();
847: return $this->_authentication_caller['file'];
848: }
849:
850: /**
851: * Answer information about the authentication caller.
852: *
853: * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
854: * and markAuthenticationCall() didn't happen.
855: *
856: * @return array Keys are 'file', 'line', and 'method'
857: */
858: public function getAuthenticationCallerLine ()
859: {
860: $this->_ensureAuthenticationCalled();
861: return $this->_authentication_caller['line'];
862: }
863:
864: /**
865: * Answer information about the authentication caller.
866: *
867: * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false
868: * and markAuthenticationCall() didn't happen.
869: *
870: * @return array Keys are 'file', 'line', and 'method'
871: */
872: public function getAuthenticationCallerMethod ()
873: {
874: $this->_ensureAuthenticationCalled();
875: return $this->_authentication_caller['method'];
876: }
877:
878: /** @} */
879:
880: // ########################################################################
881: // CONSTRUCTOR
882: // ########################################################################
883: /**
884: * @addtogroup internalConfig
885: * @{
886: */
887:
888: /**
889: * CAS_Client constructor.
890: *
891: * @param string $server_version the version of the CAS server
892: * @param bool $proxy true if the CAS client is a CAS proxy
893: * @param string $server_hostname the hostname of the CAS server
894: * @param int $server_port the port the CAS server is running on
895: * @param string $server_uri the URI the CAS server is responding on
896: * @param bool $changeSessionID Allow phpCAS to change the session_id
897: * (Single Sign Out/handleLogoutRequests
898: * is based on that change)
899: *
900: * @return a newly created CAS_Client object
901: */
902: public function __construct(
903: $server_version,
904: $proxy,
905: $server_hostname,
906: $server_port,
907: $server_uri,
908: $changeSessionID = true
909: ) {
910: // Argument validation
911: if (gettype($server_version) != 'string')
912: throw new CAS_TypeMismatchException($server_version, '$server_version', 'string');
913: if (gettype($proxy) != 'boolean')
914: throw new CAS_TypeMismatchException($proxy, '$proxy', 'boolean');
915: if (gettype($server_hostname) != 'string')
916: throw new CAS_TypeMismatchException($server_hostname, '$server_hostname', 'string');
917: if (gettype($server_port) != 'integer')
918: throw new CAS_TypeMismatchException($server_port, '$server_port', 'integer');
919: if (gettype($server_uri) != 'string')
920: throw new CAS_TypeMismatchException($server_uri, '$server_uri', 'string');
921: if (gettype($changeSessionID) != 'boolean')
922: throw new CAS_TypeMismatchException($changeSessionID, '$changeSessionID', 'boolean');
923:
924: phpCAS::traceBegin();
925: // true : allow to change the session_id(), false session_id won't be
926: // change and logout won't be handle because of that
927: $this->_setChangeSessionID($changeSessionID);
928:
929: // skip Session Handling for logout requests and if don't want it'
930: if (session_id()=="" && !$this->_isLogoutRequest()) {
931: session_start();
932: phpCAS :: trace("Starting a new session " . session_id());
933: }
934: // Only for debug purposes
935: if ($this->isSessionAuthenticated()){
936: phpCAS :: trace("Session is authenticated as: " . $_SESSION['phpCAS']['user']);
937: } else {
938: phpCAS :: trace("Session is not authenticated");
939: }
940: // are we in proxy mode ?
941: $this->_proxy = $proxy;
942:
943: // Make cookie handling available.
944: if ($this->isProxy()) {
945: if (!isset($_SESSION['phpCAS'])) {
946: $_SESSION['phpCAS'] = array();
947: }
948: if (!isset($_SESSION['phpCAS']['service_cookies'])) {
949: $_SESSION['phpCAS']['service_cookies'] = array();
950: }
951: $this->_serviceCookieJar = new CAS_CookieJar(
952: $_SESSION['phpCAS']['service_cookies']
953: );
954: }
955:
956: //check version
957: switch ($server_version) {
958: case CAS_VERSION_1_0:
959: if ( $this->isProxy() ) {
960: phpCAS::error(
961: 'CAS proxies are not supported in CAS '.$server_version
962: );
963: }
964: break;
965: case CAS_VERSION_2_0:
966: case CAS_VERSION_3_0:
967: break;
968: case SAML_VERSION_1_1:
969: break;
970: default:
971: phpCAS::error(
972: 'this version of CAS (`'.$server_version
973: .'\') is not supported by phpCAS '.phpCAS::getVersion()
974: );
975: }
976: $this->_server['version'] = $server_version;
977:
978: // check hostname
979: if ( empty($server_hostname)
980: || !preg_match('/[\.\d\-abcdefghijklmnopqrstuvwxyz]*/', $server_hostname)
981: ) {
982: phpCAS::error('bad CAS server hostname (`'.$server_hostname.'\')');
983: }
984: $this->_server['hostname'] = $server_hostname;
985:
986: // check port
987: if ( $server_port == 0
988: || !is_int($server_port)
989: ) {
990: phpCAS::error('bad CAS server port (`'.$server_hostname.'\')');
991: }
992: $this->_server['port'] = $server_port;
993:
994: // check URI
995: if ( !preg_match('/[\.\d\-_abcdefghijklmnopqrstuvwxyz\/]*/', $server_uri) ) {
996: phpCAS::error('bad CAS server URI (`'.$server_uri.'\')');
997: }
998: // add leading and trailing `/' and remove doubles
999: if(strstr($server_uri, '?') === false) $server_uri .= '/';
1000: $server_uri = preg_replace('/\/\//', '/', '/'.$server_uri);
1001: $this->_server['uri'] = $server_uri;
1002:
1003: // set to callback mode if PgtIou and PgtId CGI GET parameters are provided
1004: if ( $this->isProxy() ) {
1005: $this->_setCallbackMode(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId']));
1006: }
1007:
1008: if ( $this->_isCallbackMode() ) {
1009: //callback mode: check that phpCAS is secured
1010: if ( !$this->_isHttps() ) {
1011: phpCAS::error(
1012: 'CAS proxies must be secured to use phpCAS; PGT\'s will not be received from the CAS server'
1013: );
1014: }
1015: } else {
1016: //normal mode: get ticket and remove it from CGI parameters for
1017: // developers
1018: $ticket = (isset($_GET['ticket']) ? $_GET['ticket'] : null);
1019: if (preg_match('/^[SP]T-/', $ticket) ) {
1020: phpCAS::trace('Ticket \''.$ticket.'\' found');
1021: $this->setTicket($ticket);
1022: unset($_GET['ticket']);
1023: } else if ( !empty($ticket) ) {
1024: //ill-formed ticket, halt
1025: phpCAS::error(
1026: 'ill-formed ticket found in the URL (ticket=`'
1027: .htmlentities($ticket).'\')'
1028: );
1029: }
1030:
1031: }
1032: phpCAS::traceEnd();
1033: }
1034:
1035: /** @} */
1036:
1037: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1038: // XX XX
1039: // XX Session Handling XX
1040: // XX XX
1041: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1042:
1043: /**
1044: * @addtogroup internalConfig
1045: * @{
1046: */
1047:
1048:
1049: /**
1050: * A variable to whether phpcas will use its own session handling. Default = true
1051: * @hideinitializer
1052: */
1053: private $_change_session_id = true;
1054:
1055: /**
1056: * Set a parameter whether to allow phpCas to change session_id
1057: *
1058: * @param bool $allowed allow phpCas to change session_id
1059: *
1060: * @return void
1061: */
1062: private function _setChangeSessionID($allowed)
1063: {
1064: $this->_change_session_id = $allowed;
1065: }
1066:
1067: /**
1068: * Get whether phpCas is allowed to change session_id
1069: *
1070: * @return bool
1071: */
1072: public function getChangeSessionID()
1073: {
1074: return $this->_change_session_id;
1075: }
1076:
1077: /** @} */
1078:
1079: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1080: // XX XX
1081: // XX AUTHENTICATION XX
1082: // XX XX
1083: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1084:
1085: /**
1086: * @addtogroup internalAuthentication
1087: * @{
1088: */
1089:
1090: /**
1091: * The Authenticated user. Written by CAS_Client::_setUser(), read by
1092: * CAS_Client::getUser().
1093: *
1094: * @hideinitializer
1095: */
1096: private $_user = '';
1097:
1098: /**
1099: * This method sets the CAS user's login name.
1100: *
1101: * @param string $user the login name of the authenticated user.
1102: *
1103: * @return void
1104: */
1105: private function _setUser($user)
1106: {
1107: $this->_user = $user;
1108: }
1109:
1110: /**
1111: * This method returns the CAS user's login name.
1112: *
1113: * @return string the login name of the authenticated user
1114: *
1115: * @warning should be called only after CAS_Client::forceAuthentication() or
1116: * CAS_Client::isAuthenticated(), otherwise halt with an error.
1117: */
1118: public function getUser()
1119: {
1120: // Sequence validation
1121: $this->ensureAuthenticationCallSuccessful();
1122:
1123: return $this->_getUser();
1124: }
1125:
1126: /**
1127: * This method returns the CAS user's login name.
1128: *
1129: * @return string the login name of the authenticated user
1130: *
1131: * @warning should be called only after CAS_Client::forceAuthentication() or
1132: * CAS_Client::isAuthenticated(), otherwise halt with an error.
1133: */
1134: private function _getUser()
1135: {
1136: // This is likely a duplicate check that could be removed....
1137: if ( empty($this->_user) ) {
1138: phpCAS::error(
1139: 'this method should be used only after '.__CLASS__
1140: .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'
1141: );
1142: }
1143: return $this->_user;
1144: }
1145:
1146: /**
1147: * The Authenticated users attributes. Written by
1148: * CAS_Client::setAttributes(), read by CAS_Client::getAttributes().
1149: * @attention client applications should use phpCAS::getAttributes().
1150: *
1151: * @hideinitializer
1152: */
1153: private $_attributes = array();
1154:
1155: /**
1156: * Set an array of attributes
1157: *
1158: * @param array $attributes a key value array of attributes
1159: *
1160: * @return void
1161: */
1162: public function setAttributes($attributes)
1163: {
1164: $this->_attributes = $attributes;
1165: }
1166:
1167: /**
1168: * Get an key values arry of attributes
1169: *
1170: * @return arry of attributes
1171: */
1172: public function getAttributes()
1173: {
1174: // Sequence validation
1175: $this->ensureAuthenticationCallSuccessful();
1176: // This is likely a duplicate check that could be removed....
1177: if ( empty($this->_user) ) {
1178: // if no user is set, there shouldn't be any attributes also...
1179: phpCAS::error(
1180: 'this method should be used only after '.__CLASS__
1181: .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'
1182: );
1183: }
1184: return $this->_attributes;
1185: }
1186:
1187: /**
1188: * Check whether attributes are available
1189: *
1190: * @return bool attributes available
1191: */
1192: public function hasAttributes()
1193: {
1194: // Sequence validation
1195: $this->ensureAuthenticationCallSuccessful();
1196:
1197: return !empty($this->_attributes);
1198: }
1199: /**
1200: * Check whether a specific attribute with a name is available
1201: *
1202: * @param string $key name of attribute
1203: *
1204: * @return bool is attribute available
1205: */
1206: public function hasAttribute($key)
1207: {
1208: // Sequence validation
1209: $this->ensureAuthenticationCallSuccessful();
1210:
1211: return $this->_hasAttribute($key);
1212: }
1213:
1214: /**
1215: * Check whether a specific attribute with a name is available
1216: *
1217: * @param string $key name of attribute
1218: *
1219: * @return bool is attribute available
1220: */
1221: private function _hasAttribute($key)
1222: {
1223: return (is_array($this->_attributes)
1224: && array_key_exists($key, $this->_attributes));
1225: }
1226:
1227: /**
1228: * Get a specific attribute by name
1229: *
1230: * @param string $key name of attribute
1231: *
1232: * @return string attribute values
1233: */
1234: public function getAttribute($key)
1235: {
1236: // Sequence validation
1237: $this->ensureAuthenticationCallSuccessful();
1238:
1239: if ($this->_hasAttribute($key)) {
1240: return $this->_attributes[$key];
1241: }
1242: }
1243:
1244: /**
1245: * This method is called to renew the authentication of the user
1246: * If the user is authenticated, renew the connection
1247: * If not, redirect to CAS
1248: *
1249: * @return true when the user is authenticated; otherwise halt.
1250: */
1251: public function renewAuthentication()
1252: {
1253: phpCAS::traceBegin();
1254: // Either way, the user is authenticated by CAS
1255: if (isset( $_SESSION['phpCAS']['auth_checked'])) {
1256: unset($_SESSION['phpCAS']['auth_checked']);
1257: }
1258: if ( $this->isAuthenticated(true) ) {
1259: phpCAS::trace('user already authenticated');
1260: $res = true;
1261: } else {
1262: $this->redirectToCas(false, true);
1263: // never reached
1264: $res = false;
1265: }
1266: phpCAS::traceEnd();
1267: return $res;
1268: }
1269:
1270: /**
1271: * This method is called to be sure that the user is authenticated. When not
1272: * authenticated, halt by redirecting to the CAS server; otherwise return true.
1273: *
1274: * @return true when the user is authenticated; otherwise halt.
1275: */
1276: public function forceAuthentication()
1277: {
1278: phpCAS::traceBegin();
1279:
1280: if ( $this->isAuthenticated() ) {
1281: // the user is authenticated, nothing to be done.
1282: phpCAS::trace('no need to authenticate');
1283: $res = true;
1284: } else {
1285: // the user is not authenticated, redirect to the CAS server
1286: if (isset($_SESSION['phpCAS']['auth_checked'])) {
1287: unset($_SESSION['phpCAS']['auth_checked']);
1288: }
1289: $this->redirectToCas(false/* no gateway */);
1290: // never reached
1291: $res = false;
1292: }
1293: phpCAS::traceEnd($res);
1294: return $res;
1295: }
1296:
1297: /**
1298: * An integer that gives the number of times authentication will be cached
1299: * before rechecked.
1300: *
1301: * @hideinitializer
1302: */
1303: private $_cache_times_for_auth_recheck = 0;
1304:
1305: /**
1306: * Set the number of times authentication will be cached before rechecked.
1307: *
1308: * @param int $n number of times to wait for a recheck
1309: *
1310: * @return void
1311: */
1312: public function setCacheTimesForAuthRecheck($n)
1313: {
1314: if (gettype($n) != 'integer')
1315: throw new CAS_TypeMismatchException($n, '$n', 'string');
1316:
1317: $this->_cache_times_for_auth_recheck = $n;
1318: }
1319:
1320: /**
1321: * This method is called to check whether the user is authenticated or not.
1322: *
1323: * @return true when the user is authenticated, false when a previous
1324: * gateway login failed or the function will not return if the user is
1325: * redirected to the cas server for a gateway login attempt
1326: */
1327: public function checkAuthentication()
1328: {
1329: phpCAS::traceBegin();
1330: $res = false;
1331: if ( $this->isAuthenticated() ) {
1332: phpCAS::trace('user is authenticated');
1333: /* The 'auth_checked' variable is removed just in case it's set. */
1334: unset($_SESSION['phpCAS']['auth_checked']);
1335: $res = true;
1336: } else if (isset($_SESSION['phpCAS']['auth_checked'])) {
1337: // the previous request has redirected the client to the CAS server
1338: // with gateway=true
1339: unset($_SESSION['phpCAS']['auth_checked']);
1340: $res = false;
1341: } else {
1342: // avoid a check against CAS on every request
1343: if (!isset($_SESSION['phpCAS']['unauth_count'])) {
1344: $_SESSION['phpCAS']['unauth_count'] = -2; // uninitialized
1345: }
1346:
1347: if (($_SESSION['phpCAS']['unauth_count'] != -2
1348: && $this->_cache_times_for_auth_recheck == -1)
1349: || ($_SESSION['phpCAS']['unauth_count'] >= 0
1350: && $_SESSION['phpCAS']['unauth_count'] < $this->_cache_times_for_auth_recheck)
1351: ) {
1352: $res = false;
1353:
1354: if ($this->_cache_times_for_auth_recheck != -1) {
1355: $_SESSION['phpCAS']['unauth_count']++;
1356: phpCAS::trace(
1357: 'user is not authenticated (cached for '
1358: .$_SESSION['phpCAS']['unauth_count'].' times of '
1359: .$this->_cache_times_for_auth_recheck.')'
1360: );
1361: } else {
1362: phpCAS::trace(
1363: 'user is not authenticated (cached for until login pressed)'
1364: );
1365: }
1366: } else {
1367: $_SESSION['phpCAS']['unauth_count'] = 0;
1368: $_SESSION['phpCAS']['auth_checked'] = true;
1369: phpCAS::trace('user is not authenticated (cache reset)');
1370: $this->redirectToCas(true/* gateway */);
1371: // never reached
1372: $res = false;
1373: }
1374: }
1375: phpCAS::traceEnd($res);
1376: return $res;
1377: }
1378:
1379: /**
1380: * This method is called to check if the user is authenticated (previously or by
1381: * tickets given in the URL).
1382: *
1383: * @param bool $renew true to force the authentication with the CAS server
1384: *
1385: * @return true when the user is authenticated. Also may redirect to the
1386: * same URL without the ticket.
1387: */
1388: public function isAuthenticated($renew=false)
1389: {
1390: phpCAS::traceBegin();
1391: $res = false;
1392: $validate_url = '';
1393: if ( $this->_wasPreviouslyAuthenticated() ) {
1394: if ($this->hasTicket()) {
1395: // User has a additional ticket but was already authenticated
1396: phpCAS::trace(
1397: 'ticket was present and will be discarded, use renewAuthenticate()'
1398: );
1399: if ($this->_clearTicketsFromUrl) {
1400: phpCAS::trace("Prepare redirect to : ".$this->getURL());
1401: session_write_close();
1402: header('Location: '.$this->getURL());
1403: flush();
1404: phpCAS::traceExit();
1405: throw new CAS_GracefullTerminationException();
1406: } else {
1407: phpCAS::trace(
1408: 'Already authenticated, but skipping ticket clearing since setNoClearTicketsFromUrl() was used.'
1409: );
1410: $res = true;
1411: }
1412: } else {
1413: // the user has already (previously during the session) been
1414: // authenticated, nothing to be done.
1415: phpCAS::trace(
1416: 'user was already authenticated, no need to look for tickets'
1417: );
1418: $res = true;
1419: }
1420:
1421: // Mark the auth-check as complete to allow post-authentication
1422: // callbacks to make use of phpCAS::getUser() and similar methods
1423: $this->markAuthenticationCall($res);
1424: } else {
1425: if ($this->hasTicket()) {
1426: switch ($this->getServerVersion()) {
1427: case CAS_VERSION_1_0:
1428: // if a Service Ticket was given, validate it
1429: phpCAS::trace(
1430: 'CAS 1.0 ticket `'.$this->getTicket().'\' is present'
1431: );
1432: $this->validateCAS10(
1433: $validate_url, $text_response, $tree_response, $renew
1434: ); // if it fails, it halts
1435: phpCAS::trace(
1436: 'CAS 1.0 ticket `'.$this->getTicket().'\' was validated'
1437: );
1438: $_SESSION['phpCAS']['user'] = $this->_getUser();
1439: $res = true;
1440: $logoutTicket = $this->getTicket();
1441: break;
1442: case CAS_VERSION_2_0:
1443: case CAS_VERSION_3_0:
1444: // if a Proxy Ticket was given, validate it
1445: phpCAS::trace(
1446: 'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' is present'
1447: );
1448: $this->validateCAS20(
1449: $validate_url, $text_response, $tree_response, $renew
1450: ); // note: if it fails, it halts
1451: phpCAS::trace(
1452: 'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' was validated'
1453: );
1454: if ( $this->isProxy() ) {
1455: $this->_validatePGT(
1456: $validate_url, $text_response, $tree_response
1457: ); // idem
1458: phpCAS::trace('PGT `'.$this->_getPGT().'\' was validated');
1459: $_SESSION['phpCAS']['pgt'] = $this->_getPGT();
1460: }
1461: $_SESSION['phpCAS']['user'] = $this->_getUser();
1462: if (!empty($this->_attributes)) {
1463: $_SESSION['phpCAS']['attributes'] = $this->_attributes;
1464: }
1465: $proxies = $this->getProxies();
1466: if (!empty($proxies)) {
1467: $_SESSION['phpCAS']['proxies'] = $this->getProxies();
1468: }
1469: $res = true;
1470: $logoutTicket = $this->getTicket();
1471: break;
1472: case SAML_VERSION_1_1:
1473: // if we have a SAML ticket, validate it.
1474: phpCAS::trace(
1475: 'SAML 1.1 ticket `'.$this->getTicket().'\' is present'
1476: );
1477: $this->validateSA(
1478: $validate_url, $text_response, $tree_response, $renew
1479: ); // if it fails, it halts
1480: phpCAS::trace(
1481: 'SAML 1.1 ticket `'.$this->getTicket().'\' was validated'
1482: );
1483: $_SESSION['phpCAS']['user'] = $this->_getUser();
1484: $_SESSION['phpCAS']['attributes'] = $this->_attributes;
1485: $res = true;
1486: $logoutTicket = $this->getTicket();
1487: break;
1488: default:
1489: phpCAS::trace('Protocoll error');
1490: break;
1491: }
1492: } else {
1493: // no ticket given, not authenticated
1494: phpCAS::trace('no ticket found');
1495: }
1496:
1497: // Mark the auth-check as complete to allow post-authentication
1498: // callbacks to make use of phpCAS::getUser() and similar methods
1499: $this->markAuthenticationCall($res);
1500:
1501: if ($res) {
1502: // call the post-authenticate callback if registered.
1503: if ($this->_postAuthenticateCallbackFunction) {
1504: $args = $this->_postAuthenticateCallbackArgs;
1505: array_unshift($args, $logoutTicket);
1506: call_user_func_array(
1507: $this->_postAuthenticateCallbackFunction, $args
1508: );
1509: }
1510:
1511: // if called with a ticket parameter, we need to redirect to the
1512: // app without the ticket so that CAS-ification is transparent
1513: // to the browser (for later POSTS) most of the checks and
1514: // errors should have been made now, so we're safe for redirect
1515: // without masking error messages. remove the ticket as a
1516: // security precaution to prevent a ticket in the HTTP_REFERRER
1517: if ($this->_clearTicketsFromUrl) {
1518: phpCAS::trace("Prepare redirect to : ".$this->getURL());
1519: session_write_close();
1520: header('Location: '.$this->getURL());
1521: flush();
1522: phpCAS::traceExit();
1523: throw new CAS_GracefullTerminationException();
1524: }
1525: }
1526: }
1527: phpCAS::traceEnd($res);
1528: return $res;
1529: }
1530:
1531: /**
1532: * This method tells if the current session is authenticated.
1533: *
1534: * @return true if authenticated based soley on $_SESSION variable
1535: */
1536: public function isSessionAuthenticated ()
1537: {
1538: return !empty($_SESSION['phpCAS']['user']);
1539: }
1540:
1541: /**
1542: * This method tells if the user has already been (previously) authenticated
1543: * by looking into the session variables.
1544: *
1545: * @note This function switches to callback mode when needed.
1546: *
1547: * @return true when the user has already been authenticated; false otherwise.
1548: */
1549: private function _wasPreviouslyAuthenticated()
1550: {
1551: phpCAS::traceBegin();
1552:
1553: if ( $this->_isCallbackMode() ) {
1554: // Rebroadcast the pgtIou and pgtId to all nodes
1555: if ($this->_rebroadcast&&!isset($_POST['rebroadcast'])) {
1556: $this->_rebroadcast(self::PGTIOU);
1557: }
1558: $this->_callback();
1559: }
1560:
1561: $auth = false;
1562:
1563: if ( $this->isProxy() ) {
1564: // CAS proxy: username and PGT must be present
1565: if ( $this->isSessionAuthenticated()
1566: && !empty($_SESSION['phpCAS']['pgt'])
1567: ) {
1568: // authentication already done
1569: $this->_setUser($_SESSION['phpCAS']['user']);
1570: if (isset($_SESSION['phpCAS']['attributes'])) {
1571: $this->setAttributes($_SESSION['phpCAS']['attributes']);
1572: }
1573: $this->_setPGT($_SESSION['phpCAS']['pgt']);
1574: phpCAS::trace(
1575: 'user = `'.$_SESSION['phpCAS']['user'].'\', PGT = `'
1576: .$_SESSION['phpCAS']['pgt'].'\''
1577: );
1578:
1579: // Include the list of proxies
1580: if (isset($_SESSION['phpCAS']['proxies'])) {
1581: $this->_setProxies($_SESSION['phpCAS']['proxies']);
1582: phpCAS::trace(
1583: 'proxies = "'
1584: .implode('", "', $_SESSION['phpCAS']['proxies']).'"'
1585: );
1586: }
1587:
1588: $auth = true;
1589: } elseif ( $this->isSessionAuthenticated()
1590: && empty($_SESSION['phpCAS']['pgt'])
1591: ) {
1592: // these two variables should be empty or not empty at the same time
1593: phpCAS::trace(
1594: 'username found (`'.$_SESSION['phpCAS']['user']
1595: .'\') but PGT is empty'
1596: );
1597: // unset all tickets to enforce authentication
1598: unset($_SESSION['phpCAS']);
1599: $this->setTicket('');
1600: } elseif ( !$this->isSessionAuthenticated()
1601: && !empty($_SESSION['phpCAS']['pgt'])
1602: ) {
1603: // these two variables should be empty or not empty at the same time
1604: phpCAS::trace(
1605: 'PGT found (`'.$_SESSION['phpCAS']['pgt']
1606: .'\') but username is empty'
1607: );
1608: // unset all tickets to enforce authentication
1609: unset($_SESSION['phpCAS']);
1610: $this->setTicket('');
1611: } else {
1612: phpCAS::trace('neither user nor PGT found');
1613: }
1614: } else {
1615: // `simple' CAS client (not a proxy): username must be present
1616: if ( $this->isSessionAuthenticated() ) {
1617: // authentication already done
1618: $this->_setUser($_SESSION['phpCAS']['user']);
1619: if (isset($_SESSION['phpCAS']['attributes'])) {
1620: $this->setAttributes($_SESSION['phpCAS']['attributes']);
1621: }
1622: phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\'');
1623:
1624: // Include the list of proxies
1625: if (isset($_SESSION['phpCAS']['proxies'])) {
1626: $this->_setProxies($_SESSION['phpCAS']['proxies']);
1627: phpCAS::trace(
1628: 'proxies = "'
1629: .implode('", "', $_SESSION['phpCAS']['proxies']).'"'
1630: );
1631: }
1632:
1633: $auth = true;
1634: } else {
1635: phpCAS::trace('no user found');
1636: }
1637: }
1638:
1639: phpCAS::traceEnd($auth);
1640: return $auth;
1641: }
1642:
1643: /**
1644: * This method is used to redirect the client to the CAS server.
1645: * It is used by CAS_Client::forceAuthentication() and
1646: * CAS_Client::checkAuthentication().
1647: *
1648: * @param bool $gateway true to check authentication, false to force it
1649: * @param bool $renew true to force the authentication with the CAS server
1650: *
1651: * @return void
1652: */
1653: public function redirectToCas($gateway=false,$renew=false)
1654: {
1655: phpCAS::traceBegin();
1656: $cas_url = $this->getServerLoginURL($gateway, $renew);
1657: session_write_close();
1658: if (php_sapi_name() === 'cli') {
1659: @header('Location: '.$cas_url);
1660: } else {
1661: header('Location: '.$cas_url);
1662: }
1663: phpCAS::trace("Redirect to : ".$cas_url);
1664: $lang = $this->getLangObj();
1665: $this->printHTMLHeader($lang->getAuthenticationWanted());
1666: printf('<p>'. $lang->getShouldHaveBeenRedirected(). '</p>', $cas_url);
1667: $this->printHTMLFooter();
1668: phpCAS::traceExit();
1669: throw new CAS_GracefullTerminationException();
1670: }
1671:
1672:
1673: /**
1674: * This method is used to logout from CAS.
1675: *
1676: * @param array $params an array that contains the optional url and service
1677: * parameters that will be passed to the CAS server
1678: *
1679: * @return void
1680: */
1681: public function logout($params)
1682: {
1683: phpCAS::traceBegin();
1684: $cas_url = $this->getServerLogoutURL();
1685: $paramSeparator = '?';
1686: if (isset($params['url'])) {
1687: $cas_url = $cas_url . $paramSeparator . "url="
1688: . urlencode($params['url']);
1689: $paramSeparator = '&';
1690: }
1691: if (isset($params['service'])) {
1692: $cas_url = $cas_url . $paramSeparator . "service="
1693: . urlencode($params['service']);
1694: }
1695: header('Location: '.$cas_url);
1696: phpCAS::trace("Prepare redirect to : ".$cas_url);
1697:
1698: phpCAS::trace("Destroying session : ".session_id());
1699: session_unset();
1700: session_destroy();
1701: if (session_status() === PHP_SESSION_NONE) {
1702: phpCAS::trace("Session terminated");
1703: } else {
1704: phpCAS::error("Session was not terminated");
1705: phpCAS::trace("Session was not terminated");
1706: }
1707: $lang = $this->getLangObj();
1708: $this->printHTMLHeader($lang->getLogout());
1709: printf('<p>'.$lang->getShouldHaveBeenRedirected(). '</p>', $cas_url);
1710: $this->printHTMLFooter();
1711: phpCAS::traceExit();
1712: throw new CAS_GracefullTerminationException();
1713: }
1714:
1715: /**
1716: * Check of the current request is a logout request
1717: *
1718: * @return bool is logout request.
1719: */
1720: private function _isLogoutRequest()
1721: {
1722: return !empty($_POST['logoutRequest']);
1723: }
1724:
1725: /**
1726: * This method handles logout requests.
1727: *
1728: * @param bool $check_client true to check the client bofore handling
1729: * the request, false not to perform any access control. True by default.
1730: * @param bool $allowed_clients an array of host names allowed to send
1731: * logout requests.
1732: *
1733: * @return void
1734: */
1735: public function handleLogoutRequests($check_client=true, $allowed_clients=false)
1736: {
1737: phpCAS::traceBegin();
1738: if (!$this->_isLogoutRequest()) {
1739: phpCAS::trace("Not a logout request");
1740: phpCAS::traceEnd();
1741: return;
1742: }
1743: if (!$this->getChangeSessionID()
1744: && is_null($this->_signoutCallbackFunction)
1745: ) {
1746: phpCAS::trace(
1747: "phpCAS can't handle logout requests if it is not allowed to change session_id."
1748: );
1749: }
1750: phpCAS::trace("Logout requested");
1751: $decoded_logout_rq = urldecode($_POST['logoutRequest']);
1752: phpCAS::trace("SAML REQUEST: ".$decoded_logout_rq);
1753: $allowed = false;
1754: if ($check_client) {
1755: if (!$allowed_clients) {
1756: $allowed_clients = array( $this->_getServerHostname() );
1757: }
1758: $client_ip = $_SERVER['REMOTE_ADDR'];
1759: $client = gethostbyaddr($client_ip);
1760: phpCAS::trace("Client: ".$client."/".$client_ip);
1761: foreach ($allowed_clients as $allowed_client) {
1762: if (($client == $allowed_client)
1763: || ($client_ip == $allowed_client)
1764: ) {
1765: phpCAS::trace(
1766: "Allowed client '".$allowed_client
1767: ."' matches, logout request is allowed"
1768: );
1769: $allowed = true;
1770: break;
1771: } else {
1772: phpCAS::trace(
1773: "Allowed client '".$allowed_client."' does not match"
1774: );
1775: }
1776: }
1777: } else {
1778: phpCAS::trace("No access control set");
1779: $allowed = true;
1780: }
1781: // If Logout command is permitted proceed with the logout
1782: if ($allowed) {
1783: phpCAS::trace("Logout command allowed");
1784: // Rebroadcast the logout request
1785: if ($this->_rebroadcast && !isset($_POST['rebroadcast'])) {
1786: $this->_rebroadcast(self::LOGOUT);
1787: }
1788: // Extract the ticket from the SAML Request
1789: preg_match(
1790: "|<samlp:SessionIndex>(.*)</samlp:SessionIndex>|",
1791: $decoded_logout_rq, $tick, PREG_OFFSET_CAPTURE, 3
1792: );
1793: $wrappedSamlSessionIndex = preg_replace(
1794: '|<samlp:SessionIndex>|', '', $tick[0][0]
1795: );
1796: $ticket2logout = preg_replace(
1797: '|</samlp:SessionIndex>|', '', $wrappedSamlSessionIndex
1798: );
1799: phpCAS::trace("Ticket to logout: ".$ticket2logout);
1800:
1801: // call the post-authenticate callback if registered.
1802: if ($this->_signoutCallbackFunction) {
1803: $args = $this->_signoutCallbackArgs;
1804: array_unshift($args, $ticket2logout);
1805: call_user_func_array($this->_signoutCallbackFunction, $args);
1806: }
1807:
1808: // If phpCAS is managing the session_id, destroy session thanks to
1809: // session_id.
1810: if ($this->getChangeSessionID()) {
1811: $session_id = preg_replace('/[^a-zA-Z0-9\-]/', '', $ticket2logout);
1812: phpCAS::trace("Session id: ".$session_id);
1813:
1814: // destroy a possible application session created before phpcas
1815: if (session_id() !== "") {
1816: session_unset();
1817: session_destroy();
1818: }
1819: // fix session ID
1820: session_id($session_id);
1821: $_COOKIE[session_name()]=$session_id;
1822: $_GET[session_name()]=$session_id;
1823:
1824: // Overwrite session
1825: session_start();
1826: session_unset();
1827: session_destroy();
1828: phpCAS::trace("Session ". $session_id . " destroyed");
1829: }
1830: } else {
1831: phpCAS::error("Unauthorized logout request from client '".$client."'");
1832: phpCAS::trace("Unauthorized logout request from client '".$client."'");
1833: }
1834: flush();
1835: phpCAS::traceExit();
1836: throw new CAS_GracefullTerminationException();
1837:
1838: }
1839:
1840: /** @} */
1841:
1842: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1843: // XX XX
1844: // XX BASIC CLIENT FEATURES (CAS 1.0) XX
1845: // XX XX
1846: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1847:
1848: // ########################################################################
1849: // ST
1850: // ########################################################################
1851: /**
1852: * @addtogroup internalBasic
1853: * @{
1854: */
1855:
1856: /**
1857: * The Ticket provided in the URL of the request if present
1858: * (empty otherwise). Written by CAS_Client::CAS_Client(), read by
1859: * CAS_Client::getTicket() and CAS_Client::_hasPGT().
1860: *
1861: * @hideinitializer
1862: */
1863: private $_ticket = '';
1864:
1865: /**
1866: * This method returns the Service Ticket provided in the URL of the request.
1867: *
1868: * @return string service ticket.
1869: */
1870: public function getTicket()
1871: {
1872: return $this->_ticket;
1873: }
1874:
1875: /**
1876: * This method stores the Service Ticket.
1877: *
1878: * @param string $st The Service Ticket.
1879: *
1880: * @return void
1881: */
1882: public function setTicket($st)
1883: {
1884: $this->_ticket = $st;
1885: }
1886:
1887: /**
1888: * This method tells if a Service Ticket was stored.
1889: *
1890: * @return bool if a Service Ticket has been stored.
1891: */
1892: public function hasTicket()
1893: {
1894: return !empty($this->_ticket);
1895: }
1896:
1897: /** @} */
1898:
1899: // ########################################################################
1900: // ST VALIDATION
1901: // ########################################################################
1902: /**
1903: * @addtogroup internalBasic
1904: * @{
1905: */
1906:
1907: /**
1908: * the certificate of the CAS server CA.
1909: *
1910: * @hideinitializer
1911: */
1912: private $_cas_server_ca_cert = null;
1913:
1914:
1915: /**
1916:
1917: * validate CN of the CAS server certificate
1918:
1919: *
1920:
1921: * @hideinitializer
1922:
1923: */
1924:
1925: private $_cas_server_cn_validate = true;
1926:
1927: /**
1928: * Set to true not to validate the CAS server.
1929: *
1930: * @hideinitializer
1931: */
1932: private $_no_cas_server_validation = false;
1933:
1934:
1935: /**
1936: * Set the CA certificate of the CAS server.
1937: *
1938: * @param string $cert the PEM certificate file name of the CA that emited
1939: * the cert of the server
1940: * @param bool $validate_cn valiate CN of the CAS server certificate
1941: *
1942: * @return void
1943: */
1944: public function setCasServerCACert($cert, $validate_cn)
1945: {
1946: // Argument validation
1947: if (gettype($cert) != 'string') {
1948: throw new CAS_TypeMismatchException($cert, '$cert', 'string');
1949: }
1950: if (gettype($validate_cn) != 'boolean') {
1951: throw new CAS_TypeMismatchException($validate_cn, '$validate_cn', 'boolean');
1952: }
1953: if ( !file_exists($cert) && $this->_requestImplementation !== 'CAS_TestHarness_DummyRequest'){
1954: throw new CAS_InvalidArgumentException("Certificate file does not exist " . $this->_requestImplementation);
1955: }
1956: $this->_cas_server_ca_cert = $cert;
1957: $this->_cas_server_cn_validate = $validate_cn;
1958: }
1959:
1960: /**
1961: * Set no SSL validation for the CAS server.
1962: *
1963: * @return void
1964: */
1965: public function setNoCasServerValidation()
1966: {
1967: $this->_no_cas_server_validation = true;
1968: }
1969:
1970: /**
1971: * This method is used to validate a CAS 1,0 ticket; halt on failure, and
1972: * sets $validate_url, $text_reponse and $tree_response on success.
1973: *
1974: * @param string &$validate_url reference to the the URL of the request to
1975: * the CAS server.
1976: * @param string &$text_response reference to the response of the CAS
1977: * server, as is (XML text).
1978: * @param string &$tree_response reference to the response of the CAS
1979: * server, as a DOM XML tree.
1980: * @param bool $renew true to force the authentication with the CAS server
1981: *
1982: * @return bool true when successfull and issue a CAS_AuthenticationException
1983: * and false on an error
1984: */
1985: public function validateCAS10(&$validate_url,&$text_response,&$tree_response,$renew=false)
1986: {
1987: phpCAS::traceBegin();
1988: $result = false;
1989: // build the URL to validate the ticket
1990: $validate_url = $this->getServerServiceValidateURL()
1991: .'&ticket='.urlencode($this->getTicket());
1992:
1993: if ( $renew ) {
1994: // pass the renew
1995: $validate_url .= '&renew=true';
1996: }
1997:
1998: // open and read the URL
1999: if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
2000: phpCAS::trace(
2001: 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'
2002: );
2003: throw new CAS_AuthenticationException(
2004: $this, 'CAS 1.0 ticket not validated', $validate_url,
2005: true/*$no_response*/
2006: );
2007: $result = false;
2008: }
2009:
2010: if (preg_match('/^no\n/', $text_response)) {
2011: phpCAS::trace('Ticket has not been validated');
2012: throw new CAS_AuthenticationException(
2013: $this, 'ST not validated', $validate_url, false/*$no_response*/,
2014: false/*$bad_response*/, $text_response
2015: );
2016: $result = false;
2017: } else if (!preg_match('/^yes\n/', $text_response)) {
2018: phpCAS::trace('ill-formed response');
2019: throw new CAS_AuthenticationException(
2020: $this, 'Ticket not validated', $validate_url,
2021: false/*$no_response*/, true/*$bad_response*/, $text_response
2022: );
2023: $result = false;
2024: }
2025: // ticket has been validated, extract the user name
2026: $arr = preg_split('/\n/', $text_response);
2027: $this->_setUser(trim($arr[1]));
2028: $result = true;
2029:
2030: if ($result) {
2031: $this->_renameSession($this->getTicket());
2032: }
2033: // at this step, ticket has been validated and $this->_user has been set,
2034: phpCAS::traceEnd(true);
2035: return true;
2036: }
2037:
2038: /** @} */
2039:
2040:
2041: // ########################################################################
2042: // SAML VALIDATION
2043: // ########################################################################
2044: /**
2045: * @addtogroup internalSAML
2046: * @{
2047: */
2048:
2049: /**
2050: * This method is used to validate a SAML TICKET; halt on failure, and sets
2051: * $validate_url, $text_reponse and $tree_response on success. These
2052: * parameters are used later by CAS_Client::_validatePGT() for CAS proxies.
2053: *
2054: * @param string &$validate_url reference to the the URL of the request to
2055: * the CAS server.
2056: * @param string &$text_response reference to the response of the CAS
2057: * server, as is (XML text).
2058: * @param string &$tree_response reference to the response of the CAS
2059: * server, as a DOM XML tree.
2060: * @param bool $renew true to force the authentication with the CAS server
2061: *
2062: * @return bool true when successfull and issue a CAS_AuthenticationException
2063: * and false on an error
2064: */
2065: public function validateSA(&$validate_url,&$text_response,&$tree_response,$renew=false)
2066: {
2067: phpCAS::traceBegin();
2068: $result = false;
2069: // build the URL to validate the ticket
2070: $validate_url = $this->getServerSamlValidateURL();
2071:
2072: if ( $renew ) {
2073: // pass the renew
2074: $validate_url .= '&renew=true';
2075: }
2076:
2077: // open and read the URL
2078: if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
2079: phpCAS::trace(
2080: 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'
2081: );
2082: throw new CAS_AuthenticationException(
2083: $this, 'SA not validated', $validate_url, true/*$no_response*/
2084: );
2085: }
2086:
2087: phpCAS::trace('server version: '.$this->getServerVersion());
2088:
2089: // analyze the result depending on the version
2090: switch ($this->getServerVersion()) {
2091: case SAML_VERSION_1_1:
2092: // create new DOMDocument Object
2093: $dom = new DOMDocument();
2094: // Fix possible whitspace problems
2095: $dom->preserveWhiteSpace = false;
2096: // read the response of the CAS server into a DOM object
2097: if (!($dom->loadXML($text_response))) {
2098: phpCAS::trace('dom->loadXML() failed');
2099: throw new CAS_AuthenticationException(
2100: $this, 'SA not validated', $validate_url,
2101: false/*$no_response*/, true/*$bad_response*/,
2102: $text_response
2103: );
2104: $result = false;
2105: }
2106: // read the root node of the XML tree
2107: if (!($tree_response = $dom->documentElement)) {
2108: phpCAS::trace('documentElement() failed');
2109: throw new CAS_AuthenticationException(
2110: $this, 'SA not validated', $validate_url,
2111: false/*$no_response*/, true/*$bad_response*/,
2112: $text_response
2113: );
2114: $result = false;
2115: } else if ( $tree_response->localName != 'Envelope' ) {
2116: // insure that tag name is 'Envelope'
2117: phpCAS::trace(
2118: 'bad XML root node (should be `Envelope\' instead of `'
2119: .$tree_response->localName.'\''
2120: );
2121: throw new CAS_AuthenticationException(
2122: $this, 'SA not validated', $validate_url,
2123: false/*$no_response*/, true/*$bad_response*/,
2124: $text_response
2125: );
2126: $result = false;
2127: } else if ($tree_response->getElementsByTagName("NameIdentifier")->length != 0) {
2128: // check for the NameIdentifier tag in the SAML response
2129: $success_elements = $tree_response->getElementsByTagName("NameIdentifier");
2130: phpCAS::trace('NameIdentifier found');
2131: $user = trim($success_elements->item(0)->nodeValue);
2132: phpCAS::trace('user = `'.$user.'`');
2133: $this->_setUser($user);
2134: $this->_setSessionAttributes($text_response);
2135: $result = true;
2136: } else {
2137: phpCAS::trace('no <NameIdentifier> tag found in SAML payload');
2138: throw new CAS_AuthenticationException(
2139: $this, 'SA not validated', $validate_url,
2140: false/*$no_response*/, true/*$bad_response*/,
2141: $text_response
2142: );
2143: $result = false;
2144: }
2145: }
2146: if ($result) {
2147: $this->_renameSession($this->getTicket());
2148: }
2149: // at this step, ST has been validated and $this->_user has been set,
2150: phpCAS::traceEnd($result);
2151: return $result;
2152: }
2153:
2154: /**
2155: * This method will parse the DOM and pull out the attributes from the SAML
2156: * payload and put them into an array, then put the array into the session.
2157: *
2158: * @param string $text_response the SAML payload.
2159: *
2160: * @return bool true when successfull and false if no attributes a found
2161: */
2162: private function _setSessionAttributes($text_response)
2163: {
2164: phpCAS::traceBegin();
2165:
2166: $result = false;
2167:
2168: $attr_array = array();
2169:
2170: // create new DOMDocument Object
2171: $dom = new DOMDocument();
2172: // Fix possible whitspace problems
2173: $dom->preserveWhiteSpace = false;
2174: if (($dom->loadXML($text_response))) {
2175: $xPath = new DOMXpath($dom);
2176: $xPath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol');
2177: $xPath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:1.0:assertion');
2178: $nodelist = $xPath->query("//saml:Attribute");
2179:
2180: if ($nodelist) {
2181: foreach ($nodelist as $node) {
2182: $xres = $xPath->query("saml:AttributeValue", $node);
2183: $name = $node->getAttribute("AttributeName");
2184: $value_array = array();
2185: foreach ($xres as $node2) {
2186: $value_array[] = $node2->nodeValue;
2187: }
2188: $attr_array[$name] = $value_array;
2189: }
2190: // UGent addition...
2191: foreach ($attr_array as $attr_key => $attr_value) {
2192: if (count($attr_value) > 1) {
2193: $this->_attributes[$attr_key] = $attr_value;
2194: phpCAS::trace("* " . $attr_key . "=" . print_r($attr_value, true));
2195: } else {
2196: $this->_attributes[$attr_key] = $attr_value[0];
2197: phpCAS::trace("* " . $attr_key . "=" . $attr_value[0]);
2198: }
2199: }
2200: $result = true;
2201: } else {
2202: phpCAS::trace("SAML Attributes are empty");
2203: $result = false;
2204: }
2205: }
2206: phpCAS::traceEnd($result);
2207: return $result;
2208: }
2209:
2210: /** @} */
2211:
2212: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2213: // XX XX
2214: // XX PROXY FEATURES (CAS 2.0) XX
2215: // XX XX
2216: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2217:
2218: // ########################################################################
2219: // PROXYING
2220: // ########################################################################
2221: /**
2222: * @addtogroup internalProxy
2223: * @{
2224: */
2225:
2226: /**
2227: * A boolean telling if the client is a CAS proxy or not. Written by
2228: * CAS_Client::CAS_Client(), read by CAS_Client::isProxy().
2229: */
2230: private $_proxy;
2231:
2232: /**
2233: * Handler for managing service cookies.
2234: */
2235: private $_serviceCookieJar;
2236:
2237: /**
2238: * Tells if a CAS client is a CAS proxy or not
2239: *
2240: * @return true when the CAS client is a CAs proxy, false otherwise
2241: */
2242: public function isProxy()
2243: {
2244: return $this->_proxy;
2245: }
2246:
2247:
2248: /** @} */
2249: // ########################################################################
2250: // PGT
2251: // ########################################################################
2252: /**
2253: * @addtogroup internalProxy
2254: * @{
2255: */
2256:
2257: /**
2258: * the Proxy Grnting Ticket given by the CAS server (empty otherwise).
2259: * Written by CAS_Client::_setPGT(), read by CAS_Client::_getPGT() and
2260: * CAS_Client::_hasPGT().
2261: *
2262: * @hideinitializer
2263: */
2264: private $_pgt = '';
2265:
2266: /**
2267: * This method returns the Proxy Granting Ticket given by the CAS server.
2268: *
2269: * @return string the Proxy Granting Ticket.
2270: */
2271: private function _getPGT()
2272: {
2273: return $this->_pgt;
2274: }
2275:
2276: /**
2277: * This method stores the Proxy Granting Ticket.
2278: *
2279: * @param string $pgt The Proxy Granting Ticket.
2280: *
2281: * @return void
2282: */
2283: private function _setPGT($pgt)
2284: {
2285: $this->_pgt = $pgt;
2286: }
2287:
2288: /**
2289: * This method tells if a Proxy Granting Ticket was stored.
2290: *
2291: * @return true if a Proxy Granting Ticket has been stored.
2292: */
2293: private function _hasPGT()
2294: {
2295: return !empty($this->_pgt);
2296: }
2297:
2298: /** @} */
2299:
2300: // ########################################################################
2301: // CALLBACK MODE
2302: // ########################################################################
2303: /**
2304: * @addtogroup internalCallback
2305: * @{
2306: */
2307: /**
2308: * each PHP script using phpCAS in proxy mode is its own callback to get the
2309: * PGT back from the CAS server. callback_mode is detected by the constructor
2310: * thanks to the GET parameters.
2311: */
2312:
2313: /**
2314: * a boolean to know if the CAS client is running in callback mode. Written by
2315: * CAS_Client::setCallBackMode(), read by CAS_Client::_isCallbackMode().
2316: *
2317: * @hideinitializer
2318: */
2319: private $_callback_mode = false;
2320:
2321: /**
2322: * This method sets/unsets callback mode.
2323: *
2324: * @param bool $callback_mode true to set callback mode, false otherwise.
2325: *
2326: * @return void
2327: */
2328: private function _setCallbackMode($callback_mode)
2329: {
2330: $this->_callback_mode = $callback_mode;
2331: }
2332:
2333: /**
2334: * This method returns true when the CAs client is running i callback mode,
2335: * false otherwise.
2336: *
2337: * @return A boolean.
2338: */
2339: private function _isCallbackMode()
2340: {
2341: return $this->_callback_mode;
2342: }
2343:
2344: /**
2345: * the URL that should be used for the PGT callback (in fact the URL of the
2346: * current request without any CGI parameter). Written and read by
2347: * CAS_Client::_getCallbackURL().
2348: *
2349: * @hideinitializer
2350: */
2351: private $_callback_url = '';
2352:
2353: /**
2354: * This method returns the URL that should be used for the PGT callback (in
2355: * fact the URL of the current request without any CGI parameter, except if
2356: * phpCAS::setFixedCallbackURL() was used).
2357: *
2358: * @return The callback URL
2359: */
2360: private function _getCallbackURL()
2361: {
2362: // the URL is built when needed only
2363: if ( empty($this->_callback_url) ) {
2364: $final_uri = '';
2365: // remove the ticket if present in the URL
2366: $final_uri = 'https://';
2367: $final_uri .= $this->_getClientUrl();
2368: $request_uri = $_SERVER['REQUEST_URI'];
2369: $request_uri = preg_replace('/\?.*$/', '', $request_uri);
2370: $final_uri .= $request_uri;
2371: $this->_callback_url = $final_uri;
2372: }
2373: return $this->_callback_url;
2374: }
2375:
2376: /**
2377: * This method sets the callback url.
2378: *
2379: * @param string $url url to set callback
2380: *
2381: * @return void
2382: */
2383: public function setCallbackURL($url)
2384: {
2385: // Sequence validation
2386: $this->ensureIsProxy();
2387: // Argument Validation
2388: if (gettype($url) != 'string')
2389: throw new CAS_TypeMismatchException($url, '$url', 'string');
2390:
2391: return $this->_callback_url = $url;
2392: }
2393:
2394: /**
2395: * This method is called by CAS_Client::CAS_Client() when running in callback
2396: * mode. It stores the PGT and its PGT Iou, prints its output and halts.
2397: *
2398: * @return void
2399: */
2400: private function _callback()
2401: {
2402: phpCAS::traceBegin();
2403: if (preg_match('/PGTIOU-[\.\-\w]/', $_GET['pgtIou'])) {
2404: if (preg_match('/[PT]GT-[\.\-\w]/', $_GET['pgtId'])) {
2405: $this->printHTMLHeader('phpCAS callback');
2406: $pgt_iou = $_GET['pgtIou'];
2407: $pgt = $_GET['pgtId'];
2408: phpCAS::trace('Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\')');
2409: echo '<p>Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\').</p>';
2410: $this->_storePGT($pgt, $pgt_iou);
2411: $this->printHTMLFooter();
2412: phpCAS::traceExit("Successfull Callback");
2413: } else {
2414: phpCAS::error('PGT format invalid' . $_GET['pgtId']);
2415: phpCAS::traceExit('PGT format invalid' . $_GET['pgtId']);
2416: }
2417: } else {
2418: phpCAS::error('PGTiou format invalid' . $_GET['pgtIou']);
2419: phpCAS::traceExit('PGTiou format invalid' . $_GET['pgtIou']);
2420: }
2421:
2422: // Flush the buffer to prevent from sending anything other then a 200
2423: // Success Status back to the CAS Server. The Exception would normally
2424: // report as a 500 error.
2425: flush();
2426: throw new CAS_GracefullTerminationException();
2427: }
2428:
2429:
2430: /** @} */
2431:
2432: // ########################################################################
2433: // PGT STORAGE
2434: // ########################################################################
2435: /**
2436: * @addtogroup internalPGTStorage
2437: * @{
2438: */
2439:
2440: /**
2441: * an instance of a class inheriting of PGTStorage, used to deal with PGT
2442: * storage. Created by CAS_Client::setPGTStorageFile(), used
2443: * by CAS_Client::setPGTStorageFile() and CAS_Client::_initPGTStorage().
2444: *
2445: * @hideinitializer
2446: */
2447: private $_pgt_storage = null;
2448:
2449: /**
2450: * This method is used to initialize the storage of PGT's.
2451: * Halts on error.
2452: *
2453: * @return void
2454: */
2455: private function _initPGTStorage()
2456: {
2457: // if no SetPGTStorageXxx() has been used, default to file
2458: if ( !is_object($this->_pgt_storage) ) {
2459: $this->setPGTStorageFile();
2460: }
2461:
2462: // initializes the storage
2463: $this->_pgt_storage->init();
2464: }
2465:
2466: /**
2467: * This method stores a PGT. Halts on error.
2468: *
2469: * @param string $pgt the PGT to store
2470: * @param string $pgt_iou its corresponding Iou
2471: *
2472: * @return void
2473: */
2474: private function _storePGT($pgt,$pgt_iou)
2475: {
2476: // ensure that storage is initialized
2477: $this->_initPGTStorage();
2478: // writes the PGT
2479: $this->_pgt_storage->write($pgt, $pgt_iou);
2480: }
2481:
2482: /**
2483: * This method reads a PGT from its Iou and deletes the corresponding
2484: * storage entry.
2485: *
2486: * @param string $pgt_iou the PGT Iou
2487: *
2488: * @return mul The PGT corresponding to the Iou, false when not found.
2489: */
2490: private function _loadPGT($pgt_iou)
2491: {
2492: // ensure that storage is initialized
2493: $this->_initPGTStorage();
2494: // read the PGT
2495: return $this->_pgt_storage->read($pgt_iou);
2496: }
2497:
2498: /**
2499: * This method can be used to set a custom PGT storage object.
2500: *
2501: * @param CAS_PGTStorage_AbstractStorage $storage a PGT storage object that
2502: * inherits from the CAS_PGTStorage_AbstractStorage class
2503: *
2504: * @return void
2505: */
2506: public function setPGTStorage($storage)
2507: {
2508: // Sequence validation
2509: $this->ensureIsProxy();
2510:
2511: // check that the storage has not already been set
2512: if ( is_object($this->_pgt_storage) ) {
2513: phpCAS::error('PGT storage already defined');
2514: }
2515:
2516: // check to make sure a valid storage object was specified
2517: if ( !($storage instanceof CAS_PGTStorage_AbstractStorage) )
2518: throw new CAS_TypeMismatchException($storage, '$storage', 'CAS_PGTStorage_AbstractStorage object');
2519:
2520: // store the PGTStorage object
2521: $this->_pgt_storage = $storage;
2522: }
2523:
2524: /**
2525: * This method is used to tell phpCAS to store the response of the
2526: * CAS server to PGT requests in a database.
2527: *
2528: * @param string $dsn_or_pdo a dsn string to use for creating a PDO
2529: * object or a PDO object
2530: * @param string $username the username to use when connecting to the
2531: * database
2532: * @param string $password the password to use when connecting to the
2533: * database
2534: * @param string $table the table to use for storing and retrieving
2535: * PGTs
2536: * @param string $driver_options any driver options to use when connecting
2537: * to the database
2538: *
2539: * @return void
2540: */
2541: public function setPGTStorageDb(
2542: $dsn_or_pdo, $username='', $password='', $table='', $driver_options=null
2543: ) {
2544: // Sequence validation
2545: $this->ensureIsProxy();
2546:
2547: // Argument validation
2548: if ((is_object($dsn_or_pdo) && !($dsn_or_pdo instanceof PDO)) || gettype($dsn_or_pdo) != 'string')
2549: throw new CAS_TypeMismatchException($dsn_or_pdo, '$dsn_or_pdo', 'string or PDO object');
2550: if (gettype($username) != 'string')
2551: throw new CAS_TypeMismatchException($username, '$username', 'string');
2552: if (gettype($password) != 'string')
2553: throw new CAS_TypeMismatchException($password, '$password', 'string');
2554: if (gettype($table) != 'string')
2555: throw new CAS_TypeMismatchException($table, '$password', 'string');
2556:
2557: // create the storage object
2558: $this->setPGTStorage(
2559: new CAS_PGTStorage_Db(
2560: $this, $dsn_or_pdo, $username, $password, $table, $driver_options
2561: )
2562: );
2563: }
2564:
2565: /**
2566: * This method is used to tell phpCAS to store the response of the
2567: * CAS server to PGT requests onto the filesystem.
2568: *
2569: * @param string $path the path where the PGT's should be stored
2570: *
2571: * @return void
2572: */
2573: public function setPGTStorageFile($path='')
2574: {
2575: // Sequence validation
2576: $this->ensureIsProxy();
2577:
2578: // Argument validation
2579: if (gettype($path) != 'string')
2580: throw new CAS_TypeMismatchException($path, '$path', 'string');
2581:
2582: // create the storage object
2583: $this->setPGTStorage(new CAS_PGTStorage_File($this, $path));
2584: }
2585:
2586:
2587: // ########################################################################
2588: // PGT VALIDATION
2589: // ########################################################################
2590: /**
2591: * This method is used to validate a PGT; halt on failure.
2592: *
2593: * @param string &$validate_url the URL of the request to the CAS server.
2594: * @param string $text_response the response of the CAS server, as is
2595: * (XML text); result of
2596: * CAS_Client::validateCAS10() or
2597: * CAS_Client::validateCAS20().
2598: * @param string $tree_response the response of the CAS server, as a DOM XML
2599: * tree; result of CAS_Client::validateCAS10() or CAS_Client::validateCAS20().
2600: *
2601: * @return bool true when successfull and issue a CAS_AuthenticationException
2602: * and false on an error
2603: */
2604: private function _validatePGT(&$validate_url,$text_response,$tree_response)
2605: {
2606: phpCAS::traceBegin();
2607: if ( $tree_response->getElementsByTagName("proxyGrantingTicket")->length == 0) {
2608: phpCAS::trace('<proxyGrantingTicket> not found');
2609: // authentication succeded, but no PGT Iou was transmitted
2610: throw new CAS_AuthenticationException(
2611: $this, 'Ticket validated but no PGT Iou transmitted',
2612: $validate_url, false/*$no_response*/, false/*$bad_response*/,
2613: $text_response
2614: );
2615: } else {
2616: // PGT Iou transmitted, extract it
2617: $pgt_iou = trim(
2618: $tree_response->getElementsByTagName("proxyGrantingTicket")->item(0)->nodeValue
2619: );
2620: if (preg_match('/PGTIOU-[\.\-\w]/', $pgt_iou)) {
2621: $pgt = $this->_loadPGT($pgt_iou);
2622: if ( $pgt == false ) {
2623: phpCAS::trace('could not load PGT');
2624: throw new CAS_AuthenticationException(
2625: $this,
2626: 'PGT Iou was transmitted but PGT could not be retrieved',
2627: $validate_url, false/*$no_response*/,
2628: false/*$bad_response*/, $text_response
2629: );
2630: }
2631: $this->_setPGT($pgt);
2632: } else {
2633: phpCAS::trace('PGTiou format error');
2634: throw new CAS_AuthenticationException(
2635: $this, 'PGT Iou was transmitted but has wrong format',
2636: $validate_url, false/*$no_response*/, false/*$bad_response*/,
2637: $text_response
2638: );
2639: }
2640: }
2641: phpCAS::traceEnd(true);
2642: return true;
2643: }
2644:
2645: // ########################################################################
2646: // PGT VALIDATION
2647: // ########################################################################
2648:
2649: /**
2650: * This method is used to retrieve PT's from the CAS server thanks to a PGT.
2651: *
2652: * @param string $target_service the service to ask for with the PT.
2653: * @param string &$err_code an error code (PHPCAS_SERVICE_OK on success).
2654: * @param string &$err_msg an error message (empty on success).
2655: *
2656: * @return a Proxy Ticket, or false on error.
2657: */
2658: public function retrievePT($target_service,&$err_code,&$err_msg)
2659: {
2660: // Argument validation
2661: if (gettype($target_service) != 'string')
2662: throw new CAS_TypeMismatchException($target_service, '$target_service', 'string');
2663:
2664: phpCAS::traceBegin();
2665:
2666: // by default, $err_msg is set empty and $pt to true. On error, $pt is
2667: // set to false and $err_msg to an error message. At the end, if $pt is false
2668: // and $error_msg is still empty, it is set to 'invalid response' (the most
2669: // commonly encountered error).
2670: $err_msg = '';
2671:
2672: // build the URL to retrieve the PT
2673: $cas_url = $this->getServerProxyURL().'?targetService='
2674: .urlencode($target_service).'&pgt='.$this->_getPGT();
2675:
2676: // open and read the URL
2677: if ( !$this->_readURL($cas_url, $headers, $cas_response, $err_msg) ) {
2678: phpCAS::trace(
2679: 'could not open URL \''.$cas_url.'\' to validate ('.$err_msg.')'
2680: );
2681: $err_code = PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE;
2682: $err_msg = 'could not retrieve PT (no response from the CAS server)';
2683: phpCAS::traceEnd(false);
2684: return false;
2685: }
2686:
2687: $bad_response = false;
2688:
2689: if ( !$bad_response ) {
2690: // create new DOMDocument object
2691: $dom = new DOMDocument();
2692: // Fix possible whitspace problems
2693: $dom->preserveWhiteSpace = false;
2694: // read the response of the CAS server into a DOM object
2695: if ( !($dom->loadXML($cas_response))) {
2696: phpCAS::trace('dom->loadXML() failed');
2697: // read failed
2698: $bad_response = true;
2699: }
2700: }
2701:
2702: if ( !$bad_response ) {
2703: // read the root node of the XML tree
2704: if ( !($root = $dom->documentElement) ) {
2705: phpCAS::trace('documentElement failed');
2706: // read failed
2707: $bad_response = true;
2708: }
2709: }
2710:
2711: if ( !$bad_response ) {
2712: // insure that tag name is 'serviceResponse'
2713: if ( $root->localName != 'serviceResponse' ) {
2714: phpCAS::trace('localName failed');
2715: // bad root node
2716: $bad_response = true;
2717: }
2718: }
2719:
2720: if ( !$bad_response ) {
2721: // look for a proxySuccess tag
2722: if ( $root->getElementsByTagName("proxySuccess")->length != 0) {
2723: $proxy_success_list = $root->getElementsByTagName("proxySuccess");
2724:
2725: // authentication succeded, look for a proxyTicket tag
2726: if ( $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->length != 0) {
2727: $err_code = PHPCAS_SERVICE_OK;
2728: $err_msg = '';
2729: $pt = trim(
2730: $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->item(0)->nodeValue
2731: );
2732: phpCAS::trace('original PT: '.trim($pt));
2733: phpCAS::traceEnd($pt);
2734: return $pt;
2735: } else {
2736: phpCAS::trace('<proxySuccess> was found, but not <proxyTicket>');
2737: }
2738: } else if ($root->getElementsByTagName("proxyFailure")->length != 0) {
2739: // look for a proxyFailure tag
2740: $proxy_failure_list = $root->getElementsByTagName("proxyFailure");
2741:
2742: // authentication failed, extract the error
2743: $err_code = PHPCAS_SERVICE_PT_FAILURE;
2744: $err_msg = 'PT retrieving failed (code=`'
2745: .$proxy_failure_list->item(0)->getAttribute('code')
2746: .'\', message=`'
2747: .trim($proxy_failure_list->item(0)->nodeValue)
2748: .'\')';
2749: phpCAS::traceEnd(false);
2750: return false;
2751: } else {
2752: phpCAS::trace('neither <proxySuccess> nor <proxyFailure> found');
2753: }
2754: }
2755:
2756: // at this step, we are sure that the response of the CAS server was
2757: // illformed
2758: $err_code = PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE;
2759: $err_msg = 'Invalid response from the CAS server (response=`'
2760: .$cas_response.'\')';
2761:
2762: phpCAS::traceEnd(false);
2763: return false;
2764: }
2765:
2766: /** @} */
2767:
2768: // ########################################################################
2769: // READ CAS SERVER ANSWERS
2770: // ########################################################################
2771:
2772: /**
2773: * @addtogroup internalMisc
2774: * @{
2775: */
2776:
2777: /**
2778: * This method is used to acces a remote URL.
2779: *
2780: * @param string $url the URL to access.
2781: * @param string &$headers an array containing the HTTP header lines of the
2782: * response (an empty array on failure).
2783: * @param string &$body the body of the response, as a string (empty on
2784: * failure).
2785: * @param string &$err_msg an error message, filled on failure.
2786: *
2787: * @return true on success, false otherwise (in this later case, $err_msg
2788: * contains an error message).
2789: */
2790: private function _readURL($url, &$headers, &$body, &$err_msg)
2791: {
2792: phpCAS::traceBegin();
2793: $className = $this->_requestImplementation;
2794: $request = new $className();
2795:
2796: if (count($this->_curl_options)) {
2797: $request->setCurlOptions($this->_curl_options);
2798: }
2799:
2800: $request->setUrl($url);
2801:
2802: if (empty($this->_cas_server_ca_cert) && !$this->_no_cas_server_validation) {
2803: phpCAS::error(
2804: 'one of the methods phpCAS::setCasServerCACert() or phpCAS::setNoCasServerValidation() must be called.'
2805: );
2806: }
2807: if ($this->_cas_server_ca_cert != '') {
2808: $request->setSslCaCert(
2809: $this->_cas_server_ca_cert, $this->_cas_server_cn_validate
2810: );
2811: }
2812:
2813: // add extra stuff if SAML
2814: if ($this->getServerVersion() == SAML_VERSION_1_1) {
2815: $request->addHeader("soapaction: http://www.oasis-open.org/committees/security");
2816: $request->addHeader("cache-control: no-cache");
2817: $request->addHeader("pragma: no-cache");
2818: $request->addHeader("accept: text/xml");
2819: $request->addHeader("connection: keep-alive");
2820: $request->addHeader("content-type: text/xml");
2821: $request->makePost();
2822: $request->setPostBody($this->_buildSAMLPayload());
2823: }
2824:
2825: if ($request->send()) {
2826: $headers = $request->getResponseHeaders();
2827: $body = $request->getResponseBody();
2828: $err_msg = '';
2829: phpCAS::traceEnd(true);
2830: return true;
2831: } else {
2832: $headers = '';
2833: $body = '';
2834: $err_msg = $request->getErrorMessage();
2835: phpCAS::traceEnd(false);
2836: return false;
2837: }
2838: }
2839:
2840: /**
2841: * This method is used to build the SAML POST body sent to /samlValidate URL.
2842: *
2843: * @return the SOAP-encased SAMLP artifact (the ticket).
2844: */
2845: private function _buildSAMLPayload()
2846: {
2847: phpCAS::traceBegin();
2848:
2849: //get the ticket
2850: $sa = urlencode($this->getTicket());
2851:
2852: $body = SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST
2853: .SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE
2854: .SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE;
2855:
2856: phpCAS::traceEnd($body);
2857: return ($body);
2858: }
2859:
2860: /** @} **/
2861:
2862: // ########################################################################
2863: // ACCESS TO EXTERNAL SERVICES
2864: // ########################################################################
2865:
2866: /**
2867: * @addtogroup internalProxyServices
2868: * @{
2869: */
2870:
2871:
2872: /**
2873: * Answer a proxy-authenticated service handler.
2874: *
2875: * @param string $type The service type. One of:
2876: * PHPCAS_PROXIED_SERVICE_HTTP_GET, PHPCAS_PROXIED_SERVICE_HTTP_POST,
2877: * PHPCAS_PROXIED_SERVICE_IMAP
2878: *
2879: * @return CAS_ProxiedService
2880: * @throws InvalidArgumentException If the service type is unknown.
2881: */
2882: public function getProxiedService ($type)
2883: {
2884: // Sequence validation
2885: $this->ensureIsProxy();
2886: $this->ensureAuthenticationCallSuccessful();
2887:
2888: // Argument validation
2889: if (gettype($type) != 'string')
2890: throw new CAS_TypeMismatchException($type, '$type', 'string');
2891:
2892: switch ($type) {
2893: case PHPCAS_PROXIED_SERVICE_HTTP_GET:
2894: case PHPCAS_PROXIED_SERVICE_HTTP_POST:
2895: $requestClass = $this->_requestImplementation;
2896: $request = new $requestClass();
2897: if (count($this->_curl_options)) {
2898: $request->setCurlOptions($this->_curl_options);
2899: }
2900: $proxiedService = new $type($request, $this->_serviceCookieJar);
2901: if ($proxiedService instanceof CAS_ProxiedService_Testable) {
2902: $proxiedService->setCasClient($this);
2903: }
2904: return $proxiedService;
2905: case PHPCAS_PROXIED_SERVICE_IMAP;
2906: $proxiedService = new CAS_ProxiedService_Imap($this->_getUser());
2907: if ($proxiedService instanceof CAS_ProxiedService_Testable) {
2908: $proxiedService->setCasClient($this);
2909: }
2910: return $proxiedService;
2911: default:
2912: throw new CAS_InvalidArgumentException(
2913: "Unknown proxied-service type, $type."
2914: );
2915: }
2916: }
2917:
2918: /**
2919: * Initialize a proxied-service handler with the proxy-ticket it should use.
2920: *
2921: * @param CAS_ProxiedService $proxiedService service handler
2922: *
2923: * @return void
2924: *
2925: * @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
2926: * The code of the Exception will be one of:
2927: * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
2928: * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
2929: * PHPCAS_SERVICE_PT_FAILURE
2930: * @throws CAS_ProxiedService_Exception If there is a failure getting the
2931: * url from the proxied service.
2932: */
2933: public function initializeProxiedService (CAS_ProxiedService $proxiedService)
2934: {
2935: // Sequence validation
2936: $this->ensureIsProxy();
2937: $this->ensureAuthenticationCallSuccessful();
2938:
2939: $url = $proxiedService->getServiceUrl();
2940: if (!is_string($url)) {
2941: throw new CAS_ProxiedService_Exception(
2942: "Proxied Service ".get_class($proxiedService)
2943: ."->getServiceUrl() should have returned a string, returned a "
2944: .gettype($url)." instead."
2945: );
2946: }
2947: $pt = $this->retrievePT($url, $err_code, $err_msg);
2948: if (!$pt) {
2949: throw new CAS_ProxyTicketException($err_msg, $err_code);
2950: }
2951: $proxiedService->setProxyTicket($pt);
2952: }
2953:
2954: /**
2955: * This method is used to access an HTTP[S] service.
2956: *
2957: * @param string $url the service to access.
2958: * @param int &$err_code an error code Possible values are
2959: * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE,
2960: * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE,
2961: * PHPCAS_SERVICE_NOT_AVAILABLE.
2962: * @param string &$output the output of the service (also used to give an error
2963: * message on failure).
2964: *
2965: * @return true on success, false otherwise (in this later case, $err_code
2966: * gives the reason why it failed and $output contains an error message).
2967: */
2968: public function serviceWeb($url,&$err_code,&$output)
2969: {
2970: // Sequence validation
2971: $this->ensureIsProxy();
2972: $this->ensureAuthenticationCallSuccessful();
2973:
2974: // Argument validation
2975: if (gettype($url) != 'string')
2976: throw new CAS_TypeMismatchException($url, '$url', 'string');
2977:
2978: try {
2979: $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_GET);
2980: $service->setUrl($url);
2981: $service->send();
2982: $output = $service->getResponseBody();
2983: $err_code = PHPCAS_SERVICE_OK;
2984: return true;
2985: } catch (CAS_ProxyTicketException $e) {
2986: $err_code = $e->getCode();
2987: $output = $e->getMessage();
2988: return false;
2989: } catch (CAS_ProxiedService_Exception $e) {
2990: $lang = $this->getLangObj();
2991: $output = sprintf(
2992: $lang->getServiceUnavailable(), $url, $e->getMessage()
2993: );
2994: $err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
2995: return false;
2996: }
2997: }
2998:
2999: /**
3000: * This method is used to access an IMAP/POP3/NNTP service.
3001: *
3002: * @param string $url a string giving the URL of the service, including
3003: * the mailing box for IMAP URLs, as accepted by imap_open().
3004: * @param string $serviceUrl a string giving for CAS retrieve Proxy ticket
3005: * @param string $flags options given to imap_open().
3006: * @param int &$err_code an error code Possible values are
3007: * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE,
3008: * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE,
3009: * PHPCAS_SERVICE_NOT_AVAILABLE.
3010: * @param string &$err_msg an error message on failure
3011: * @param string &$pt the Proxy Ticket (PT) retrieved from the CAS
3012: * server to access the URL on success, false on error).
3013: *
3014: * @return object an IMAP stream on success, false otherwise (in this later
3015: * case, $err_code gives the reason why it failed and $err_msg contains an
3016: * error message).
3017: */
3018: public function serviceMail($url,$serviceUrl,$flags,&$err_code,&$err_msg,&$pt)
3019: {
3020: // Sequence validation
3021: $this->ensureIsProxy();
3022: $this->ensureAuthenticationCallSuccessful();
3023:
3024: // Argument validation
3025: if (gettype($url) != 'string')
3026: throw new CAS_TypeMismatchException($url, '$url', 'string');
3027: if (gettype($serviceUrl) != 'string')
3028: throw new CAS_TypeMismatchException($serviceUrl, '$serviceUrl', 'string');
3029: if (gettype($flags) != 'integer')
3030: throw new CAS_TypeMismatchException($flags, '$flags', 'string');
3031:
3032: try {
3033: $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_IMAP);
3034: $service->setServiceUrl($serviceUrl);
3035: $service->setMailbox($url);
3036: $service->setOptions($flags);
3037:
3038: $stream = $service->open();
3039: $err_code = PHPCAS_SERVICE_OK;
3040: $pt = $service->getImapProxyTicket();
3041: return $stream;
3042: } catch (CAS_ProxyTicketException $e) {
3043: $err_msg = $e->getMessage();
3044: $err_code = $e->getCode();
3045: $pt = false;
3046: return false;
3047: } catch (CAS_ProxiedService_Exception $e) {
3048: $lang = $this->getLangObj();
3049: $err_msg = sprintf(
3050: $lang->getServiceUnavailable(),
3051: $url,
3052: $e->getMessage()
3053: );
3054: $err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
3055: $pt = false;
3056: return false;
3057: }
3058: }
3059:
3060: /** @} **/
3061:
3062: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3063: // XX XX
3064: // XX PROXIED CLIENT FEATURES (CAS 2.0) XX
3065: // XX XX
3066: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3067:
3068: // ########################################################################
3069: // PT
3070: // ########################################################################
3071: /**
3072: * @addtogroup internalService
3073: * @{
3074: */
3075:
3076: /**
3077: * This array will store a list of proxies in front of this application. This
3078: * property will only be populated if this script is being proxied rather than
3079: * accessed directly.
3080: *
3081: * It is set in CAS_Client::validateCAS20() and can be read by
3082: * CAS_Client::getProxies()
3083: *
3084: * @access private
3085: */
3086: private $_proxies = array();
3087:
3088: /**
3089: * Answer an array of proxies that are sitting in front of this application.
3090: *
3091: * This method will only return a non-empty array if we have received and
3092: * validated a Proxy Ticket.
3093: *
3094: * @return array
3095: * @access public
3096: */
3097: public function getProxies()
3098: {
3099: return $this->_proxies;
3100: }
3101:
3102: /**
3103: * Set the Proxy array, probably from persistant storage.
3104: *
3105: * @param array $proxies An array of proxies
3106: *
3107: * @return void
3108: * @access private
3109: */
3110: private function _setProxies($proxies)
3111: {
3112: $this->_proxies = $proxies;
3113: if (!empty($proxies)) {
3114: // For proxy-authenticated requests people are not viewing the URL
3115: // directly since the client is another application making a
3116: // web-service call.
3117: // Because of this, stripping the ticket from the URL is unnecessary
3118: // and causes another web-service request to be performed. Additionally,
3119: // if session handling on either the client or the server malfunctions
3120: // then the subsequent request will not complete successfully.
3121: $this->setNoClearTicketsFromUrl();
3122: }
3123: }
3124:
3125: /**
3126: * A container of patterns to be allowed as proxies in front of the cas client.
3127: *
3128: * @var CAS_ProxyChain_AllowedList
3129: */
3130: private $_allowed_proxy_chains;
3131:
3132: /**
3133: * Answer the CAS_ProxyChain_AllowedList object for this client.
3134: *
3135: * @return CAS_ProxyChain_AllowedList
3136: */
3137: public function getAllowedProxyChains ()
3138: {
3139: if (empty($this->_allowed_proxy_chains)) {
3140: $this->_allowed_proxy_chains = new CAS_ProxyChain_AllowedList();
3141: }
3142: return $this->_allowed_proxy_chains;
3143: }
3144:
3145: /** @} */
3146: // ########################################################################
3147: // PT VALIDATION
3148: // ########################################################################
3149: /**
3150: * @addtogroup internalProxied
3151: * @{
3152: */
3153:
3154: /**
3155: * This method is used to validate a cas 2.0 ST or PT; halt on failure
3156: * Used for all CAS 2.0 validations
3157: *
3158: * @param string &$validate_url the url of the reponse
3159: * @param string &$text_response the text of the repsones
3160: * @param string &$tree_response the domxml tree of the respones
3161: * @param bool $renew true to force the authentication with the CAS server
3162: *
3163: * @return bool true when successfull and issue a CAS_AuthenticationException
3164: * and false on an error
3165: */
3166: public function validateCAS20(&$validate_url,&$text_response,&$tree_response, $renew=false)
3167: {
3168: phpCAS::traceBegin();
3169: phpCAS::trace($text_response);
3170: $result = false;
3171: // build the URL to validate the ticket
3172: if ($this->getAllowedProxyChains()->isProxyingAllowed()) {
3173: $validate_url = $this->getServerProxyValidateURL().'&ticket='
3174: .urlencode($this->getTicket());
3175: } else {
3176: $validate_url = $this->getServerServiceValidateURL().'&ticket='
3177: .urlencode($this->getTicket());
3178: }
3179:
3180: if ( $this->isProxy() ) {
3181: // pass the callback url for CAS proxies
3182: $validate_url .= '&pgtUrl='.urlencode($this->_getCallbackURL());
3183: }
3184:
3185: if ( $renew ) {
3186: // pass the renew
3187: $validate_url .= '&renew=true';
3188: }
3189:
3190: // open and read the URL
3191: if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) {
3192: phpCAS::trace(
3193: 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'
3194: );
3195: throw new CAS_AuthenticationException(
3196: $this, 'Ticket not validated', $validate_url,
3197: true/*$no_response*/
3198: );
3199: $result = false;
3200: }
3201:
3202: // create new DOMDocument object
3203: $dom = new DOMDocument();
3204: // Fix possible whitspace problems
3205: $dom->preserveWhiteSpace = false;
3206: // CAS servers should only return data in utf-8
3207: $dom->encoding = "utf-8";
3208: // read the response of the CAS server into a DOMDocument object
3209: if ( !($dom->loadXML($text_response))) {
3210: // read failed
3211: throw new CAS_AuthenticationException(
3212: $this, 'Ticket not validated', $validate_url,
3213: false/*$no_response*/, true/*$bad_response*/, $text_response
3214: );
3215: $result = false;
3216: } else if ( !($tree_response = $dom->documentElement) ) {
3217: // read the root node of the XML tree
3218: // read failed
3219: throw new CAS_AuthenticationException(
3220: $this, 'Ticket not validated', $validate_url,
3221: false/*$no_response*/, true/*$bad_response*/, $text_response
3222: );
3223: $result = false;
3224: } else if ($tree_response->localName != 'serviceResponse') {
3225: // insure that tag name is 'serviceResponse'
3226: // bad root node
3227: throw new CAS_AuthenticationException(
3228: $this, 'Ticket not validated', $validate_url,
3229: false/*$no_response*/, true/*$bad_response*/, $text_response
3230: );
3231: $result = false;
3232: } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
3233: // authentication failed, extract the error code and message and throw exception
3234: $auth_fail_list = $tree_response
3235: ->getElementsByTagName("authenticationFailure");
3236: throw new CAS_AuthenticationException(
3237: $this, 'Ticket not validated', $validate_url,
3238: false/*$no_response*/, false/*$bad_response*/,
3239: $text_response,
3240: $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
3241: trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/
3242: );
3243: $result = false;
3244: } else if ($tree_response->getElementsByTagName("authenticationSuccess")->length != 0) {
3245: // authentication succeded, extract the user name
3246: $success_elements = $tree_response
3247: ->getElementsByTagName("authenticationSuccess");
3248: if ( $success_elements->item(0)->getElementsByTagName("user")->length == 0) {
3249: // no user specified => error
3250: throw new CAS_AuthenticationException(
3251: $this, 'Ticket not validated', $validate_url,
3252: false/*$no_response*/, true/*$bad_response*/, $text_response
3253: );
3254: $result = false;
3255: } else {
3256: $this->_setUser(
3257: trim(
3258: $success_elements->item(0)->getElementsByTagName("user")->item(0)->nodeValue
3259: )
3260: );
3261: $this->_readExtraAttributesCas20($success_elements);
3262: // Store the proxies we are sitting behind for authorization checking
3263: $proxyList = array();
3264: if ( sizeof($arr = $success_elements->item(0)->getElementsByTagName("proxy")) > 0) {
3265: foreach ($arr as $proxyElem) {
3266: phpCAS::trace("Found Proxy: ".$proxyElem->nodeValue);
3267: $proxyList[] = trim($proxyElem->nodeValue);
3268: }
3269: $this->_setProxies($proxyList);
3270: phpCAS::trace("Storing Proxy List");
3271: }
3272: // Check if the proxies in front of us are allowed
3273: if (!$this->getAllowedProxyChains()->isProxyListAllowed($proxyList)) {
3274: throw new CAS_AuthenticationException(
3275: $this, 'Proxy not allowed', $validate_url,
3276: false/*$no_response*/, true/*$bad_response*/,
3277: $text_response
3278: );
3279: $result = false;
3280: } else {
3281: $result = true;
3282: }
3283: }
3284: } else {
3285: throw new CAS_AuthenticationException(
3286: $this, 'Ticket not validated', $validate_url,
3287: false/*$no_response*/, true/*$bad_response*/,
3288: $text_response
3289: );
3290: $result = false;
3291: }
3292: if ($result) {
3293: $this->_renameSession($this->getTicket());
3294: }
3295: // at this step, Ticket has been validated and $this->_user has been set,
3296:
3297: phpCAS::traceEnd($result);
3298: return $result;
3299: }
3300:
3301:
3302: /**
3303: * This method will parse the DOM and pull out the attributes from the XML
3304: * payload and put them into an array, then put the array into the session.
3305: *
3306: * @param string $success_elements payload of the response
3307: *
3308: * @return bool true when successfull, halt otherwise by calling
3309: * CAS_Client::_authError().
3310: */
3311: private function _readExtraAttributesCas20($success_elements)
3312: {
3313: phpCAS::traceBegin();
3314:
3315: $extra_attributes = array();
3316:
3317: // "Jasig Style" Attributes:
3318: //
3319: // <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
3320: // <cas:authenticationSuccess>
3321: // <cas:user>jsmith</cas:user>
3322: // <cas:attributes>
3323: // <cas:attraStyle>RubyCAS</cas:attraStyle>
3324: // <cas:surname>Smith</cas:surname>
3325: // <cas:givenName>John</cas:givenName>
3326: // <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
3327: // <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf>
3328: // </cas:attributes>
3329: // <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
3330: // </cas:authenticationSuccess>
3331: // </cas:serviceResponse>
3332: //
3333: if ($this->_casAttributeParserCallbackFunction !== null
3334: && is_callable($this->_casAttributeParserCallbackFunction)
3335: ) {
3336: array_unshift($this->_casAttributeParserCallbackArgs, $success_elements->item(0));
3337: phpCas :: trace("Calling attritubeParser callback");
3338: $extra_attributes = call_user_func_array(
3339: $this->_casAttributeParserCallbackFunction,
3340: $this->_casAttributeParserCallbackArgs
3341: );
3342: } elseif ( $success_elements->item(0)->getElementsByTagName("attributes")->length != 0) {
3343: $attr_nodes = $success_elements->item(0)
3344: ->getElementsByTagName("attributes");
3345: phpCas :: trace("Found nested jasig style attributes");
3346: if ($attr_nodes->item(0)->hasChildNodes()) {
3347: // Nested Attributes
3348: foreach ($attr_nodes->item(0)->childNodes as $attr_child) {
3349: phpCas :: trace(
3350: "Attribute [".$attr_child->localName."] = "
3351: .$attr_child->nodeValue
3352: );
3353: $this->_addAttributeToArray(
3354: $extra_attributes, $attr_child->localName,
3355: $attr_child->nodeValue
3356: );
3357: }
3358: }
3359: } else {
3360: // "RubyCAS Style" attributes
3361: //
3362: // <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
3363: // <cas:authenticationSuccess>
3364: // <cas:user>jsmith</cas:user>
3365: //
3366: // <cas:attraStyle>RubyCAS</cas:attraStyle>
3367: // <cas:surname>Smith</cas:surname>
3368: // <cas:givenName>John</cas:givenName>
3369: // <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf>
3370: // <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf>
3371: //
3372: // <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
3373: // </cas:authenticationSuccess>
3374: // </cas:serviceResponse>
3375: //
3376: phpCas :: trace("Testing for rubycas style attributes");
3377: $childnodes = $success_elements->item(0)->childNodes;
3378: foreach ($childnodes as $attr_node) {
3379: switch ($attr_node->localName) {
3380: case 'user':
3381: case 'proxies':
3382: case 'proxyGrantingTicket':
3383: continue;
3384: default:
3385: if (strlen(trim($attr_node->nodeValue))) {
3386: phpCas :: trace(
3387: "Attribute [".$attr_node->localName."] = ".$attr_node->nodeValue
3388: );
3389: $this->_addAttributeToArray(
3390: $extra_attributes, $attr_node->localName,
3391: $attr_node->nodeValue
3392: );
3393: }
3394: }
3395: }
3396: }
3397:
3398: // "Name-Value" attributes.
3399: //
3400: // Attribute format from these mailing list thread:
3401: // http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html
3402: // Note: This is a less widely used format, but in use by at least two institutions.
3403: //
3404: // <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
3405: // <cas:authenticationSuccess>
3406: // <cas:user>jsmith</cas:user>
3407: //
3408: // <cas:attribute name='attraStyle' value='Name-Value' />
3409: // <cas:attribute name='surname' value='Smith' />
3410: // <cas:attribute name='givenName' value='John' />
3411: // <cas:attribute name='memberOf' value='CN=Staff,OU=Groups,DC=example,DC=edu' />
3412: // <cas:attribute name='memberOf' value='CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu' />
3413: //
3414: // <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket>
3415: // </cas:authenticationSuccess>
3416: // </cas:serviceResponse>
3417: //
3418: if (!count($extra_attributes)
3419: && $success_elements->item(0)->getElementsByTagName("attribute")->length != 0
3420: ) {
3421: $attr_nodes = $success_elements->item(0)
3422: ->getElementsByTagName("attribute");
3423: $firstAttr = $attr_nodes->item(0);
3424: if (!$firstAttr->hasChildNodes()
3425: && $firstAttr->hasAttribute('name')
3426: && $firstAttr->hasAttribute('value')
3427: ) {
3428: phpCas :: trace("Found Name-Value style attributes");
3429: // Nested Attributes
3430: foreach ($attr_nodes as $attr_node) {
3431: if ($attr_node->hasAttribute('name')
3432: && $attr_node->hasAttribute('value')
3433: ) {
3434: phpCas :: trace(
3435: "Attribute [".$attr_node->getAttribute('name')
3436: ."] = ".$attr_node->getAttribute('value')
3437: );
3438: $this->_addAttributeToArray(
3439: $extra_attributes, $attr_node->getAttribute('name'),
3440: $attr_node->getAttribute('value')
3441: );
3442: }
3443: }
3444: }
3445: }
3446:
3447: $this->setAttributes($extra_attributes);
3448: phpCAS::traceEnd();
3449: return true;
3450: }
3451:
3452: /**
3453: * Add an attribute value to an array of attributes.
3454: *
3455: * @param array &$attributeArray reference to array
3456: * @param string $name name of attribute
3457: * @param string $value value of attribute
3458: *
3459: * @return void
3460: */
3461: private function _addAttributeToArray(array &$attributeArray, $name, $value)
3462: {
3463: // If multiple attributes exist, add as an array value
3464: if (isset($attributeArray[$name])) {
3465: // Initialize the array with the existing value
3466: if (!is_array($attributeArray[$name])) {
3467: $existingValue = $attributeArray[$name];
3468: $attributeArray[$name] = array($existingValue);
3469: }
3470:
3471: $attributeArray[$name][] = trim($value);
3472: } else {
3473: $attributeArray[$name] = trim($value);
3474: }
3475: }
3476:
3477: /** @} */
3478:
3479: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3480: // XX XX
3481: // XX MISC XX
3482: // XX XX
3483: // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3484:
3485: /**
3486: * @addtogroup internalMisc
3487: * @{
3488: */
3489:
3490: // ########################################################################
3491: // URL
3492: // ########################################################################
3493: /**
3494: * the URL of the current request (without any ticket CGI parameter). Written
3495: * and read by CAS_Client::getURL().
3496: *
3497: * @hideinitializer
3498: */
3499: private $_url = '';
3500:
3501:
3502: /**
3503: * This method sets the URL of the current request
3504: *
3505: * @param string $url url to set for service
3506: *
3507: * @return void
3508: */
3509: public function setURL($url)
3510: {
3511: // Argument Validation
3512: if (gettype($url) != 'string')
3513: throw new CAS_TypeMismatchException($url, '$url', 'string');
3514:
3515: $this->_url = $url;
3516: }
3517:
3518: /**
3519: * This method returns the URL of the current request (without any ticket
3520: * CGI parameter).
3521: *
3522: * @return The URL
3523: */
3524: public function getURL()
3525: {
3526: phpCAS::traceBegin();
3527: // the URL is built when needed only
3528: if ( empty($this->_url) ) {
3529: $final_uri = '';
3530: // remove the ticket if present in the URL
3531: $final_uri = ($this->_isHttps()) ? 'https' : 'http';
3532: $final_uri .= '://';
3533:
3534: $final_uri .= $this->_getClientUrl();
3535: $request_uri = explode('?', $_SERVER['REQUEST_URI'], 2);
3536: $final_uri .= $request_uri[0];
3537:
3538: if (isset($request_uri[1]) && $request_uri[1]) {
3539: $query_string= $this->_removeParameterFromQueryString('ticket', $request_uri[1]);
3540:
3541: // If the query string still has anything left,
3542: // append it to the final URI
3543: if ($query_string !== '') {
3544: $final_uri .= "?$query_string";
3545: }
3546: }
3547:
3548: phpCAS::trace("Final URI: $final_uri");
3549: $this->setURL($final_uri);
3550: }
3551: phpCAS::traceEnd($this->_url);
3552: return $this->_url;
3553: }
3554:
3555: /**
3556: * This method sets the base URL of the CAS server.
3557: *
3558: * @param string $url the base URL
3559: *
3560: * @return string base url
3561: */
3562: public function setBaseURL($url)
3563: {
3564: // Argument Validation
3565: if (gettype($url) != 'string')
3566: throw new CAS_TypeMismatchException($url, '$url', 'string');
3567:
3568: return $this->_server['base_url'] = $url;
3569: }
3570:
3571:
3572: /**
3573: * Try to figure out the phpCas client URL with possible Proxys / Ports etc.
3574: *
3575: * @return string Server URL with domain:port
3576: */
3577: private function _getClientUrl()
3578: {
3579: $server_url = '';
3580: if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
3581: // explode the host list separated by comma and use the first host
3582: $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
3583: // see rfc7239#5.3 and rfc7230#2.7.1: port is in HTTP_X_FORWARDED_HOST if non default
3584: return $hosts[0];
3585: } else if (!empty($_SERVER['HTTP_X_FORWARDED_SERVER'])) {
3586: $server_url = $_SERVER['HTTP_X_FORWARDED_SERVER'];
3587: } else {
3588: if (empty($_SERVER['SERVER_NAME'])) {
3589: $server_url = $_SERVER['HTTP_HOST'];
3590: } else {
3591: $server_url = $_SERVER['SERVER_NAME'];
3592: }
3593: }
3594: if (!strpos($server_url, ':')) {
3595: if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
3596: $server_port = $_SERVER['SERVER_PORT'];
3597: } else {
3598: $ports = explode(',', $_SERVER['HTTP_X_FORWARDED_PORT']);
3599: $server_port = $ports[0];
3600: }
3601:
3602: if ( ($this->_isHttps() && $server_port!=443)
3603: || (!$this->_isHttps() && $server_port!=80)
3604: ) {
3605: $server_url .= ':';
3606: $server_url .= $server_port;
3607: }
3608: }
3609: return $server_url;
3610: }
3611:
3612: /**
3613: * This method checks to see if the request is secured via HTTPS
3614: *
3615: * @return bool true if https, false otherwise
3616: */
3617: private function _isHttps()
3618: {
3619: if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
3620: return ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
3621: } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) {
3622: return ($_SERVER['HTTP_X_FORWARDED_PROTOCOL'] === 'https');
3623: } elseif ( isset($_SERVER['HTTPS'])
3624: && !empty($_SERVER['HTTPS'])
3625: && strcasecmp($_SERVER['HTTPS'], 'off') !== 0
3626: ) {
3627: return true;
3628: }
3629: return false;
3630:
3631: }
3632:
3633: /**
3634: * Removes a parameter from a query string
3635: *
3636: * @param string $parameterName name of parameter
3637: * @param string $queryString query string
3638: *
3639: * @return string new query string
3640: *
3641: * @link http://stackoverflow.com/questions/1842681/regular-expression-to-remove-one-parameter-from-query-string
3642: */
3643: private function _removeParameterFromQueryString($parameterName, $queryString)
3644: {
3645: $parameterName = preg_quote($parameterName);
3646: return preg_replace(
3647: "/&$parameterName(=[^&]*)?|^$parameterName(=[^&]*)?&?/",
3648: '', $queryString
3649: );
3650: }
3651:
3652: /**
3653: * This method is used to append query parameters to an url. Since the url
3654: * might already contain parameter it has to be detected and to build a proper
3655: * URL
3656: *
3657: * @param string $url base url to add the query params to
3658: * @param string $query params in query form with & separated
3659: *
3660: * @return url with query params
3661: */
3662: private function _buildQueryUrl($url, $query)
3663: {
3664: $url .= (strstr($url, '?') === false) ? '?' : '&';
3665: $url .= $query;
3666: return $url;
3667: }
3668:
3669: /**
3670: * Renaming the session
3671: *
3672: * @param string $ticket name of the ticket
3673: *
3674: * @return void
3675: */
3676: private function _renameSession($ticket)
3677: {
3678: phpCAS::traceBegin();
3679: if ($this->getChangeSessionID()) {
3680: if (!empty($this->_user)) {
3681: $old_session = $_SESSION;
3682: phpCAS :: trace("Killing session: ". session_id());
3683: session_destroy();
3684: // set up a new session, of name based on the ticket
3685: $session_id = preg_replace('/[^a-zA-Z0-9\-]/', '', $ticket);
3686: phpCAS :: trace("Starting session: ". $session_id);
3687: session_id($session_id);
3688: session_start();
3689: phpCAS :: trace("Restoring old session vars");
3690: $_SESSION = $old_session;
3691: } else {
3692: phpCAS :: trace (
3693: 'Session should only be renamed after successfull authentication'
3694: );
3695: }
3696: } else {
3697: phpCAS :: trace(
3698: "Skipping session rename since phpCAS is not handling the session."
3699: );
3700: }
3701: phpCAS::traceEnd();
3702: }
3703:
3704:
3705: // ########################################################################
3706: // AUTHENTICATION ERROR HANDLING
3707: // ########################################################################
3708: /**
3709: * This method is used to print the HTML output when the user was not
3710: * authenticated.
3711: *
3712: * @param string $failure the failure that occured
3713: * @param string $cas_url the URL the CAS server was asked for
3714: * @param bool $no_response the response from the CAS server (other
3715: * parameters are ignored if true)
3716: * @param bool $bad_response bad response from the CAS server ($err_code
3717: * and $err_msg ignored if true)
3718: * @param string $cas_response the response of the CAS server
3719: * @param int $err_code the error code given by the CAS server
3720: * @param string $err_msg the error message given by the CAS server
3721: *
3722: * @return void
3723: */
3724: private function _authError(
3725: $failure,
3726: $cas_url,
3727: $no_response,
3728: $bad_response='',
3729: $cas_response='',
3730: $err_code='',
3731: $err_msg=''
3732: ) {
3733: phpCAS::traceBegin();
3734: $lang = $this->getLangObj();
3735: $this->printHTMLHeader($lang->getAuthenticationFailed());
3736: printf(
3737: $lang->getYouWereNotAuthenticated(), htmlentities($this->getURL()),
3738: isset($_SERVER['SERVER_ADMIN']) ? $_SERVER['SERVER_ADMIN']:''
3739: );
3740: phpCAS::trace('CAS URL: '.$cas_url);
3741: phpCAS::trace('Authentication failure: '.$failure);
3742: if ( $no_response ) {
3743: phpCAS::trace('Reason: no response from the CAS server');
3744: } else {
3745: if ( $bad_response ) {
3746: phpCAS::trace('Reason: bad response from the CAS server');
3747: } else {
3748: switch ($this->getServerVersion()) {
3749: case CAS_VERSION_1_0:
3750: phpCAS::trace('Reason: CAS error');
3751: break;
3752: case CAS_VERSION_2_0:
3753: case CAS_VERSION_3_0:
3754: if ( empty($err_code) ) {
3755: phpCAS::trace('Reason: no CAS error');
3756: } else {
3757: phpCAS::trace(
3758: 'Reason: ['.$err_code.'] CAS error: '.$err_msg
3759: );
3760: }
3761: break;
3762: }
3763: }
3764: phpCAS::trace('CAS response: '.$cas_response);
3765: }
3766: $this->printHTMLFooter();
3767: phpCAS::traceExit();
3768: throw new CAS_GracefullTerminationException();
3769: }
3770:
3771: // ########################################################################
3772: // PGTIOU/PGTID and logoutRequest rebroadcasting
3773: // ########################################################################
3774:
3775: /**
3776: * Boolean of whether to rebroadcast pgtIou/pgtId and logoutRequest, and
3777: * array of the nodes.
3778: */
3779: private $_rebroadcast = false;
3780: private $_rebroadcast_nodes = array();
3781:
3782: /**
3783: * Constants used for determining rebroadcast node type.
3784: */
3785: const HOSTNAME = 0;
3786: const IP = 1;
3787:
3788: /**
3789: * Determine the node type from the URL.
3790: *
3791: * @param String $nodeURL The node URL.
3792: *
3793: * @return string hostname
3794: *
3795: */
3796: private function _getNodeType($nodeURL)
3797: {
3798: phpCAS::traceBegin();
3799: if (preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $nodeURL)) {
3800: phpCAS::traceEnd(self::IP);
3801: return self::IP;
3802: } else {
3803: phpCAS::traceEnd(self::HOSTNAME);
3804: return self::HOSTNAME;
3805: }
3806: }
3807:
3808: /**
3809: * Store the rebroadcast node for pgtIou/pgtId and logout requests.
3810: *
3811: * @param string $rebroadcastNodeUrl The rebroadcast node URL.
3812: *
3813: * @return void
3814: */
3815: public function addRebroadcastNode($rebroadcastNodeUrl)
3816: {
3817: // Argument validation
3818: if ( !(bool)preg_match("/^(http|https):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i", $rebroadcastNodeUrl))
3819: throw new CAS_TypeMismatchException($rebroadcastNodeUrl, '$rebroadcastNodeUrl', 'url');
3820:
3821: // Store the rebroadcast node and set flag
3822: $this->_rebroadcast = true;
3823: $this->_rebroadcast_nodes[] = $rebroadcastNodeUrl;
3824: }
3825:
3826: /**
3827: * An array to store extra rebroadcast curl options.
3828: */
3829: private $_rebroadcast_headers = array();
3830:
3831: /**
3832: * This method is used to add header parameters when rebroadcasting
3833: * pgtIou/pgtId or logoutRequest.
3834: *
3835: * @param string $header Header to send when rebroadcasting.
3836: *
3837: * @return void
3838: */
3839: public function addRebroadcastHeader($header)
3840: {
3841: if (gettype($header) != 'string')
3842: throw new CAS_TypeMismatchException($header, '$header', 'string');
3843:
3844: $this->_rebroadcast_headers[] = $header;
3845: }
3846:
3847: /**
3848: * Constants used for determining rebroadcast type (logout or pgtIou/pgtId).
3849: */
3850: const LOGOUT = 0;
3851: const PGTIOU = 1;
3852:
3853: /**
3854: * This method rebroadcasts logout/pgtIou requests. Can be LOGOUT,PGTIOU
3855: *
3856: * @param int $type type of rebroadcasting.
3857: *
3858: * @return void
3859: */
3860: private function _rebroadcast($type)
3861: {
3862: phpCAS::traceBegin();
3863:
3864: $rebroadcast_curl_options = array(
3865: CURLOPT_FAILONERROR => 1,
3866: CURLOPT_FOLLOWLOCATION => 1,
3867: CURLOPT_RETURNTRANSFER => 1,
3868: CURLOPT_CONNECTTIMEOUT => 1,
3869: CURLOPT_TIMEOUT => 4);
3870:
3871: // Try to determine the IP address of the server
3872: if (!empty($_SERVER['SERVER_ADDR'])) {
3873: $ip = $_SERVER['SERVER_ADDR'];
3874: } else if (!empty($_SERVER['LOCAL_ADDR'])) {
3875: // IIS 7
3876: $ip = $_SERVER['LOCAL_ADDR'];
3877: }
3878: // Try to determine the DNS name of the server
3879: if (!empty($ip)) {
3880: $dns = gethostbyaddr($ip);
3881: }
3882: $multiClassName = 'CAS_Request_CurlMultiRequest';
3883: $multiRequest = new $multiClassName();
3884:
3885: for ($i = 0; $i < sizeof($this->_rebroadcast_nodes); $i++) {
3886: if ((($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::HOSTNAME) && !empty($dns) && (stripos($this->_rebroadcast_nodes[$i], $dns) === false))
3887: || (($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::IP) && !empty($ip) && (stripos($this->_rebroadcast_nodes[$i], $ip) === false))
3888: ) {
3889: phpCAS::trace(
3890: 'Rebroadcast target URL: '.$this->_rebroadcast_nodes[$i]
3891: .$_SERVER['REQUEST_URI']
3892: );
3893: $className = $this->_requestImplementation;
3894: $request = new $className();
3895:
3896: $url = $this->_rebroadcast_nodes[$i].$_SERVER['REQUEST_URI'];
3897: $request->setUrl($url);
3898:
3899: if (count($this->_rebroadcast_headers)) {
3900: $request->addHeaders($this->_rebroadcast_headers);
3901: }
3902:
3903: $request->makePost();
3904: if ($type == self::LOGOUT) {
3905: // Logout request
3906: $request->setPostBody(
3907: 'rebroadcast=false&logoutRequest='.$_POST['logoutRequest']
3908: );
3909: } else if ($type == self::PGTIOU) {
3910: // pgtIou/pgtId rebroadcast
3911: $request->setPostBody('rebroadcast=false');
3912: }
3913:
3914: $request->setCurlOptions($rebroadcast_curl_options);
3915:
3916: $multiRequest->addRequest($request);
3917: } else {
3918: phpCAS::trace(
3919: 'Rebroadcast not sent to self: '
3920: .$this->_rebroadcast_nodes[$i].' == '.(!empty($ip)?$ip:'')
3921: .'/'.(!empty($dns)?$dns:'')
3922: );
3923: }
3924: }
3925: // We need at least 1 request
3926: if ($multiRequest->getNumRequests() > 0) {
3927: $multiRequest->send();
3928: }
3929: phpCAS::traceEnd();
3930: }
3931:
3932: /** @} */
3933: }
3934:
3935: ?>
3936: