1<?php 2/* 3* e107 website system 4* 5* Copyright 2008-2013 e107 Inc (e107.org) 6* Released under the terms and conditions of the 7* GNU General Public License (http://www.gnu.org/licenses/gpl.txt) 8* 9* IP Address related routines, including banning-related code 10* 11* $URL$ 12* $Revision$ 13* $Id$ 14* 15*/ 16 17 18/** 19* @package e107 20* @subpackage e107_handlers 21* @version $Id$; 22* 23* Routines to manage IP addresses and banning. 24*/ 25 26 27 28/** 29 * Class to handle ban-related checks, and provide some utility functions related to IP addresses 30 * There are two parts to the class: 31 * 32 * Part 1 33 * ------ 34 * This part intentionally does NO database access, and requires an absolute minimum of file paths to be set up 35 * (this is to minimise processing load in the event of an access from a banned IP address) 36 * It works only with the user's IP address, and potentially browser 'signature' 37 * 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 38 * (If DB access is required to handle a ban, it should only need to be done occasionally) 39 * 40 * Part 2 41 * ------ 42 * This part handles those functions which require DB access. 43 * The intention is that Part 1 will catch most existing bans, to reduce the incidence of abortive DB opens 44 * If part 1 signals that a ban has expired, part 2 removes it from the database 45 * 46 * Elsewhere 47 * --------- 48 * 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 49 * eliminate the benefits of this handler - a DB access would be needed on every access from a banned IP address). 50 * @todo Implement the ban retriggering cron job (elsewhere) 51 * - do we have a separate text file for the accesses in need of retriggering? Could then delete it once actioned; keeps it small 52 * @todo Implement flood bans - needs db access - maybe leave to the second part of this file or the online handler 53 * 54 * All IP addresses are stored in 'normal' form - a fixed length IPV6 format with separator colons. 55 * 56 * To use: 57 * include this file, early on (before DB accesses started), and instantiate class ipHandler. 58 * 59 */ 60 61 62class eIPHandler 63{ 64 /** 65 * IPV6 string for localhost - as stored in DB 66 */ 67// const LOCALHOST_IP = '0000:0000:0000:0000:0000:ffff:7f00:0001'; 68 69 70 const BAN_REASON_COUNT = 7; // Update as more ban reasons added (max 10 supported) 71 72 const BAN_TYPE_LEGACY = 0; // Shouldn't get these unless update process not run 73 const BAN_TYPE_MANUAL = -1; /// Manually entered bans 74 const BAN_TYPE_FLOOD = -2; /// Flood ban 75 const BAN_TYPE_HITS = -3; 76 const BAN_TYPE_LOGINS = -4; 77 const BAN_TYPE_IMPORTED = -5; /// Imported bans 78 const BAN_TYPE_USER = -6; /// User is banned 79 // Spare value 80 const BAN_TYPE_UNKNOWN = -8; 81 const BAN_TYPE_TEMPORARY = -9; /// Used during CSV import - giving it this value highlights problems 82 83 const BAN_TYPE_WHITELIST = 100; /// Entry for whitelist - actually not a ban at all! Keep at this value for BC 84 85 86 const BAN_FILE_DIRECTORY = 'cache/'; /// Directory containing the text files (within e_SYSTEM) 87 const BAN_LOG_DIRECTORY = 'logs/'; /// Directory containing the log file (within e_SYSTEM) 88 89 const BAN_FILE_LOG_NAME = 'banlog.log'; /// Logs bans etc 90 // Note for the following file names - the code appends the extension 91 const BAN_FILE_IP_NAME = 'banlist'; /// Saves list of banned and whitelisted IP addresses 92 const BAN_FILE_ACTION_NAME = 'banactions'; /// Details of actions for different ban types 93 const BAN_FILE_HTACCESS = 'banhtaccess'; /// File in format for direct paste into .htaccess 94 const BAN_FILE_CSV_NAME = 'banlistcsv'; /// Output file in CSV format 95 const BAN_FILE_RETRIGGER_NAME = 'banretrigger'; /// Any bans needing retriggering 96 const BAN_FILE_EXTENSION = '.php'; /// File extension to use 97 98 /** 99 * IP address of current user, in 'normal' form 100 */ 101 private $ourIP = ''; 102 103 private $serverIP = ''; 104 105 private $debug = false; 106 /** 107 * Host name of current user 108 * Initialised when requested 109 */ 110 private $_host_name_cache = array(); 111 112 113 /** 114 * Token for current user, calculated from browser settings. 115 * Supplements IP address (Can be spoofed, but helps differentiate among honest users at the same IP address) 116 */ 117 private $accessID = ''; 118 119 /** 120 * Path to directory containing current config file(s) 121 */ 122 private $ourConfigDir = ''; 123 124 /** 125 * Current user's IP address status. Usually zero (neutral); may be one of the BAN_TYPE_xxx constants 126 */ 127 private $ipAddressStatus = 0; 128 129 130 /** 131 * Flag set to the IP address that triggered the match, if current IP has an expired ban to clear 132 */ 133 private $clearBan = FALSE; 134 135 136 /** 137 * IP Address from ban list file which matched (may have wildcards) 138 */ 139 private $matchAddress = ''; 140 141 /** 142 * Number of entries read from banlist/whitelist 143 */ 144 private $actionCount = 0; 145 146 /** 147 * Constructor 148 * 149 * Only one instance of this class is ever loaded, very early on in the initialisation sequence 150 * 151 * @param string $configDir Path to the directory containing the files used by this class 152 * If not set, defaults to BAN_FILE_DIRECTORY constant 153 * 154 * On load it gets the user's IP address, and checks it against whitelist and blacklist files 155 * If the address is blacklisted, displays an appropriate message (as configured) and aborts 156 * Otherwise sets up 157 */ 158 public function __construct($configDir = '') 159 { 160 $configDir = trim($configDir); 161 162 if ($configDir) 163 { 164 $this->ourConfigDir = realpath($configDir); 165 } 166 else 167 { 168 $this->ourConfigDir = e_SYSTEM.eIPHandler::BAN_FILE_DIRECTORY; 169 } 170 171 172 $this->ourIP = $this->ipEncode($this->getCurrentIP()); 173 174 $this->serverIP = $this->ipEncode(isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : 'x.x.x.x'); 175 176 $this->makeUserToken(); 177 $ipStatus = $this->checkIP($this->ourIP); 178 if ($ipStatus != 0) 179 { 180 if ($ipStatus < 0) 181 { // Blacklisted 182 $this->logBanItem($ipStatus, 'result --> '.$ipStatus); // only log blacklist 183 $this->banAction($ipStatus); // This will abort if appropriate 184 } 185 elseif ($ipStatus > 0) 186 { // Whitelisted - we may want to set a specific indicator 187 } 188 } 189 // Continue here - user not banned (so far) 190 } 191 192 public function setIP($ip) 193 { 194 $this->ourIP = $this->ipEncode($ip); 195 196 } 197 198 199 public function debug($value) 200 { 201 $this->debug = ($value === true) ? true: false; 202 } 203 204 205 206 207 /** 208 * Add an entry to the banlist log file (which is a simple text file) 209 * A date/time string is prepended to the line 210 * 211 * @param int $reason - numeric reason code, usually in range -10..+10 212 * @param string $message - additional text as required (length not checked, but should be less than 100 characters or so 213 * 214 * @return void 215 */ 216 private function logBanItem($reason, $message) 217 { 218 if ($tmp = fopen(e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME, 'a')) 219 { 220 $logLine = time().' '.$this->ourIP.' '.$reason.' '.$message."\n"; 221 fwrite($tmp,$logLine); 222 fclose($tmp); 223 } 224 } 225 226 227 228 /** 229 * Generate relatively unique user token from browser info 230 * (but don't believe that the browser info is accurate - can readily be spoofed) 231 * 232 * This supplements use of the IP address in some places; both to improve user identification, and to help deal with dynamic IP allocations 233 * 234 * May be replaced by a 'global' e107 token at some point 235 */ 236 private function makeUserToken() 237 { 238 $tmpStr = ''; 239 foreach (array('HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_ACCEPT_ENCODING') as $v) 240 { 241 if (isset($_SERVER[$v])) 242 { 243 $tmpStr .= $_SERVER[$v]; 244 } 245 else 246 { 247 $tmpStr .= 'dummy'.$v; 248 } 249 } 250 $this->accessID = md5($tmpStr); 251 } 252 253 254 255 /** 256 * Return browser-characteristics token 257 */ 258 public function getUserToken() 259 { 260 return $this->accessID; // Should always be defined at this point 261 } 262 263 264 265 /** 266 * Check whether an IP address is routable 267 * 268 * @param string $ip - IPV4 or IPV6 numeric address. 269 * 270 * @return boolean TRUE if routable, FALSE if not 271 272 @todo handle IPV6 fully 273 */ 274 public function isAddressRoutable($ip) 275 { 276 $ignore = array( 277 '0\..*' , '^127\..*' , // Local loopbacks 278 '192\.168\..*' , // RFC1918 - Private Network 279 '172\.(?:1[6789]|2\d|3[01])\..*' , // RFC1918 - Private network 280 '10\..*' , // RFC1918 - Private Network 281 '169\.254\..*' , // RFC3330 - Link-local, auto-DHCP 282 '2(?:2[456789]|[345][0-9])\..*' // Single check for Class D and Class E 283 ); 284 285 286 287 $pattern = '#^('.implode('|',$ignore).')#'; 288 289 if(preg_match($pattern,$ip)) 290 { 291 return false; 292 } 293 294 295 /* XXX preg_match doesn't accept arrays. 296 if (preg_match(array( 297 '#^0\..*#' , '#^127\..*#' , // Local loopbacks 298 '#^192\.168\..*#' , // RFC1918 - Private Network 299 '#^172\.(?:1[6789]|2\d|3[01])\..*#' , // RFC1918 - Private network 300 '#^10\..*#' , // RFC1918 - Private Network 301 '#^169\.254\..*#' , // RFC3330 - Link-local, auto-DHCP 302 '#^2(?:2[456789]|[345][0-9])\..*#' // Single check for Class D and Class E 303 ), $ip)) 304 { 305 return FALSE; 306 } 307 */ 308 309 if (strpos(':', $ip) === FALSE) return TRUE; 310 // Must be an IPV6 address here 311 // @todo need to handle IPV4 addresses in IPV6 format 312 $ip = strtolower($ip); 313 if ($ip == 'ff02::1') return FALSE; // link-local all nodes multicast group 314 if ($ip == 'ff02:0000:0000:0000:0000:0000:0000:0001') return FALSE; 315 if ($ip == '::1') return FALSE; // localhost 316 if ($ip == '0000:0000:0000:0000:0000:0000:0000:0001') return FALSE; 317 if (substr($ip, 0, 5) == 'fc00:') return FALSE; // local addresses 318 // @todo add: 319 // ::0 (all zero) - invalid 320 // ff02::1:ff00:0/104 - Solicited-Node multicast addresses - add? 321 // 2001:0000::/29 through 2001:01f8::/29 - special purpose addresses 322 // 2001:db8::/32 - used in documentation 323 return TRUE; 324 } 325 326 327 328 /** 329 * Get current user's IP address in 'normal' form. 330 * Likely to be very similar to existing e107::getIP() function 331 * May log X-FORWARDED-FOR cases - or could generate a special IPV6 address, maybe? 332 */ 333 private function getCurrentIP() 334 { 335 if(!$this->ourIP) 336 { 337 $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'x.x.x.x'; 338 if ($ip4 = getenv('HTTP_X_FORWARDED_FOR')) 339 { 340 if (!$this->isAddressRoutable($ip)) 341 { 342 $ip3 = explode(',', $ip4); // May only be one address; could be several, comma separated, if multiple proxies used 343 $ip = trim($ip3[sizeof($ip3) - 1]); // If IP address is unroutable, replace with any forwarded_for address 344 $this->logBanItem(0, 'X_Forward '.$ip4.' --> '.$ip); // Just log for interest ATM 345 } 346 } 347 $this->ourIP = $this->ipEncode($ip); // Normalise for storage 348 } 349 return $this->ourIP; 350 } 351 352 353 354 /** 355 * Return the user's IP address, in normal or display-friendly form as requested 356 * 357 * @param boolean $forDisplay - TRUE for minimum-length display-friendly format. FALSE for 'normal' form (to be used when storing into DB etc) 358 * 359 * @return string IP address 360 * 361 * 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 362 */ 363 public function getIP($forDisplay = FALSE) 364 { 365 if ($forDisplay == FALSE) return $this->ourIP; 366 return $this->ipDecode($this->ourIP); 367 } 368 369 370 371 /** 372 * Takes appropriate action for a blacklisted IP address 373 * 374 * @param int $code - integer value < 0 specifying the ban reason. 375 * 376 * @return void (may not even return) 377 * 378 * Looks up the reason code, and extracts the corresponding text. 379 * If this text begins with 'http://' or 'https://', assumed to be a link to a web page, and redirects. 380 * Otherwise displays an error message to the user (if configured) then aborts. 381 */ 382 private function banAction($code) 383 { 384 $search = '['.$code.']'; 385 $fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION; 386 387 if(!is_readable($fileName)) // Note readable, but the IP is still banned, so half further script execution. 388 { 389 if($this->debug === true || e_DEBUG === true) 390 { 391 echo "Your IP is banned!"; 392 } 393 394 die(); 395 // return; // 396 } 397 398 $vals = file($fileName); 399 if ($vals === FALSE || count($vals) == 0) return; 400 if (substr($vals[0], 0, 5) != '<?php') 401 { 402 echo 'Invalid message file'; 403 die(); 404 } 405 unset($vals[0]); 406 foreach ($vals as $line) 407 { 408 if (substr($line, 0, 1) == ';') continue; 409 if (strpos($line, $search) === 0) 410 { // Found the action line 411 if (e107::getPref('ban_retrigger')) 412 { 413 if ($tmp = fopen($this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION, 'a')) 414 { 415 $logLine = time().' '.$this->matchAddress.' '.$code.' Retrigger: '.$this->ourIP."\n"; // Same format as log entries - can share routines 416 fwrite($tmp,$logLine); 417 fclose($tmp); 418 } 419 } 420 $line = trim(substr($line, strlen($search))); 421 if ((strpos($line, 'http://') === 0) || (strpos($line, 'https://') === 0)) 422 { // Display a specific web page 423 if (strpos($line, '?') === FALSE) 424 { 425 $line .= '?'.$search; // Add on the ban reason - may be useful in the page 426 } 427 e107::redirect($line); 428 exit(); 429 } 430 // Otherwise just display any message and die 431 if($this->debug) 432 { 433 print_a("User Banned"); 434 } 435 436 echo $line; 437 438 die(); 439 } 440 } 441 $this->logBanItem($code, 'Unmatched action: '.$search.' - no block implemented'); 442 } 443 444 445 446 /** 447 * Get whitelist and blacklist 448 * 449 * @return array - each element is an array with elements 'ip', 'action, and 'time_limit' 450 * 451 * Note: Intentionally a single call, so the two lists can be split across files as convenient 452 * 453 * At present the list is a single file, one entry per line, whitelist entries first. Most precisely defined addresses before larger subnets 454 * 455 * Format of each line is: 456 * IP_address action expiry_time additional_parameters 457 * 458 * where action is: >0 = whitelisted, <0 blacklisted, value is 'reason code' 459 * expiry_time is zero for an indefinite ban, time stamp for a limited ban 460 * additional_parameters may be required for certain actions in the future 461 */ 462 private function getWhiteBlackList() 463 { 464 $ret = array(); 465 $fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_IP_NAME.eIPHandler::BAN_FILE_EXTENSION; 466 if (!is_readable($fileName)) return $ret; 467 468 $vals = file($fileName); 469 if ($vals === FALSE || count($vals) == 0) return $ret; 470 if (substr($vals[0], 0, 5) != '<?php') 471 { 472 echo 'Invalid list file'; 473 die(); // Debatable, because admins can't get in if this fails. But can manually delete the file. 474 } 475 unset($vals[0]); 476 foreach ($vals as $line) 477 { 478 if (substr($line, 0, 1) == ';') continue; 479 if (trim($line)) 480 { 481 $tmp = explode(' ',$line); 482 if (count($tmp) >= 2) 483 { 484 $ret[] = array('ip' => $tmp[0], 'action' => $tmp[1], 'time_limit' => intval(varset($tmp[2], 0))); 485 } 486 } 487 } 488 $this->actionCount = count($ret); // Note how many entries in list 489 return $ret; 490 } 491 492 493 494 /** 495 * Checks whether IP address is in the whitelist or blacklist. 496 * 497 * @param string $addr - IP address in 'normal' form 498 * 499 * @return int - >0 = whitelisted, 0 = not listed (= 'OK'), <0 is 'reason code' for ban 500 * 501 * note: Could maybe combine this with getWhiteBlackList() for efficiency, but makes it less general 502 */ 503 private function checkIP($addr) 504 { 505 $now = time(); 506 $checkLists = $this->getWhiteBlackList(); 507 508 if($this->debug) 509 { 510 echo "<h4>Banlist.php</h4>"; 511 print_a($checkLists); 512 print_a("Now: ".$now. " ".date('r',$now)); 513 } 514 515 516 foreach ($checkLists as $val) 517 { 518 if (strpos($addr, $val['ip']) === 0) // See if our address begins with an entry - handles wildcards 519 { // Match found 520 521 if($this->debug) 522 { 523 print_a("Found ".$addr." in file. TimeLimit: ".date('r',$val['time_limit'])); 524 } 525 526 if (($val['time_limit'] == 0) || ($val['time_limit'] > $now)) 527 { // Indefinite ban, or timed ban (not expired) or whitelist entry 528 if ($val['action']== eIPHandler::BAN_TYPE_LEGACY) return eIPHandler::BAN_TYPE_MANUAL; // Precautionary 529 $this->matchAddress = $val['ip']; 530 return $val['action']; // OK to just return - PHP should release the memory used by $checkLists 531 } 532 // Time limit expired 533 $this->clearBan = $val['ip']; // Note what triggered the match - it could be a wildcard (although timed ban unlikely!) 534 return 0; // Can just return - shouldn't be another entry 535 } 536 537 } 538 return 0; 539 } 540 541 542 /** 543 * Encode an IPv4 address into IPv6 544 * Similar functionality to ipEncode 545 * 546 * @param $ip 547 * @param bool $wildCards 548 * @param string $div 549 * @return string - the 'ip4' bit of an IPv6 address (i.e. last 32 bits) 550 */ 551 private function ip4Encode($ip, $wildCards = FALSE, $div = ':') 552 { 553 $ipa = explode('.', $ip); 554 $temp = ''; 555 for ($s = 0; $s < 4; $s++) 556 { 557 if (!isset($ipa[$s])) $ipa[$s] = '*'; 558 if ((($ipa[$s] == '*') || (strpos($ipa[$s], 'x') !== FALSE)) && $wildCards) 559 { 560 $temp .= 'xx'; 561 } 562 else 563 { // Put a zero in if wildcards not allowed 564 $temp .= sprintf('%02x', $ipa[$s]); 565 } 566 if ($s == 1) $temp .= $div; 567 } 568 return $temp; 569 } 570 571 572 /** 573 * Encode an IP address to internal representation. Returns string if successful; FALSE on error 574 * Default separates fields with ':'; set $div='' to produce a 32-char packed hex string 575 * 576 * @param string $ip - 'raw' IP address. May be IPv4, IPv6 577 * @param boolean $wildCards - if TRUE, wildcard characters allowed at the end of an address: 578 * '*' replaces 2 hex characters (primarily for 8-bit subnets of IPv4 addresses) 579 * 'x' replaces a single hex character 580 * @param string $div separator between 4-character blocks of the IPv6 address 581 * 582 * @return bool|string encoded IP. Always exactly 32 characters plus separators if conversion successful 583 * FALSE if conversion unsuccessful 584 */ 585 public function ipEncode($ip, $wildCards = FALSE, $div = ':') 586 { 587 $ret = ''; 588 $divider = ''; 589 if(strpos($ip, ':')!==FALSE) 590 { // Its IPV6 (could have an IP4 'tail') 591 if(strpos($ip, '.')!==FALSE) 592 { // IPV4 'tail' to deal with 593 $temp = strrpos($ip, ':')+1; 594 $ip = substr($ip, 0, $temp).$this->ip4Encode(substr($ip, $temp), $wildCards, $div); 595 } 596 // Now 'normalise' the address 597 $temp = explode(':', $ip); 598 $s = 8-count($temp); // One element will of course be the blank 599 foreach($temp as $f) 600 { 601 if($f=='') 602 { 603 $ret .= $divider.'0000'; // Always put in one set of zeros for the blank 604 $divider = $div; 605 if($s>0) 606 { 607 $ret .= str_repeat($div.'0000', $s); 608 $s = 0; 609 } 610 } 611 else 612 { 613 $ret .= $divider.sprintf('%04x', hexdec($f)); 614 $divider = $div; 615 } 616 } 617 return $ret; 618 } 619 if(strpos($ip, '.')!==FALSE) 620 { // Its IPV4 621 return str_repeat('0000'.$div, 5).'ffff'.$div.$this->ip4Encode($ip, $wildCards, $div); 622 } 623 return FALSE; // Unknown 624 } 625 626 627 /** 628 * Given a potentially truncated IPV6 address as used in the ban list files, adds 'x' characters etc to create 629 * a normalised IPV6 address as stored in the DB. Returned length is exactly 39 characters 630 * @param $address 631 * @return string 632 */ 633 public function ip6AddWildcards($address) 634 { 635 while (($togo = (39 - strlen($address))) > 0) 636 { 637 if (($togo % 5) == 0) 638 { 639 $address .= ':'; 640 } 641 else 642 { 643 $address .= 'x'; 644 } 645 } 646 return $address; 647 } 648 649 650 /** 651 * Takes an encoded IP address - returns a displayable one 652 * Set $IP4Legacy TRUE to display 'old' (IPv4) addresses in the familiar dotted format, 653 * FALSE to display in standard IPV6 format 654 * Should handle most things that can be thrown at it. 655 * If wildcard characters ('x' found, incorporated 'as is' 656 * 657 * @param string $ip encoded IP 658 * @param boolean $IP4Legacy 659 * @return string decoded IP 660 */ 661 public function ipDecode($ip, $IP4Legacy = TRUE) 662 { 663 if (strstr($ip,'.')) 664 { 665 if ($IP4Legacy) return $ip; // Assume its unencoded IPV4 666 $ipa = explode('.', $ip); 667 $ip = '0:0:0:0:0:ffff:'.sprintf('%02x%02x:%02x%02x', $ipa[0], $ipa[1], $ipa[2], $ipa[3]); 668 $ip = str_repeat('0000'.':', 5).'ffff:'.$this->ip4Encode($ip, TRUE, ':'); 669 } 670 if (strstr($ip,'::')) return $ip; // Assume its a compressed IPV6 address already 671 if ((strlen($ip) == 8) && !strstr($ip,':')) 672 { // Assume a 'legacy' IPV4 encoding 673 $ip = '0:0:0:0:0:ffff:'.implode(':',str_split($ip,4)); // Turn it into standard IPV6 674 } 675 elseif ((strlen($ip) == 32) && !strstr($ip,':')) 676 { // Assume a compressed hex IPV6 677 $ip = implode(':',str_split($ip,4)); 678 } 679 if (!strstr($ip,':')) return FALSE; // Return on problem - no ':'! 680 $temp = explode(':',$ip); 681 $z = 0; // State of the 'zero manager' - 0 = not started, 1 = running, 2 = done 682 $ret = ''; 683 $zc = 0; // Count zero fields (not always required) 684 foreach ($temp as $t) 685 { 686 $v = hexdec($t); 687 if (($v != 0) || ($z == 2) || (strpos($t, 'x') !== FALSE)) 688 { 689 if ($z == 1) 690 { // Just finished a run of zeros 691 $z++; 692 $ret .= ':'; 693 } 694 if ($ret) $ret .= ':'; 695 if (strpos($t, 'x') !== FALSE) 696 { 697 $ret .= $t; 698 } 699 else 700 { 701 $ret .= sprintf('%x',$v); // Drop leading zeros 702 } 703 } 704 else 705 { // Zero field 706 $z = 1; 707 $zc++; 708 } 709 } 710 if ($z == 1) 711 { // Need to add trailing zeros, or double colon 712 if ($zc > 1) $ret .= '::'; else $ret .= ':0'; 713 } 714 if ($IP4Legacy && (substr($ret,0,7) == '::ffff:')) 715 { 716 $temp = str_replace(':', '', substr($ip,-9, 9)); 717 $tmp = str_split($temp, 2); // Four 2-character hex values 718 $z = array(); 719 foreach ($tmp as $t) 720 { 721 if ($t == 'xx') 722 { 723 $z[] = '*'; 724 } 725 else 726 { 727 $z[] = hexdec($t); 728 } 729 } 730 $ret = implode('.',$z); 731 } 732 return $ret; 733 } 734 735 736 737 /** 738 * Given a string which may be IP address, email address etc, tries to work out what it is 739 * Uses a fairly simplistic (but quick) approach - does NOT check formatting etc 740 * 741 * @param string $string 742 * @return string ip|email|url|ftp|unknown 743 */ 744 public function whatIsThis($string) 745 { 746 $string = trim($string); 747 if (strpos($string, '@') !== FALSE) return 'email'; // Email address 748 if (strpos($string, 'http://') === 0) return 'url'; 749 if (strpos($string, 'https://') === 0) return 'url'; 750 if (strpos($string, 'ftp://') === 0) return 'ftp'; 751 if (strpos($string, ':') !== FALSE) return 'ip'; // Identify ipv6 752 $string = strtolower($string); 753 if (str_replace(' ', '', strtr($string,'0123456789abcdef.*', ' ')) == '') // Delete all characters found in ipv4 addresses, plus wildcards 754 { 755 return 'ip'; 756 } 757 return 'unknown'; 758 } 759 760 761 /** 762 * Retrieve & cache host name 763 * 764 * @param string $ip_address 765 * @return string host name 766 */ 767 public function get_host_name($ip_address) 768 { 769 if(!isset($this->_host_name_cache[$ip_address])) 770 { 771 $this->_host_name_cache[$ip_address] = gethostbyaddr($ip_address); 772 } 773 return $this->_host_name_cache[$ip_address]; 774 } 775 776 777 /** 778 * Generate DB query for domain name-related checks 779 * 780 * If an email address is passed, discards the individual's name 781 * 782 * @param string $email - an email address or domain name string 783 * @param string $fieldName 784 * @return array|bool false if invalid domain name format 785 * false if invalid domain name format 786 * array of values to compare 787 * @internal param string $fieldname - if non-empty, each array entry is a comparison with this field 788 * 789 */ 790 function makeDomainQuery($email, $fieldName = 'banlist_ip') 791 { 792 $tp = e107::getParser(); 793 if (($tv = strrpos('@', $email)) !== FALSE) 794 { 795 $email = substr($email, $tv+1); 796 } 797 $tmp = strtolower($tp -> toDB(trim($email))); 798 if ($tmp == '') return FALSE; 799 if (strpos($tmp,'.') === FALSE) return FALSE; 800 $em = array_reverse(explode('.',$tmp)); 801 $line = ''; 802 $out = array('*@'.$tmp); // First element looks for domain as email address 803 foreach ($em as $e) 804 { 805 $line = '.'.$e.$line; 806 $out[] = '*'.$line; 807 } 808 if ($fieldName) 809 { 810 foreach ($out as $k => $v) 811 { 812 $out[$k] = '(`'.$fieldName."`='".$v."')"; 813 } 814 } 815 return $out; 816 } 817 818 819 820 /** 821 * Split up an email address to check for banned domains. 822 * @param string $email - email address to process 823 * @param string $fieldname - name of field being searched in DB 824 * 825 * @return bool|string false if invalid address. Otherwise returns a set of values to check 826 * (Moved in from user_handler.php) 827 */ 828 public function makeEmailQuery($email, $fieldname = 'banlist_ip') 829 { 830 $tp = e107::getParser(); 831 $tmp = strtolower($tp -> toDB(trim(substr($email, strrpos($email, "@")+1)))); // Pull out the domain name 832 if ($tmp == '') return FALSE; 833 if (strpos($tmp,'.') === FALSE) return FALSE; 834 $em = array_reverse(explode('.',$tmp)); 835 $line = ''; 836 $out = array($fieldname."='*@{$tmp}'"); // First element looks for domain as email address 837 foreach ($em as $e) 838 { 839 $line = '.'.$e.$line; 840 $out[] = '`'.$fieldname."`='*{$line}'"; 841 } 842 return implode(' OR ',$out); 843 } 844 845 846 847/** 848 * Routines beyond here are to handle banlist-related tasks which involve the DB 849 * note: Most of these routines already existed; moved in from e107_class.php 850 */ 851 852 853 /** 854 * Check if current user is banned 855 * 856 * This is called soon after the DB is opened, to do checks which require it. 857 * Previous checks have already done IP-based bans. 858 * 859 * Starts by removing expired bans if $this->clearBan is set 860 * 861 * Generates the queries to interrogate the ban list, then calls $this->check_ban(). 862 * If the user is banned, $check_ban() never returns - so a return from this routine indicates a non-banned user. 863 * 864 * @return void 865 * 866 * @todo should be possible to simplify, since IP addresses already checked earlier 867 */ 868 public function ban() 869 { 870 $sql = e107::getDb(); 871 872 if ($this->clearBan !== FALSE) 873 { // Expired ban to clear - match exactly the address which triggered this action - could be a wildcard 874 $clearAddress = $this->ip6AddWildcards($this->clearBan); 875 if ($sql->delete('banlist',"`banlist_ip`='{$clearAddress}'")) 876 { 877 $this->actionCount--; // One less item on list 878 $this->logBanItem(0,'Ban cleared: '.$clearAddress); 879 // Now regenerate the text files - so no further triggers from this entry 880 $this->regenerateFiles(); 881 } 882 } 883 884 885 // do other checks - main IP check is in _construct() 886 if($this->actionCount) 887 { 888 $ip = $this->getIP(); // This will be in normalised IPV6 form 889 890 if ($ip !== e107::LOCALHOST_IP && ($ip !== e107::LOCALHOST_IP2) && ($ip !== $this->serverIP)) // Check host name, user email to see if banned 891 { 892 $vals = array(); 893 if (e107::getPref('enable_rdns')) 894 { 895 $vals = array_merge($vals, $this->makeDomainQuery($this->get_host_name($ip), '')); 896 } 897 if ((defined('USEREMAIL') && USEREMAIL)) 898 { 899 // @todo is there point to this? Usually avoid a complete query if we skip it 900 $vals = array_merge($vals, $this->makeDomainQuery(USEREMAIL, '')); 901 } 902 if (count($vals)) 903 { 904 $vals = array_unique($vals); // Could get identical values from domain name check and email check 905 906 if($this->debug) 907 { 908 print_a($vals); 909 } 910 911 912 $match = "`banlist_ip`='".implode("' OR `banlist_ip`='", $vals)."'"; 913 $this->checkBan($match); 914 } 915 } 916 elseif($this->debug) 917 { 918 print_a("IP is LocalHost - skipping ban-check"); 919 } 920 } 921 } 922 923 924 925 /** 926 * Check the banlist table. $query is used to determine the match. 927 * If $do_return, will always return with ban status - TRUE for OK, FALSE for banned. 928 * If return permitted, will never display a message for a banned user; otherwise will display any message then exit 929 * @todo consider whether can be simplified 930 * 931 * @param string $query - the 'WHERE' part of the DB query to be executed 932 * @param boolean $show_error - if true, adds a '403 Forbidden' header for a banned user 933 * @param boolean $do_return - if TRUE, returns regardless without displaying anything. if FALSE, for a banned user displays any message and exits 934 * @return boolean TRUE for OK, FALSE for banned. 935 */ 936 public function checkBan($query, $show_error = true, $do_return = false) 937 { 938 $sql = e107::getDb(); 939 $pref = e107::getPref(); 940 $tp = e107::getParser(); 941 $admin_log = e107::getAdminLog(); 942 943 //$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Check for Ban",$query,FALSE,LOG_TO_ROLLING); 944 if ($sql->select('banlist', '*', $query.' ORDER BY `banlist_bantype` DESC')) 945 { 946 // Any whitelist entries will be first, because they are positive numbers - so we can answer based on the first DB record read 947 $row = $sql->fetch(); 948 if($row['banlist_bantype'] >= eIPHandler::BAN_TYPE_WHITELIST) 949 { 950 //$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Whitelist hit",$query,FALSE,LOG_TO_ROLLING); 951 return true; // Whitelisted entry 952 } 953 954 // Found banlist entry in table here 955 if(($row['banlist_banexpires'] > 0) && ($row['banlist_banexpires'] < time())) 956 { // Ban has expired - delete from DB 957 $sql->delete('banlist', $query); 958 $this->regenerateFiles(); 959 960 return true; 961 } 962 963 // User is banned hereafter - just need to sort out the details. 964 // May need to retrigger ban period 965 if (!empty($pref['ban_retrigger']) && !empty($pref['ban_durations'][$row['banlist_bantype']])) 966 { 967 $dur = (int) $pref['ban_durations'][$row['banlist_bantype']]; 968 $updateQry = array( 969 'banlist_banexpires' => (time() + ($dur * 60 * 60)), 970 'WHERE' => "banlist_ip ='".$row['banlist_ip']."'" 971 ); 972 973 $sql->update('banlist', $updateQry); 974 $this->regenerateFiles(); 975 //$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Retrigger Ban",$row['banlist_ip'],FALSE,LOG_TO_ROLLING); 976 } 977 //$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Active Ban",$query,FALSE,LOG_TO_ROLLING); 978 if ($show_error) 979 { 980 header('HTTP/1.1 403 Forbidden', true); 981 } 982 // May want to display a message 983 if (!empty($pref['ban_messages'])) 984 { 985 // Ban still current here 986 if($do_return) 987 { 988 return false; 989 } 990 991 echo $tp->toHTML(varset($pref['ban_messages'][$row['banlist_bantype']])); // Show message if one set 992 } 993 //$admin_log->e_log_event(4, __FILE__."|".__FUNCTION__."@".__LINE__, 'BAN_03', 'LAN_AUDIT_LOG_003', $query, FALSE, LOG_TO_ROLLING); 994 995 if($this->debug) 996 { 997 echo "<pre>query: ".$query; 998 echo "\nBanned</pre>"; 999 } 1000 1001 // added missing if clause 1002 if ($do_return) 1003 { 1004 return false; 1005 } 1006 1007 exit(); 1008 } 1009 1010 if($this->debug) 1011 { 1012 echo "query: ".$query; 1013 echo "<br />Not Banned "; 1014 } 1015 1016 1017 //$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","No ban found",$query,FALSE,LOG_TO_ROLLING); 1018 return true; // Email address OK 1019 } 1020 1021 1022 1023 /** 1024 * Add an entry to the banlist. $bantype = 1 for manual, 2 for flooding, 4 for multiple logins 1025 * Returns TRUE if ban accepted. 1026 * Returns FALSE if ban not accepted (e.g. because on whitelist, or invalid IP specified) 1027 * 1028 * @param integer $bantype - either one of the BAN_TYPE_xxx constants, or a legacy value as above 1029 * @param string $ban_message 1030 * @param string $ban_ip 1031 * @param integer $ban_user 1032 * @param string $ban_notes 1033 * 1034 * @return boolean|integer check result - FALSE if ban rejected. TRUE if ban added. 1 if IP address already banned 1035 */ 1036 public function add_ban($bantype, $ban_message = '', $ban_ip = '', $ban_user = 0, $ban_notes = '') 1037 { 1038 1039 if ($ban_ip == e107::LOCALHOST_IP || $ban_ip == e107::LOCALHOST_IP2) 1040 { 1041 return false; 1042 } 1043 1044 1045 $sql = e107::getDb(); 1046 $pref = e107::getPref(); 1047 1048 switch ($bantype) // Convert from 'internal' ban types to those used in the DB 1049 { 1050 case 1 : $bantype = eIPHandler::BAN_TYPE_MANUAL; break; 1051 case 2 : $bantype = eIPHandler::BAN_TYPE_FLOOD; break; 1052 case 4 : $bantype = eIPHandler::BAN_TYPE_LOGINS; break; 1053 } 1054 if (!$ban_message) 1055 { 1056 $ban_message = 'No explanation given'; 1057 } 1058 if (!$ban_ip) 1059 { 1060 $ban_ip = $this->getIP(); 1061 } 1062 $ban_ip = preg_replace('/[^\w@\.:]*/', '', urldecode($ban_ip)); // Make sure no special characters 1063 if (!$ban_ip) 1064 { 1065 return FALSE; 1066 } 1067 // See if address already in the banlist 1068 if ($sql->select('banlist', '`banlist_bantype`', "`banlist_ip`='{$ban_ip}'")) 1069 { 1070 list($banType) = $sql->fetch(); 1071 1072 if ($banType >= eIPHandler::BAN_TYPE_WHITELIST) 1073 { // Got a whitelist entry for this 1074 //$admin_log->e_log_event(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING); 1075 return FALSE; 1076 } 1077 return 1; // Already in ban list 1078 } 1079 /* 1080 // See if the address is in the whitelist 1081 if ($sql->db_Select('banlist', '*', "`banlist_ip`='{$ban_ip}' AND `banlist_bantype` >= ".eIPHandler::BAN_TYPE_WHITELIST)) 1082 { // Got a whitelist entry for this 1083 //$admin_log->e_log_event(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING); 1084 return FALSE; 1085 } */ 1086 if(vartrue($pref['enable_rdns_on_ban'])) 1087 { 1088 $ban_message .= 'Host: '.$this->get_host_name($ban_ip); 1089 } 1090 // Add using an array - handles DB changes better 1091 $sql->insert('banlist', 1092 array( 1093 'banlist_id' => 0, 1094 'banlist_ip' => $ban_ip , 1095 'banlist_bantype' => $bantype , 1096 'banlist_datestamp' => time() , 1097 'banlist_banexpires' => (vartrue($pref['ban_durations'][$bantype]) ? time()+($pref['ban_durations'][$bantype]*60*60) : 0) , 1098 'banlist_admin' => $ban_user , 1099 'banlist_reason' => $ban_message , 1100 'banlist_notes' => $ban_notes 1101 )); 1102 1103 $this->regenerateFiles(); 1104 return TRUE; 1105 } 1106 1107 1108 /** 1109 * Regenerate the text-based banlist files (called after a banlist table mod) 1110 */ 1111 public function regenerateFiles() 1112 { 1113 // Now regenerate the text files - so accesses of this IP address don't use the DB 1114 $ipAdministrator = new banlistManager; 1115 $ipAdministrator->writeBanListFiles('ip,htaccess'); 1116 } 1117 1118 1119 1120 public function getConfigDir() 1121 { 1122 return $this->ourConfigDir; 1123 } 1124 1125 1126 1127 /** 1128 * Routine checks whether a file or directory has sufficient permissions 1129 * 1130 * ********** @todo this is in the wrong place! Move it to a more appropriate class! ************* 1131 * 1132 * @param string $name - file with path (if ends in anything other than '/' or '\') or directory (if ends in '/' or '\') 1133 * @param string(?) $perms - required permissions as standard *nix 3-digit string 1134 * @param boolean $message - if TRUE, and insufficient rights, a message is output (in 0.8, to the message handler) 1135 * 1136 * @return boolean TRUE if sufficient permissions, FALSE if not (or error) 1137 * 1138 * For each mode character: 1139 * 1 - execute 1140 * 2 - writable 1141 * 4 - readable 1142 */ 1143 public function checkFilePerms($name, $perms, $message = TRUE) 1144 { 1145 $isDir = ((substr($name, -1,1) == '\\') || (substr($name, -1,1) == '/')); 1146 $result = FALSE; 1147 $msg = ''; 1148 $dest = $isDir ? 'Directory' : 'File'; 1149 $reqPerms = intval('0'.$perms) & 511; // We want an integer value to match the return from fileperms() 1150 if (!file_exists($name)) 1151 { 1152 $msg = $dest.': '.$name.' does not exist'; 1153 } 1154 if ($msg == '') 1155 { 1156 $realPerms = fileperms($name); 1157 $mgs = $name.' is not a '.$dest; // Assume an error to start; clear messsage if all OK 1158 switch ($realPerms & 0xf000) 1159 { 1160 case 0x8000 : 1161 if (!$isDir) 1162 { 1163 $msg = ''; 1164 } 1165 break; 1166 case 0x4000 : 1167 if ($isDir) 1168 { 1169 $msg = ''; 1170 } 1171 break; 1172 } 1173 } 1174 if ($msg == '') 1175 { 1176 if (($reqPerms & $realPerms) == $reqPerms) 1177 { 1178 $result = TRUE; 1179 } 1180 else 1181 { 1182 $msg = $name.': Insufficient permissions. Required: '.$this->permsToString($reqPerms).' Actual: '.$this->permsToString($realPerms); 1183 } 1184 } 1185 if ($message && $msg) 1186 { // Do something with the error message 1187 } 1188 return $result; 1189 } 1190 1191 1192 /** 1193 * Decode file/directory permissions into human-readable characters 1194 * 1195 * @param int $val representing permissions (LS 9 bits used) 1196 * 1197 * @return string exactly 9 characters, with blocks of 3 representing user, group and world permissions 1198 */ 1199 public function permsToString($val) 1200 { 1201 $perms = 'rwxrwxrwx'; 1202 $mask = 0x100; 1203 1204 for ($i = 0; $i < 9; $i++) 1205 { 1206 if (($mask & $val) == 0) $perms[$i] = '-'; 1207 $mask = $mask >> 1; 1208 } 1209 return $perms; 1210 } 1211 1212 1213 /** 1214 * Function to see whether a user is already logged as being online 1215 * 1216 * @todo - this is possibly in the wrong place! 1217 * 1218 * @param string $ip - in 'normalised' IPV6 form 1219 * @param string $browser - browser token as logged 1220 * 1221 * @return boolean|array FALSE if DB error or not found. Best match table row if found 1222 */ 1223 public function isUserLogged($ip, $browser) 1224 { 1225 $ourDB = e107::getDb('olcheckDB'); // @todo is this OK, or should an existing one be used? 1226 1227 $result = $ourDB->select('online', '*', "`user_ip` = '{$ip}' OR `user_token` = '{$browser}'"); 1228 if ($result === FALSE) return FALSE; 1229 $gotIP = FALSE; 1230 $gotBrowser = FALSE; 1231 $bestRow = FALSE; 1232 while (FALSE !== ($row = $ourDB->fetch())) 1233 { 1234 if ($row['user_token'] == $browser) 1235 { 1236 if ($row['user_ip'] == $ip) 1237 { // Perfect match 1238 return $row; 1239 } 1240 // Just browser token match here 1241 if ($bestRow === FALSE) 1242 { 1243 $bestRow = $row; 1244 $gotBrowser = TRUE; 1245 } 1246 else 1247 { // Problem - two or more rows with same browser token. What to do? 1248 } 1249 } 1250 elseif ($row['user_ip'] == $ip) 1251 { // Just IP match here 1252 if ($bestRow === FALSE) 1253 { 1254 $bestRow = $row; 1255 $gotIP = TRUE; 1256 } 1257 else 1258 { // Problem - two or more rows with same IP address. Hopefully better offer later! 1259 } 1260 } 1261 } 1262 return $bestRow; 1263 } 1264} 1265 1266 1267 1268 1269 1270 1271/** 1272 * Routines involved with the management of the ban list and associated files 1273 */ 1274class banlistManager 1275{ 1276 private $ourConfigDir = ''; 1277 public $banTypes = array(); 1278 1279 public function __construct() 1280 { 1281 e107_include_once(e_LANGUAGEDIR.e_LANGUAGE."/admin/lan_banlist.php"); 1282 $this->ourConfigDir = e107::getIPHandler()->getConfigDir(); 1283 $this->banTypes = array( // Used in Admin-ui. 1284 '-1' => BANLAN_101, // manual 1285 '-2' => BANLAN_102, // Flood 1286 '-3' => BANLAN_103, // Hits 1287 '-4' => BANLAN_104, // Logins 1288 '-5' => BANLAN_105, // Imported 1289 '-6' => BANLAN_106, // Users 1290 '-8' => BANLAN_107, // Imported 1291 '100' => BANLAN_120 // Whitelist 1292 ); 1293 1294 1295 } 1296 1297 /** 1298 * Return an array of valid ban types (for use as indices into array, generally) 1299 */ 1300 public function getValidReasonList() 1301 { 1302 return array( 1303 eIPHandler::BAN_TYPE_LEGACY, 1304 eIPHandler::BAN_TYPE_MANUAL, 1305 eIPHandler::BAN_TYPE_FLOOD, 1306 eIPHandler::BAN_TYPE_HITS, 1307 eIPHandler::BAN_TYPE_LOGINS, 1308 eIPHandler::BAN_TYPE_IMPORTED, 1309 eIPHandler::BAN_TYPE_USER, 1310 // Spare value 1311 eIPHandler::BAN_TYPE_UNKNOWN 1312 ); 1313 } 1314 1315 1316 /** 1317 * Create banlist-related text files as requested: 1318 * List of whitelisted and blacklisted IP addresses 1319 * file for easy import into .htaccess file (allow from...., deny from....) 1320 * Generic CSV-format export file 1321 * 1322 * @param string $options {ip|htaccess|csv} - comma separated list (no spaces) to select which files to write 1323 * @param string $typeList - optional comma-separated list of ban types required (default is all) 1324 * Uses constants: 1325 * BAN_FILE_IP_NAME Saves list of banned and whitelisted IP addresses 1326 * BAN_FILE_ACTION_NAME Details of actions for different ban types 1327 * BAN_FILE_HTACCESS File in format for direct paste into .htaccess 1328 * BAN_FILE_CSV_NAME 1329 * BAN_FILE_EXTENSION File extension to append 1330 * 1331 */ 1332 public function writeBanListFiles($options = 'ip', $typeList = '') 1333 { 1334 e107::getMessage()->addDebug("Writing new Banlist files."); 1335 $sql = e107::getDb(); 1336 $ipManager = e107::getIPHandler(); 1337 1338 $optList = explode(',',$options); 1339 $fileList = array(); // Array of file handles once we start 1340 1341 $fileNameList = array('ip' => eIPHandler::BAN_FILE_IP_NAME, 'htaccess' => eIPHandler::BAN_FILE_HTACCESS, 'csv' => eIPHandler::BAN_FILE_CSV_NAME); 1342 1343 $qry = 'SELECT * FROM `#banlist` '; 1344 if ($typeList != '') $qry .= " WHERE`banlist_bantype` IN ({$typeList})"; 1345 $qry .= ' ORDER BY `banlist_bantype` DESC'; // Order ensures whitelisted addresses appear first 1346 1347 // Create a temporary file for each type as demanded. Vet the options array on this pass, as well 1348 foreach($optList as $k => $opt) 1349 { 1350 if (isset($fileNameList[$opt])) 1351 { 1352 if ($tmp = fopen($this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION, 'w')) 1353 { 1354 $fileList[$opt] = $tmp; // Save file handle 1355 fwrite($fileList[$opt], "<?php\n; die();\n"); 1356 //echo "Open File for write: ".$this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION.'<br />'; 1357 } 1358 else 1359 { 1360 unset($optList[$k]); 1361 /// @todo - flag error? 1362 } 1363 } 1364 else 1365 { 1366 unset($optList[$k]); 1367 } 1368 } 1369 1370 if ($sql->gen($qry)) 1371 { 1372 while ($row = $sql->db_Fetch()) 1373 { 1374 $row['banlist_ip'] = $this->trimWildcard($row['banlist_ip']); 1375 if ($row['banlist_ip'] == '') continue; // Ignore empty IP addresses 1376 if ($ipManager->whatIsThis($row['banlist_ip']) != 'ip') continue; // Ignore non-numeric IP Addresses 1377 if ($row['banlist_bantype'] == eIPHandler::BAN_TYPE_LEGACY) $row['banlist_bantype'] = eIPHandler::BAN_TYPE_UNKNOWN; // Handle legacy bans 1378 foreach ($optList as $opt) 1379 { 1380 $line = ''; 1381 switch ($opt) 1382 { 1383 case 'ip' : 1384 // IP_address action expiry_time additional_parameters 1385 $line = $row['banlist_ip'].' '.$row['banlist_bantype'].' '.$row['banlist_banexpires']."\n"; 1386 break; 1387 case 'htaccess' : 1388 $line = (($row['banlist_bantype'] > 0) ? 'allow from ' : 'deny from ').$row['banlist_ip']."\n"; 1389 break; 1390 case 'csv' : /// @todo - when PHP5.1 is minimum, can use fputcsv() function 1391 $line = $row['banlist_ip'].','.$this->dateFormat($row['banlist_datestamp']).','.$this->dateFormat($row['banlist_expires']).','; 1392 $line .= $row['banlist_bantype'].',"'.$row['banlist_reason'].'","'.$row['banlist_notes'].'"'."\n"; 1393 break; 1394 } 1395 fwrite($fileList[$opt], $line); 1396 } 1397 } 1398 } 1399 1400 // Now close each file 1401 foreach ($optList as $opt) 1402 { 1403 fclose($fileList[$opt]); 1404 } 1405 1406 // Finally, delete the working file, rename the temporary one 1407 // Docs suggest that 'newname' is auto-deleted if it exists (as it usually should) 1408 // - but didn't appear to work, hence copy then delete 1409 foreach ($optList as $opt) 1410 { 1411 $oldName = $this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION; 1412 $newName = $this->ourConfigDir.$fileNameList[$opt].eIPHandler::BAN_FILE_EXTENSION; 1413 copy($oldName, $newName); 1414 unlink($oldName); 1415 } 1416 } 1417 1418 1419 /** 1420 * Trim wildcards from IP addresses 1421 * 1422 * @param string $ip - IP address in any normal form 1423 * 1424 * Note - this removes all characters after (and including) the first '*' or 'x' found. So an '*' or 'x' in the middle of a string may 1425 * cause unexpected results. 1426 * @return string 1427 */ 1428 private function trimWildcard($ip) 1429 { 1430 $ip = trim($ip); 1431 $temp = strpos($ip, 'x'); 1432 if ($temp !== FALSE) 1433 { 1434 return substr($ip, 0, $temp); 1435 } 1436 $temp = strpos($ip, '*'); 1437 if ($temp !== FALSE) 1438 { 1439 return substr($ip, 0, $temp); 1440 } 1441 return $ip; 1442 } 1443 1444 1445 /** 1446 * Format date and time for export into a text file. 1447 * 1448 * @param int $date - standard Unix time stamp 1449 * 1450 * @return string. '0' if date is zero, else formatted in consistent way. 1451 */ 1452 private function dateFormat($date) 1453 { 1454 if ($date == 0) return '0'; 1455 return strftime('%Y%m%d_%H%M%S',$date); 1456 } 1457 1458 1459 1460 /** 1461 * Return string corresponding to a ban type 1462 * @param int $banType - constant representing the ban type 1463 * @param bool $forMouseover - if true, its the (usually longer) explanatory string for a mouseover 1464 * 1465 * @return string 1466 */ 1467 public function getBanTypeString($banType, $forMouseover = FALSE) 1468 { 1469 switch ($banType) 1470 { 1471 case eIPHandler::BAN_TYPE_LEGACY : $listOffset = 0; break; 1472 case eIPHandler::BAN_TYPE_MANUAL : $listOffset = 1; break; 1473 case eIPHandler::BAN_TYPE_FLOOD : $listOffset = 2; break; 1474 case eIPHandler::BAN_TYPE_HITS : $listOffset = 3; break; 1475 case eIPHandler::BAN_TYPE_LOGINS : $listOffset = 4; break; 1476 case eIPHandler::BAN_TYPE_IMPORTED : $listOffset = 5; break; 1477 case eIPHandler::BAN_TYPE_USER : $listOffset = 6; break; 1478 case eIPHandler::BAN_TYPE_TEMPORARY : $listOffset = 9; break; 1479 1480 case eIPHandler::BAN_TYPE_WHITELIST : 1481 return BANLAN_120; // Special case - may never occur 1482 case eIPHandler::BAN_TYPE_UNKNOWN : 1483 default : 1484 if (($banType > 0) && ($banType < 9)) 1485 { 1486 $listOffset = $banType; // BC conversions 1487 } 1488 else 1489 { 1490 $listOffset = 8; 1491 } 1492 } 1493 if ($forMouseover) return constant('BANLAN_11'.$listOffset); 1494 return constant('BANLAN_10'.$listOffset); 1495 } 1496 1497 1498 1499 /** 1500 * Write a text file containing the ban messages related to each ban reason 1501 */ 1502 public function writeBanMessageFile() 1503 { 1504 $pref['ban_messages'] = e107::getPref('ban_messages'); 1505 1506 $oldName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.'_tmp'.eIPHandler::BAN_FILE_EXTENSION; 1507 if ($tmp = fopen($oldName, 'w')) 1508 { 1509 fwrite($tmp, "<?php\n; die();\n"); 1510 foreach ($this->getValidReasonList() as $type) 1511 { 1512 fwrite($tmp,'['.$type.']'.$pref['ban_messages'][$type]."\n"); 1513 } 1514 fclose($tmp); 1515 $newName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION; 1516 copy($oldName, $newName); 1517 unlink($oldName); 1518 } 1519 } 1520 1521 1522 1523 /** 1524 * Check whether the message file (containing responses to ban types) exists 1525 * 1526 * @return boolean TRUE if exists, FALSE if doesn't exist 1527 */ 1528 public function doesMessageFileExist() 1529 { 1530 return is_readable($this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION); 1531 } 1532 1533 1534 1535 /** 1536 * Get entries from the ban action log 1537 * 1538 * @param int $start - offset into list (zero is first entry) 1539 * @param int $count - number of entries to return - zero is a special case 1540 * @param int $numEntry - filled in on return with the total number of entries in the log file 1541 * 1542 * @return array of strings; each string is a single log entry, newest first. 1543 * 1544 * Returns an empty array if an error occurs (or if no entries) 1545 * If $count is zero, all entries are returned, in ascending order. 1546 */ 1547 public function getLogEntries($start, $count, &$numEntry) 1548 { 1549 $ret = array(); 1550 $numEntry = 0; 1551 $fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME; 1552 if (!is_readable($fileName)) return $ret; 1553 1554 $vals = file($fileName); 1555 if ($vals === FALSE) return $ret; 1556 if (substr($vals[0], 0, 5) == '<?php') 1557 { 1558 unset($vals[0]); 1559 } 1560 if (substr($vals[0], 0, 1) == ';') unset($vals[0]); 1561 $numEntry = count($vals); 1562 if ($start > $numEntry) return $ret; // Empty return if beyond the end 1563 if ($count == 0) return $vals; // Special case - return the lot in ascending date order 1564 // Array is built up with newest last - but we want newest first. And we don't want to duplicate the array! 1565 if (($start + $count) > $numEntry) $count = $numEntry - $start; // Last segment might not have enough entries 1566 $ret = array_slice($vals, -$start - $count, $count); 1567 return array_reverse($ret); 1568 } 1569 1570 1571 /** 1572 * Converts one of the strings returned in a getLogEntries string into an array of values 1573 * 1574 * @param string $string - a text line, possibly including a 'newline' at the end 1575 * 1576 * @return array of up to $count entries 1577 * ['banDate'] - time/date stamp 1578 * ['banIP'] - IP address involved 1579 * ['banReason'] - Numeric reason code for entry 1580 * ['banNotes'] = any text appended 1581 */ 1582 public function splitLogEntry($string) 1583 { 1584 $temp = explode(' ',$string, 4); 1585 while (count($temp) < 4) $temp[] = ''; 1586 $ret['banDate'] = $temp[0]; 1587 $ret['banIP'] = $temp[1]; 1588 $ret['banReason'] = $temp[2]; 1589 $ret['banNotes'] = str_replace("\n", '', $temp[3]); 1590 return $ret; 1591 } 1592 1593 1594 /** 1595 * Delete ban Log file 1596 * 1597 * @return boolean TRUE on success, FALSE on failure 1598 */ 1599 public function deleteLogFile() 1600 { 1601 $fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME; 1602 return unlink($fileName); 1603 } 1604 1605 1606 /** 1607 * Update expiry time for IP addresses that have accessed the site while banned. 1608 * Processes the entries in the 'ban retrigger' action file, and deletes the file 1609 * 1610 * Needs to be called from a cron job, at least once per hour, and ideally every few minutes. Otherwise banned users who access 1611 * 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 1612 * an issue in practice) 1613 * 1614 * @return int number of IP addresses updated 1615 * 1616 * @todo - implement cron job and test 1617 */ 1618 public function banRetriggerAction() 1619 { 1620 //if (!e107::getPref('ban_retrigger')) return 0; // Should be checked earlier 1621 1622 $numEntry = 0; // Make sure this variable declared before passing it - total number of log entries. 1623 $ipAction = array(); // Array of IP addresses to action 1624 $fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION; 1625 $entries = file($fileName); 1626 if (!is_array($entries)) 1627 { 1628 return 0; // Probably no retrigger actions 1629 } 1630 @unlink($fileName); // Delete the action file now we've read it in. 1631 1632 // Scan the list completely before doing any processing - this will ensure we only process the most recent entry for each IP address 1633 while (count($entries) > 0) 1634 { 1635 $line = array_shift($entries); 1636 $info = $this->splitLogEntry($line); 1637 if ($info['banReason'] < 0) 1638 { 1639 $ipAction[$info['banIP']] = array('date' => $info['banDate'], 'reason' => $info['banReason']); // This will result in us gathering the most recent access from each IP address 1640 } 1641 } 1642 1643 if (count($ipAction) == 0) return 0; // Nothing more to do 1644 1645 // Now run through the database updating times 1646 $numRet = 0; 1647 $pref['ban_durations'] = e107::getPref('ban_durations'); 1648 $ourDb = e107::getDb(); // Should be able to use $sql, $sql2 at this point 1649 $writeDb = e107::getDb('sql2'); 1650 1651 foreach ($ipAction as $ipKey => $ipInfo) 1652 { 1653 if ($ourDb->select('banlist', '*', "`banlist_ip`='".$ipKey."'") === 1) 1654 { 1655 if ($row = $ourDb->fetch()) 1656 { 1657 // @todo check next line 1658 $writeDb->db_Update('banlist', 1659 '`banlist_banexpires` = '.intval($row['banlist_banexpires'] + $pref['ban_durations'][$row['banlist_banreason']])); 1660 $numRet++; 1661 } 1662 } 1663 } 1664 if ($numRet) 1665 { 1666 $this->writeBanListFiles('ip'); // Just rewrite the ban list - the actual IP addresses won't have changed 1667 } 1668 return $numRet; 1669 } 1670} 1671 1672 1673