<commit_before><?php

namespace Blog\Core\Security;
use Blog\Models;
use Blog\Core;
use Blog\Messaging;

class Sensor {
	
	/**
	 * @var array Global variable names
	 */
	private $global_vars = array(
		'$_SERVER',
		'$_GET',
		'$_POST',
		'$_FILES',
		'$_SESSION',
		'$_ENV',
		'$_COOKIE'
	);
	
	/**
	 * @var array Whitelist of acceptable methods
	 */
	private $methods 	= array( 'get', 'head', 'post', 'put' );
	
	/**
	 * @var string Request method
	 */
	private $method;
	
	/**
	 * @var array Whitelist of acceptable ports
	 */
	private $ports		= array( 80, 443 );
	
	/**
	 * @var object ServerRequest 
	 */
	private $request;
	
	/**
	 * @var object App configuration
	 */
	private $config;
	
	/**
	 * @var object Cryptography
	 */
	private $crypto;
	
	/**
	 * @var object BrowserProfile 
	 */
	private $browser;
	
	/**
	 * @var object IP class
	 */
	private $ip;
	
	const HOST_SIZE		= 255;
	const HOST_CHUNKS	= 8;
	const HOST_RX		= 
	'~^([\w-]+://?|www[\.])?([^\-\s\,\;\:\+\/\\\?\^\`\=\&\%\"\'\*\#\<\>]*)\.[a-z]{2,9}$~i';
	
	public function __construct(
		Messaging\ServerRequest $request,
		Core\Config $config,
		Core\Crypto $crypto
	) {
		$this->request	= $request;
		$this->config	= $config;
		$this->crypto	= $crypto;
		
		$this->ip	= new IP();
		$this->browser	= $request->getBrowserProfile();
		$this->method	= 
			strtolower( $this->request->getMethod() );
		
		# Prepare model for future scans
		Models\Model::setConfig( $config );
		Models\Model::setCrypto( $crypto );
		
		$this->sessionCheck();
	}
	
	/**
	 * Run firewall
	 */
	public function run() {
		$this->checkPort();
		$this->accept( $this->methods );
		$this->requestScan();
		
		
		$ip		= $this->ip->getIP();
		$headers	= $this->request->getHeaders();
		$hash		= $this->browser->headerHash() . $ip;
		
		# Prevent redundant scans if it's the same user
		if ( $this->expired( $hash ) ) {
			$this->ipScan( $ip );
			$this->checkHeaders();
			$this->uaScan();
			
			$_SESSION[$hash]	= true;
		}
		
		# Put and Post require extra scrutiny
		$search	= array( 'put', 'post' );
		if ( in_array( $this->method, $search ) ) {
			$this->bodyScan();
		}
	}
	
	/**
	 * Add to the list of allowed ports
	 */
	public function addPort( $port ) {
		$port = ( int ) $port;
		if ( $port < 1 || $port > 65535 ) {
			$this->end( 'Invalid port number' );
		}
		$this->ports[]	= $port;
		$this->ports	= array_unique( $this->ports );
	}
	
	/**
	 * Detect global variable pollution
	 */
	private function globalInjection() {
		foreach ( $this->global_vars as $pre ) {
			if ( !isset( $pre ) ) {
				continue;
			}
			
			foreach ( $pre as $k => $v ) {
				if ( $this->isearch( $pre, $v ) ) {
					$this->end( 'Global injection' );
				}
			}
		}
	}
	
	/**
	 * Check if a scan is needed from the last time using headers
	 */
	private function expired( $hash ) {
		if ( \session_status() === \PHP_SESSION_ACTIVE ) {
			return isset( $_SESSION[$hash] ) ? 
					false : true;
		}
		
		return true;
	}
	
	/**
	 * Scan request path for anomalies
	 */
	private function requestScan() {
		$uri		= $this->request
					->getUri()
					->getRawPath();
		
		$this->blacklist( 
			'firewall_uri',
			function( $u ) use ( $uri ) {
				if ( false !== stripos( $uri, $u ) ) {
					$this->end( 'Invalid URI' );
				}
			}
		);
	}
	
