<?php
// Thrown when internal error occurs
class JasigException extends Exception {}
// Thrown when CAS server returns authentication error
class JasigAuthException extends JasigException {}

class JasigUser {
	public $user;
	public $attributes = array();
}

class uphpCAS {
	const VERSION = '1.0';
	protected $serverUrl = '';
	protected $serviceUrl;
	protected $sessionName = 'uphpCAS-user';
	protected $sessionStarted = FALSE;
	protected $method = 'POST';
	protected $caFile = NULL;
	
	function __construct($serverUrl = NULL, $serviceUrl = NULL, $sessionName = NULL) {
		if($serverUrl != NULL) {
			$this->serverUrl = rtrim($serverUrl, '/');
		}
		
		if($serviceUrl != NULL) {
			$this->serviceUrl = $serviceUrl;
		} else {
			$this->serviceUrl = $this->getCurrentUrl();
		}
		
		if($sessionName) {
			$this->sessionName = $sessionName;
		}
		
		if(version_compare(PHP_VERSION, '5.6', '<')) {
			$this->caFile = $this->findCaFile();
		}
	}
	
	public function getCurrentUrl() {
		$url = 'http://';
		$port = 0;
		if(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
			$url = 'https://';
			if(isset($_SERVER['SERVER_PORT'])
					&& $_SERVER['SERVER_PORT'] != '443') {
				$port = $_SERVER['SERVER_PORT'];
			}
		} elseif(isset($_SERVER['SERVER_PORT'])
				&& $_SERVER['SERVER_PORT'] != '80') {
			$port = $_SERVER['SERVER_PORT'];
		}
		
		$url .= $_SERVER['SERVER_NAME'];
		
		if($port != 0) {
			$url .= ':'.$port;
		}
		
		$url .= $_SERVER['REQUEST_URI'];
		
		if(isset($_GET['ticket'])) {
			$pos = max(
				strrpos($url, '?ticket='),
				strrpos($url, '&ticket=')
			);
			$url = substr($url, 0, $pos);
		}
		
		return $url;
	}
	
	public function getServerUrl() {
		return $this->serverUrl;
	}
	public function setServerUrl($serverUrl) {
		$this->serverUrl = $serverUrl;
	}
	
	public function getServiceUrl() {
		return $this->serviceUrl;
	}
	public function setServiceUrl($serviceUrl) {
		$this->serviceUrl = $serviceUrl;
	}
	
	public function getSessionName() {
		return $this->sessionName;
	}
	public function setSessionName($sessionName) {
		$this->sessionName = $sessionName;
	}
	
	public function getMethod() {
		return $this->method;
	}
	public function setMethod($method) {
		if($method != 'GET' && $method != 'POST') {
			throw new DomainException('Unsupported CAS response'
				.' method: '.$method);
		}
		$this->method = $method;
	}
	
	public function getCaFile() {
		return $this->caFile;
	}
	public function setCaFile($caFile) {
		if(!is_file($caFile)) {
			throw new DomainException('Invalid CA file: '.$caFile);
		}
		$this->caFile = $caFile;
	}
	
	public function session_start() {
		if($this->sessionStarted) {
			return TRUE;
		}
		if(version_compare(PHP_VERSION, '7.1.0', '<')) {
			@session_start();
		} else {
			if(!isset($_SESSION)) {
				if(!session_start()) {
					throw new RuntimeException('Cannot start session');
				}
			}
		}
		$this->sessionStarted = TRUE;
		return TRUE;
	}
	
	public function loginUrl() {
		return $this->serverUrl.'/login?method='.$this->method
			.'&service='.urlencode($this->serviceUrl);
	}
	
	public function logoutUrl($returnUrl = NULL) {
		return $this->serverUrl.'/logout'
			.($returnUrl ? '?service='.urlencode($returnUrl) : '');
	}
	
	public function logoutLocal() {
		$this->session_start();
		unset($_SESSION[$this->sessionName]);
	}
	
	public function logout($returnUrl = NULL) {
		$this->logoutLocal();
		if($this->isAuthenticated()) {
			header('Location: '.$this->logoutUrl($returnUrl));
			die();
		} elseif($returnUrl) {
			header('Location: '.$returnUrl);
			die();
		}
	}
	
	public function isAuthenticated() {
		$this->session_start();
		return isset($_SESSION[$this->sessionName]);
	}
	
	public function authenticate() {
		$this->session_start();
		if($this->isAuthenticated()) {
			return $_SESSION[$this->sessionName];
		} elseif(isset($_REQUEST['ticket'])) {
			$user = $this->verifyTicket($_REQUEST['ticket']);
			session_regenerate_id();
			$_SESSION[$this->sessionName] = $user;
			return $user;
		} else {
			header('Location: '.$this->loginUrl());
			die();
		}
	}
	
