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], '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], '= 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 "

Banlist.php

"; 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 "
query: ".$query;
				echo "\nBanned
"; } // added missing if clause if ($do_return) { return false; } exit(); } if($this->debug) { echo "query: ".$query; echo "
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], "ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION.'
'; } 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, "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], ' $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; } }