PHP Classes

File: engine/class.www-limiter.php

Recommend this page to a friend!
  Classes of Kristo Vaher  >  Wave Framework  >  engine/class.www-limiter.php  >  Download  
File: engine/class.www-limiter.php
Role: Class source
Content type: text/plain
Description: Limiter Class
Class: Wave Framework
MVC framework for building Web sites and APIs
Author: By
Last change: Updated jQuery 1.10.0 and 2.0.1. Added better API versioning support that also takes into account overrides folders. Refactoring of Imager class to reduce duplicate code between Image Handler and Imager. Also fixed the bug introduced in 3.6.2 that generated incorrect image sprite sizes in dynamic image loading. Code review and some minor changes.
Date: 8 years ago
Size: 16,488 bytes
 

Contents

Class file image Download
<?php

/**
 * Wave Framework <http://www.waveframework.com>
 * Limiter Class
 *
 * This is an optional class that is used to limit HTTP requests based on user agent, IP, 
 * server condition and other information. This class is loaded by Index Gateway. WWW_Limiter 
 * can be used to block IP's if they make too many requests per minute, block requests if 
 * server load is detected as too high, block the request if it comes from blacklist provided 
 * by the system, allow only whitelisted IP's to access, ask for HTTP authentication or force 
 * the user agent to use HTTPS. Note that some of this functionality can be achieved by Apache 
 * configuration and modules, but it is provided here for cases where the project developer 
 * might not have control over server configuration.
 *
 * @package    Limiter
 * @author     Kristo Vaher <kristo@waher.net>
 * @copyright  Copyright (c) 2012, Kristo Vaher
 * @license    GNU Lesser General Public License Version 3
 * @tutorial   /doc/pages/limiter.htm
 * @since      1.0.0
 * @version    3.6.4
 */

class WWW_Limiter {

	/**
	 * This is the main address of the folder where limiter stores log files for 
	 * request limiter.
	 */
	private $logDir='./';
	
	/**
	 * This holds the WWW_Logger object, if it is used. This makes it possible for Limiter 
	 * to write proper log files through Logger in case requests are blocked.
	 */
	public $logger=false;
	
	/**
	 * Construction method of Logger expects just one variable: $logDir, which is the folder where 
	 * limiter stores files for specific limiter methods. This folder should be writable by PHP.
	 *
	 * @param string $logDir location of directory to store log files at
	 * @return WWW_Limiter
	 */
	public function __construct($logDir='./'){
	
		// Defining IP
		if(!defined('__IP__')){
			define('__IP__',$_SERVER['REMOTE_ADDR']);
		}
		
		// Checking if log directory is valid
		if(is_dir($logDir)){
			// Log directory is assigned
			$this->logDir=$logDir;
		} else {
			// Assigned folder is not detected as being a folder
			trigger_error('Assigned limiter folder does not exist',E_USER_ERROR);
		}
		
	}
	