	protected function findCaFile() {
		$cafiles = array(
			'/etc/ssl/certs/ca-certificates.crt',
			'/etc/ssl/certs/ca-bundle.crt',
			'/etc/pki/tls/certs/ca-bundle.crt',
		);
		
		$cafile = NULL;
		foreach($cafiles as $file) {
			if(is_file($file)) {
				$cafile = $file;
				break;
			}
		}
		
		return $cafile;
	}
	
	protected function createStreamContext($hostname) {
		$context = array(
			'http' => array(
				'method' => 'GET',
				'user_agent' => 'uphpCAS/'.self::VERSION,
				'max_redirects' => 3,
			),
			'ssl' => array(
				'verify_peer' => TRUE,
				'verify_peer_name' => TRUE,
				'verify_depth' => 5,
				'allow_self_signed' => FALSE,
				'disable_compression' => TRUE,
			),
		);
		
		if($this->caFile) {
			$context['ssl']['cafile'] = $this->caFile;
		}
		
		if(version_compare(PHP_VERSION, '5.6', '<')) {
			$context['ssl']['ciphers'] = 'ECDH:DH:AES:CAMELLIA:!SSLv2:!aNULL'
				.':!eNULL:!EXPORT:!DES:!3DES:!MD5:!RC4:!ADH:!PSK:!SRP';
			$context['ssl']['CN_match'] = $hostname;
		}
		
		return stream_context_create($context);
	}
	
	public function verifyTicket($ticket) {
		$url = parse_url($this->serverUrl);
		$context = $this->createStreamContext($url['host']);
		
		$data = file_get_contents($this->serverUrl
					.'/serviceValidate?service='.urlencode($this->serviceUrl)
					.'&ticket='.urlencode($ticket), FALSE, $context);
		if($data === FALSE) {
			throw new JasigException('Authentication error: CAS server is unavailable');
		}
		
		$xmlEntityLoader = libxml_disable_entity_loader(TRUE);
		$xmlInternalErrors = libxml_use_internal_errors(TRUE);
		try {
			$xml = new DOMDocument();
			$xml->loadXML($data);
			
			foreach(libxml_get_errors() as $error) {
				$e = new ErrorException($error->message, $error->code, 1,
						$error->file, $error->line);
				switch ($error->level) {
					case LIBXML_ERR_ERROR:
					case LIBXML_ERR_FATAL:
						throw new Exception('Fatal error during XML parsing',
								0, $e);
						break;
				}
			}
		} catch(Exception $e) {
			libxml_clear_errors();
			libxml_disable_entity_loader($xmlEntityLoader);
			libxml_use_internal_errors($xmlInternalErrors);
			throw new JasigException('Authentication error: CAS server'
					.' response invalid - parse error', 0, $e);
		}
		libxml_clear_errors();
		libxml_disable_entity_loader($xmlEntityLoader);
		libxml_use_internal_errors($xmlInternalErrors);
		
		$failure = $xml->getElementsByTagName('authenticationFailure');
		$success = $xml->getElementsByTagName('authenticationSuccess');
		
		if($failure->length > 0) {
			$failure = $failure->item(0);
			if(!($failure instanceof DOMElement)) {
				throw new JasigException('Authentication error: CAS server'
						.' response invalid - authenticationFailure');
			}
			throw new JasigAuthException('Authentication error: '
					.$failure->textContent);
		} elseif($success->length > 0) {
			$success = $success->item(0);
			if(!($success instanceof DOMElement)) {
				throw new JasigException('Authentication error: CAS server'
						.' response invalid - authenticationSuccess');
			}
			
			$user = $success->getElementsByTagName('user');
			if($user->length == 0) {
				throw new JasigException('Authentication error: CAS server'
						.' response invalid - user');
			}
			
			$user = trim($user->item(0)->textContent);
			if(strlen($user) < 1) {
				throw new JasigException('Authentication error: CAS server'
						.' response invalid - user value');
			}
			
			$jusr = new JasigUser();
			$jusr->user = $user;
			
			$attrs = $success->getElementsByTagName('attributes');
			if($attrs->length > 0) {
				$attrs = $attrs->item(0);
				foreach($attrs->childNodes as $node) {
					$jusr->attributes[$node->localName] = $node->textContent;
				}
			}
			
			return $jusr;
		} else {
			throw new JasigException('Authentication error: CAS server'
					.' response invalid - required tag not found');
		}
	}
}