<?php 
/**
* @version		$Id: defender.class.php 2403 2021-04-10 17:15:42Z IOS $
* @package		Elxis
* @copyright	Copyright (c) 2006-2021 Elxis CMS (https://www.elxis.org). All rights reserved.
* @license		Elxis Public License ( https://www.elxis.org/elxis-public-license.html )
* @author		Elxis Team ( https://www.elxis.org )
* @description 	Elxis CMS is free software. Read the license for copyright notices and details
*/

defined('_ELXIS_') or die ('Direct access to this location is not allowed.');


$defstart = microtime(true);

if (isset($_SERVER['QUERY_STRING'])) {
	if (strpos(urldecode($_SERVER['QUERY_STRING']), chr(0)) !== false) {
		exitPage::make('security', 'DEF-0001');
	}
    if (preg_match("#((\%0d)|(\%0a)|(\\\r)|(\\\n))#", $_SERVER['QUERY_STRING'])) {
    	exitPage::make('security', 'DEF-0002', 'Possible CRLF injection/HTTP response split.');
    }
}

if (!empty($_COOKIE)) {
    if (preg_match("#((\%0d)|(\%0a)|(\\\r)|(\\\n))#", serialize($_COOKIE))) {
    	exitPage::make('security', 'DEF-0003', 'Possible CRLF injection/HTTP response split.');
    }
}


class elxisDefender {

	private $cfg = NULL;
	private $types = array();
	private $repo_path = '';
	private $query = '';
	private $requesturi = '';
	private $address = '';
	private $host = '';
	private $referer = '';
	private $useragent = '';
	private $rawpost = '';
	private $bantimes = 3;
	private $banmessage = '';
	private $ipcheckafter = false;
	private $triggered_rule = '';
	private $banduration = 864000;//in seconds, by default 10 days