	/**
	 * This method will block requests from the request-making IP address for $duration amount 
	 * of seconds, if the IP address makes more than $limit amount of requests per minute. It 
	 * keeps track of the amount of requests by storing minimal log files in filesystem, in 
	 * $logDir subfolder. Returns true if not limited, throws 403 error if limit exceeded.
	 *
	 * @param integer $limit amount of requests that cannot be exceeded per minute
	 * @param integer $duration duration of how long the IP will be blocked if limit is exceeded
	 * @return boolean or exit if limiter hit
	 */
	public function limitRequestCount($limit=400,$duration=3600){
	
		// Limiter is only used if limit is set higher than 0 and request does not originate from the same server
		if($limit!=0 && __IP__!=$_SERVER['SERVER_ADDR']){
		
			// Log filename is hashed user agents IP
			$logFilename=md5(__IP__);
			// Subfolder name is derived from log filename
			$cacheSubfolder=substr($logFilename,0,2);
			
			// If log directory does not exist, then it is created
			$this->logDir.=$cacheSubfolder.DIRECTORY_SEPARATOR;
			if(!is_dir($this->logDir)){
				// Error is returned if creating the limiter folder with proper permissions does not work
				if(!mkdir($this->logDir,0755)){
					trigger_error('Cannot create limiter folder',E_USER_ERROR);
				}
			}
			
			// If file exists, then the amount of requests are checked, if file does not exist then it is created
			if(file_exists($this->logDir.$logFilename.'.tmp')){
			
				// Loading current contents of the file
				$data=file_get_contents($this->logDir.$logFilename.'.tmp');
				
				// If current file does not say that the IP is blocked, the request frequency is checked
				if($data!='BLOCKED'){
				
					// Limit is checked by counting the most recent requests stored in the file					
					$data=explode("\n",$data);
					if(count($data)>=$limit){
						// Limited amount of rows is taken from the file before data is flipped, minimizing the timestamps for check
						$data=array_slice($data,-$limit); 
						$checkData=array_flip($data);
						// Limit has been reached by all of the requests happening in the same minute
						if(count($checkData)==1){
							// Request is logged and can be used for performance review later
							if($this->logger){
								$this->logger->setCustomLogData(array('response-code'=>429,'category'=>'limiter','reason'=>'Too many requests'));
								$this->logger->writeLog();
							}
							// Block file is created and 403 page thrown to the user agent
							file_put_contents($this->logDir.$logFilename.'.tmp','BLOCKED');
							// Returning proper header
							header('HTTP/1.1 429 Too Many Requests');
							header('Retry-After: '.$duration);
							// Response to be displayed in browser
							echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 429 Too Many Requests</div>';
							echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">YOUR IP HAS MADE TOO MANY REQUESTS TO THIS SERVER, TRY AGAIN IN '.$duration.' SECONDS</div>';
							die();
						}
					}
					
					// When limit was not exceeded, file is stored again with new data
					$limiterData=implode("\n",$data);
					file_put_contents($this->logDir.$logFilename.'.tmp',$limiterData."\n".date('Y-m-d H:i',$_SERVER['REQUEST_TIME']));
					
				} else {
				
					// The time when lock was created
					$blockDuration=filemtime($this->logDir.$logFilename.'.tmp');
					// If the file that has blocked the requests is older than the limit duration, then block is deleted, otherwise 403 page is shown
					if(time()-$blockDuration>=$duration){
						// Block file is removed
						unlink($this->logDir.$logFilename.'.tmp');
					} else {
						// Request is logged and can be used for performance review later
						if($this->logger){
							$this->logger->setCustomLogData(array('response-code'=>429,'category'=>'limiter','reason'=>'Too many requests'));
							$this->logger->writeLog();
						}
						// Returning 403 header
						$retryAfter=($duration-(time()-$blockDuration));
						header('HTTP/1.1 429 Too Many Requests');
						header('Retry-After: '.$retryAfter);
						// Response to be displayed in browser
						echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 429 Too Many Requests</div>';
						echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">YOUR IP HAS MADE TOO MANY REQUESTS TO THIS SERVER, TRY AGAIN IN '.$retryAfter.' SECONDS</div>';
						die();
					}
					
				}
				
			} else {
			
				// Current date, hour and minute are stored in the file
				file_put_contents($this->logDir.$logFilename.'.tmp',date('Y-m-d H:i',$_SERVER['REQUEST_TIME']));
				
			}
		}
		
		// Request limiter processed
		return true;
		
	}
	
	/**
	 * This method will block HTTP requests if server load is more than $limit. It throws 
	 * 503 Service Unavailable message should that happen.
	 *
	 * @param integer $limit server load that, if exceeded, causes the user agents request to be blocked
	 * @return boolean or exit if limiter hit
	 */
	public function limitServerLoad($limit=80){
	
		// System load is checked only if limit is not set
		if($limit!=0){
			// This function does not return on Windows servers
			if(function_exists('sys_getloadavg')){
				// Returns system load in the last 1, 5 and 15 minutes.
				$load=sys_getloadavg();
				// 503 page is returned if load is above limit
				if($load[0]>$limit){
					// Request is logged and can be used for performance review later
					if($this->logger){
						$this->logger->setCustomLogData(array('response-code'=>503,'category'=>'limiter','reason'=>'Server load exceeded, current load is '.$load[0].', limit is '.$limit));
						$this->logger->writeLog();
					}
					// Returning 503 header
					header('HTTP/1.1 503 Service Unavailable');
					// Response to be displayed in browser
					echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 503 Service Unavailable</div>';
					echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">SERVER IS UNDER A LOT OF STRESS, YOUR REQUEST IS CURRENTLY BLOCKED, PLEASE TRY AGAIN LATER</div>';
					die();
				}
			} else {
				return true;
			}
		}
		
		// Server load limiter processed
		return true;
	
	}
	
