mirror of
https://github.com/e107inc/e107.git
synced 2025-01-17 20:58:30 +01:00
1683 lines
50 KiB
PHP
1683 lines
50 KiB
PHP
<?php
|
|
/*
|
|
* e107 website system
|
|
*
|
|
* Copyright 2008-2013 e107 Inc (e107.org)
|
|
* Released under the terms and conditions of the
|
|
* GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
|
|
*
|
|
* IP Address related routines, including banning-related code
|
|
*
|
|
* $URL$
|
|
* $Revision$
|
|
* $Id$
|
|
*
|
|
*/
|
|
|
|
|
|
/**
|
|
* @package e107
|
|
* @subpackage e107_handlers
|
|
* @version $Id$;
|
|
*
|
|
* Routines to manage IP addresses and banning.
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* Class to handle ban-related checks, and provide some utility functions related to IP addresses
|
|
* There are two parts to the class:
|
|
*
|
|
* Part 1
|
|
* ------
|
|
* This part intentionally does NO database access, and requires an absolute minimum of file paths to be set up
|
|
* (this is to minimise processing load in the event of an access from a banned IP address)
|
|
* It works only with the user's IP address, and potentially browser 'signature'
|
|
* The objective of this part is to do only those things which can be done without the database open, and without complicating things later on
|
|
* (If DB access is required to handle a ban, it should only need to be done occasionally)
|
|
*
|
|
* Part 2
|
|
* ------
|
|
* This part handles those functions which require DB access.
|
|
* The intention is that Part 1 will catch most existing bans, to reduce the incidence of abortive DB opens
|
|
* If part 1 signals that a ban has expired, part 2 removes it from the database
|
|
*
|
|
* Elsewhere
|
|
* ---------
|
|
* if ban retriggering is enabled, cron task needs to scan the ban log periodically to update the expiry times. (Can't do on every access, since it would
|
|
* eliminate the benefits of this handler - a DB access would be needed on every access from a banned IP address).
|
|
* @todo Implement the ban retriggering cron job (elsewhere)
|
|
* - do we have a separate text file for the accesses in need of retriggering? Could then delete it once actioned; keeps it small
|
|
* @todo Implement flood bans - needs db access - maybe leave to the second part of this file or the online handler
|
|
*
|
|
* All IP addresses are stored in 'normal' form - a fixed length IPV6 format with separator colons.
|
|
*
|
|
* To use:
|
|
* include this file, early on (before DB accesses started), and instantiate class ipHandler.
|
|
*
|
|
*/
|
|
|
|
|
|
class eIPHandler
|
|
{
|
|
/**
|
|
* IPV6 string for localhost - as stored in DB
|
|
*/
|
|
// const LOCALHOST_IP = '0000:0000:0000:0000:0000:ffff:7f00:0001';
|
|
|
|
|
|
const BAN_REASON_COUNT = 7; // Update as more ban reasons added (max 10 supported)
|
|
|
|
const BAN_TYPE_LEGACY = 0; // Shouldn't get these unless update process not run
|
|
const BAN_TYPE_MANUAL = -1; /// Manually entered bans
|
|
const BAN_TYPE_FLOOD = -2; /// Flood ban
|
|
const BAN_TYPE_HITS = -3;
|
|
const BAN_TYPE_LOGINS = -4;
|
|
const BAN_TYPE_IMPORTED = -5; /// Imported bans
|
|
const BAN_TYPE_USER = -6; /// User is banned
|
|
// Spare value
|
|
const BAN_TYPE_UNKNOWN = -8;
|
|
const BAN_TYPE_TEMPORARY = -9; /// Used during CSV import - giving it this value highlights problems
|
|
|
|
const BAN_TYPE_WHITELIST = 100; /// Entry for whitelist - actually not a ban at all! Keep at this value for BC
|
|
|
|
|
|
const BAN_FILE_DIRECTORY = 'cache/'; /// Directory containing the text files (within e_SYSTEM)
|
|
const BAN_LOG_DIRECTORY = 'logs/'; /// Directory containing the log file (within e_SYSTEM)
|
|
|
|
const BAN_FILE_LOG_NAME = 'banlog.log'; /// Logs bans etc
|
|
// Note for the following file names - the code appends the extension
|
|
const BAN_FILE_IP_NAME = 'banlist'; /// Saves list of banned and whitelisted IP addresses
|
|
const BAN_FILE_ACTION_NAME = 'banactions'; /// Details of actions for different ban types
|
|
const BAN_FILE_HTACCESS = 'banhtaccess'; /// File in format for direct paste into .htaccess
|
|
const BAN_FILE_CSV_NAME = 'banlistcsv'; /// Output file in CSV format
|
|
const BAN_FILE_RETRIGGER_NAME = 'banretrigger'; /// Any bans needing retriggering
|
|
const BAN_FILE_EXTENSION = '.php'; /// File extension to use
|
|
|
|
/**
|
|
* IP address of current user, in 'normal' form
|
|
*/
|
|
private $ourIP = '';
|
|
|
|
private $serverIP = '';
|
|
|
|
private $debug = false;
|
|
/**
|
|
* Host name of current user
|
|
* Initialised when requested
|
|
*/
|
|
private $_host_name_cache = array();
|
|
|
|
|
|
/**
|
|
* Token for current user, calculated from browser settings.
|
|
* Supplements IP address (Can be spoofed, but helps differentiate among honest users at the same IP address)
|
|
*/
|
|
private $accessID = '';
|
|
|
|
/**
|
|
* Path to directory containing current config file(s)
|
|
*/
|
|
private $ourConfigDir = '';
|
|
|
|
/**
|
|
* Current user's IP address status. Usually zero (neutral); may be one of the BAN_TYPE_xxx constants
|
|
*/
|
|
private $ipAddressStatus = 0;
|
|
|
|
|
|
/**
|
|
* Flag set to the IP address that triggered the match, if current IP has an expired ban to clear
|
|
*/
|
|
private $clearBan = FALSE;
|
|
|
|
|
|
/**
|
|
* IP Address from ban list file which matched (may have wildcards)
|
|
*/
|
|
private $matchAddress = '';
|
|
|
|
/**
|
|
* Number of entries read from banlist/whitelist
|
|
*/
|
|
private $actionCount = 0;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* Only one instance of this class is ever loaded, very early on in the initialisation sequence
|
|
*
|
|
* @param string $configDir Path to the directory containing the files used by this class
|
|
* If not set, defaults to BAN_FILE_DIRECTORY constant
|
|
*
|
|
* On load it gets the user's IP address, and checks it against whitelist and blacklist files
|
|
* If the address is blacklisted, displays an appropriate message (as configured) and aborts
|
|
* Otherwise sets up
|
|
*/
|
|
public function __construct($configDir = '')
|
|
{
|
|
$configDir = trim((string) $configDir);
|
|
|
|
if ($configDir)
|
|
{
|
|
$this->ourConfigDir = realpath($configDir);
|
|
}
|
|
else
|
|
{
|
|
$this->ourConfigDir = e_SYSTEM.eIPHandler::BAN_FILE_DIRECTORY;
|
|
}
|
|
|
|
|
|
$this->ourIP = $this->ipEncode($this->getCurrentIP());
|
|
|
|
$this->serverIP = $this->ipEncode(isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : 'x.x.x.x');
|
|
|
|
$this->makeUserToken();
|
|
$ipStatus = $this->checkIP($this->ourIP);
|
|
if ($ipStatus != 0)
|
|
{
|
|
if ($ipStatus < 0)
|
|
{ // Blacklisted
|
|
$this->logBanItem($ipStatus, 'result --> '.$ipStatus); // only log blacklist
|
|
$this->banAction($ipStatus); // This will abort if appropriate
|
|
}
|
|
//elseif ($ipStatus > 0)
|
|
// { // Whitelisted - we may want to set a specific indicator
|
|
// }
|
|
}
|
|
// Continue here - user not banned (so far)
|
|
}
|
|
|
|
/**
|
|
* @param $ip
|
|
* @return void
|
|
*/
|
|
public function setIP($ip)
|
|
{
|
|
$this->ourIP = $this->ipEncode($ip);
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @param $value
|
|
* @return void
|
|
*/
|
|
public function debug($value)
|
|
{
|
|
$this->debug = $value === true;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Add an entry to the banlist log file (which is a simple text file)
|
|
* A date/time string is prepended to the line
|
|
*
|
|
* @param int $reason - numeric reason code, usually in range -10..+10
|
|
* @param string $message - additional text as required (length not checked, but should be less than 100 characters or so
|
|
*
|
|
* @return void
|
|
*/
|
|
private function logBanItem($reason, $message)
|
|
{
|
|
if ($tmp = fopen(e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME, 'a'))
|
|
{
|
|
$logLine = time().' '.$this->ourIP.' '.$reason.' '.$message."\n";
|
|
fwrite($tmp,$logLine);
|
|
fclose($tmp);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Generate relatively unique user token from browser info
|
|
* (but don't believe that the browser info is accurate - can readily be spoofed)
|
|
*
|
|
* This supplements use of the IP address in some places; both to improve user identification, and to help deal with dynamic IP allocations
|
|
*
|
|
* May be replaced by a 'global' e107 token at some point
|
|
*/
|
|
private function makeUserToken()
|
|
{
|
|
$tmpStr = '';
|
|
foreach (array('HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_ACCEPT_ENCODING') as $v)
|
|
{
|
|
if (isset($_SERVER[$v]))
|
|
{
|
|
$tmpStr .= $_SERVER[$v];
|
|
}
|
|
else
|
|
{
|
|
$tmpStr .= 'dummy'.$v;
|
|
}
|
|
}
|
|
$this->accessID = md5($tmpStr);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Return browser-characteristics token
|
|
*/
|
|
public function getUserToken()
|
|
{
|
|
return $this->accessID; // Should always be defined at this point
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Check whether an IP address is routable
|
|
*
|
|
* @param string $ip - IPV4 or IPV6 numeric address.
|
|
*
|
|
* @return boolean TRUE if routable, FALSE if not
|
|
|
|
@todo handle IPV6 fully
|
|
*/
|
|
public function isAddressRoutable($ip)
|
|
{
|
|
$ignore = array(
|
|
'0\..*' , '^127\..*' , // Local loopbacks
|
|
'192\.168\..*' , // RFC1918 - Private Network
|
|
'172\.(?:1[6789]|2\d|3[01])\..*' , // RFC1918 - Private network
|
|
'10\..*' , // RFC1918 - Private Network
|
|
'169\.254\..*' , // RFC3330 - Link-local, auto-DHCP
|
|
'2(?:2[456789]|[345][0-9])\..*' // Single check for Class D and Class E
|
|
);
|
|
|
|
|
|
|
|
$pattern = '#^('.implode('|',$ignore).')#';
|
|
|
|
if(preg_match($pattern,$ip))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
|
|
/* XXX preg_match doesn't accept arrays.
|
|
if (preg_match(array(
|
|
'#^0\..*#' , '#^127\..*#' , // Local loopbacks
|
|
'#^192\.168\..*#' , // RFC1918 - Private Network
|
|
'#^172\.(?:1[6789]|2\d|3[01])\..*#' , // RFC1918 - Private network
|
|
'#^10\..*#' , // RFC1918 - Private Network
|
|
'#^169\.254\..*#' , // RFC3330 - Link-local, auto-DHCP
|
|
'#^2(?:2[456789]|[345][0-9])\..*#' // Single check for Class D and Class E
|
|
), $ip))
|
|
{
|
|
return FALSE;
|
|
}
|
|
*/
|
|
|
|
if (strpos(':', $ip) === FALSE) return TRUE;
|
|
// Must be an IPV6 address here
|
|
// @todo need to handle IPV4 addresses in IPV6 format
|
|
$ip = strtolower($ip);
|
|
if ($ip == 'ff02::1') return FALSE; // link-local all nodes multicast group
|
|
if ($ip == 'ff02:0000:0000:0000:0000:0000:0000:0001') return FALSE;
|
|
if ($ip == '::1') return FALSE; // localhost
|
|
if ($ip == '0000:0000:0000:0000:0000:0000:0000:0001') return FALSE;
|
|
if (strpos($ip, 'fc00:') === 0) return FALSE; // local addresses
|
|
// @todo add:
|
|
// ::0 (all zero) - invalid
|
|
// ff02::1:ff00:0/104 - Solicited-Node multicast addresses - add?
|
|
// 2001:0000::/29 through 2001:01f8::/29 - special purpose addresses
|
|
// 2001:db8::/32 - used in documentation
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get current user's IP address in 'normal' form.
|
|
* Likely to be very similar to existing e107::getIP() function
|
|
* May log X-FORWARDED-FOR cases - or could generate a special IPV6 address, maybe?
|
|
*/
|
|
private function getCurrentIP()
|
|
{
|
|
if(!$this->ourIP)
|
|
{
|
|
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'x.x.x.x';
|
|
if ($ip4 = getenv('HTTP_X_FORWARDED_FOR'))
|
|
{
|
|
if (!$this->isAddressRoutable($ip))
|
|
{
|
|
$ip3 = explode(',', $ip4); // May only be one address; could be several, comma separated, if multiple proxies used
|
|
$ip = trim($ip3[count($ip3) - 1]); // If IP address is unroutable, replace with any forwarded_for address
|
|
$this->logBanItem(0, 'X_Forward '.$ip4.' --> '.$ip); // Just log for interest ATM
|
|
}
|
|
}
|
|
$this->ourIP = $this->ipEncode($ip); // Normalise for storage
|
|
}
|
|
return $this->ourIP;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Return the user's IP address, in normal or display-friendly form as requested
|
|
*
|
|
* @param boolean $forDisplay - TRUE for minimum-length display-friendly format. FALSE for 'normal' form (to be used when storing into DB etc)
|
|
*
|
|
* @return string IP address
|
|
*
|
|
* Note: if we define USER_IP (and maybe USER_DISPLAY_IP) constant, this function is strictly unnecessary. But we still need a format conversion routine
|
|
*/
|
|
public function getIP($forDisplay = FALSE)
|
|
{
|
|
if ($forDisplay == FALSE) return $this->ourIP;
|
|
return $this->ipDecode($this->ourIP);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Takes appropriate action for a blacklisted IP address
|
|
*
|
|
* @param int $code - integer value < 0 specifying the ban reason.
|
|
*
|
|
* @return void (may not even return)
|
|
*
|
|
* Looks up the reason code, and extracts the corresponding text.
|
|
* If this text begins with 'http://' or 'https://', assumed to be a link to a web page, and redirects.
|
|
* Otherwise displays an error message to the user (if configured) then aborts.
|
|
*/
|
|
private function banAction($code)
|
|
{
|
|
$search = '['.$code.']';
|
|
$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION;
|
|
|
|
if(!is_readable($fileName)) // Note readable, but the IP is still banned, so half further script execution.
|
|
{
|
|
if($this->debug === true || defset('e_DEBUG') === true)
|
|
{
|
|
echo "Your IP is banned!";
|
|
}
|
|
|
|
die();
|
|
// return; //
|
|
}
|
|
|
|
$vals = file($fileName);
|
|
if ($vals === FALSE || count($vals) == 0) return;
|
|
if (strpos($vals[0], '<?php') !== 0)
|
|
{
|
|
echo 'Invalid message file';
|
|
die();
|
|
}
|
|
unset($vals[0]);
|
|
foreach ($vals as $line)
|
|
{
|
|
if (strpos($line, ';') === 0) continue;
|
|
if (strpos($line, $search) === 0)
|
|
{ // Found the action line
|
|
if (e107::getPref('ban_retrigger'))
|
|
{
|
|
if ($tmp = fopen($this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION, 'a'))
|
|
{
|
|
$logLine = time().' '.$this->matchAddress.' '.$code.' Retrigger: '.$this->ourIP."\n"; // Same format as log entries - can share routines
|
|
fwrite($tmp,$logLine);
|
|
fclose($tmp);
|
|
}
|
|
}
|
|
$line = trim(substr($line, strlen($search)));
|
|
if ((strpos($line, 'http://') === 0) || (strpos($line, 'https://') === 0))
|
|
{ // Display a specific web page
|
|
if (strpos($line, '?') === FALSE)
|
|
{
|
|
$line .= '?'.$search; // Add on the ban reason - may be useful in the page
|
|
}
|
|
e107::redirect($line);
|
|
exit();
|
|
}
|
|
// Otherwise just display any message and die
|
|
if($this->debug)
|
|
{
|
|
print_a("User Banned");
|
|
}
|
|
|
|
echo $line;
|
|
|
|
die();
|
|
}
|
|
}
|
|
$this->logBanItem($code, 'Unmatched action: '.$search.' - no block implemented');
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get whitelist and blacklist
|
|
*
|
|
* @return array - each element is an array with elements 'ip', 'action, and 'time_limit'
|
|
*
|
|
* Note: Intentionally a single call, so the two lists can be split across files as convenient
|
|
*
|
|
* At present the list is a single file, one entry per line, whitelist entries first. Most precisely defined addresses before larger subnets
|
|
*
|
|
* Format of each line is:
|
|
* IP_address action expiry_time additional_parameters
|
|
*
|
|
* where action is: >0 = whitelisted, <0 blacklisted, value is 'reason code'
|
|
* expiry_time is zero for an indefinite ban, time stamp for a limited ban
|
|
* additional_parameters may be required for certain actions in the future
|
|
*/
|
|
private function getWhiteBlackList()
|
|
{
|
|
$ret = array();
|
|
$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_IP_NAME.eIPHandler::BAN_FILE_EXTENSION;
|
|
if (!is_readable($fileName)) return $ret;
|
|
|
|
$vals = file($fileName);
|
|
if ($vals === FALSE || count($vals) == 0) return $ret;
|
|
if (strpos($vals[0], '<?php') !== 0)
|
|
{
|
|
echo 'Invalid list file';
|
|
die(); // Debatable, because admins can't get in if this fails. But can manually delete the file.
|
|
}
|
|
unset($vals[0]);
|
|
foreach ($vals as $line)
|
|
{
|
|
if (strpos($line, ';') === 0) continue;
|
|
if (trim($line))
|
|
{
|
|
$tmp = explode(' ',$line);
|
|
if (count($tmp) >= 2)
|
|
{
|
|
$ret[] = array('ip' => $tmp[0], 'action' => $tmp[1], 'time_limit' => intval(varset($tmp[2], 0)));
|
|
}
|
|
}
|
|
}
|
|
$this->actionCount = count($ret); // Note how many entries in list
|
|
return $ret;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Checks whether IP address is in the whitelist or blacklist.
|
|
*
|
|
* @param string $addr - IP address in 'normal' form
|
|
*
|
|
* @return int - >0 = whitelisted, 0 = not listed (= 'OK'), <0 is 'reason code' for ban
|
|
*
|
|
* note: Could maybe combine this with getWhiteBlackList() for efficiency, but makes it less general
|
|
*/
|
|
private function checkIP($addr)
|
|
{
|
|
$now = time();
|
|
$checkLists = $this->getWhiteBlackList();
|
|
|
|
if($this->debug)
|
|
{
|
|
echo "<h4>Banlist.php</h4>";
|
|
print_a($checkLists);
|
|
print_a("Now: ".$now. " ".date('r',$now));
|
|
}
|
|
|
|
|
|
foreach ($checkLists as $val)
|
|
{
|
|
if (strpos($addr, $val['ip']) === 0) // See if our address begins with an entry - handles wildcards
|
|
{ // Match found
|
|
|
|
if($this->debug)
|
|
{
|
|
print_a("Found ".$addr." in file. TimeLimit: ".date('r',$val['time_limit']));
|
|
}
|
|
|
|
if (($val['time_limit'] == 0) || ($val['time_limit'] > $now))
|
|
{ // Indefinite ban, or timed ban (not expired) or whitelist entry
|
|
if ($val['action']== eIPHandler::BAN_TYPE_LEGACY) return eIPHandler::BAN_TYPE_MANUAL; // Precautionary
|
|
$this->matchAddress = $val['ip'];
|
|
return $val['action']; // OK to just return - PHP should release the memory used by $checkLists
|
|
}
|
|
// Time limit expired
|
|
$this->clearBan = $val['ip']; // Note what triggered the match - it could be a wildcard (although timed ban unlikely!)
|
|
return 0; // Can just return - shouldn't be another entry
|
|
}
|
|
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Encode an IPv4 address into IPv6
|
|
* Similar functionality to ipEncode
|
|
*
|
|
* @param $ip
|
|
* @param bool $wildCards
|
|
* @param string $div
|
|
* @return string - the 'ip4' bit of an IPv6 address (i.e. last 32 bits)
|
|
*/
|
|
private function ip4Encode($ip, $wildCards = FALSE, $div = ':')
|
|
{
|
|
$ipa = explode('.', $ip);
|
|
$temp = '';
|
|
for ($s = 0; $s < 4; $s++)
|
|
{
|
|
if (!isset($ipa[$s])) $ipa[$s] = '*';
|
|
if ((($ipa[$s] == '*') || (strpos($ipa[$s], 'x') !== FALSE)) && $wildCards)
|
|
{
|
|
$temp .= 'xx';
|
|
}
|
|
else
|
|
{ // Put a zero in if wildcards not allowed
|
|
$temp .= sprintf('%02x', $ipa[$s]);
|
|
}
|
|
if ($s == 1) $temp .= $div;
|
|
}
|
|
return $temp;
|
|
}
|
|
|
|
|
|
/**
|
|
* Encode an IP address to internal representation. Returns string if successful; FALSE on error
|
|
* Default separates fields with ':'; set $div='' to produce a 32-char packed hex string
|
|
*
|
|
* @param string $ip - 'raw' IP address. May be IPv4, IPv6
|
|
* @param boolean $wildCards - if TRUE, wildcard characters allowed at the end of an address:
|
|
* '*' replaces 2 hex characters (primarily for 8-bit subnets of IPv4 addresses)
|
|
* 'x' replaces a single hex character
|
|
* @param string $div separator between 4-character blocks of the IPv6 address
|
|
*
|
|
* @return bool|string encoded IP. Always exactly 32 characters plus separators if conversion successful
|
|
* FALSE if conversion unsuccessful
|
|
*/
|
|
public function ipEncode($ip, $wildCards = FALSE, $div = ':')
|
|
{
|
|
$ret = '';
|
|
$divider = '';
|
|
if(strpos($ip, ':')!==FALSE)
|
|
{ // Its IPV6 (could have an IP4 'tail')
|
|
if(strpos($ip, '.')!==FALSE)
|
|
{ // IPV4 'tail' to deal with
|
|
$temp = strrpos($ip, ':')+1;
|
|
$ip = substr($ip, 0, $temp).$this->ip4Encode(substr($ip, $temp), $wildCards, $div);
|
|
}
|
|
// Now 'normalise' the address
|
|
$temp = explode(':', $ip);
|
|
$s = 8-count($temp); // One element will of course be the blank
|
|
foreach($temp as $f)
|
|
{
|
|
if($f=='')
|
|
{
|
|
$ret .= $divider.'0000'; // Always put in one set of zeros for the blank
|
|
$divider = $div;
|
|
if($s>0)
|
|
{
|
|
$ret .= str_repeat($div.'0000', $s);
|
|
$s = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$ret .= $divider.sprintf('%04x', hexdec($f));
|
|
$divider = $div;
|
|
}
|
|
}
|
|
return $ret;
|
|
}
|
|
if(strpos($ip, '.')!==FALSE)
|
|
{ // Its IPV4
|
|
return str_repeat('0000'.$div, 5).'ffff'.$div.$this->ip4Encode($ip, $wildCards, $div);
|
|
}
|
|
return FALSE; // Unknown
|
|
}
|
|
|
|
|
|
/**
|
|
* Given a potentially truncated IPV6 address as used in the ban list files, adds 'x' characters etc to create
|
|
* a normalised IPV6 address as stored in the DB. Returned length is exactly 39 characters
|
|
* @param $address
|
|
* @return string
|
|
*/
|
|
public function ip6AddWildcards($address)
|
|
{
|
|
while (($togo = (39 - strlen($address))) > 0)
|
|
{
|
|
if (($togo % 5) == 0)
|
|
{
|
|
$address .= ':';
|
|
}
|
|
else
|
|
{
|
|
$address .= 'x';
|
|
}
|
|
}
|
|
return $address;
|
|
}
|
|
|
|
|
|
/**
|
|
* Takes an encoded IP address - returns a displayable one
|
|
* Set $IP4Legacy TRUE to display 'old' (IPv4) addresses in the familiar dotted format,
|
|
* FALSE to display in standard IPV6 format
|
|
* Should handle most things that can be thrown at it.
|
|
* If wildcard characters ('x' found, incorporated 'as is'
|
|
*
|
|
* @param string $ip encoded IP
|
|
* @param boolean $IP4Legacy
|
|
* @return string decoded IP
|
|
*/
|
|
public function ipDecode($ip, $IP4Legacy = TRUE)
|
|
{
|
|
if (strpos($ip, '.') !== false)
|
|
{
|
|
if ($IP4Legacy) return $ip; // Assume its unencoded IPV4
|
|
$ipa = explode('.', $ip);
|
|
$ip = '0:0:0:0:0:ffff:'.sprintf('%02x%02x:%02x%02x', $ipa[0], $ipa[1], $ipa[2], $ipa[3]);
|
|
$ip = str_repeat('0000'.':', 5).'ffff:'.$this->ip4Encode($ip, TRUE, ':');
|
|
}
|
|
if (strpos($ip, '::') !== false) return $ip; // Assume its a compressed IPV6 address already
|
|
if ((strlen($ip) == 8) && strpos($ip, ':') === false)
|
|
{ // Assume a 'legacy' IPV4 encoding
|
|
$ip = '0:0:0:0:0:ffff:'.implode(':',str_split($ip,4)); // Turn it into standard IPV6
|
|
}
|
|
elseif ((strlen($ip) == 32) && strpos($ip, ':') === false)
|
|
{ // Assume a compressed hex IPV6
|
|
$ip = implode(':',str_split($ip,4));
|
|
}
|
|
if (strpos($ip, ':') === false) return FALSE; // Return on problem - no ':'!
|
|
$temp = explode(':',$ip);
|
|
$z = 0; // State of the 'zero manager' - 0 = not started, 1 = running, 2 = done
|
|
$ret = '';
|
|
$zc = 0; // Count zero fields (not always required)
|
|
foreach ($temp as $t)
|
|
{
|
|
$v = hexdec($t);
|
|
if (($v != 0) || ($z == 2) || (strpos($t, 'x') !== FALSE))
|
|
{
|
|
if ($z == 1)
|
|
{ // Just finished a run of zeros
|
|
$z++;
|
|
$ret .= ':';
|
|
}
|
|
if ($ret) $ret .= ':';
|
|
if (strpos($t, 'x') !== FALSE)
|
|
{
|
|
$ret .= $t;
|
|
}
|
|
else
|
|
{
|
|
$ret .= sprintf('%x',$v); // Drop leading zeros
|
|
}
|
|
}
|
|
else
|
|
{ // Zero field
|
|
$z = 1;
|
|
$zc++;
|
|
}
|
|
}
|
|
if ($z == 1)
|
|
{ // Need to add trailing zeros, or double colon
|
|
if ($zc > 1) $ret .= '::'; else $ret .= ':0';
|
|
}
|
|
if ($IP4Legacy && (strpos($ret, '::ffff:') === 0))
|
|
{
|
|
$temp = str_replace(':', '', substr($ip,-9, 9));
|
|
$tmp = str_split($temp, 2); // Four 2-character hex values
|
|
$z = array();
|
|
foreach ($tmp as $t)
|
|
{
|
|
if ($t == 'xx')
|
|
{
|
|
$z[] = '*';
|
|
}
|
|
else
|
|
{
|
|
$z[] = hexdec($t);
|
|
}
|
|
}
|
|
$ret = implode('.',$z);
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Given a string which may be IP address, email address etc, tries to work out what it is
|
|
* Uses a fairly simplistic (but quick) approach - does NOT check formatting etc
|
|
*
|
|
* @param string $string
|
|
* @return string ip|email|url|ftp|unknown
|
|
*/
|
|
public function whatIsThis($string)
|
|
{
|
|
$string = trim($string);
|
|
if (strpos($string, '@') !== FALSE) return 'email'; // Email address
|
|
if (strpos($string, 'http://') === 0) return 'url';
|
|
if (strpos($string, 'https://') === 0) return 'url';
|
|
if (strpos($string, 'ftp://') === 0) return 'ftp';
|
|
if (strpos($string, ':') !== FALSE) return 'ip'; // Identify ipv6
|
|
$string = strtolower($string);
|
|
if (str_replace(' ', '', strtr($string,'0123456789abcdef.*', ' ')) == '') // Delete all characters found in ipv4 addresses, plus wildcards
|
|
{
|
|
return 'ip';
|
|
}
|
|
return 'unknown';
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve & cache host name
|
|
*
|
|
* @param string $ip_address
|
|
* @return string host name
|
|
*/
|
|
public function get_host_name($ip_address)
|
|
{
|
|
if(!isset($this->_host_name_cache[$ip_address]))
|
|
{
|
|
$this->_host_name_cache[$ip_address] = gethostbyaddr($ip_address);
|
|
}
|
|
return $this->_host_name_cache[$ip_address];
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate DB query for domain name-related checks
|
|
*
|
|
* If an email address is passed, discards the individual's name
|
|
*
|
|
* @param string $email - an email address or domain name string
|
|
* @param string $fieldName
|
|
* @return array|bool false if invalid domain name format
|
|
* false if invalid domain name format
|
|
* array of values to compare
|
|
* @internal param string $fieldname - if non-empty, each array entry is a comparison with this field
|
|
*
|
|
*/
|
|
function makeDomainQuery($email, $fieldName = 'banlist_ip')
|
|
{
|
|
$tp = e107::getParser();
|
|
if (($tv = strrpos('@', $email)) !== FALSE)
|
|
{
|
|
$email = substr($email, $tv+1);
|
|
}
|
|
$tmp = strtolower($tp -> toDB(trim($email)));
|
|
if ($tmp == '') return FALSE;
|
|
if (strpos($tmp,'.') === FALSE) return FALSE;
|
|
$em = array_reverse(explode('.',$tmp));
|
|
$line = '';
|
|
$out = array('*@'.$tmp); // First element looks for domain as email address
|
|
foreach ($em as $e)
|
|
{
|
|
$line = '.'.$e.$line;
|
|
$out[] = '*'.$line;
|
|
}
|
|
if ($fieldName)
|
|
{
|
|
foreach ($out as $k => $v)
|
|
{
|
|
$out[$k] = '(`'.$fieldName."`='".$v."')";
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Split up an email address to check for banned domains.
|
|
* @param string $email - email address to process
|
|
* @param string $fieldname - name of field being searched in DB
|
|
*
|
|
* @return bool|string false if invalid address. Otherwise returns a set of values to check
|
|
* (Moved in from user_handler.php)
|
|
*/
|
|
public function makeEmailQuery($email, $fieldname = 'banlist_ip')
|
|
{
|
|
$tp = e107::getParser();
|
|
$tmp = strtolower($tp -> toDB(trim(substr($email, strrpos($email, "@")+1)))); // Pull out the domain name
|
|
if ($tmp == '') return FALSE;
|
|
if (strpos($tmp,'.') === FALSE) return FALSE;
|
|
$em = array_reverse(explode('.',$tmp));
|
|
$line = '';
|
|
$out = array($fieldname."='*@{$tmp}'"); // First element looks for domain as email address
|
|
foreach ($em as $e)
|
|
{
|
|
$line = '.'.$e.$line;
|
|
$out[] = '`'.$fieldname."`='*{$line}'";
|
|
}
|
|
return implode(' OR ',$out);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Routines beyond here are to handle banlist-related tasks which involve the DB
|
|
* note: Most of these routines already existed; moved in from e107_class.php
|
|
*/
|
|
|
|
|
|
/**
|
|
* Check if current user is banned
|
|
*
|
|
* This is called soon after the DB is opened, to do checks which require it.
|
|
* Previous checks have already done IP-based bans.
|
|
*
|
|
* Starts by removing expired bans if $this->clearBan is set
|
|
*
|
|
* Generates the queries to interrogate the ban list, then calls $this->check_ban().
|
|
* If the user is banned, $check_ban() never returns - so a return from this routine indicates a non-banned user.
|
|
*
|
|
* @return void
|
|
*
|
|
* @todo should be possible to simplify, since IP addresses already checked earlier
|
|
*/
|
|
public function ban()
|
|
{
|
|
$sql = e107::getDb();
|
|
|
|
if ($this->clearBan !== FALSE)
|
|
{ // Expired ban to clear - match exactly the address which triggered this action - could be a wildcard
|
|
$clearAddress = $this->ip6AddWildcards($this->clearBan);
|
|
if ($sql->delete('banlist',"`banlist_ip`='{$clearAddress}'"))
|
|
{
|
|
$this->actionCount--; // One less item on list
|
|
$this->logBanItem(0,'Ban cleared: '.$clearAddress);
|
|
// Now regenerate the text files - so no further triggers from this entry
|
|
$this->regenerateFiles();
|
|
}
|
|
}
|
|
|
|
|
|
// do other checks - main IP check is in _construct()
|
|
if($this->actionCount)
|
|
{
|
|
$ip = $this->getIP(); // This will be in normalised IPV6 form
|
|
|
|
if ($ip !== e107::LOCALHOST_IP && ($ip !== e107::LOCALHOST_IP2) && ($ip !== $this->serverIP)) // Check host name, user email to see if banned
|
|
{
|
|
$vals = array();
|
|
if (e107::getPref('enable_rdns'))
|
|
{
|
|
$vals = array_merge($vals, $this->makeDomainQuery($this->get_host_name($ip), ''));
|
|
}
|
|
if ((defined('USEREMAIL') && USEREMAIL))
|
|
{
|
|
// @todo is there point to this? Usually avoid a complete query if we skip it
|
|
$vals = array_merge($vals, $this->makeDomainQuery(USEREMAIL, ''));
|
|
}
|
|
if (count($vals))
|
|
{
|
|
$vals = array_unique($vals); // Could get identical values from domain name check and email check
|
|
|
|
if($this->debug)
|
|
{
|
|
print_a($vals);
|
|
}
|
|
|
|
|
|
$match = "`banlist_ip`='".implode("' OR `banlist_ip`='", $vals)."'";
|
|
$this->checkBan($match);
|
|
}
|
|
}
|
|
elseif($this->debug)
|
|
{
|
|
print_a("IP is LocalHost - skipping ban-check");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Check the banlist table. $query is used to determine the match.
|
|
* If $do_return, will always return with ban status - TRUE for OK, FALSE for banned.
|
|
* If return permitted, will never display a message for a banned user; otherwise will display any message then exit
|
|
* @todo consider whether can be simplified
|
|
*
|
|
* @param string $query - the 'WHERE' part of the DB query to be executed
|
|
* @param boolean $show_error - if true, adds a '403 Forbidden' header for a banned user
|
|
* @param boolean $do_return - if TRUE, returns regardless without displaying anything. if FALSE, for a banned user displays any message and exits
|
|
* @return boolean TRUE for OK, FALSE for banned.
|
|
*/
|
|
public function checkBan($query, $show_error = true, $do_return = false)
|
|
{
|
|
$sql = e107::getDb();
|
|
$pref = e107::getPref();
|
|
$tp = e107::getParser();
|
|
$admin_log = e107::getLog();
|
|
|
|
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Check for Ban",$query,FALSE,LOG_TO_ROLLING);
|
|
if ($sql->select('banlist', '*', $query.' ORDER BY `banlist_bantype` DESC'))
|
|
{
|
|
// Any whitelist entries will be first, because they are positive numbers - so we can answer based on the first DB record read
|
|
$row = $sql->fetch();
|
|
if($row['banlist_bantype'] >= eIPHandler::BAN_TYPE_WHITELIST)
|
|
{
|
|
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Whitelist hit",$query,FALSE,LOG_TO_ROLLING);
|
|
return true; // Whitelisted entry
|
|
}
|
|
|
|
// Found banlist entry in table here
|
|
if(($row['banlist_banexpires'] > 0) && ($row['banlist_banexpires'] < time()))
|
|
{ // Ban has expired - delete from DB
|
|
$sql->delete('banlist', $query);
|
|
$this->regenerateFiles();
|
|
|
|
return true;
|
|
}
|
|
|
|
// User is banned hereafter - just need to sort out the details.
|
|
// May need to retrigger ban period
|
|
if (!empty($pref['ban_retrigger']) && !empty($pref['ban_durations'][$row['banlist_bantype']]))
|
|
{
|
|
$dur = (int) $pref['ban_durations'][$row['banlist_bantype']];
|
|
$updateQry = array(
|
|
'banlist_banexpires' => (time() + ($dur * 60 * 60)),
|
|
'WHERE' => "banlist_ip ='".$row['banlist_ip']."'"
|
|
);
|
|
|
|
$sql->update('banlist', $updateQry);
|
|
$this->regenerateFiles();
|
|
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Retrigger Ban",$row['banlist_ip'],FALSE,LOG_TO_ROLLING);
|
|
}
|
|
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Active Ban",$query,FALSE,LOG_TO_ROLLING);
|
|
if ($show_error)
|
|
{
|
|
header('HTTP/1.1 403 Forbidden', true);
|
|
}
|
|
// May want to display a message
|
|
if (!empty($pref['ban_messages']))
|
|
{
|
|
// Ban still current here
|
|
if($do_return)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
echo $tp->toHTML(varset($pref['ban_messages'][$row['banlist_bantype']])); // Show message if one set
|
|
}
|
|
//$admin_log->addEvent(4, __FILE__."|".__FUNCTION__."@".__LINE__, 'BAN_03', 'LAN_AUDIT_LOG_003', $query, FALSE, LOG_TO_ROLLING);
|
|
|
|
if($this->debug)
|
|
{
|
|
echo "<pre>query: ".$query;
|
|
echo "\nBanned</pre>";
|
|
}
|
|
|
|
// added missing if clause
|
|
if ($do_return)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
exit();
|
|
}
|
|
|
|
if($this->debug)
|
|
{
|
|
echo "query: ".$query;
|
|
echo "<br />Not Banned ";
|
|
}
|
|
|
|
|
|
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","No ban found",$query,FALSE,LOG_TO_ROLLING);
|
|
return true; // Email address OK
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Add an entry to the banlist. $bantype = 1 for manual, 2 for flooding, 4 for multiple logins
|
|
* Returns TRUE if ban accepted.
|
|
* Returns FALSE if ban not accepted (e.g. because on whitelist, or invalid IP specified)
|
|
*
|
|
* @param integer $bantype - either one of the BAN_TYPE_xxx constants, or a legacy value as above
|
|
* @param string $ban_message
|
|
* @param string $ban_ip
|
|
* @param integer $ban_user
|
|
* @param string $ban_notes
|
|
*
|
|
* @return boolean|integer check result - FALSE if ban rejected. TRUE if ban added. 1 if IP address already banned
|
|
*/
|
|
public function add_ban($bantype, $ban_message = '', $ban_ip = '', $ban_user = 0, $ban_notes = '')
|
|
{
|
|
|
|
if ($ban_ip == e107::LOCALHOST_IP || $ban_ip == e107::LOCALHOST_IP2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
|
|
$sql = e107::getDb();
|
|
$pref = e107::getPref();
|
|
|
|
switch ($bantype) // Convert from 'internal' ban types to those used in the DB
|
|
{
|
|
case 1 : $bantype = eIPHandler::BAN_TYPE_MANUAL; break;
|
|
case 2 : $bantype = eIPHandler::BAN_TYPE_FLOOD; break;
|
|
case 4 : $bantype = eIPHandler::BAN_TYPE_LOGINS; break;
|
|
}
|
|
if (!$ban_message)
|
|
{
|
|
$ban_message = 'No explanation given';
|
|
}
|
|
if (!$ban_ip)
|
|
{
|
|
$ban_ip = $this->getIP();
|
|
}
|
|
$ban_ip = preg_replace('/[^\w@\.:]*/', '', urldecode($ban_ip)); // Make sure no special characters
|
|
if (!$ban_ip)
|
|
{
|
|
return FALSE;
|
|
}
|
|
// See if address already in the banlist
|
|
if ($sql->select('banlist', '`banlist_bantype`', "`banlist_ip`='{$ban_ip}'"))
|
|
{
|
|
list($banType) = $sql->fetch();
|
|
|
|
if ($banType >= eIPHandler::BAN_TYPE_WHITELIST)
|
|
{ // Got a whitelist entry for this
|
|
//$admin_log->addEvent(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING);
|
|
return FALSE;
|
|
}
|
|
return 1; // Already in ban list
|
|
}
|
|
/*
|
|
// See if the address is in the whitelist
|
|
if ($sql->select('banlist', '*', "`banlist_ip`='{$ban_ip}' AND `banlist_bantype` >= ".eIPHandler::BAN_TYPE_WHITELIST))
|
|
{ // Got a whitelist entry for this
|
|
//$admin_log->addEvent(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING);
|
|
return FALSE;
|
|
} */
|
|
if(!empty($pref['enable_rdns_on_ban']))
|
|
{
|
|
$ban_message .= 'Host: '.$this->get_host_name($ban_ip);
|
|
}
|
|
// Add using an array - handles DB changes better
|
|
$sql->insert('banlist',
|
|
array(
|
|
'banlist_id' => 0,
|
|
'banlist_ip' => $ban_ip ,
|
|
'banlist_bantype' => $bantype ,
|
|
'banlist_datestamp' => time() ,
|
|
'banlist_banexpires' => (vartrue($pref['ban_durations'][$bantype]) ? time()+($pref['ban_durations'][$bantype]*60*60) : 0) ,
|
|
'banlist_admin' => $ban_user ,
|
|
'banlist_reason' => $ban_message ,
|
|
'banlist_notes' => $ban_notes
|
|
));
|
|
|
|
$this->regenerateFiles();
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
/**
|
|
* Regenerate the text-based banlist files (called after a banlist table mod)
|
|
*/
|
|
public function regenerateFiles()
|
|
{
|
|
// Now regenerate the text files - so accesses of this IP address don't use the DB
|
|
$ipAdministrator = new banlistManager;
|
|
$ipAdministrator->writeBanListFiles('ip,htaccess');
|
|
}
|
|
|
|
|
|
/**
|
|
* @return false|string
|
|
*/
|
|
public function getConfigDir()
|
|
{
|
|
return $this->ourConfigDir;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Routine checks whether a file or directory has sufficient permissions
|
|
*
|
|
* ********** @todo this is in the wrong place! Move it to a more appropriate class! *************
|
|
*
|
|
* @param string $name - file with path (if ends in anything other than '/' or '\') or directory (if ends in '/' or '\')
|
|
* @param string(?) $perms - required permissions as standard *nix 3-digit string
|
|
* @param boolean $message - if TRUE, and insufficient rights, a message is output (in 0.8, to the message handler)
|
|
*
|
|
* @return boolean TRUE if sufficient permissions, FALSE if not (or error)
|
|
*
|
|
* For each mode character:
|
|
* 1 - execute
|
|
* 2 - writable
|
|
* 4 - readable
|
|
*/
|
|
public function checkFilePerms($name, $perms, $message = TRUE)
|
|
{
|
|
$isDir = ((substr($name, -1,1) == '\\') || (substr($name, -1,1) == '/'));
|
|
$result = FALSE;
|
|
$msg = '';
|
|
$dest = $isDir ? 'Directory' : 'File';
|
|
$reqPerms = intval('0'.$perms) & 511; // We want an integer value to match the return from fileperms()
|
|
if (!file_exists($name))
|
|
{
|
|
$msg = $dest.': '.$name.' does not exist';
|
|
}
|
|
if ($msg == '')
|
|
{
|
|
$realPerms = fileperms($name);
|
|
$mgs = $name.' is not a '.$dest; // Assume an error to start; clear messsage if all OK
|
|
switch ($realPerms & 0xf000)
|
|
{
|
|
case 0x8000 :
|
|
if (!$isDir)
|
|
{
|
|
$msg = '';
|
|
}
|
|
break;
|
|
case 0x4000 :
|
|
if ($isDir)
|
|
{
|
|
$msg = '';
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if ($msg == '')
|
|
{
|
|
if (($reqPerms & $realPerms) == $reqPerms)
|
|
{
|
|
$result = TRUE;
|
|
}
|
|
else
|
|
{
|
|
$msg = $name.': Insufficient permissions. Required: '.$this->permsToString($reqPerms).' Actual: '.$this->permsToString($realPerms);
|
|
}
|
|
}
|
|
//if ($message && $msg)
|
|
// { // Do something with the error message
|
|
// }
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Decode file/directory permissions into human-readable characters
|
|
*
|
|
* @param int $val representing permissions (LS 9 bits used)
|
|
*
|
|
* @return string exactly 9 characters, with blocks of 3 representing user, group and world permissions
|
|
*/
|
|
public function permsToString($val)
|
|
{
|
|
$perms = 'rwxrwxrwx';
|
|
$mask = 0x100;
|
|
|
|
for ($i = 0; $i < 9; $i++)
|
|
{
|
|
if (($mask & $val) == 0) $perms[$i] = '-';
|
|
$mask = $mask >> 1;
|
|
}
|
|
return $perms;
|
|
}
|
|
|
|
|
|
/**
|
|
* Function to see whether a user is already logged as being online
|
|
*
|
|
* @todo - this is possibly in the wrong place!
|
|
*
|
|
* @param string $ip - in 'normalised' IPV6 form
|
|
* @param string $browser - browser token as logged
|
|
*
|
|
* @return boolean|array FALSE if DB error or not found. Best match table row if found
|
|
*/
|
|
public function isUserLogged($ip, $browser)
|
|
{
|
|
$ourDB = e107::getDb('olcheckDB'); // @todo is this OK, or should an existing one be used?
|
|
|
|
$result = $ourDB->select('online', '*', "`user_ip` = '{$ip}' OR `user_token` = '{$browser}'");
|
|
if ($result === FALSE) return FALSE;
|
|
$gotIP = FALSE;
|
|
$gotBrowser = FALSE;
|
|
$bestRow = FALSE;
|
|
while (FALSE !== ($row = $ourDB->fetch()))
|
|
{
|
|
if ($row['user_token'] == $browser)
|
|
{
|
|
if ($row['user_ip'] == $ip)
|
|
{ // Perfect match
|
|
return $row;
|
|
}
|
|
// Just browser token match here
|
|
if ($bestRow === FALSE)
|
|
{
|
|
$bestRow = $row;
|
|
$gotBrowser = TRUE;
|
|
}
|
|
// else
|
|
// { // Problem - two or more rows with same browser token. What to do?
|
|
// }
|
|
}
|
|
elseif ($row['user_ip'] == $ip)
|
|
{ // Just IP match here
|
|
if ($bestRow === FALSE)
|
|
{
|
|
$bestRow = $row;
|
|
$gotIP = TRUE;
|
|
}
|
|
//else
|
|
//{ // Problem - two or more rows with same IP address. Hopefully better offer later!
|
|
//}
|
|
}
|
|
}
|
|
return $bestRow;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Routines involved with the management of the ban list and associated files
|
|
*/
|
|
class banlistManager
|
|
{
|
|
private $ourConfigDir = '';
|
|
public $banTypes = array();
|
|
|
|
public function __construct()
|
|
{
|
|
e107_include_once(e_LANGUAGEDIR.e_LANGUAGE."/admin/lan_banlist.php");
|
|
$this->ourConfigDir = e107::getIPHandler()->getConfigDir();
|
|
$this->banTypes = array( // Used in Admin-ui.
|
|
'-1' => BANLAN_101, // manual
|
|
'-2' => BANLAN_102, // Flood
|
|
'-3' => BANLAN_103, // Hits
|
|
'-4' => BANLAN_104, // Logins
|
|
'-5' => BANLAN_105, // Imported
|
|
'-6' => BANLAN_106, // Users
|
|
'-8' => BANLAN_107, // Imported
|
|
'100' => BANLAN_120 // Whitelist
|
|
);
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Return an array of valid ban types (for use as indices into array, generally)
|
|
*/
|
|
public static function getValidReasonList()
|
|
{
|
|
return array(
|
|
eIPHandler::BAN_TYPE_LEGACY,
|
|
eIPHandler::BAN_TYPE_MANUAL,
|
|
eIPHandler::BAN_TYPE_FLOOD,
|
|
eIPHandler::BAN_TYPE_HITS,
|
|
eIPHandler::BAN_TYPE_LOGINS,
|
|
eIPHandler::BAN_TYPE_IMPORTED,
|
|
eIPHandler::BAN_TYPE_USER,
|
|
// Spare value
|
|
eIPHandler::BAN_TYPE_UNKNOWN
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create banlist-related text files as requested:
|
|
* List of whitelisted and blacklisted IP addresses
|
|
* file for easy import into .htaccess file (allow from...., deny from....)
|
|
* Generic CSV-format export file
|
|
*
|
|
* @param string $options {ip|htaccess|csv} - comma separated list (no spaces) to select which files to write
|
|
* @param string $typeList - optional comma-separated list of ban types required (default is all)
|
|
* Uses constants:
|
|
* BAN_FILE_IP_NAME Saves list of banned and whitelisted IP addresses
|
|
* BAN_FILE_ACTION_NAME Details of actions for different ban types
|
|
* BAN_FILE_HTACCESS File in format for direct paste into .htaccess
|
|
* BAN_FILE_CSV_NAME
|
|
* BAN_FILE_EXTENSION File extension to append
|
|
*
|
|
*/
|
|
public function writeBanListFiles($options = 'ip', $typeList = '')
|
|
{
|
|
e107::getMessage()->addDebug("Writing new Banlist files.");
|
|
$sql = e107::getDb();
|
|
$ipManager = e107::getIPHandler();
|
|
|
|
$optList = explode(',',$options);
|
|
$fileList = array(); // Array of file handles once we start
|
|
|
|
$fileNameList = array('ip' => eIPHandler::BAN_FILE_IP_NAME, 'htaccess' => eIPHandler::BAN_FILE_HTACCESS, 'csv' => eIPHandler::BAN_FILE_CSV_NAME);
|
|
|
|
$qry = 'SELECT * FROM `#banlist` ';
|
|
if ($typeList != '') $qry .= " WHERE`banlist_bantype` IN ({$typeList})";
|
|
$qry .= ' ORDER BY `banlist_bantype` DESC'; // Order ensures whitelisted addresses appear first
|
|
|
|
// Create a temporary file for each type as demanded. Vet the options array on this pass, as well
|
|
foreach($optList as $k => $opt)
|
|
{
|
|
if (isset($fileNameList[$opt]))
|
|
{
|
|
if ($tmp = fopen($this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION, 'w'))
|
|
{
|
|
$fileList[$opt] = $tmp; // Save file handle
|
|
fwrite($fileList[$opt], "<?php\n; die();\n");
|
|
//echo "Open File for write: ".$this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION.'<br />';
|
|
}
|
|
else
|
|
{
|
|
unset($optList[$k]);
|
|
/// @todo - flag error?
|
|
}
|
|
}
|
|
else
|
|
{
|
|
unset($optList[$k]);
|
|
}
|
|
}
|
|
|
|
if ($sql->gen($qry))
|
|
{
|
|
while ($row = $sql->fetch())
|
|
{
|
|
$row['banlist_ip'] = $this->trimWildcard($row['banlist_ip']);
|
|
if ($row['banlist_ip'] == '') continue; // Ignore empty IP addresses
|
|
if ($ipManager->whatIsThis($row['banlist_ip']) != 'ip') continue; // Ignore non-numeric IP Addresses
|
|
if ($row['banlist_bantype'] == eIPHandler::BAN_TYPE_LEGACY) $row['banlist_bantype'] = eIPHandler::BAN_TYPE_UNKNOWN; // Handle legacy bans
|
|
foreach ($optList as $opt)
|
|
{
|
|
$line = '';
|
|
switch ($opt)
|
|
{
|
|
case 'ip' :
|
|
// IP_address action expiry_time additional_parameters
|
|
$line = $row['banlist_ip'].' '.$row['banlist_bantype'].' '.$row['banlist_banexpires']."\n";
|
|
break;
|
|
case 'htaccess' :
|
|
$line = (($row['banlist_bantype'] > 0) ? 'allow from ' : 'deny from ').$row['banlist_ip']."\n";
|
|
break;
|
|
case 'csv' : /// @todo - when PHP5.1 is minimum, can use fputcsv() function
|
|
$line = $row['banlist_ip'].','.$this->dateFormat($row['banlist_datestamp']).','.$this->dateFormat($row['banlist_expires']).',';
|
|
$line .= $row['banlist_bantype'].',"'.$row['banlist_reason'].'","'.$row['banlist_notes'].'"'."\n";
|
|
break;
|
|
}
|
|
fwrite($fileList[$opt], $line);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now close each file
|
|
foreach ($optList as $opt)
|
|
{
|
|
fclose($fileList[$opt]);
|
|
}
|
|
|
|
// Finally, delete the working file, rename the temporary one
|
|
// Docs suggest that 'newname' is auto-deleted if it exists (as it usually should)
|
|
// - but didn't appear to work, hence copy then delete
|
|
foreach ($optList as $opt)
|
|
{
|
|
$oldName = $this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION;
|
|
$newName = $this->ourConfigDir.$fileNameList[$opt].eIPHandler::BAN_FILE_EXTENSION;
|
|
copy($oldName, $newName);
|
|
unlink($oldName);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Trim wildcards from IP addresses
|
|
*
|
|
* @param string $ip - IP address in any normal form
|
|
*
|
|
* Note - this removes all characters after (and including) the first '*' or 'x' found. So an '*' or 'x' in the middle of a string may
|
|
* cause unexpected results.
|
|
* @return string
|
|
*/
|
|
private function trimWildcard($ip)
|
|
{
|
|
$ip = trim($ip);
|
|
$temp = strpos($ip, 'x');
|
|
if ($temp !== FALSE)
|
|
{
|
|
return substr($ip, 0, $temp);
|
|
}
|
|
$temp = strpos($ip, '*');
|
|
if ($temp !== FALSE)
|
|
{
|
|
return substr($ip, 0, $temp);
|
|
}
|
|
return $ip;
|
|
}
|
|
|
|
|
|
/**
|
|
* Format date and time for export into a text file.
|
|
*
|
|
* @param int $date - standard Unix time stamp
|
|
*
|
|
* @return string. '0' if date is zero, else formatted in consistent way.
|
|
*/
|
|
private function dateFormat($date)
|
|
{
|
|
if ($date == 0) return '0';
|
|
return eShims::strftime('%Y%m%d_%H%M%S',$date);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Return string corresponding to a ban type
|
|
* @param int $banType - constant representing the ban type
|
|
* @param bool $forMouseover - if true, its the (usually longer) explanatory string for a mouseover
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getBanTypeString($banType, $forMouseover = FALSE)
|
|
{
|
|
switch ($banType)
|
|
{
|
|
case eIPHandler::BAN_TYPE_LEGACY : $listOffset = 0; break;
|
|
case eIPHandler::BAN_TYPE_MANUAL : $listOffset = 1; break;
|
|
case eIPHandler::BAN_TYPE_FLOOD : $listOffset = 2; break;
|
|
case eIPHandler::BAN_TYPE_HITS : $listOffset = 3; break;
|
|
case eIPHandler::BAN_TYPE_LOGINS : $listOffset = 4; break;
|
|
case eIPHandler::BAN_TYPE_IMPORTED : $listOffset = 5; break;
|
|
case eIPHandler::BAN_TYPE_USER : $listOffset = 6; break;
|
|
case eIPHandler::BAN_TYPE_TEMPORARY : $listOffset = 9; break;
|
|
|
|
case eIPHandler::BAN_TYPE_WHITELIST :
|
|
return BANLAN_120; // Special case - may never occur
|
|
case eIPHandler::BAN_TYPE_UNKNOWN :
|
|
default :
|
|
if (($banType > 0) && ($banType < 9))
|
|
{
|
|
$listOffset = $banType; // BC conversions
|
|
}
|
|
else
|
|
{
|
|
$listOffset = 8;
|
|
}
|
|
}
|
|
if ($forMouseover) return constant('BANLAN_11'.$listOffset);
|
|
return constant('BANLAN_10'.$listOffset);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Write a text file containing the ban messages related to each ban reason
|
|
*/
|
|
public function writeBanMessageFile()
|
|
{
|
|
$pref['ban_messages'] = e107::getPref('ban_messages');
|
|
|
|
$oldName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.'_tmp'.eIPHandler::BAN_FILE_EXTENSION;
|
|
if ($tmp = fopen($oldName, 'w'))
|
|
{
|
|
fwrite($tmp, "<?php\n; die();\n");
|
|
foreach ($this->getValidReasonList() as $type)
|
|
{
|
|
fwrite($tmp,'['.$type.']'.$pref['ban_messages'][$type]."\n");
|
|
}
|
|
fclose($tmp);
|
|
$newName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION;
|
|
copy($oldName, $newName);
|
|
unlink($oldName);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Check whether the message file (containing responses to ban types) exists
|
|
*
|
|
* @return boolean TRUE if exists, FALSE if doesn't exist
|
|
*/
|
|
public function doesMessageFileExist()
|
|
{
|
|
return is_readable($this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get entries from the ban action log
|
|
*
|
|
* @param int $start - offset into list (zero is first entry)
|
|
* @param int $count - number of entries to return - zero is a special case
|
|
* @param int $numEntry - filled in on return with the total number of entries in the log file
|
|
*
|
|
* @return array of strings; each string is a single log entry, newest first.
|
|
*
|
|
* Returns an empty array if an error occurs (or if no entries)
|
|
* If $count is zero, all entries are returned, in ascending order.
|
|
*/
|
|
public function getLogEntries($start, $count, &$numEntry)
|
|
{
|
|
$ret = array();
|
|
$numEntry = 0;
|
|
$fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME;
|
|
if (!is_readable($fileName)) return $ret;
|
|
|
|
$vals = file($fileName);
|
|
if ($vals === FALSE) return $ret;
|
|
if (strpos($vals[0], '<?php') === 0)
|
|
{
|
|
unset($vals[0]);
|
|
}
|
|
if (strpos($vals[0], ';') === 0) unset($vals[0]);
|
|
$numEntry = count($vals);
|
|
if ($start > $numEntry) return $ret; // Empty return if beyond the end
|
|
if ($count == 0) return $vals; // Special case - return the lot in ascending date order
|
|
// Array is built up with newest last - but we want newest first. And we don't want to duplicate the array!
|
|
if (($start + $count) > $numEntry) $count = $numEntry - $start; // Last segment might not have enough entries
|
|
$ret = array_slice($vals, -$start - $count, $count);
|
|
return array_reverse($ret);
|
|
}
|
|
|
|
|
|
/**
|
|
* Converts one of the strings returned in a getLogEntries string into an array of values
|
|
*
|
|
* @param string $string - a text line, possibly including a 'newline' at the end
|
|
*
|
|
* @return array of up to $count entries
|
|
* ['banDate'] - time/date stamp
|
|
* ['banIP'] - IP address involved
|
|
* ['banReason'] - Numeric reason code for entry
|
|
* ['banNotes'] = any text appended
|
|
*/
|
|
public function splitLogEntry($string)
|
|
{
|
|
$temp = explode(' ',$string, 4);
|
|
while (count($temp) < 4) $temp[] = '';
|
|
$ret['banDate'] = $temp[0];
|
|
$ret['banIP'] = $temp[1];
|
|
$ret['banReason'] = $temp[2];
|
|
$ret['banNotes'] = str_replace("\n", '', $temp[3]);
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete ban Log file
|
|
*
|
|
* @return boolean TRUE on success, FALSE on failure
|
|
*/
|
|
public function deleteLogFile()
|
|
{
|
|
$fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME;
|
|
return unlink($fileName);
|
|
}
|
|
|
|
|
|
/**
|
|
* Update expiry time for IP addresses that have accessed the site while banned.
|
|
* Processes the entries in the 'ban retrigger' action file, and deletes the file
|
|
*
|
|
* Needs to be called from a cron job, at least once per hour, and ideally every few minutes. Otherwise banned users who access
|
|
* the site in the period since the last call to this routine may be able to get in because their ban has expired. (Unlikely to be
|
|
* an issue in practice)
|
|
*
|
|
* @return int number of IP addresses updated
|
|
*
|
|
* @todo - implement cron job and test
|
|
*/
|
|
public function banRetriggerAction()
|
|
{
|
|
//if (!e107::getPref('ban_retrigger')) return 0; // Should be checked earlier
|
|
|
|
$numEntry = 0; // Make sure this variable declared before passing it - total number of log entries.
|
|
$ipAction = array(); // Array of IP addresses to action
|
|
$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION;
|
|
$entries = file($fileName);
|
|
if (!is_array($entries))
|
|
{
|
|
return 0; // Probably no retrigger actions
|
|
}
|
|
@unlink($fileName); // Delete the action file now we've read it in.
|
|
|
|
// Scan the list completely before doing any processing - this will ensure we only process the most recent entry for each IP address
|
|
while (count($entries) > 0)
|
|
{
|
|
$line = array_shift($entries);
|
|
$info = $this->splitLogEntry($line);
|
|
if ($info['banReason'] < 0)
|
|
{
|
|
$ipAction[$info['banIP']] = array('date' => $info['banDate'], 'reason' => $info['banReason']); // This will result in us gathering the most recent access from each IP address
|
|
}
|
|
}
|
|
|
|
if (count($ipAction) == 0) return 0; // Nothing more to do
|
|
|
|
// Now run through the database updating times
|
|
$numRet = 0;
|
|
$pref['ban_durations'] = e107::getPref('ban_durations');
|
|
$ourDb = e107::getDb(); // Should be able to use $sql, $sql2 at this point
|
|
$writeDb = e107::getDb('sql2');
|
|
|
|
foreach ($ipAction as $ipKey => $ipInfo)
|
|
{
|
|
if ($ourDb->select('banlist', '*', "`banlist_ip`='".$ipKey."'") === 1)
|
|
{
|
|
if ($row = $ourDb->fetch())
|
|
{
|
|
// @todo check next line
|
|
$writeDb->update('banlist',
|
|
'`banlist_banexpires` = '.intval($row['banlist_banexpires'] + $pref['ban_durations'][$row['banlist_banreason']]));
|
|
$numRet++;
|
|
}
|
|
}
|
|
}
|
|
if ($numRet)
|
|
{
|
|
$this->writeBanListFiles('ip'); // Just rewrite the ban list - the actual IP addresses won't have changed
|
|
}
|
|
return $numRet;
|
|
}
|
|
}
|
|
|
|
|