	/**
	 * Scan user agent for anomalies
	 */
	private function uaScan() {
		$ua		= $this->request
					->getHeader( 'User-Agent' );
		$this->blacklist( 
			'firewall_ua',
			function( $u ) use ( $ua ) {
				if ( false !== stripos( $ua[0], $u ) ) {
					$this->end( 'Invalid browser' );
				}
			}
		);
	}
	
	/**
	 * Scan request body for malicious content
	 */
	private function bodyScan() {
		# TODO
		# $body	= $this->request->getBody();
		
	}
	
	/**
	 * Scan IP blocklist
	 */
	private function ipScan( $ip ) {
		
		# Running locally? Skip IP check
		if ( $this->getSetting( 'firewall_local' ) ) {
			$ip	= '127.0.0.1';
		} else {
			if ( !$this->ip->validateIP( $ip ) ) {
				$this->end( 'Denied IP' );
			}
		}
		
		
		if ( $this->getSetting( 'firewall_hosts' ) ) {
			if ( $this->getSetting( 'firewall_local' ) ) {
				$host	= 'localhost';
			} else {
				$host	= 
				trim( strtolower( gethostbyaddr( $ip ) ) );
				if ( !$this->validateHost( $host ) ) {
					$this->end( 'Denied host' );
				}
			}
		} else {
			$host	= '';
		}
		
		# TODO Move this into a database search due to the 
		# 	potentially large number of blocks
		$this->blacklist( 
			'firewall_ip',
			function( $u ) use ( $ip, $host ) {
				$len	= mb_strlen( $u, '8bit' );
				if ( 0 === strncmp( $ip, $u, $len ) ) {
					$this->end( 'Denied IP' );
				}
				
				if ( empty( $host ) ) {
					return;
				}
				
				if ( 0 === strncmp( $host, $u, $len ) ) {
					$this->end( 'Denied host' );
				}
			}
		);
	}
	
	/**
	 * Get combined IP and host ranges for database search
	 */
	private function getSearchRanges() {
		$ip	= $this->ip->getIP();
		$search	= $this->getIpRange( $ip );
		
		# Check host names if enabled
		if ( $this->getSetting( 'firewall_hosts' ) ) {
			$search =  array_merge( 
					$search, 
					$this->getHostRange( $ip )
				);
		}
		
		
	}
	
	/**
	 * Prepare IP for databases
	 */
	private function getIpRange( $ip ) {
		# Running locally? Skip IP check
		if ( $this->getSetting( 'firewall_local' ) ) {
			$ip	= '127.0.0.1';
		} else {
			if ( !$this->ip->validateIP( $ip ) ) {
				$this->end( 'Denied IP' );
			}
		}
		$ish	= $this->hostSplit( $ip );	# IP chunks
		
		return $ish;
	}
	
	/**
	 * Prepare host for database search
	 */
	private function getHostRange( $ip ) {
		if ( $this->getSetting( 'firewall_local' ) ) {
			$host	= 'localhost';
		} else {
			$host	= 
			trim( strtolower( gethostbyaddr( $ip ) ) );
			
			if ( !$this->validateHost( $host ) ) {
				$this->end( 'Denied host' );
			}
		}
		
		$hsh	= $this->hostSplit( $host );	# Host chunks
		if( false === $hsh ) {
			$this->end( 'Denied host' );
		}
		
		return $hsh;
	}
	