	/*********************/
	/* MAGIC CONSTRUCTOR */
	/*********************/
	public function __construct() {
		elxisLoader::loadFile('configuration.php');
		$this->cfg = new elxisConfig();

		$this->repo_path = rtrim($this->cfg->get('REPO_PATH'), '/');
		if ($this->repo_path == '') { $this->repo_path = ELXIS_PATH.'/repository'; }
		if (!file_exists($this->repo_path.'/logs/') || !is_dir($this->repo_path.'/logs/')) {
			die('Folder logs in Elxis repository does not exist!');
		}

		if ($this->cfg->get('SECURITY_LEVEL', 0) > 0) {
			$this->ipcheckafter = false;
		} else {
			$this->ipcheckafter = ($this->cfg->get('DEFENDER_IPAFTER', 1) == 1) ? true : false;
		}

		$this->setTypes();
		if (count($this->types) == 0) { return; }

		if (isset($_SERVER['QUERY_STRING'])) {
			$this->query = strtolower(urldecode($_SERVER['QUERY_STRING']));
			$this->query = htmlspecialchars_decode($this->query, ENT_QUOTES);
		}

		$this->requesturi = strtolower(urldecode($this->getURI()));
		$this->requesturi = htmlspecialchars_decode($this->requesturi, ENT_QUOTES);

		$this->address = $this->getIP();
		if ($this->address == '') {
			$this->securityLogger(true, 'DEFB-0002', 'Empty IP address', 'IP');
			exitPage::make('security', 'DEFB-0002', 'Your IP address could not be detected!');
		}

		if (strpos($this->address, ':') !== false) { //IPv6
			if (filter_var($this->address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) {
				$this->securityLogger(true, 'DEFB-0003', 'Invalid IP v6 address', 'IP');
				exitPage::make('security', 'DEFB-0003', 'Your IP address is not valid IPv6!');
			}
		} else { //IPv4
			if (filter_var($this->address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
				$this->securityLogger(true, 'DEFB-0004', 'Invalid IP v4 address', 'IP');
				exitPage::make('security', 'DEFB-0004', 'Your IP address is not valid IPv4!');
			}
		}

		if (function_exists('gethostbyaddr') && is_callable('gethostbyaddr')) {
			$this->host = strtolower(gethostbyaddr($this->address)); 
		}

		if (isset($_SERVER['HTTP_REFERER'])) {
			$this->referer = strtolower($_SERVER['HTTP_REFERER']);
		}

		if (isset($_SERVER['HTTP_USER_AGENT'])) {
			$this->useragent = strtolower(urldecode($_SERVER['HTTP_USER_AGENT']));
			$this->useragent = htmlspecialchars_decode($this->useragent, ENT_QUOTES);
		}

		if (isset($_SERVER['REQUEST_METHOD'])) {
			if (($_SERVER['REQUEST_METHOD'] == 'PUT') || ($_SERVER['REQUEST_METHOD'] == 'DELETE') || ($_SERVER['REQUEST_METHOD'] == 'TRACE')) {
				$msg = 'Request method '.$_SERVER['REQUEST_METHOD'].' is not allowed';
				$this->securityLogger(true, 'DEFB-0005', $msg, 'OTHER');
				exitPage::make('security', 'DEFB-0005', $msg);
			}
		}

		if (!empty($_POST)) {
			$this->rawpost = file_get_contents('php://input');
			$this->rawpost = strtolower(substr($this->rawpost, 0, 4194304));
		}
	}


	/***************************/
	/* PERFORM SECURITY CHECKS */
	/***************************/
	public function check() {
		if (count($this->types) == 0) { return; }

		$this->checkBanned();

		if ($this->cfg->get('DEFENDER_WHITELIST', '') != '') {
			$parts = explode(',', $this->cfg->get('DEFENDER_WHITELIST', ''));
			if (in_array($this->address, $parts)) { return; }
		}

		if ($this->cfg->get('SECURITY_LEVEL', 0) > 0) {
			if (isset($_SERVER['REQUEST_METHOD']) && ($_SERVER['REQUEST_METHOD'] == 'POST')) {
				if ($this->referer == '') {//If POST, then there must be a referer! 
					//Be careful: causes problems on third party services like Paypal IPN
					$this->securityLogger(true, 'DEFB-0006', 'Empty HTTP REFERER on POST request', 'POST');
					exitPage::make('security', 'DEFB-0006', 'Empty HTTP REFERER on POST request!');
				}
			}

			if ($this->useragent == '') {
				$this->securityLogger(true, 'DEFB-0007', 'Empty user agent!', 'AGENT');
				exitPage::make('security', 'DEFB-0007', 'Empty user agent!');
			}
		}

		if (!$this->ipcheckafter) {
			if (in_array('R', $this->types)) {//IP RANGES
				$block = $this->checkAutoIPRanges();
				if ($block) {
					$this->banIP('DEFR-AUTO');
					$this->securityLogger(true, 'DEFR-AUTO', 'Blacklisted IP banned. Checked using IP ranges.', 'IP');
					if ($this->cfg->get('DEFENDER_NOTIFY') == 2) {
						$mailmsg = "IP address ".$this->address." belongs to a blacklisted IPs range!\r\n";
						$mailmsg .= "The list of blacklisted IP ranges is updated automatically every 24 hours.\r\n";
						$this->sendAlert('SEC-DEFR-AUTO', $mailmsg, 1);
					}
					exitPage::make('security', 'DEFR-AUTO', 'Your IP address is blacklisted!');
				}
				unset($block);
			}

			if (in_array('I', $this->types)) {//SPECIFIC IPs
				$block = $this->checkAutoIP();
				if ($block) {
					$this->banIP('DEFI-AUTO');
					$this->securityLogger(true, 'DEFI-AUTO', 'Blacklisted IP banned. Checked using IPs list.', 'IP');
					if ($this->cfg->get('DEFENDER_NOTIFY') == 2) {
						$mailmsg = "IP address ".$this->address." is blacklisted!\r\n";
						$mailmsg .= "The list of blacklisted IPs is updated automatically every 12 hours.\r\n";
						$this->sendAlert('SEC-DEFI-AUTO', $mailmsg, 1);
					}
					exitPage::make('security', 'DEFI-AUTO', 'Your IP address is blacklisted!');
				}
				unset($block);
			}
		}

		//Check for ascii chars 0-31 and 127
		$pat = '@[\x00-\x1F\x7F]@u';
		//For POST and USER AGENT exclude Horizontal Tab, Line Feed and Carriage Return 
		$patpost = '@[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]@u';
		$block = false;
		$blockmsg = '';
		$gvar = '';
		if (!empty($_GET)) {
			foreach ($_GET as $k => $v) {
				if (is_array($v)) {
					foreach ($v as $ka => $va) {
						if (is_numeric($va)) { continue; }
						if (is_array($va)) {
							$testva = '';
							array_walk_recursive($va, function($xva, $xk) use (&$testva) { $testva .= $xva.','; });
						} else {
							$testva = $va;
						}
						$v2 = preg_replace($pat, '', $testva);
						if (strcmp($v2, $testva) <> 0) {
							$block = true;
							$gvar = 'GET';
							$blockmsg = 'Unacceptable ASCII character in '.$gvar;
							break 2;
						}
					}
				} else {
					if (is_numeric($v)) { continue; }
					$v2 = preg_replace($pat, '', $v);
					if (strcmp($v2, $v) <> 0) {
						$block = true;
						$gvar = 'GET';
						$blockmsg = 'Unacceptable ASCII character in '.$gvar;
						break;
					}
				}
			}
		}

		if (!$block && !empty($_POST)) {
			foreach ($_POST as $k => $v) {
				if (is_array($v)) {
					foreach ($v as $ka => $va) {
						if (is_numeric($va)) { continue; }
						if (is_array($va)) {
							$testva = '';
							array_walk_recursive($va, function($xva, $xk) use (&$testva) { $testva .= $xva.','; });
						} else {
							$testva = $va;
						}
						$v2 = preg_replace($patpost, '', $testva);
						if (strcmp($v2, $testva) <> 0) {
							$block = true;
							$gvar = 'POST';
							$blockmsg = 'Unacceptable ASCII character in '.$gvar;
							break 2;
						}
					}
				} else {
					if (is_numeric($v)) { continue; }
					$v2 = preg_replace($patpost, '', $v);
					if (strcmp($v2, $v) <> 0) {
						$block = true;
						$gvar = 'POST';
						$blockmsg = 'Unacceptable ASCII character in '.$gvar;
						break;
					}					
				}
			}
		}

		if (!$block && ($this->useragent != '')) {
			$v2 = preg_replace($patpost, '', $this->useragent);
			if (strcmp($v2, $this->useragent) <> 0) {
				$block = true;
				$gvar = 'USER AGENT';
				$blockmsg = 'Unacceptable ASCII character in '.$gvar;
			}
		}

		if ($block) {
			$this->banIP('DEFB-0008');
			$this->securityLogger(true, 'DEFB-0008', $blockmsg, $gvar);
			if ($this->cfg->get('DEFENDER_NOTIFY') > 0) {
				$mailmsg = "Elxis Defender blocked an attack to your site!\r\n";
				$mailmsg .= "Elxis Defender report\r\n";
				$mailmsg .= $blockmsg;
				$this->sendAlert('SEC-DEFB-0008', $mailmsg, 1);
			}
			exitPage::make('security', 'DEFB-0008', $blockmsg);
		}
		unset($pat, $patpost, $block, $gvar, $blockmsg);

		foreach ($this->types as $type) {
			switch($type) {
				case 'G': $this->checkRules('general'); break;
				case 'C': $this->checkRules('custom'); break;
				case 'F': $this->checkFS(); break;
				case 'I': break; //checked previously with checkIP
				case 'R': break; //checked previously with checkIPRanges
				default: break;
			}
		}
	}


	/***********************************************************/
	/* RE-CHECK ONLY FOR AUTO IPS AFTER SESSION HAS INITIALIZE */
	/***********************************************************/
	public function reCheck() {
		if (!class_exists('elxisSession', false)) { return; }
		if (!$this->ipcheckafter) { return; }

		if ($this->cfg->get('DEFENDER_WHITELIST', '') != '') {
			$parts = explode(',', $this->cfg->get('DEFENDER_WHITELIST', ''));
			if (in_array($this->address, $parts)) { return; }
		}

		$eSession = eFactory::getSession();
		$checked = (int)$eSession->get('elxisdefips', '0');
		if ($checked == 1) { return; }

		$eSession->set('elxisdefips', '1');

		if (in_array('R', $this->types)) {//IP RANGES
			$block = $this->checkAutoIPRanges();
			if ($block) {
				$this->banIP('DEFR-AUTO');
				$this->securityLogger(true, 'DEFR-AUTO', 'Blacklisted IP banned. Checked using IP ranges.', 'IP');
				if ($this->cfg->get('DEFENDER_NOTIFY') == 2) {
					$mailmsg = "IP address ".$this->address." belongs to a blacklisted IPs range!\r\n";
					$mailmsg .= "The list of blacklisted IP ranges is updated automatically every 24 hours.\r\n";
					$this->sendAlert('SEC-DEFR-AUTO', $mailmsg, 1);
				}
				exitPage::make('security', 'DEFR-AUTO', 'Your IP address is blacklisted!');
			}
			unset($block);
		}

		if (in_array('I', $this->types)) {//SPECIFIC IPs
			$block = $this->checkAutoIP();
			if ($block) {
				$this->banIP('DEFI-AUTO');
				$this->securityLogger(true, 'DEFI-AUTO', 'Blacklisted IP banned. Checked using IPs list.', 'IP');
				if ($this->cfg->get('DEFENDER_NOTIFY') == 2) {
					$mailmsg = "IP address ".$this->address." is blacklisted!\r\n";
					$mailmsg .= "The list of blacklisted IPs is updated automatically every 12 hours.\r\n";
					$this->sendAlert('SEC-DEFI-AUTO', $mailmsg, 1);
				}
				exitPage::make('security', 'DEFI-AUTO', 'Your IP address is blacklisted!');
			}
			unset($block);
		}
	}


	/*******************/
	/* SET CHECK TYPES */
	/*******************/
	private function setTypes() {
		if (trim($this->cfg->get('DEFENDER')) != '') {
			$this->types = str_split($this->cfg->get('DEFENDER'));
			if (!in_array('C', $this->types)) { $this->types[] = 'C'; }
		}

		switch ($this->cfg->get('SECURITY_LEVEL')) {
			case 1:
				if (!in_array('G', $this->types)) { $this->types[] = 'G'; }
				if (!in_array('C', $this->types)) { $this->types[] = 'C'; }
				if (!in_array('F', $this->types)) { $this->types[] = 'F'; }
			break;
			case 2:
				if (!in_array('G', $this->types)) { $this->types[] = 'G'; }
				if (!in_array('C', $this->types)) { $this->types[] = 'C'; }
				if (!in_array('I', $this->types)) { $this->types[] = 'I'; }
				if (!in_array('R', $this->types)) { $this->types[] = 'R'; }
				if (!in_array('F', $this->types)) { $this->types[] = 'F'; }
			break;
			case 0: default: break;
		}

		if (!$this->types) { return; }

		if (in_array('R', $this->types)) {
			$update_ips_db = true;
			if (file_exists($this->repo_path.'/logs/defender_ip_ranges.php')) {
				$ts = filemtime($this->repo_path.'/logs/defender_ip_ranges.php');
				if ((time() - $ts) < 86400) { $update_ips_db = false; } //once per day
			} else {
				$fp = @fopen($this->repo_path.'/logs/defender_ip_ranges.php', 'w');
				if ($fp) {
					$ok = false;
					if (flock($fp, LOCK_EX)) {
						$ok = fwrite($fp, $txt);
						flock($fp, LOCK_UN);
					}
					fclose($fp);
					if ($ok) {
						$this->securityLogger(false, '', 'Bad IP ranges database updated successfully', 'UPDATE');
					} else {
						$this->securityLogger(false, '', 'Bad IP ranges database update FAILED', 'UPDATE');
					}
				} else {
					die('Could not create file logs/defender_ip_ranges.php in Elxis repository!');
				}
			}
			if ($update_ips_db) { $this->updateIPDB(true); }
		}

		if (in_array('I', $this->types)) {
			$update_ips_db = true;
			if (file_exists($this->repo_path.'/logs/defender_ips.php')) {
				$ts = filemtime($this->repo_path.'/logs/defender_ips.php');
				if ((time() - $ts) < 43200) { $update_ips_db = false; } //twice per day
			} else {
				$fp = @fopen($this->repo_path.'/logs/defender_ips.php', 'w');
				if ($fp) {
					$ok = false;
					if (flock($fp, LOCK_EX)) {
						$ok = fwrite($fp, $txt);
						flock($fp, LOCK_UN);
					}
					fclose($fp);
					if ($ok) {
						$this->securityLogger(false, '', 'Bad IPs list updated successfully', 'UPDATE');
					} else {
						$this->securityLogger(false, '', 'Bad IPs list update FAILED', 'UPDATE');
					}
				} else {
					die('Could not create file logs/defender_ips.php in Elxis repository!');
				}
			}
			if ($update_ips_db) { $this->updateIPDB(false); }
		}
	}


	/******************************/
	/* FILESYSTEM INTEGRITY CHECK */
	/******************************/
	private function checkFS() {
		$files = $this->getLockFiles();
		$hashfile = $this->repo_path.'/other/elxis_hashes_'.md5($this->cfg->get('ENCRYPT_KEY')).'.php';
		if (!file_exists($hashfile)) {
			$buffer = '<?php '._LEND._LEND;
			$buffer .= '//Elxis Defender - Filesystem hash fingerprint generated on '.gmdate('Y-m-d H:i:s').' (UTC)'._LEND._LEND;
			$buffer .= 'defined(\'_ELXIS_\') or die (\'Protected by Elxis Defender\');'._LEND._LEND;
			$buffer .= '$hashes = array('._LEND;
			foreach ($files as $file) {
				$m = md5_file(ELXIS_PATH.'/'.$file);
				$buffer.= "\t".'array(\''.$file.'\', \''.$m.'\'),'._LEND;
			}
			$buffer .= ');'._LEND._LEND;
			$buffer .= '?>';

			$ok = false;
			if ($handler = @fopen($hashfile, 'w')) {
				if (flock($handler, LOCK_EX)) {
					$ok = @fwrite($handler, $buffer);
					flock($handler, LOCK_UN);
				}
            	fclose($handler);
        	}
			if (!$ok) {
				$msg = 'Elxis Defender could not save fingerprint (hashes) file! Please make sure Elxis repository is writable.';
				exitPage::make('fatal', 'DEFF-0001', $msg);
			}
        	return true;
		}

		include($hashfile);
		if (!isset($hashes) || !is_array($hashes) || (count($hashes) == 0)) {
			$msg = 'Elxis Defender detected an empty or invalid fingerprint (hashes) file! If you dont 
			know what to do consider Elxis documentation or visit Elxis forums for support.';
			exitPage::make('fatal', 'DEFF-0002', $msg);
		}

		$i = 1;
		foreach ($hashes as $hash) {
			$f = $hash[0];
			if (!in_array($f, $files)) {
				$n = sprintf("%04d", $i);
				$this->securityLogger(true, 'DEFF-'.$n, 'A protected file has been deleted! '.$f, 'FSDEL');
				if ($this->cfg->get('DEFENDER_NOTIFY') > 0) {
					$mailmsg = 'A protected file has been deleted!'."\r\n";
					$mailmsg .= 'The deleted file is: '.$f."\r\n\r\n";
					$mailmsg .= 'Actions to perform (pick one):'."\r\n";
					$mailmsg .= '1. If the deletion made by an unauthorized person, or accidently by you, restore the original protected file.'."\r\n";
					$mailmsg .= '2. If you accept this deletion then you must delete the Elxis hashes file in order for the Elxis Defender to regenarate it without the deleted file.'."\r\n\r\n";
					$mailmsg .= "Elxis Defender hashes file: ".$hashfile."\r\n";
					$mailmsg .= "The site wont come back online until you perform one of the above actions.\r\n"; 
					$this->sendAlert('SEC-DEFF-'.$n, $mailmsg, 0);
				}
				$msg = 'A protected file has been deleted! If you dont know what 
				to do consider Elxis documentation or visit Elxis forums for support.';
				exitPage::make('security', 'DEFF-'.$n, $msg);
			}
			$m = md5_file(ELXIS_PATH.'/'.$f);
			if ($m != $hash[1]) {
				$n = sprintf("%04d", $i);
				$this->securityLogger(true, 'DEFF-'.$n, 'A protected file has been modified! '.$f, 'FSLOCK');
				if ($this->cfg->get('DEFENDER_NOTIFY') > 0) {
					$mailmsg = 'A protected file has been modified!'."\r\n";
					$mailmsg .= 'The modified file is: '.$f."\r\n\r\n";
					$mailmsg .= 'Actions to perform (pick one):'."\r\n";
					$mailmsg .= '1. If the modification made by an unauthorized person, or accidently by you, restore the original protected file.'."\r\n";
					$mailmsg .= '2. If you accept this modification then you must delete the Elxis hashes file in order for the Elxis Defender to regenarate it without the modified file.'."\r\n\r\n";
					$mailmsg .= "Elxis Defender hashes file: ".$hashfile."\r\n";
					$mailmsg .= "The site wont come back online until you perform one of the above actions.\r\n"; 
					$this->sendAlert('SEC-DEFF-'.$n, $mailmsg, 0);
				}
				$msg = 'A protected file has been modified! If you dont know what 
				to do consider Elxis documentation or visit Elxis forums for support.';
				exitPage::make('security', 'DEFF-'.$n, $msg);
			}
			$i++;
		}
	}


	/*******************************************/
	/* CHECK IF IP IS BANNED BY ELXIS DEFENDER */
	/*******************************************/
	private function checkBanned() {
		if ($this->address == '') { return; }
		$file = $this->repo_path.'/logs/defender_ban.php';
		if (!file_exists($file)) { return; }
		include($file);
		if (!isset($ban) || !is_array($ban) || (count($ban) == 0)) { return; }
		$ip = str_replace('.', 'x', $this->address);
		$ip = str_replace(':', 'y', $ip);
		if (isset($ban[$ip])) {
			if ($ban[$ip]['times'] >= $this->bantimes) {
				$msg = 'You have been banned! If you think this is wrong contact the site administrator.';
				exitPage::make('security', 'DEFB-0001', $msg);
			}
		}
	}


	/************************/
	/* CHECK BLOCKING RULES */
	/************************/
	private function checkRules($type) {
		if (!file_exists(ELXIS_PATH.'/includes/libraries/elxis/defender/'.$type.'.rules.php')) { return; }
		include(ELXIS_PATH.'/includes/libraries/elxis/defender/'.$type.'.rules.php');
		if (!isset($rules) || !is_array($rules) || (count($rules) == 0)) { return; }

		$this->triggered_rule = '';

		foreach ($rules as $k => $rule) {
			$methods = explode(',', $rule[0]);
			foreach ($methods as $method) {
				$func = 'check'.$method;
				if ($this->$func($rule[1])) {
					$char = strtoupper(substr($type, 0, 1));
					$n = sprintf("%04d", $k);
					$this->banIP('DEF'.$char.'-'.$n);
					if ($this->triggered_rule != '') {
						$logtxt = 'Request blocked, Method: '.$method.', Rule: '.$this->triggered_rule.', Reason: '.$rule[2];
					} else {
						$logtxt = 'Request blocked, Method: '.$method.', Reason: '.$rule[2];
					}
					$this->securityLogger(true, 'DEF'.$char.'-'.$n, $logtxt, $method);

					$notify = false;
					if ($this->cfg->get('DEFENDER_NOTIFY') == 2) {
						$notify = true;
					} elseif ($this->cfg->get('DEFENDER_NOTIFY') == 1) {
						if (!in_array($method, array('AGENT', 'HOST', 'IP'))) {
							$notify = true;
						}
					}

					if ($notify) {
						$mailmsg = "Rules: \t".$type."\r\n";
						$mailmsg .= "Match where: \t".$method."\r\n";
						$mailmsg .= "Regex match number: \t".($k+1)."\r\n";
						if ($this->triggered_rule != '') {
							$mailmsg .= "Match rule: \t".$this->triggered_rule."\r\n";
						}
						$mailmsg .= "Reason: \t".$rule[2]."\r\n";
						if ($this->banmessage != '') { $mailmsg .= $this->banmessage."\r\n"; }
						$this->sendAlert('SEC-DEF'.$char.'-'.$n, $mailmsg, 1);
					}
					exitPage::make('security', 'DEF'.$char.'-'.$n, $rule[2]);
				}
			}
		}
	}


	/* CHECK POST DATA */
	private function checkPOST($rule) {
		if ($this->rawpost == '') { return false; }
		if (preg_match('~'. $rule .'~', $this->rawpost)) {
			preg_match('~'. $rule .'~', $this->rawpost, $matches);
			if (isset($matches[0]) && ($matches[0] != '')) { $this->triggered_rule = $matches[0]; }
			return true;
		}
		return false;
	}


	private function checkSESSION($rule) {
		return false;//cannot check session before Elxis init
	}


	private function checkCOOKIE($rule) {
		return false;//cannot check cookie
	}


	private function checkURI($rule) {
		if ($this->requesturi == '') { return false; }
		if (preg_match('~'. $rule .'~', $this->requesturi)) {
			preg_match('~'. $rule .'~', $this->requesturi, $matches);
			if (isset($matches[0]) && ($matches[0] != '')) { $this->triggered_rule = $matches[0]; }
			return true;
		}
		return false;
	}


	private function checkREFERER($rule) {
		if ($this->referer == '') { return false; }
		if (preg_match('~'. $rule .'~', $this->referer)) {
			preg_match('~'. $rule .'~', $this->referer, $matches);
			if (isset($matches[0]) && ($matches[0] != '')) { $this->triggered_rule = $matches[0]; }
			return true;
		}
		return false;
	}


	private function checkAGENT($rule) {
		if ($this->useragent == '') { return false; }
		if (preg_match('~'. $rule .'~', $this->useragent)) {
			preg_match('~'. $rule .'~', $this->useragent, $matches);
			if (isset($matches[0]) && ($matches[0] != '')) { $this->triggered_rule = $matches[0]; }
			return true;
		}
		return false;
	}


	private function checkQUERY($rule) {
		if ($this->query == '') { return false; }
		if (preg_match('~'. $rule .'~', $this->query)) {
			preg_match('~'. $rule .'~', $this->query, $matches);
			if (isset($matches[0]) && ($matches[0] != '')) { $this->triggered_rule = $matches[0]; }
			return true;
		}
		return false;
	}


	/***********************/
	/* ALIAS OF checkQUERY */
	/***********************/
	private function checkGET($rule) {
		return $this->checkQUERY($rule);
	}


	private function checkHOST($rule) {
		if ($this->host == '') { return false; }
		if (preg_match('~'. $rule .'~', $this->host)) {
			preg_match('~'. $rule .'~', $this->host, $matches);
			if (isset($matches[0]) && ($matches[0] != '')) { $this->triggered_rule = $matches[0]; }
			return true;
		}
		return false;
	}


	private function checkIP($rule) {
		if ($this->address == '') { return false; }
		if (preg_match('~'. $rule .'~', $this->address)) {
			preg_match('~'. $rule .'~', $this->address, $matches);
			if (isset($matches[0]) && ($matches[0] != '')) { $this->triggered_rule = $matches[0]; }
			return true;
		}
		return false;
	}


	/****************************************/
	/* CHECK IP AGAINST DANGEROUS IP RANGES */
	/****************************************/
	private function checkAutoIPRanges() {
		if (strpos($this->address, ':') !== false) { return; } //not supported IP v6
		@include($this->repo_path.'/logs/defender_ip_ranges.php');
		if (!isset($ips) || !is_array($ips) || (count($ips) == 0)) { return; }

		$userip = sprintf('%u', ip2long($this->address));
		$block = false;
		foreach ($ips as $ip) {
			if (($userip >= $ip[2]) && ($userip <= $ip[3])) {
				$block = true;
				break;
			}
		}
		return $block;
    }


	/**********************************/
	/* CHECK IP AGAINST DANGEROUS IPS */
	/**********************************/
	private function checkAutoIP() {
		@include($this->repo_path.'/logs/defender_ips.php');
		if (!isset($ips) || !is_array($ips) || (count($ips) == 0)) { return; }

		$block = false;
		foreach ($ips as $ip) {
			if ($ip == $this->address) {
				$block = true;
				break;
			}
		}
		return $block;
    }


	/*******************/
	/* SEND MAIL ALERT */
	/*******************/
	private function sendAlert($code, $msg='', $attack=1) {
		$file = $this->repo_path.'/logs/defender_notify.txt';
		@clearstatcache();
		if (!file_exists($file)) {
			@touch($file);
			$proceed = true;
		} else {
			$last = filemtime($file);
			if ((time() - filemtime($file)) > 300) {
				@touch($file);
				$proceed = true;
			} else {
				$proceed = false;
			}
		}

		if (!$proceed) { return; }

		$uri = addslashes($this->requesturi);
    	$parsed = parse_url($this->cfg->get('URL')); 
 		$host = preg_replace('#^(www\.)#i', '', $parsed['host']);
		$subject = 'Message from Elxis Defender on '.$host;
		if ($attack == 1) {
			$text = "Elxis Defender blocked an attack to your site!\r\n";
		} else {
			$text = "Elxis Defender detected a change in site filesystem!\r\n";
		}
		$text .= 'Reference code: '.$code."\r\n";
		if ($msg != '') { $text .= "\r\nElxis Defender report\r\n".$msg."\r\n"; }
		$text .= "\r\n";
		$text .= "Requested URI: \t".$uri."\r\n";
		if ($this->address != '') { $text .= "IP address: \t".$this->address."\r\n"; }
		if ($this->host != '') { $text .= "Hostname: \t".$this->host."\r\n"; }
		if ($this->referer != '') { $text .= "HTTP Referrer: \t".$this->referer."\r\n"; }
		if (isset($_SERVER['HTTP_USER_AGENT'])) { $text .= "User agent: \t".$_SERVER['HTTP_USER_AGENT']."\r\n"; }

		if (!empty($_POST)) {
			$text .= "\r\n";
			$text .= '=== POST vars submitted ==='."\r\n";
			foreach ($_POST as $k => $v) {
				if (is_array($v)) {
					$text .= addslashes($k).' = ARRAY'."\r\n";
				} else if (strlen($v) > 200) {
					$text .= addslashes($k).' = LONG STRING'."\r\n";
				} else {
					$text .= addslashes($k).' = '.addslashes($v)."\r\n";
				}
			}
			$text .= "\r\n";
		}

		$text .= "Date (UTC): \t".gmdate('Y-m-d H:i:s')."\r\n";
		$text .= "Site URL: \t".$this->cfg->get('URL')."\r\n\r\n\r\n\r\n";
		$text .= "----------------------------------------------------------\r\n";
		$text .= "Elxis Defender by Elxis Team\r\n";
		$text .= 'Please do not reply to this message as it was generated automatically by the Elxis Defender ';
		$text .= 'and it was sent for informational purposes. Elxis Defender will not send you an other notification for ';
		$text .= 'the next 5 minutes even if more attacks occur.'."\r\n";
		$text .= "----------------------------------------------------------\r\n";

		require_once(ELXIS_PATH.'/includes/libraries/swift/swift_required.php');

		$message = Swift_Message::newInstance();
		$message->setCharset('UTF-8');
		$message->setPriority(2);
		$message->setSubject($subject);
		$message->setBody($text, 'text/plain');
		$message->addTo($this->cfg->get('MAIL_MANAGER_EMAIL'), $this->cfg->get('MAIL_MANAGER_NAME'));

		if (($this->cfg->get('MAIL_METHOD') == 'smtp') && (strpos($this->cfg->get('MAIL_SMTP_HOST'), '.gmail.') !== false)) {
			$message->setFrom(array($this->cfg->get('MAIL_FROM_EMAIL') => $this->cfg->get('MAIL_FROM_NAME')));
		} else {
			$message->setFrom(array('defender@'.$host => 'Elxis Defender'));
		}

		$headers = $message->getHeaders();
		$headers->addTextHeader('X-Mailer', 'Elxis');

		switch ($this->cfg->get('MAIL_METHOD')) {
			case 'smtp':
				$transport = Swift_SmtpTransport::newInstance(
					$this->cfg->get('MAIL_SMTP_HOST'), 
					$this->cfg->get('MAIL_SMTP_PORT'),
					$this->cfg->get('MAIL_SMTP_SECURE')
				);
				if ($this->cfg->get('MAIL_SMTP_AUTH') == 1) {
					if ($this->cfg->get('MAIL_AUTH_METHOD') != '') {
						$transport->setAuthMode($this->cfg->get('MAIL_AUTH_METHOD'));
					}
					$transport->setUsername($this->cfg->get('MAIL_SMTP_USER'));
					$transport->setPassword($this->cfg->get('MAIL_SMTP_PASS'));
				}
			break;
			case 'sendmail':
				$transport = Swift_SendmailTransport::newInstance();
			break;
			case 'mail': default:
				$transport = Swift_MailTransport::newInstance();
			break;
		}

		$mailer = Swift_Mailer::newInstance($transport);
		try {
			$mailer->send($message);
		} catch (\Swift_TransportException $Ste) {
		}
	}


	/*********************/
	/* BAN AN IP ADDRESS */
	/*********************/
	private function banIP($refcode) {
		if ($this->address == '') { return; }
		$file = $this->repo_path.'/logs/defender_ban.php';
		$ip = str_replace('.', 'x', $this->address);
		$ip = str_replace(':', 'y', $ip);
		$unban_ts = time() - $this->banduration;
		$unban_date = gmdate('Y-m-d H:i:s', $unban_ts);

		$nowtimes = 1;
		$buffer = '<?php '._LEND._LEND;
		$buffer .= '//Elxis Defender - Banned IPs - Last updated on '.gmdate('Y-m-d H:i:s').' (UTC)'._LEND._LEND;
		$buffer .= 'defined(\'_ELXIS_\') or die (\'Protected by Elxis Defender\');'._LEND._LEND;
		$buffer .= '$ban = array('._LEND;
		if (!file_exists($file)) {
			$buffer .= '\''.$ip.'\' => array(\'times\' => '.$nowtimes.', \'refcode\' => \'SEC-'.$refcode.'\', \'date\' => \''.gmdate('Y-m-d H:i:s').'\'),'._LEND;
		} else {
			include($file);
			$found = false;
			if (isset($ban) && is_array($ban) && (count($ban) > 0)) {
				foreach ($ban as $key => $row) {
					if ($key == $ip) {
						$found = true;
						$nowtimes = $row['times'] + 1;
						$buffer .= '\''.$ip.'\' => array(\'times\' => '.$nowtimes.', \'refcode\' => \'SEC-'.$refcode.'\', \'date\' => \''.gmdate('Y-m-d H:i:s').'\'),'._LEND;
					} else {
						if ($row['date'] > $unban_date) {//continue ban only IPs within the last 10 days (or whateven banduration value is) 
							$buffer .= '\''.$key.'\' => array(\'times\' => '.$row['times'].', \'refcode\' => \''.$row['refcode'].'\', \'date\' => \''.$row['date'].'\'),'._LEND;
						}
					}
				}
			}
			unset($ban);

			if (!$found) {
				$buffer .= '\''.$ip.'\' => array(\'times\' => '.$nowtimes.', \'refcode\' => \'SEC-'.$refcode.'\', \'date\' => \''.gmdate('Y-m-d H:i:s').'\'),'._LEND;
			}
		}

		$buffer .= ');'._LEND._LEND;
		$buffer .= '?>';

		if ($nowtimes >= $this->bantimes) {
			$this->banmessage .= 'The guest has been BANNED as he was blocked by Elxis Defender '.$nowtimes.' times!'."\r\n";
		}

		if ($handler = @fopen($file, 'w')) {
			$ok = false;
			if (flock($handler, LOCK_EX)) {
				$ok = @fwrite($handler, $buffer);
				flock($handler, LOCK_UN);
			}
			fclose($handler);

			if ($ok) {
				@clearstatcache();
				$fsize = intval(filesize($file) / 1024);
				if ($fsize > 200) {
					$this->banmessage .= 'Bans log file is '.$fsize.'KB! Please remove old bans to make it load faster.'."\r\n";
					$this->banmessage .= 'Bans log file: '.$file;
				}
			}
		}
	}


	/*******************/
	/* SECURITY LOGGER */
	/*******************/
	private function securityLogger($is_attack=true, $refcode='', $message='', $method='') {
		$dlog = (int)$this->cfg->get('DEFENDER_LOG');
		if ($dlog < 1) { return; }
		if ($dlog < 2) {
			if (in_array($method, array('AGENT', 'USER AGENT', 'HOST', 'IP'))) {
				return;
			}
		}

		$file = $this->repo_path.'/logs/security.log';

		if ($this->cfg->get('LOG_ROTATE') == 1) {
			if (file_exists($file)) {
				$modym = date('Ym', filemtime($file));
				$creym = date('Ym');
				if ($creym > $modym) {
					$new_file = $this->repo_path.'/logs/security_'.$modym.'.log';
					@copy($file, $new_file);
					@unlink($file);
				}
			}
		}

		if ($is_attack) {
			$txt = gmdate('Y-m-d H:i:s').' GMT ['.$this->address;
			if ($this->host != '') { $txt .= ' - '.$this->host; }
			$txt .= ']'._LEND;
			if (isset($_SERVER['HTTP_USER_AGENT'])) { $txt .= addslashes($_SERVER['HTTP_USER_AGENT'])._LEND; }
			if (isset($_SERVER['HTTP_REFERER'])) { $txt .= 'REFERER: '.addslashes($_SERVER['HTTP_REFERER'])._LEND; }
			if (isset($_SERVER['REQUEST_METHOD'])) {
				$txt .= $_SERVER['REQUEST_METHOD'].' ';
			} else if (!empty($_POST)) {
				$txt .= 'POST ';
			} else {
				$txt .= 'GET ';
			}
			$txt .= addslashes($this->getURI())._LEND;
			$txt .= 'REFCODE: '.$refcode;
			if ($message != '') { $txt .= ' '.$message; }
			$txt .= _LEND._LEND;
		} else {
			$txt = gmdate('Y-m-d H:i:s').' GMT   '.$message._LEND._LEND;
		}

		$ok = false;
		if (!$fp = @fopen($file, 'a')) { return false; }
		if (flock($fp, LOCK_EX)) {
			$ok = fwrite($fp, $txt);
			flock($fp, LOCK_UN);			
		}
		fclose($fp);
		return $ok;
	}


	/*********************/
	/* GET REQUESTED URI */
	/*********************/
	private function getURI() {
		if (isset($_SERVER['REQUEST_URI'])) { return $_SERVER['REQUEST_URI']; }
		if (isset($_SERVER['QUERY_STRING'])) {
			$query_str = $_SERVER['QUERY_STRING'];
		} else if (@getenv('QUERY_STRING')) {
			$query_str = getenv('QUERY_STRING');
		} else {
			$query_str = '';
		}
		if ($query_str != '') { $query_str = '?'.$query_str; }

		if (isset($_SERVER['PATH_INFO'])) {
			return $_SERVER['PATH_INFO'].$query_str;
		} elseif (@getenv('PATH_INFO')) {
			return getenv('PATH_INFO').$query_str;
		}

		if (isset($_SERVER['PHP_SELF'])) {
			return $_SERVER['PHP_SELF'].$query_str;
		} elseif (@getenv('PHP_SELF')) {
			return getenv('PHP_SELF').$query_str;
		} else {
			return $query_str;
		}
	}


	/*******************/
	/* GET CLIENT'S IP */
	/*******************/
	private function getIP() {
		if (isset($_SERVER['HTTP_CLIENT_IP']) && ($_SERVER['HTTP_CLIENT_IP'] != '')) {
			$ip = $_SERVER['HTTP_CLIENT_IP'];
		} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && ($_SERVER['HTTP_X_FORWARDED_FOR'] != '')) {
			$n = strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',');//Required in case we have multiple IPs
			if ($n === false) {
				$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
			} else {
				$ip = substr($_SERVER['HTTP_X_FORWARDED_FOR'], 0, $n);//get the first IP in the list
			}
		} elseif (isset($_SERVER['REMOTE_ADDR']) && ($_SERVER['REMOTE_ADDR'] != '')) {
			$ip = $_SERVER['REMOTE_ADDR'];
		} else {
			$ip = '';
		}
		return $ip;
	}


