\/ */ $anti_hammer_version = '0.9.3.2'; if (realpath($_SERVER['SCRIPT_FILENAME']) == realpath(__FILE__)) { die( 'This script is designed to run as a php auto-prepend, like so (in .htaccess)..

php_value auto_prepend_file "/real/full/server/path/to/anti-hammer.php"'); } /* Anti-Hammer FREE Automatically set temporary bans for web site hammering. Protect your valuable server resources for genuine clients. Full details here.. http://corz.org/server/tools/anti-hammer/ Have fun! ;o) Cor (c) 2007->tomorrow! cor + corz.org ;o) */ /* prefs.. */ /* Anti-Hammer data directory [default: $_SERVER['DOCUMENT_ROOT'].'/Anti-Hammer/anti-hammer';] When using Anti-Hammer's built-in client-tracking (the default), files will be stored, in this directory.. If you are using the built-in sessions, and this directory isn't writable, Anti-Hammer won't work. */ $anti_hammer['info_path'] = $_SERVER['DOCUMENT_ROOT'].'/anti-hammer/sessions'; /* Client ID File Prefix [default: $anti_hammer['ID_prefix'] = 'HammerID_';] This text is placed before the client ID in the ID filename. e.g.. "HammerID_06fa71c938a108f4a2b1f1ef091653ef" You may wish to use a different name.. */ $anti_hammer['ID_prefix'] = 'HammerID_'; /* File Types [default: $anti_hammer['types'] = 'php,html';] Which file types (extensions) to protect with the anti-hammer? We only want to count hits on the main pages, not associated files, css, javascript includes, and such (if they are generated by php), as many of these will normally be requested within miliseconds of the initial page hit. If we run the anti-hammer indiscriminately, such files, would automatically count towards hammering, and folk would probably be penalized on their first visit. If you don't use php to generate other (non - .php) files, the anti-hammer won't be running anyway - it only runs before php scripts, as it's designed to protect server resources, not bandwidth; basic requests get spat out without any real processing power or memory usage. These list items matches the extension of the *actual* physical script file, regardless of the requested URI, so for example, these.. http://mysite.com/index.php http://mysite.com/foo.php?page=bar.htm http://mysite.com/genny.php?image=img1.jpg .. would *all* match 'php'. Other extensions are fine, so long as they are parsed by php on your setup. Separate entries with commas, and put the whole thing in quotes.. Extensionless files are not supported. */ $anti_hammer['types'] = 'php,html'; /* Generated Extensions [default: $anti_hammer['gen_types'] = 'jpg,png';] This is an list of (usually) image extensions which you serve via php. As there may be many of these on a single page, we want to skip these, too. These list items match the extension of the *request*, regardless of the physical script file generating the output. Links such as.. http://mysite.com/gen.php?image=foo.jpg http://mysite.com/png-pusher.html/foo.jpg .. would match "jpg". Separate entries with commas and put the whole thing in quotes.. */ $anti_hammer['gen_types'] = 'jpg,png'; /* NOTES: The file type *generating* these url's MUST be included in your $anti_hammer['types'] array (above), presumably. 'php'. You could also use the above preference array to skip other non- image generated types, if you have such things onsite. */ /* Skip certain files and folders.. aka, basic "Ignore".. [default: $anti_hammer['skip'] = '/chat/,/foobar/members,rdf.php,/blog/rss.php';] A list of areas/folders and specific files you DON'T want the anti-hammer to cover. Enter the full path (from site root) to each file/folder. You can also skip ALL the instances of "rss.php", etc. on your entire site by using only the file name, e.g.. $anti_hammer['skip'] = 'rdf.php,rss.php'; This also works for folder. Using the full path enables you to target specific files and folders, using only the name gives you blanket coverage. Your call. Basically, if your string is contained anywhere within the requested URI, the script returns control to your page immediately, bypassing Anti-Hammer. Do put comments *between* entries. */ $anti_hammer['skip'] = '/chat/,/foobar/members,rdf.php,/blog/rss.php'; /* RSS feeds are a good example of a file to skip (assuming they are php-generated). Firefox, for example, will often grab all the feeds on a page at-once, quickly notching up a user's hammer count. */ /* Hammer Time! [default: $anti_hammer['hammer_time'] = 100;] (One Second) If they make two requests within this time, the counter increases by one. The faster and more capable your server, the lower this setting can be. The higher you set this, the more likely they are to get a warning. 100 is a reasonable setting for a fast server, enabling one-hit-per-second spidering, but penalizing anything faster. Enter an integer, representing 100th/s.. */ $anti_hammer['hammer_time'] = 90; /* Trigger levels. [default: $anti_hammer['trigger_levels'] = '5,10,20,30';] Enter the number of violations that will trigger each of the four levels.. i.e. At the default settings, they get their first warning after five violations (with a ban time of three seconds, set below). The time penalty increases after ten and twenty violations, up to the maximum level of 30 violations (which imposes the maximum ban time of 20 seconds). You can set the actual times in the next preference. Specify four integer values, separated by commas, whole thing in quotes. */ $anti_hammer['trigger_levels'] = '5,10,20,30'; /* Ban Times. [default: $anti_hammer['waiting_times'] = '3,5,10,20';] This list sets the individual times that offenders will be 'banned' for. They will have to wait *this* long before they can try again. Each of the four setting corresponds to one of the above trigger_levels. Specify four integer values, separated by commas, whole thing in quotes. */ $anti_hammer['waiting_times'] = '3,5,10,20'; /* Rolling Trigger Times [default: $anti_hammer['rolling_trigger'] = false;] This increases the ban time automatically with EACH hammer. You must wait three seconds.. You must wait four seconds.. You must wait five seconds.. And so on. */ $anti_hammer['rolling_trigger'] = false; /* Cut-Off [default: $anti_hammer['cut_off'] = 50] You can also set an absolute cut-off point. Anyone receiving this many hammer violations is simply dropped, and from that point onward, their pages die before they even begin - blank. This works with both preset and rolling triggers. Leave blank to disable the cut-off. e.g.. $anti_hammer['cut_off'] = ''; */ $anti_hammer['cut_off'] = 50; /* Bye Bye! Message. [default: $anti_hammer['cut_off_msg'] = '

Bye Now!

';] A final word from our sponsor? This is the final message they see before it all goes blank. No other text is presented. */ $anti_hammer['cut_off_msg'] = '

Bye Now!

'; /* Ban Time [default: $anti_hammer['ban_time'] = '12';] And for how many hours will the above cut-off (ban) last? */ $anti_hammer['ban_time'] = '12'; // NOTE: If you set your Garbage Collection age to any less than this, you // effectively reset all bans older than THAT figure. // // In other words, ensure your garbage collection age ('GC_age', below) // is larger than your 'ban_time' setting here, probably x2. // Think: if GC happened one minute after someone was banned, and their // session ID file was >= GC_age, it would be cleaned up! Then no ban! // // Also Note: Humans are daily creatures, for them a 12h ban, is effectively 24! /* Log File [default: $anti_hammer['log'] = $_SERVER['DOCUMENT_ROOT'].'/log/.ht_hammers';] We will log each banned hit, for reference. Enter full path to log location.. NOTE: If the parent directory does not exist, Anti-Hammer will not attempt to create it, and you will get no logging. */ $anti_hammer['log'] = $_SERVER['DOCUMENT_ROOT'].'/log/.ht_hammers'; // It is recommend you watch this log very carefully for the first // NOTE: few minutes/ days after installation, in case of unexpected side- // effects. And in that case, please do mail me about it! /* Kill Message. [default: $anti_hammer['kill_msg'] = 'Please do not hammer this site.
';] When a request is killed - send this message (before the other text). You can use any calid HTML in here, header tags, or whatever you like.. */ $anti_hammer['kill_msg'] = 'Please do not hammer this site!
'; /* NOTE: No
is placed after this text. If you aren't using tags, and want a break, add it yourself. */ /* Page Title. [default: $anti_hammer['page_title'] = 'Please do not hammer this site!';] This is what is displayed in the title bar of their browser. Keep this one plain text. */ $anti_hammer['page_title'] = 'Please do not hammer this site!'; /* WebMaster's Name [default: $anti_hammer['webmaster'] = 'the webmaster';] Name of the webmaster, will be included in the kill page. e.g. "If you believe this is in error, please mail about it!" */ $anti_hammer['webmaster'] = 'the webmaster'; /* Admin Bypass [default: $anti_hammer['admin_agent_string'] = 'MyCrazyUserAgentString';] If you insert this exact string into your web browser's user-agent string (just tag it onto the end), you can bypass the hammer altogether. Very handy for busy webmasters. */ $anti_hammer['admin_agent_string'] = 'MyCrazyUniqueUserAgentString'; // It's not advisable to go messing with the main body of your // NOTE: browser's user agent string. Lots of web designers rely on this // information to serve you beautiful, functional web pages. /* WebMaster email address (string). [default: $anti_hammer['error_mail'] = 'bugs at mydomain dot com';] The usual text format of so-and-so at such-and-such dot com works well. This is tagged on to the end of the massage inside <> angle brackets, to look like an address. */ $anti_hammer['error_mail'] = 'bugs at mydomain dot com'; /* Lookup Failures. When an event worth logging occurs, we can lookup the host name of the client to add to our logs. This takes a moment, but only occurs while logging bad clients, and can be useful in quickly identifying abusers (or good bots using bad user agent string - to come) */ $anti_hammer['lookup_failures'] = true; /* Allow known bots? [default: $anti_hammer['allow_bots'] = false;] We can allow certain bots to bypass the Anti-Hammer. Do do this, specify the expected user agent strings in.. path-to/anti-hammer/exemptions/exemptions.ini and then supply an IP-mask file where said user agent is expected to be making requests FROM, one ip per line, in the standard Spider IP list format as found here.. http://www.iplists.com/ http://www.iplists.com/nw/ <- updated, reorganised, with msnbot+more A blog URI is listed there, where list updates are posted. (this doesn't happen a lot, maybe 2-3 times a year) NOTE: User agent string matches are CaSe SenSiTivE! If you want to match "msnbot" and "MSNBOT", you need two entries. (a case-insensitive test is roughly five times slower than case-sensitive; so testing two separate entries is much faster) NOTE: If cooking up your own anti-hammer.ini, you probably do not want to include the generic user agent strings (e.g. Yahoo's "Mozilla/4.0"), which would create a lot of processing overhead, as ALL browsers send that. Doh! (More notes within that file.) You can set this to "true" (no quotes), in which case, all specified bots are simply allowd to bypass the hammer. You can also set it to an integer, e.g.. $anti_hammer['allow_bots'] = 50; ..that integer representing the hammer_time that will apply to the specified clients. "50" would enable 2 hits-per-second spidering, but nothing faster, which is half the normal hammer_time of One Second (hammer_time=100). */ $anti_hammer['allow_bots'] = false; /* The following two preferences control Anti-Hammer's built-in Client session Garbage collection routines.. */ /* Garbage Collection Limit [default: $anti_hammer['GC_limit'] = 10000;] To prevent your server's hard drive filling up with stale client sessions, we run a periodic garbage collection routine to sweep up the old files. How periodically, is up to you. By default, Anti-Hammer will check for garbage every 10,000 hits. I'm thinking this would be around a 2-daily hit rate for a small site (@ 5000 hits per day). Obviously, you can chage this number to anything you like, depending on how busy your site is, and how much space you have on the disks. If you don't want Anti-Hammer to clean up its garbage, set this to 0. Remember to ensure that this limit falls well outside your longest ban time, probably at least 2x that. */ $anti_hammer['GC_limit'] = 10000; /* GarbAge! [default: $anti_hammer['GC_age'] = 24;] How old, in hours, is considered "stale"? Any ID files older than this will be swept away (deleted). */ $anti_hammer['GC_age'] = 24; /* NOTE: The previous two preferences have no effect if you set the following preference ('use_php_sessions') to true. They are only for AntiHammer's built-in client session files. */ /* Use php sessions.. [default: $anti_hammer['use_php_sessions'] = false;] You would think it might be a nice idea to detect if the client has cookies enabled, and if so, use php sessions, only falling-back to some other method when they have not. However, it is not possible to detect whether or not a client has cookies enabled, with a single request. You need Two. Clearly, that isn't a lot of use for a protection mechanism designed to operate before they have even had one. So you gotta choose, now.. By default, anti-hammer will use its own session mechanism, writing client- unique data to files in a directory of your choosing, irrespective of their ability to accept cookies. As it is an independant system, it in no way interferes with any session magic you may have running on your site, and in most scenarios is just as fast as php's own session handling. However, you may wish to use that, instead; particularly if you have millions of hits a day, and your web server stores the php sessions in a some uberfast /tmp space you can't otherwise get to, where the difference might be worth it. Or if in-website space is extremely limited. At any rate, you have a choice. NOTE: if you enable this, you will ALWAYS start a php session with each request. This usually presents no problems, but you and your server may know better. Testing is always advised! I ran it this way for may months on corz.org, with no issues whatsoever, and I use php sessions all over the site. If you use proper names in your session, everything should work fine. Also NOTE: With this enabled, if the client/spider/script kiddie/etc. has cookies disabled in their web browser, they bypass anti-hammer protection! This is why, by default, Anti-Hammer uses its own session mechanism. There should be no performance concerns; Anti-Hammer writes the data in the same way as a php session - it's a simple serialized array in a flat file. */ $anti_hammer['use_php_sessions'] = false; /* :end prefs: */ // let's go.. // $killpage = false; $gentime = explode(' ', microtime()); $anti_hammer['now_time'] = $gentime[1].substr($gentime[0], 6, -2); // 1/100th of a second accuracy! settype($anti_hammer['now_time'], "double"); // scientifically tested! $anti_hammer['final_time'] = 0; // will be used to set the retry header on killed page (503) // Collect all usable client data.. $anti_hammer['remote_ip'] = $_SERVER['REMOTE_ADDR']; $anti_hammer['user_agent'] = @$_SERVER['HTTP_USER_AGENT']; $anti_hammer['referrer'] = @$_SERVER['HTTP_REFERER']; $anti_hammer['request'] = $_SERVER['REQUEST_URI']; $anti_hammer['user_accept'] = @$_SERVER['HTTP_ACCEPT']; $anti_hammer['user_charset'] = @$_SERVER['HTTP_ACCEPT_CHARSET']; $anti_hammer['user_encoding'] = @$_SERVER['HTTP_ACCEPT_ENCODING']; $anti_hammer['user_language'] = @$_SERVER['HTTP_ACCEPT_LANGUAGE']; // Admin Bypass.. // Is this the admin user? let's see.. if (stristr($anti_hammer['user_agent'], $anti_hammer['admin_agent_string'])) { return; } // local server access (for readfile() requests.. // (and as a potential catch-all for user pref errors!)) if ($anti_hammer['remote_ip'] == $_SERVER['SERVER_ADDR']) { return; }/* A note about readfile().. If you use readfile() to include resources on your pages, remember, those requests will come in right after the first, and as they are technically brand new hits, they count towards the hammer. Use of include() is preferred. However, the code right above this notice should prevent any issues. If it does /not/, and include() isn't working, you might want to hack in the actual IP Address of the local server. See my debug-report.zip for a way to easily get this sort of information in your browser. NOTE: If you are having difficulty include()ing URI resource in your pages, remember you need to enable BOTH php allow_url_* flags (this is the .htaccess version of those two switches..) php_flag allow_url_fopen on php_flag allow_url_include on */ // skip protection for known bots and spiders.. // // okay, this is some cute code! simple, but effective. // we load an ini file of user-agent=ip-mask-file pairs, and check our client's // user agent string for a match (at must match the beginning of the string // exactly). If there is a match, we load the associated IP Mask file, and // run through the IP/masks, again looking for a perfect match at the start of // the two strings. Commented lines are no problem. We use strpos() for both // tests, so it's nice and fast, and the IP test covers our comments, too! // // having said (coded) all this, you gotta ask yourself, why are they hammering? // Surely it would be better get them to slow down, instead! $IP_file = ''; $anti_hammer['ini_file'] = $anti_hammer['info_path'].'/exemptions/exemptions.ini'; if ($anti_hammer['allow_bots']) { $bot_agent_array = read_bots_iniFREE($anti_hammer['ini_file']); if (is_array($bot_agent_array)) { foreach ($bot_agent_array as $bot_agent_string => $IP_file) { if ($bot_agent_string and strpos($anti_hammer['user_agent'], $bot_agent_string) === 0) { break; } } if ($IP_file) { $ip_array = file($anti_hammer['info_path'].'/exemptions/'.$IP_file); } if (is_array($ip_array)) { foreach($ip_array as $bot_ip) { if (@strpos($anti_hammer['remote_ip'], trim($bot_ip)) === 0) { if ($anti_hammer['allow_bots'] > 1) { $anti_hammer['hammer_time'] = $anti_hammer['allow_bots']; } else { return; } } } } } } // User prefs.. // Get user values into usable arrays, do some error-checking. // trigger thresholds.. if (!stristr($anti_hammer['trigger_levels'], ',') or (str_word_count($anti_hammer['trigger_levels'], 0, "0123456789") != 4)) { $anti_hammer['trigger_levels'] = '5,10,20,30'; } // A neat way to create a array from numeric prefs.. $anti_hammer['trigger_levels'] = str_word_count($anti_hammer['trigger_levels'], 1, "0123456789"); // Get user penalty times into correct values.. if (!stristr($anti_hammer['waiting_times'], ',') or (str_word_count($anti_hammer['waiting_times'], 0, "0123456789") != 4)) { $anti_hammer['waiting_times'] = '3,5,10,20'; } $anti_hammer['waiting_times'] = str_word_count($anti_hammer['waiting_times'], 1, "0123456789"); // file types to protect.. if (!$anti_hammer['types']) { return; } // no types specified, forget it! $anti_hammer['types'] = explode(',', $anti_hammer['types']); // generated types to skip.. $anti_hammer['gen_types'] = explode(',', $anti_hammer['gen_types']); // ignored locations.. $anti_hammer['skip'] = explode(',', $anti_hammer['skip']); // run through ignored locations and if matched, return immediately.. // foreach($anti_hammer['skip'] as $nogo) { if (stristr($anti_hammer['request'], trim($nogo))) { return; } } // Anti-Hammer only for php files, not generated css, etc.. // $ah_type_ok = false; foreach($anti_hammer['types'] as $ah_type) { if (!$ah_type) { continue; } // @ to avoid strict php5 errors if (@end(explode('.', $_SERVER['SCRIPT_FILENAME'])) == trim($ah_type)) { $ah_type_ok = true; } //2do.. could make this code more efficient, for those using MANY types. } if ($ah_type_ok /* still! */ == false) { return; } // skip protection for selected generated types.. // if (in_array(@end(explode('.', $_SERVER['REQUEST_URI'])), $anti_hammer['gen_types'])) { return; } /* okay, let's do it.. */ // read session data.. $session = array(); if ($anti_hammer['use_php_sessions']) { // Regular php session.. session_start(); $session = $_SESSION['anti_hammer']; } else { // Anti-Hammer's built-in session mechanism.. // Create a unique Client ID for this client.. // we simply MD5 all the browser data concatenated together (and blanks are not a problem).. $anti_hammer['client_id'] = md5($anti_hammer['user_agent']. $anti_hammer['user_accept']. $anti_hammer['user_language']. $anti_hammer['user_encoding']. $anti_hammer['user_charset']. $anti_hammer['remote_ip']); $fake_sess_file = $anti_hammer['info_path'].'/'.$anti_hammer['ID_prefix'].$anti_hammer['client_id']; if (file_exists($fake_sess_file)) { $session = read_fake_sessionFREE($fake_sess_file); } } /* Useful use of a "cat".. It seems to me that I unwittingly created a system whereby the less information a client is wiling to give, the more likely they are to be banned. I say "seems", because we create an md5 of this information, so the actual likelyhood of colliding session ID's is astronomically low. However, I like the *principle* of the thing. */ // Calculate the Hammer Rate.. // // How much time since their last request (in 100/th Second) $hammer_rate = $anti_hammer['now_time'] - @$session['start_time'] + 1; // CUT_OFF has already been set -- BYE NOW! if ($anti_hammer['cut_off'] and isset($session['cut_off'])) { // Their ban has elapsed (but GC has not swept up their session).. if ($hammer_rate > ($anti_hammer['ban_time']*60*60*100)) { // 8640000 = 24 hours (in 100th/second) $session['start_time'] = $anti_hammer['now_time'] - 1; $hammer_rate = $anti_hammer['hammer_time']; $session['hammer'] = $anti_hammer['trigger_levels'][0]-1; // repeat-offenders do not get to start from 0! unset($session['cut_off']); // do not return here - we still need to write the updated session data. } else { die(); } } // okay, still here.. // Start with Garbage Collection.. if (!$anti_hammer['use_php_sessions']) { CollectGarbageFREE($anti_hammer['info_path'].'/Counter', $anti_hammer['GC_limit']); } // Anti-Hammer Protection has been activated! if ($hammer_rate < $anti_hammer['hammer_time']) { $retry_str = 'a few '; @$session['hammer'] += 1; if ($session['hammer'] > ($anti_hammer['trigger_levels'][0]-1)) { // cut-off.. if ($anti_hammer['cut_off'] and $session['hammer'] > $anti_hammer['cut_off']) { $anti_hammer['kill_msg'] = $anti_hammer['cut_off_msg']; $session['cut_off'] = true; } if ($anti_hammer['cut_off'] and $session['hammer'] == $anti_hammer['cut_off']) { $anti_hammer['kill_msg'] = '

THIS IS YOUR LAST WARNING!

'.$anti_hammer['kill_msg']; } // rolling ban time, increments with each hammer.. if ($anti_hammer['rolling_trigger']) { $session['start_time'] = $anti_hammer['now_time'] + (($session['hammer']*100)-1); $retry_str = ah_int2engFREE($session['hammer']); } else { // predefined ban levels.. these are more effective, as they shock the user with increasing jumps! if (($session['hammer'] > $anti_hammer['trigger_levels'][0]) and ($session['hammer'] <= $anti_hammer['trigger_levels'][1])) { // we simply nudge their start time forward by *this* many seconds (into the future!).. $session['start_time'] = $anti_hammer['now_time'] + (($anti_hammer['waiting_times'][0]*100)-1); // 299 = Three second penalty. $retry_str = ah_int2engFREE($anti_hammer['waiting_times'][0]); } elseif (($session['hammer'] > $anti_hammer['trigger_levels'][1]) and ($session['hammer'] <= $anti_hammer['trigger_levels'][2])) { $session['start_time'] = $anti_hammer['now_time'] + (($anti_hammer['waiting_times'][1]*100)-1); // Five second penalty! (by default) $retry_str = ah_int2engFREE($anti_hammer['waiting_times'][1]); } elseif (($session['hammer'] >= $anti_hammer['trigger_levels'][2]) and ($session['hammer'] <= $anti_hammer['trigger_levels'][3])) { $session['start_time'] = $anti_hammer['now_time'] + (($anti_hammer['waiting_times'][2]*100)-1); // Ten second penalty! (etc.) $retry_str = ah_int2engFREE($anti_hammer['waiting_times'][2]); } elseif ($session['hammer'] >= $anti_hammer['trigger_levels'][3]) { $session['start_time'] = $anti_hammer['now_time'] + (($anti_hammer['waiting_times'][3]*100)-1); // Twenty second penalty! $retry_str = ah_int2engFREE($anti_hammer['waiting_times'][3]); } } $killpage = true; } } else { $session['start_time'] = $anti_hammer['now_time']; } // write client session data.. SetHammerFREE(); if ($killpage) { $km = ''.$anti_hammer['page_title'].''.$anti_hammer['kill_msg']; if (!isset($session['cut_off'])) { $km .= ' You must wait '.$retry_str.'seconds before trying again.

If you believe this is in error, please mail '.$anti_hammer['webmaster'].' about it!
<'.$anti_hammer['error_mail'].'>
Get Anti-Hammer protection for your own site!'; } kill_pageFREE($km); } if (function_exists('debug')) { debug('out'); } //:debug: //2do.. // include auto-ban.php ? hmm. /* fin */ // You're outta here! function kill_pageFREE($msg) { global $anti_hammer; $r_host = ''; if ($anti_hammer['lookup_failures']) { $r_host = gethostbyaddr($anti_hammer['remote_ip']).' '; } if (file_exists(dirname($anti_hammer['log']))) { $this_hit = '' ."page: "."\t".$anti_hammer['request']."\n" ."time: "."\t".date('Y.m.d h:i:s A')."\t".'ID: '.$anti_hammer['client_id']."\t"."x ".$GLOBALS['session']['hammer']."\n" ."visitor:"."\t".$r_host.'['.$anti_hammer['remote_ip'].']'."\t"."(".$anti_hammer['user_agent'].")"."\n" ."accepts:"."\t".$anti_hammer['user_accept']."\n" ."referer:"."\t".$anti_hammer['referrer']."\n" ; add_dataFREE($anti_hammer['log'], $this_hit."\n"); } header('Content-Type: text/html; charset=utf-8'); // Old IE probably still won't play ball, though. header('HTTP/1.1 503 Service Temporarily Unavailable'); // For CGI/*suexec use.. if (substr(php_sapi_name(), 0, 3) == 'cgi') { header('Status: 503 Service Temporarily Unavailable'); } header('Retry-After: '.($anti_hammer['final_time']+1)); // the calculation needs to be enclosed in braces to work. die($msg); } // write the updated hammer info to the fake/session file.. function SetHammerFREE() { if ($GLOBALS['anti_hammer']['use_php_sessions']) { $_SESSION['anti_hammer']['start_time'] = $GLOBALS['session']['start_time']; $_SESSION['anti_hammer']['hammer'] = $GLOBALS['session']['hammer']; $_SESSION['anti_hammer']['cut_off'] = $GLOBALS['session']['cut_off']; } else { write_fake_sessionFREEFREE($GLOBALS['fake_sess_file'], $GLOBALS['session']); } } /* Append data to a file. Pass true as the 3rd paramater to wipe the file. */ function add_dataFREE($file, $data, $wipe=false) { // if it's not there, try to create it.. if (!file_exists($file)) $fp = fopen($file, 'wb'); $flag = 'ab'; if ($wipe) { $flag = 'wb'; } if (is_writable($file)) { $fp = fopen($file, $flag); $lock = flock($fp, LOCK_EX); if ($lock) { fwrite($fp, $data); flock ($fp, LOCK_UN); } else { $GLOBALS['errors']['add_dataFREE'] = "couldn't lock $file"; } fclose($fp); } else { $GLOBALS['errors']['add_dataFREE'] = "can't write to $file"; } } // read serialized array data from a file, and return as an array.. function read_fake_sessionFREE($no_cookie_file) { if (file_exists($no_cookie_file)) { $file_handle = fopen($no_cookie_file, 'rb'); $file_contents = @fread($file_handle, filesize($no_cookie_file)); fclose($file_handle); } else { return false; } $file_contents = unserialize($file_contents); if (is_array($file_contents)) { return $file_contents; } } // serialize an array and write the string data to a file.. function write_fake_sessionFREEFREE($no_cookie_file, $array) { $data = serialize($array); if (empty($data)) { return; } $fp = @fopen($no_cookie_file, 'wb'); if ($fp) { $lock = flock($fp, LOCK_EX); if ($lock) { fwrite($fp, $data); flock ($fp, LOCK_UN); } fclose($fp); clearstatcache(); return (1); } } /* CollectGarbageFREE You couldtransplant this into another web app fairly easily. Useful. */ function CollectGarbageFREE($count_file, $limit) { if ($limit === 0) { return; } if (increment_hit_counterFREE($count_file) >= $limit) { $file_list = array(); if ($the_dir = @opendir(dirname($count_file))) { while (false != ($file = readdir($the_dir))) { if ((ord($file) != 46) and strpos($file, $GLOBALS['anti_hammer']['ID_prefix']) === 0) { $file_path = dirname($count_file).'/'.$file; if (filemtime($file_path) < (time() - $GLOBALS['anti_hammer']['GC_age']*60*60)) { unlink($file_path); } } } } increment_hit_counterFREE($count_file, 0, 1); // reset the counter } }//2do.. // Run this in another thread? Or maybe a simple http request, perhaps // with $_GET, to flip Ant-Hammer to GC mode in the Background - this task // could be done after the request is already sent, even simultaneously; // there may be a *lot* of files in this directory. // // Having said that, it's *very* fast, and only runs once per 10,000 or so // ($limit) hits. // /* increment a counter() from my "file-tools.php", available elsewhere. */ function increment_hit_counterFREE($count_file, $report_only=false, $reset=false) { $count = false; if (!file_exists($count_file) or $reset) { $file_pointer = fopen($count_file, 'wb'); fwrite ($file_pointer, '0'); fclose ($file_pointer); } // now the counter.. if (file_exists($count_file)) { // read in the old score.. $count = trim(file_get_contents($count_file)); if ($report_only) { return $count; } if (!$count) { $count = 0; } $count++; // write out new score.. if (is_writable($count_file)) { $file_pointer = fopen($count_file, 'wb+'); $lock = flock($file_pointer, LOCK_EX); if ($lock) { fwrite($file_pointer, $count); flock ($file_pointer, LOCK_UN); } fclose($file_pointer); clearstatcache(); } } return $count; } /* Integers To English Words. Converts 1145432 into.. "one million, one hundred and forty five thousand, four hundred and thirty two" Fairly groovy. ;o) The regular version is in my "text-func.php", with some other stuff. */ function ah_int2engFREE($number) { $output = ''; if ($number < 1) $number = 1; $GLOBALS['anti_hammer']['final_time'] = $number; $units = array(' ', 'one ', 'two ', 'three ', 'four ', 'five ', 'six ', 'seven ', 'eight ', 'nine '); $teens = array('ten ', 'eleven ', 'twelve ', 'thirteen ', 'fourteen ', 'fifteen ', 'sixteen ', 'seventeen ', 'eighteen ', 'nineteen '); $tenners = array('', '', 'twenty ', 'thirty ', 'fourty ', 'fifty ', 'sixty ', 'seventy ', 'eighty ', 'ninety '); $lint = strlen($number); if ($lint > 2) $bigger = true; for ($x = $lint ; $x >= 1 ; $x--) { $last = substr($output, -5, 4); $digit = substr($number, 0, 1); $number = substr($number, 1); if ($x % 3 == 2) { if ($digit == 1) { // 10-19.. $digit = substr($number, 0, 1); $number = substr($number, 1); $x--; if ($last == 'sand') { $output .= 'and '; } $output .= $teens[$digit]; } else { // 20-99.. if (($last == 'sand') ) { $output .= 'and '; } $output .= $tenners[$digit]; } } else { if (($x % 3 != 1) and ($digit > 0) and (!empty($output))) { $output .= ', '; } $output .= $units[$digit]; } if ((strlen($number) % 3) == 0) { $bignum = ah_bignumbersFREE(strlen($number) / 3); if (($last == 'dred') and ($bignum != 'thousand')) { $output .= 'and ';} $output .= $bignum; } if ((strlen($number) % 3) == 2 and $digit > 0) { $output .= 'hundred and '; } } // clean up the output.. $output = str_replace(' ', ' ', $output); $output = str_replace('red and thou', 'red thou', $output); $output = str_replace('red and mill', 'red mill', $output); $output = str_replace('lion thousand', 'lion ', $output); if (substr($output, -5) == ' and ') { $output = substr($output, 0, -5).' '; } return $output; } /* it just looks better, okay! */ function ah_bignumbersFREE($test) { switch ($test) { case 0: $test = ""; break; case 1: $test = "thousand"; break; case 2: $test = "million"; break; case 3: $test = "trillion"; // <- that's a lot of comments! break; } return $test; } /* function read_ini() [from my 'ini-tools.php'] pull the data from the ini file and return as an array Usage: array (string {path to file}) returns false on failure. */ function read_bots_iniFREE($data_file) { $ini_array = array(); if (is_readable($data_file)) { $file = file($data_file); foreach($file as $conf) { // if first real character isn't '#' or ';' and there is a '=' in the line.. if ( (substr(trim($conf),0,1) != '#') and (substr(trim($conf),0,1) != ';') and (substr_count($conf,'=') >= 1) ) { $eq = strpos($conf, '='); $ini_array[trim(substr($conf,0,$eq))] = trim(substr($conf, $eq + 1)); } } unset($file); return $ini_array; } else { $GLOBALS['errors']['read_bots_iniFREE'] = "ini file: $file does not exist."; return false; } } /* changes: 0.9.3 + You now have the option to perfomr a quick DNS lookup of the IP Address of bad clients, and have this added to the logging. This was already enabled, you now have the option to *disable* it, if required. + Anti-Hammer now send a valid "Retry-After" header, which is set to the client's current hammer delay + 1 second. + Added a link to the Anti-Hammer page, should lessen the wtf-factor. 0.9.2 + You can now choose whether to allow your specified clients (aka "exemptions") to either completely bypass anti-hammer (current exemption method).. $anti_hammer['allow_bots'] = true; Or else specify an integer, representing a hammer_time, in 1/100th Second, which will apply to *only* these clients.. $anti_hammer['allow_bots'] = 50; This setting would enable your specified clients to hammer the site at a rate of two hits-per-second, but no faster. Effectively, we now have two hammer rates, one for known good clients, and one for everyone else. 0.9 + Good bots & spiders can now be allowed to bypass the hammer. This is achieved through the use of standard spider IP lists, as published here.. http://www.iplists.com/ along with a simple ini file, detailing which user-agent links to which IP list. A working ini, and more details, will be included in the preference section (above), as well as the release. 0.8.* + Anti-Hammer now sends a proper 503 (service temporarily unavailable) message, rather than a 200 OK message. This will be useful in situations where valid bots are temporarily hammering, and is more correct in this scenario. The reource *will* be back, if they cut out the crazy hammering! If you are running under cgi/*suexec (non-module), the extra required header is automatically sent. In use, this causes many bots to back-off immediately. Excellent! ~ Improved the ban resetting (which needs to work independantly of the Garbage collection mechanism). After the ban time, the client's cut-off is wiped, and their start time set to *now*, just like a new client, however, their hammer_count is set to one hammer below the first trigger level. In other words, a single hammer gets them the NO Hammer! page; and to the final page quicker than new clients. Even if you use rolling triggers, Anti_hammer will still use the first ban level to calculate this number, so set that to whatever you want. ~ ban_times and ban_levels have been renamed to waiting_times and trigger_levels, to avoid confusion with the ban_time (for the new total cut-off). These also make more sense, as they are not bans, simply delays. 0.7.* + Added rolling ban times. Rather than have set limits which the client can cross, this simply increments the ban time with each and every hammer attempt. 1-2-3-4-5-6-7.. etc. cut-off still functions as before for each system (rolling or preset levels). This was, in fact, the original system, which I replaced with the level presets early on, but it's kinda fun, and the code is simple. ~ Removed the file-tools.php include statement, and put the functions directly into here (slightly renamed). I figure anyone smart enough to be including my file-tools, will be smart enough to figure out how to put that back, if required. + More things are configurable, like the page title. Why not! 0.6.* + Added capability to work with clients who do not accept, or have chosen not to accept (read: disabled) cookies. Basically, we write a "fake" session. The fake session uses a serialized array in a flat file, just like regular php sessions, and is created before they even get receive their first page. From that point on, they are known (by Anti-Hammer) by this ID. The name of the client's session ID file is the session ID itself; an MD5 of all the known usable client data concatenated together. ~ php session usage is still available as an option, if required. + Added Garbage Collection for the fake session files. Both how often this happens (every 'so-many' requests), and how old is considered "stale", are configurable. + Ban time is now configurable (in hours). Remember to ensure that Garbage Collection isn't happening before this time. + Added penultimate message for cut-off. You get one *final* warning! ~ Cleaned-up the code regarding sessions. We now make a clean break, converting whichever type of session data into a local array, and then work with that. At the end, we write the pertinent data back to whichever type of session is being utilized (built-in or php). 0.5.* + You can now configure a cut-off point. When the number of violations reaches this number, their pages simply die. This is disabled by default. This point is, of course, configurable. (actually, I got called away in the middle of this, so I'll need to check how far I got!) 0.4.* + Added user preferences for lots of the settings, voilation levels, times, etc. Added error checking for these, so they should be fairly foolproof (good movie, by the way, "Foolproof", 2003). 0.3.* + Added configurable protection skipping for certain file types (usually associated files and such). This replaces a nasty hack that lived at the top of the script. + Added skipping for generated images, too (GD images, etc.). This can also be used to skip other tpyes. See the preferences for more details. + Added configurable messages. I'll likely put this out eventually, it's kinda useful. 0.2.* + Added ignored areas, for chat scripts and such. places where either hammering is allowed, or is dealt with by the local script. */ ?>