	/**
	 * This method only allows HTTP requests from a comma-separated list of IP addresses 
	 * sent with $whitelist. For every other IP address it throws a 403 Forbidden error.
	 *
	 * @param string $whiteList comma-separated list of whitelisted IP addresses
	 * @return boolean or exit if limiter hit
	 */
	public function limitWhitelisted($whiteList=''){
	
		// This value should be a comma-separated string of blacklisted IP's
		if($whiteList!=''){
			// Exploding string of IP's into an array
			$whiteList=explode(',',$whiteList);
			// Checking if the user agent IP is set in blacklist array
			if(empty($whiteList) || !in_array(__IP__,$whiteList)){
				// Request is logged and can be used for performance review later
				if($this->logger){
					$this->logger->setCustomLogData(array('response-code'=>403,'category'=>'limiter','reason'=>'Not whitelisted'));
					$this->logger->writeLog();
				}
				// Returning 403 data
				header('HTTP/1.1 403 Forbidden');
				// Response to be displayed in browser
				echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 403 Forbidden</div>';
				echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">YOUR IP IS NOT ALLOWED TO USE THIS SERVICE</div>';
				die();
			}
		}
		
		// Blacklist processed
		return true;
		
	}
	
	/**
	 * This method blocks IP addresses sent with $blackList as a comma-separated list. If HTTP 
	 * request has an IP defined in that list, then Limiter throws a 403 Forbidden error.
	 *
	 * @param string $blackList comma-separated list of blacklisted IP addresses
	 * @return boolean or exit if limiter hit
	 */
	public function limitBlacklisted($blackList=''){
	
		// This value should be a comma-separated string of blacklisted IP's
		if($blackList!=''){
			// Exploding string of IP's into an array
			$blackList=explode(',',$blackList);
			// Checking if the user agent IP is set in blacklist array
			if(!empty($blackList) && in_array(__IP__,$blackList)){
				// Request is logged and can be used for performance review later
				if($this->logger){
					$this->logger->setCustomLogData(array('response-code'=>403,'category'=>'limiter','reason'=>'Blacklisted'));
					$this->logger->writeLog();
				}
				// Returning 403 data
				header('HTTP/1.1 403 Forbidden');
				// Response to be displayed in browser
				echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 403 Forbidden</div>';
				echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">YOUR IP IS NOT ALLOWED TO USE THIS SERVICE</div>';
				die();
			}
		}
		
		// Blacklist processed
		return true;
		
	}
	
	/**
	 * This method asks for basic HTTP authentication $username and $password and throws a 
	 * 403 Forbidden error if provided credentials are incorrect or missing. It is also 
	 * possible to provide a comma-separated list of IP addresses in $ip that allow this 
	 * type of authentication for additional security.
	 *
	 * @param string $username correct username for the request
	 * @param string $password correct password for the request
	 * @param string $ip comma separated list of allowed IP addresses
	 * @return boolean or exit if limiter hit
	 */
	public function limitUnauthorized($username,$password,$ip='*'){
	
		// If all IP's are not allowed
		if($ip!='*'){
			$ip=explode(',',$ip);
		}
	
		// If provided username and password are not correct, then 401 page is displayed to the user agent
		if(is_array($ip) && !in_array(__IP__,$ip)){
			header('HTTP/1.1 401 Unauthorized');
			// Response to be displayed in browser
			echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 401 Unauthorized</div>';
			echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">YOUR IP IS NOT ALLOWED TO USE THIS SERVICE</div>';
			die();
		} elseif(!isset($_SERVER['PHP_AUTH_USER']) || $_SERVER['PHP_AUTH_USER']!=$username || !isset($_SERVER['PHP_AUTH_PW']) || $_SERVER['PHP_AUTH_PW']!=$password){
			// Request is logged and can be used for performance review later
			if($this->logger){
				$this->logger->setCustomLogData(array('response-code'=>401,'category'=>'limiter','reason'=>'Authorization required'));
				$this->logger->writeLog();
			}
			// Returning 401 headers
			header('WWW-Authenticate: Basic realm="'.$_SERVER['HTTP_HOST'].'"');
			header('HTTP/1.1 401 Unauthorized');
			// Response to be displayed in browser
			echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 401 Unauthorized</div>';
			echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">AUTHORIZATION DETAILS ARE REQUIRED</div>';
			die();
		}
		
		// HTTP authorization processed
		return true;
		
	}
	