	/***********************************/
	/* GET ELXIS FILESYSTEM LOCK FILES */
	/***********************************/
	private function getLockFiles() {
		$files = array('index.php', 'inner.php', 'includes/loader.php');
		$libs = $this->listFiles('includes/libraries/elxis/');
		if ($libs) {
			foreach ($libs as $lib) { $files[] = $lib; }
		}
		unset($libs);
		$tpls = $this->listFolders('templates/');
		if ($tpls) {
			foreach ($tpls as $tpl) {
				if (file_exists(ELXIS_PATH.'/templates/'.$tpl.'/index.php')) {
					$files[] = 'templates/'.$tpl.'/index.php';
				}
				if (file_exists(ELXIS_PATH.'/templates/'.$tpl.'/inner.php')) {
					$files[] = 'templates/'.$tpl.'/inner.php';
				}
			}
		}
		unset($tpls);
		$comps = $this->listFolders('components/');
		if ($comps) {
			foreach ($comps as $comp) {
				$f = str_replace('com_', '', $comp);
				if (file_exists(ELXIS_PATH.'/components/'.$comp.'/'.$f.'.php')) {
					$files[] = 'components/'.$comp.'/'.$f.'.php';
				}
			}
		}
		unset($tpls);
		return $files;
	}


	/*****************************/
	/* LIST FILES IN A DIRECTORY */
	/*****************************/
	private function listFiles($dir, $onlyphp=true) {
		$path = ELXIS_PATH.'/'.$dir;
		if (!is_dir($path)) { return array(); }
		$arr = array();
		$handle = opendir($path);
		while ($entry = readdir($handle)) {
			if (($entry != '.') && ($entry != '..')) {
				if ($onlyphp) {
					if (preg_match('#(\.php)$#i', $entry)) { $arr[] = $dir.$entry; }
				} else {
					$arr[] = $dir.$entry;
				}
			}
		}
		closedir($handle);
		asort($arr);
		return $arr;
	}