	# https://stackoverflow.com/questions/14313849/how-to-validate-internationalized-domain-names
	private function validateHost( $host ) {
		if ( mb_strlen( $host, '8bit' ) > self::HOST_SIZE ) {
			return false;
		}
		
		if ( preg_match( self::HOST_RX, $host ) ) {
			return true;
		}
		<commit_msg>Update Sensor.php<commit_after><?php

namespace Blog\Core\Security;
use Blog\Models;
use Blog\Core;
use Blog\Messaging;

class Sensor {
	
	/**
	 * @var array Global variable names
	 */
	private $global_vars = array(
		'$_SERVER',
		'$_GET',
		'$_POST',
		'$_FILES',
		'$_SESSION',
		'$_ENV',
		'$_COOKIE'
	);
	
	/**
	 * @var array Whitelist of acceptable methods
	 */
	private $methods 	= array( 'get', 'head', 'post', 'put' );
	
	/**
	 * @var string Request method
	 */
	private $method;
	
	/**
	 * @var array Whitelist of acceptable ports
	 */
	private $ports		= array( 80, 443 );
	
	/**
	 * @var object ServerRequest 
	 */
	private $request;
	
	/**
	 * @var object App configuration
	 */
	private $config;
	
	/**
	 * @var object Cryptography
	 */
	private $crypto;
	
	/**
	 * @var object BrowserProfile 
	 */
	private $browser;
	
	/**
	 * @var object IP class
	 */
	private $ip;
	
	const HOST_SIZE		= 255;
	const HOST_CHUNKS	= 8;
	const HOST_RX		= 
	'~^([\w-]+://?|www[\.])?([^\-\s\,\;\:\+\/\\\?\^\`\=\&\%\"\'\*\#\<\>]*)\.[a-z]{2,9}$~i';
	
	public function __construct(
		Messaging\ServerRequest $request,
		Core\Config $config,
		Core\Crypto $crypto
	) {
		$this->request	= $request;
		$this->config	= $config;
		$this->crypto	= $crypto;
		
		$this->ip	= new IP();
		$this->browser	= $request->getBrowserProfile();
		$this->method	= 
			strtolower( $this->request->getMethod() );
		
		# Prepare model for future scans
		Models\Model::setConfig( $config );
		Models\Model::setCrypto( $crypto );
		
		$this->sessionCheck();
	}
	
	/**
	 * Run firewall
	 */
	public function run() {
		$this->checkPort();
		$this->accept( $this->methods );
		$this->requestScan();
		
		
		$ip		= $this->ip->getIP();
		$headers	= $this->request->getHeaders();
		$hash		= $this->browser->headerHash() . $ip;
		
		# Prevent redundant scans if it's the same user
		if ( $this->expired( $hash ) ) {
			$this->ipScan( $ip );
			$this->checkHeaders();
			$this->uaScan();
			
			$_SESSION[$hash]	= true;
		}
		
		# Put and Post require extra scrutiny
		$search	= array( 'put', 'post' );
		if ( in_array( $this->method, $search ) ) {
			$this->bodyScan();
		}
	}
	
	/**
	 * Add to the list of allowed ports
	 */
	public function addPort( $port ) {
		$port = ( int ) $port;
		if ( $port < 1 || $port > 65535 ) {
			$this->end( 'Invalid port number' );
		}
		$this->ports[]	= $port;
		$this->ports	= array_unique( $this->ports );
	}
	
	/**
	 * Detect global variable pollution
	 */
	private function globalInjection() {
		foreach ( $this->global_vars as $pre ) {
			if ( !isset( $pre ) ) {
				continue;
			}
			
			foreach ( $pre as $k => $v ) {
				if ( $this->isearch( $pre, $v ) ) {
					$this->end( 'Global injection' );
				}
			}
		}
	}
	
	/**
	 * Check if a scan is needed from the last time using headers
	 */
	private function expired( $hash ) {
		if ( \session_status() === \PHP_SESSION_ACTIVE ) {
			return isset( $_SESSION[$hash] ) ? 
					false : true;
		}
		
		return true;
	}
	
	/**
	 * Scan request path for anomalies
	 */
	private function requestScan() {
		$uri		= $this->request
					->getUri()
					->getRawPath();
		
		$this->blacklist( 
			'firewall_uri',
			function( $u ) use ( $uri ) {
				if ( false !== stripos( $uri, $u ) ) {
					$this->end( 'Invalid URI' );
				}
			}
		);
	}
	
	/**
	 * Scan user agent for anomalies
	 */
	private function uaScan() {
		$ua		= $this->request
					->getHeader( 'User-Agent' );
		$this->blacklist( 
			'firewall_ua',
			function( $u ) use ( $ua ) {
				if ( false !== stripos( $ua[0], $u ) ) {
					$this->end( 'Invalid browser' );
				}
			}
		);
	}
	
	/**
	 * Scan request body for malicious content
	 */
	private function bodyScan() {
		# TODO
		# $body	= $this->request->getBody();
		
	}
	
	/**
	 * Scan IP blocklist
	 */
	private function ipScan( $ip ) {
		
		# Running locally? Skip IP check
		if ( $this->getSetting( 'firewall_local' ) ) {
			$ip	= '127.0.0.1';
		} else {
			if ( !$this->ip->validateIP( $ip ) ) {
				$this->end( 'Denied IP' );
			}
		}
		
		
		if ( $this->getSetting( 'firewall_hosts' ) ) {
			if ( $this->getSetting( 'firewall_local' ) ) {
				$host	= 'localhost';
			} else {
				$host	= 
				trim( strtolower( gethostbyaddr( $ip ) ) );
				if ( !$this->validateHost( $host ) ) {
					$this->end( 'Denied host' );
				}
			}
		} else {
			$host	= '';
		}
		
		# TODO Move this into a database search due to the 
		# 	potentially large number of blocks
		$this->blacklist( 
			'firewall_ip',
			function( $u ) use ( $ip, $host ) {
				$len	= mb_strlen( $u, '8bit' );
				if ( 0 === strncmp( $ip, $u, $len ) ) {
					$this->end( 'Denied IP' );
				}
				
				if ( empty( $host ) ) {
					return;
				}
				
				if ( 0 === strncmp( $host, $u, $len ) ) {
					$this->end( 'Denied host' );
				}
			}
		);
	}
	
	/**
	 * Get combined IP and host ranges for database search
	 */
	private function getSearchRanges() {
		$ip	= $this->ip->getIP();
		$search	= $this->getIpRange( $ip );
		
		# Check host names if enabled
		if ( $this->getSetting( 'firewall_hosts' ) ) {
			$search =  array_merge( 
					$search, 
					$this->getHostRange( $ip )
				);
		}
		
		
	}
	
	/**
	 * Prepare IP for databases
	 */
	private function getIpRange( $ip ) {
		# Running locally? Skip IP check
		if ( $this->getSetting( 'firewall_local' ) ) {
			$ip	= '127.0.0.1';
		} else {
			if ( !$this->ip->validateIP( $ip ) ) {
				$this->end( 'Denied IP' );
			}
		}
		$ish	= $this->hostSplit( $ip );	# IP chunks
		
		return $ish;
	}
	
	/**
	 * Prepare host for database search
	 * 
	 * https://secure.php.net/manual/de/function.gethostbyaddr.php
	 * http://us2.php.net/manual/en/function.idn-to-ascii.php
	 * https://github.com/koraktor/steam-condenser/issues/154
	 */
	private function getHostRange( $ip ) {
		if ( $this->getSetting( 'firewall_local' ) ) {
			$host	= 'localhost';
		} else {
			$host	= 
			trim( strtolower( gethostbyaddr( $ip ) ) );
			
			if ( !$this->validateHost( $host ) ) {
				$this->end( 'Denied host' );
			}
		}
		
		$hsh	= $this->hostSplit( $host );	# Host chunks
		if( false === $hsh ) {
			$this->end( 'Denied host' );
		}
		
		return $hsh;
	}
	
	# https://stackoverflow.com/questions/14313849/how-to-validate-internationalized-domain-names
	private function validateHost( $host ) {
		if ( mb_strlen( $host, '8bit' ) > self::HOST_SIZE ) {
			return false;
		}
		
		if ( preg_match( self::HOST_RX, $host ) ) {
			return true;
		}
		
{'fraction_non_alphanumeric': 0.18912266772528782, 'fraction_numerical': 0.0069868995633187774, 'mean_word_length': 2.4651994497936727, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 151, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1440758', 'n_tokens_mistral': 5656, 'n_tokens_neox': 4638, 'n_words': 1066}