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: Fixed tiny errors as a result of PHP version updates
Date: 14 days ago
Size: 16,504 bytes
 

Contents

Class file image Download
<?php /** * Wave Framework <http://github.com/kristovaher/Wave-Framework> * 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; } } ?>