	/*******************************/
	/* LIST FOLDERS IN A DIRECTORY */
	/*******************************/
	private function listFolders($dir) {
		$path = ELXIS_PATH.'/'.$dir;
		if (!is_dir($path)) { return array(); }
		$arr = array();
		$handle = opendir($path);
		while ($entry = readdir($handle)) {
			if (($entry != '.') && ($entry != '..') && is_dir($path.$entry)) {
				$arr[] = $entry;
			}
		}
		closedir($handle);
		asort($arr);
		return $arr;
	}


	/************************************/
	/* UPDATE IPs OR IP RANGES DATABASE */
	/************************************/
	private function updateIPDB($ranges=false) {
		/* 
		Many thanks to Stop Forum Spam and blocklist.de teams for their work! 
		http://www.stopforumspam.com - http://www.blocklist.de
		*/
		if ($ranges) {
			$url = 'https://www.stopforumspam.com/downloads/toxic_ip_range.txt';
		} else {
			$url = 'https://api.blocklist.de/getlast.php?time=28800';
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_HTTPGET, true);
		curl_setopt($ch, CURLOPT_HEADER, false);
		curl_setopt($ch, CURLOPT_FAILONERROR, true);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 8);
		curl_setopt($ch, CURLOPT_TIMEOUT, 15);
		curl_setopt($ch, CURLOPT_REFERER, $this->cfg->get('URL'));
		$data = curl_exec($ch);
		if (curl_errno($ch) == 0) {
			curl_close($ch);
		} else {
			curl_close($ch);
			return false;
		}
		if (trim($data) == '') { return false; }

