1<?php 2/** 3 * Cf licence informations in README 4 */ 5 6// must be run within Dokuwiki 7if(!defined('DOKU_INC')) 8 die('yeurk!'); 9 10if(!defined('DOKU_PLUGIN')) 11 define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 12 13require_once(DOKU_PLUGIN . 'action.php'); 14 15 16class action_plugin_tokenbucketauth extends DokuWiki_Action_Plugin 17{ 18 /** Lock file */ 19 protected $lockfh; 20 21 /** Array of IPs=>[visited_time1,visited_time2,...] */ 22 protected $users_tracker; 23 24 /** Array of blocked IP addresses => when_blocked_timestamp */ 25 protected $blocked; 26 27 /** 28 * Constructor, initialize class' variables 29 */ 30 function __construct() 31 { 32 $this->lockfh = null; 33 $this->users_tracker = null; 34 $this->blocked = null; 35 } 36 37 /** 38 * Register plugin's handlers 39 */ 40 public function register(Doku_Event_Handler $controller) 41 { 42 $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'disable_login', array()); 43 $controller->register_hook('AUTH_LOGIN_CHECK', 'AFTER', $this, 'register_login_fail', array()); 44 } 45 46 /** 47 * Look if we have to disable login for this particuliar IP address 48 */ 49 public function disable_login(&$event, $param) 50 { 51 global $ACT, $conf; 52 53 if($ACT === 'login' || $this->getConf('tba_block_whole_wiki')) 54 { 55 $this->lock(); 56 57 $content = ''; 58 $banned_f = $conf['cachedir'] . '/' . $this->getConf('tba_block_file'); 59 60 /* Get the users which are blocked */ 61 if(is_readable($banned_f)) 62 $content = file_get_contents($banned_f); 63 64 if(empty($content)) 65 $this->blocked = array(); 66 else 67 $this->blocked = @unserialize($content); 68 69 /* Deal with the case of unserialize() failing */ 70 if($this->blocked === false) 71 $this->blocked = array(); 72 73 74 $content = ''; 75 $track_f = $conf['cachedir'] . '/' . $this->getConf('tba_iptime_file'); 76 77 /* Get the previous, the registered array of visits */ 78 if(is_readable($track_f)) 79 $content = file_get_contents($track_f); 80 81 /* Initialize from the file or not */ 82 if(empty($content)) 83 $this->users_tracker = array(); 84 else 85 $this->users_tracker = @unserialize($content); 86 87 /* Deal with the case of unserialize() failing */ 88 if($this->users_tracker === false) 89 $this->users_tracker = array(); 90 91 92 $ip = $_SERVER['REMOTE_ADDR']; 93 $time = time(); 94 95 /* If the user come from a whitelisted address */ 96 if(in_array($ip, preg_split('/[\s,]+/', $this->getConf('tba_whitelist'), null, PREG_SPLIT_NO_EMPTY))) 97 { 98 $this->unlock(); 99 return; 100 } 101 102 /* If the user is already blocked */ 103 if(array_key_exists($ip, $this->blocked)) 104 { 105 if($this->blocked[$ip] + $this->getConf('tba_block_time') > $time) 106 { 107 /* If the time isn't elapsed yet */ 108 $this->_do_disable_login($ip, $this->blocked[$ip]); 109 $this->unlock(); 110 return; 111 } 112 else 113 { 114 /* If the user is no longer banned */ 115 unset($this->blocked[$ip]); 116 } 117 } 118 119 $ts = $this->users_tracker[$ip]; 120 $time = $time - $this->getConf('tba_mean_time'); 121 $max = $this->getConf('tba_nb_attempt'); 122 $cpt = 0; 123 124 $i = 0; 125 $to_unset = array(); 126 127 /* Check whether to block or not the IP */ 128 if(!is_null($ts)) 129 { 130 foreach($ts as $onets) 131 { 132 if($time < $onets) 133 $cpt++; 134 else 135 $to_unset[] = $i; 136 137 $i++; 138 } 139 } 140 141 /* Clean old timestamps */ 142 foreach($to_unset as $i) 143 unset($ts[$i]); 144 145 /* Update the tracker array */ 146 $this->users_tracker[$ip] = $ts; 147 148 /* If there's more attempts than authorized, block the IP */ 149 if($cpt >= $max) 150 $this->blocked[$ip] = $time + $this->getConf('tba_mean_time'); 151 152 /* Save the timestamps file */ 153 io_saveFile($track_f, serialize($this->users_tracker)); 154 155 /* Save the blocked-IP file */ 156 io_saveFile($banned_f, serialize($this->blocked)); 157 158 /* Don't forget to unlock */ 159 $this->unlock(); 160 161 if(array_key_exists($ip, $this->blocked)) 162 $this->_do_disable_login($ip, $this->blocked[$ip], true); 163 } 164 } 165 166 /** 167 * Register failed attempts to login 168 */ 169 public function register_login_fail(&$event, $param) 170 { 171 global $ACT, $conf; 172 173 if($ACT === 'login' && !empty($event->data['user']) && !isset($_SESSION['REMOTE_USER'])) 174 { 175 $this->lock(); 176 177 $ip = $_SERVER['REMOTE_ADDR']; 178 $time = time(); 179 180 /* Add an entry for this visit */ 181 if(empty($this->users_tracker[$ip])) 182 $this->users_tracker[$ip] = array($time); 183 else 184 $this->users_tracker[$ip][] = $time; 185 186 /* Save the file */ 187 io_saveFile($conf['cachedir'] . '/' . $this->getConf('tba_iptime_file'), serialize($this->users_tracker)); 188 189 /* Don't forget to unlock */ 190 $this->unlock(); 191 } 192 } 193 194 /** 195 * Use a lock file not to update files concurrently 196 */ 197 protected function lock() 198 { 199 global $conf; 200 201 $lockf = $conf['cachedir'] . '/' . $this->getConf('tba_lockfile'); 202 203 $this->lockfh = fopen($lockf, 'w', false); 204 205 if($this->lockfh === false) 206 return false; 207 208 if(flock($this->lockfh, LOCK_EX) === false) 209 { 210 fclose($this->lockfh); 211 $this->lockfh = null; 212 return false; 213 } 214 215 return true; 216 } 217 218 /** 219 * Unlock previously locked file 220 */ 221 protected function unlock() 222 { 223 if(!is_null($this->lockfh)) 224 { 225 flock($this->lockfh, LOCK_UN); 226 fclose($this->lockfh); 227 $this->lockfh = null; 228 } 229 } 230 231 /** 232 * Change the login action to the show one 233 * 234 * @param $ip The blocked IP 235 * @param $block_ts The timestamp when blocking happened 236 * @param $new False if it's not a new IP which is banned, true otherwise 237 */ 238 protected function _do_disable_login($ip, $block_ts, $new = false) 239 { 240 global $conf; 241 242 $title = $this->getLang('page title'); 243 $text = $this->locale_xhtml('banned'); 244 $text .= sprintf('<p>'.$this->getLang('page content').'</p>', $ip, strftime($conf['dformat'], $block_ts)); 245 246 header("HTTP/1.0 403 Forbidden"); 247 echo<<<EOT 248<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 249<html> 250<head><title>$title</title></head> 251<body style="font-family: Arial, sans-serif"> 252 <div style="width:60%; margin: auto; background-color: #fcc; 253 border: 1px solid #faa; padding: 0.5em 1em;"> 254 $text 255 </div> 256</body> 257</html> 258EOT; 259 260 // Send email for a new banned IP address 261 $to_be_notified = $this->getConf('tba_send_mail'); 262 if(!empty($to_be_notified) && $new) 263 { 264 $to_be_notified = str_replace(' ', '', $to_be_notified); 265 $to_be_notifieds = explode(',', $to_be_notified); 266 267 // Prepare fields 268 $subject = sprintf($this->getLang('mailsubject'), $conf['title']); 269 $body = $this->locale_xhtml('mailbody'); 270 $from = $conf['mailfrom']; 271 272 // Do some replacements 273 $body = str_replace('@IP@', $ip, $body); 274 $body = str_replace('@DOKUWIKIURL@', DOKU_URL, $body); 275 276 // Finally send mail 277 foreach($to_be_notifieds as $email) { 278 mail_send($email, $subject, $body, $from); 279 } 280 } 281 282 exit; 283 } 284} 285 286