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