	/**
	 * This method either throws a 403 Forbidden error if non-HTTPS connection is used to make 
	 * a request, or redirects the request to HTTPS. If $autoRedirect is set to true, then HTTP 
	 * requests are automatically redirected.
	 *
	 * @param boolean $autoRedirect if this is set to true, then system redirects user agent to HTTPS
	 * @return boolean or exit if limiter hit
	 */
	public function limitNonSecureRequests($autoRedirect=true){
	
		// HTTPS is detected from $_SERVER variables
		if(!isset($_SERVER['HTTPS']) || ($_SERVER['HTTPS']!=1 && $_SERVER['HTTPS']!='on')){
			// If auto redirect is on, user agent is forwarded by replacing the http:// protocol with https://
			if($autoRedirect){
				// Redirecting to HTTPS address
				header('Location: https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
			} else {
				// Request is logged and can be used for performance review later
				if($this->logger){
					$this->logger->setCustomLogData(array('response-code'=>401,'category'=>'limiter','reason'=>'HTTPS required'));
					$this->logger->writeLog();
				}
				// Returning 401 header
				header('HTTP/1.1 403 Forbidden');
				// Response to be displayed in browser
				echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 403 Forbidden</div>';
				echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">HTTPS CONNECTION IS REQUIRED</div>';
			}
			// Script is halted
			die();
		} else {
			// This tells the browser to remember to use HTTPS when accessing the site.
			header('Strict-Transport-Security: max-age=2147483647 [; includeSubdomains]');
		}
		
		// HTTPS check processed
		return true;
		
	}
	
	/**
	 * This method either throws a 403 Forbidden error if a referrer is used that is not accepted.
	 *
	 * @param string $allowed This is comma-separated list of domains that are allowed or not to be the referrer
	 * @return boolean or exit if limiter hit
	 */
	public function limitReferrer($allowed='*'){
	
		if(isset($_SERVER['HTTP_REFERER'])){
			// Allowed setting can be a comma-separated string
			$allowed=explode(',',$allowed);
			// Parsing the referrer URL
			$referrer=parse_url($_SERVER['HTTP_REFERER']);
			// Checking for domain name existence and returning true, if accepted
			if(in_array('*',$allowed) && !in_array('!'.$referrer['host'],$allowed)){
				return true;
			} elseif(in_array($referrer['host'],$allowed)){
				return true;
			}
			// Request is logged and can be used for performance review later
			if($this->logger){
				$this->logger->setCustomLogData(array('response-code'=>401,'category'=>'limiter','reason'=>'Incorrect referrer'));
				$this->logger->writeLog();
			}
			// Returning 401 header
			header('HTTP/1.1 403 Forbidden');
			// Response to be displayed in browser
			echo '<div style="font:18px Tahoma; text-align:center;padding:100px 50px 10px 50px;">HTTP/1.1 403 Forbidden</div>';
			echo '<div style="font:14px Tahoma; text-align:center;padding:10px 50px 100px 50px;">THIS REFERRER IS NOT ALLOWED</div>';
			// Script is halted
			die();
		}
		
		// Referrer check processed
		return true;
		
	}

}
	
?>
For more information send a message to info at phpclasses dot org.