		$lines = preg_split("@((\r?\n)|(\r\n?))@", $data);
		if (!$lines) { return false; }

		$txt = '<?php '._LEND;
		if ($ranges) {
			$txt .= '//Elxis Defender - Blocked IP ranges - Last update '.gmdate('Y-m-d H:i:s').' GMT'._LEND._LEND;
		} else {
			if (strpos($lines[0], '<') !== false) { return false; } //nothing found
			$txt .= '//Elxis Defender - Blocked IPs - Last update '.gmdate('Y-m-d H:i:s').' GMT'._LEND._LEND;
		}

		$i = 0;
		$txt .= '$ips = array('._LEND;
		foreach($lines as $line) {
			$line = trim($line);
			if ($line == '') { continue; }
			if ($ranges) {
				$parts = explode('-', $line);
				if (!isset($parts[1])) { continue; }
				$first = ip2long($parts[0]);
				$last = ip2long($parts[1]);
				if (($first == -1) || ($first === false) || ($last == -1) || ($last === false)) { continue; }
				//I. Sannos note: we use "%u" to get the unsigned ip address because on 32bit systems ip2long might return negative number
				$txt .= "\t".'array(\''.$parts[0].'\', \''.$parts[1].'\', \''.sprintf('%u', $first).'\', \''.sprintf('%u', $last).'\'),'._LEND;
			} else {
				if ($i > 4000) { break; }
				$txt .= '\''.$line.'\','._LEND;
				$i++;
			}
		}
		$txt .= ');'._LEND._LEND;
		$txt .= '?>';
		unset($lines);

		$file = ($ranges) ? $this->repo_path.'/logs/defender_ip_ranges.php' : $this->repo_path.'/logs/defender_ips.php';
		$fp = @fopen($file, 'w');
		if ($fp) {
			if (flock($fp, LOCK_EX)) {
				fwrite($fp, $txt);
				flock($fp, LOCK_UN);
			}
			fclose($fp);
		}
		return true;
	}

}

?>