1use Irssi 20020300;
2use 5.6.0;
3use strict;
4use Socket;
5use POSIX;
6
7use vars qw($VERSION %IRSSI %HELP);
8$HELP{ban} = "
9BAN [channel] [-normal|-host|-user|-domain|-crap|-ip|-class -before \"command\"|-after \"command\" nicks|masks] ...
10
11Bans the specified nicks or userhost masks.
12
13If nick is given as parameter, the ban type is used to generate the ban mask.
14/SET banpl_type specified the default ban type. Ban type is one of the following:
15
16    normal - *!fahren\@*.ds14.agh.edu.pl
17    host   - *!*\@plus.ds14.agh.edu.pl
18    user   - *!fahren@*
19    domain - *!*\@*.agh.edu.pl
20    crap   - *?fah???\@?l??.?s??.??h.???.?l
21    ip     - *!fahren\@149.156.124.*
22    class  - *!*\@149.156.124.*
23
24Only one flag can be specified for a given nick.
25Script removes any conflicting bans before banning.
26
27You can specify command that will be executed before or after
28banning nick/mask using -before or -after.
29
30Examples:
31      /BAN fahren       - Bans the nick 'fahren'
32      /BAN -ip fahren   - Bans the ip of nick 'fahren'
33      /BAN fahren -ip fantazja -crap nerhaf -normal ff
34                        - Bans 'fahren' (using banpl_type set), ip of 'fantazja',
35                          host with crap mask of 'nerhaf' and 'ff' with normal bantype.
36      /BAN *!*fahren@*  - Bans '*!*fahren@*'
37      /BAN #chan -after \"KICK #chan fahren :reason\" fahren
38                        - Bans and kicks 'fahren' from channel '#chan' with reason 'reason'.
39
40      /ALIAS ipkb ban \$C -after \"KICK \$C \$0 \$1-\" -ip \$0
41                        - Adds command /ipkb <nick> [reason] which kicks 'nick' and bans it's ip address.
42";
43$VERSION = "1.4d";
44%IRSSI = (
45	authors         => "Maciek \'fahren\' Freudenheim",
46	contact         => "fahren\@bochnia.pl",
47	name            => "ban",
48	description     => "/BAN [channel] [-normal|-host|-user|-domain|-crap|-ip|-class -before|-after \"cmd\" nick|mask] ... - bans several nicks/masks on channel, removes any conflicting bans before banning",
49	license         => "GNU GPLv2 or later",
50	changed         => "Tue Nov 19 18:11:09 CET 2002"
51);
52
53# Changelog:
54# 1.4d
55# - getting user@host of someone who isn't on channel was broken
56# 1.4c
57# - fixed banning of unresolved hosts
58# - fixed problem with /ban unexisting_nick other_nick
59# 1.4b
60# - doesn't require op to see banlist :)
61# 1.4
62# - few fixes
63# - using banpl_type instead of irssi's builtin ban_type
64# - changed -normal behaviour
65# 1.3
66# - :( fixed crap banning (yes, i'm to stupid to code it)
67# 1.2
68# - queuing MODES for nicks that aren't on channel
69# 1.11
70# - fixed .. surprise! crap banning
71# - added use 5.6.0
72# 1.1
73# - fixed banning 10-char long idents
74# - fixed crap banning (once more)
75# - added -before and -after [command] for executing command before/after setting ban
76# 1.0
77# - -o+b if banning opped nick
78# - fixed -crap banning
79# - always banning with *!*ident@ (instead of *!ident@)
80# - can take channel as first argument now
81# - displays error if it couldn't resolve host for -ip / -class ban
82# - groups all modes and sends them at once, ie. -bbo\n+b-o+b
83# - gets user@host via USERHOST if requested ban of someone who is not on channel
84# - added help
85
86my (%ftag, $parent, %modes, %modes_args, %b, @userhosts);
87
88sub cmd_ban {
89        my ($args, $server, $winit) = @_;
90
91	my $chan;
92	my ($channel) = $args =~ /^([^\s]+)/;
93
94	if (($server->ischannel($channel))) {
95		$args =~ s/^[^\s]+\s?//;
96		return unless ($args);
97		unless (($chan = $server->channel_find($channel)) && $chan->{chanop}) {
98			Irssi::print("%R>>%n You are not on $channel or you are not opped.");
99			Irssi::signal_stop();
100			return;
101		}
102	} else {
103		return unless ($args);
104		unless ($winit && $winit->{type} eq "CHANNEL" && $winit->{chanop}) {
105			Irssi::print("%R>>%n You don't have active channel in that window or you are not opped.");
106			Irssi::signal_stop();
107			return;
108		}
109		$chan = $winit;
110		$channel = $chan->{name};
111	}
112
113	Irssi::signal_stop();
114
115	my $bantype = Irssi::settings_get_str("banpl_type");
116	my $max = $server->{max_modes_in_cmd};
117	my ($cmdwhat, $cmdwhen) = (0, 0);
118	$b{$channel} = 0;
119
120	# counts nicks/masks to ban, lame :|
121	for my $cmd (split("\"", $args)) {
122		($cmdwhen) and $cmdwhen = 0, next;
123		for (split(/ +/, $cmd)) {
124			next unless $_;
125			/^-(normal|host|user|domain|crap|ip|class)$/ and next;
126			/^-(before|after)$/ and $cmdwhen = 1, next;
127			$b{$channel}++;
128		}
129	}
130
131	for my $cmd (split("\"", $args)) {
132		($cmdwhen && !$cmdwhat) and $cmdwhat = $cmd, next;
133	for my $arg (split(/ +/, $cmd)) {
134		next unless $arg;
135		$arg =~ /^-(normal|host|user|domain|crap|ip|class)$/ and $bantype = $1, next;
136		$arg eq "-before" and $cmdwhen = 1, next;
137		$arg eq "-after" and $cmdwhen = 2, next;
138
139		if (index($arg, "@") == -1) {
140			my $n;
141			if ($n = $chan->nick_find($arg)) {
142				# nick is on channel
143
144				my ($user, $host) = split("@", $n->{host});
145
146				if ($bantype eq "ip" || $bantype eq "class") {
147					# requested ip ban, forking
148					my $pid = &ban_fork;
149					unless (defined $pid) {	# error
150						$cmdwhen = $cmdwhat = 0;
151						$b{$channel}--;
152						next;
153					} elsif ($pid) {	# parent
154						$cmdwhen = $cmdwhat = 0;
155						next;
156					}
157					my $ia = gethostbyname($host);
158					unless ($ia) {
159						print($parent "error $channel %R>>%n Couldn't resolve $host.\n");
160					} else {
161						print($parent "execute $server->{tag} $channel " . (($n->{op})? $arg : 0) . " " . make_ban($user, inet_ntoa($ia), $bantype) . " $cmdwhen $cmdwhat\n");
162					}
163					close $parent; POSIX::_exit(1);
164				}
165				ban_execute($chan, (($n->{op})? $arg : 0), make_ban($user, $host, $bantype), $max, $cmdwhen, $cmdwhat);
166			} else {
167				# nick is not on channel, trying to get addres via /userhost
168				$server->redirect_event('userhost', 1, $arg, 0, undef, {
169						'event 302' => 'redir ban userhost',
170						'' => 'event empty' } );
171				$server->send_raw("USERHOST :$arg");
172				my $uh = {
173					tag 	=> $server->{tag},
174					nick 	=> lc($arg),
175					channel => $channel,
176					chanhash => $chan,
177					bantype	=> $bantype,
178					cmdwhen	=> $cmdwhen,
179					cmdwhat	=> $cmdwhat
180				};
181				push @userhosts, $uh;
182			}
183		} else {
184			# specified mask
185			my $ban;
186			$ban = "*!" if (index($arg, "!") == -1);
187			$ban .= $arg;
188			ban_execute($chan, 0, $ban, $max, $cmdwhen, $cmdwhat);
189		}
190
191		$cmdwhen = $cmdwhat = 0;
192	}
193	}
194}
195
196sub push_mode ($$$$) {
197	my ($chan, $mode, $arg, $max) = @_;
198
199	my $channel = $chan->{name};
200	$modes{$channel} .= $mode;
201	$modes_args{$channel} .= "$arg ";
202
203	flush_mode($chan) if (length($modes{$channel}) >= ($max * 2));
204}
205
206sub flush_mode ($) {
207	my $chan = shift;
208
209	my $channel = $chan->{name};
210	return unless (defined $modes{$channel});
211#	Irssi::print("MODE $channel $modes{$channel} $modes_args{$channel}");
212	$chan->command("MODE $channel $modes{$channel} $modes_args{$channel}");
213	undef $modes{$channel}; undef $modes_args{$channel};
214}
215
216sub userhost_red {
217	my ($server, $data) = @_;
218	$data =~ s/^[^ ]* :?//;
219
220	my $uh = shift @userhosts;
221
222	unless ($data && $data =~ /^([^=\*]*)\*?=.(.*)@(.*)/ && lc($1) eq $uh->{nick}) {
223		Irssi::print("%R>>%n No such nickname: $uh->{nick}");
224		$b{$uh->{channel}}--;
225		flush_mode($uh->{chanhash}) unless ($b{$uh->{channel}});
226		return;
227	}
228
229	my ($user, $host) = (lc($2), lc($3));
230
231	if ($uh->{bantype} eq "ip" || $uh->{bantype} eq "class") {
232		# requested ip ;/
233		my $pid = &ban_fork;
234		unless (defined $pid) {	# error
235			$b{$uh->{channel}}--;
236			return;
237		} elsif ($pid) {	# parent
238			return;
239		}
240		my $ia = gethostbyname($host);
241		unless ($ia) {
242			print($parent "error " . $uh->{channel} . " %R>>%n Couldn't resolve $host.\n");
243		} else {
244			print($parent "execute " . $uh->{tag} . " " . $uh->{channel} . " 0 " . make_ban($user, inet_ntoa($ia), $uh->{bantype}) . " " . $uh->{cmdwhen} . " " . $uh->{cmdwhat} . "\n");
245		}
246		close $parent; POSIX::_exit(1);
247	}
248
249	my $serv = Irssi::server_find_tag($uh->{tag});
250	ban_execute($uh->{chanhash}, 0, make_ban($user, $host, $uh->{bantype}), $serv->{max_modes_in_cmd}, $uh->{cmdwhen}, $uh->{cmdwhat});
251}
252
253sub ban_execute ($$$$$$) {
254	my ($chan, $nick, $ban, $max, $cmdwhen, $cmdwhat) = @_;
255
256	my $no = 0;
257	my $channel = $chan->{name};
258
259	for my $hash ($chan->bans()) {
260		if (mask_match($ban, $hash->{ban})) {
261			# should display also who set the ban (if available)
262			Irssi::print("%Y>>%n $channel: ban $hash->{ban}");
263			$no = 1;
264			last;
265		} elsif (mask_match($hash->{ban}, $ban)) {
266			push_mode($chan, "-b", $hash->{ban}, $max);
267		}
268	}
269
270	unless ($no) {
271		my ($cmdmode, $cmdarg);
272		# is requested command a MODE so we can put it to queue?
273	 	($cmdmode, $cmdarg) = $cmdwhat =~ /^MODE\s+[^\s]+\s+([^\s]+)\s+([^\s]+)/i if $cmdwhen;
274		if ($cmdwhen == 1) { # command requested *before* banning
275			unless ($cmdmode) { # command isn't mode, ie: KICK
276				flush_mode($chan); # flush all -b conflicting bans
277				$chan->command($cmdwhat); # execute
278			} else { # command is MODE, we can add it to queue
279				push_mode($chan, $cmdmode, $cmdarg, $max);
280			}
281		}
282		push_mode($chan, "-o", $nick, $max) if ($nick);
283		push_mode($chan, "+b", $ban, $max);
284		if ($cmdwhen == 2) { # command requested *after* banning
285			unless ($cmdmode) {
286				flush_mode($chan); # flush all modes
287				$chan->command($cmdwhat);
288			} else {
289				push_mode($chan, $cmdmode, $cmdarg, $max);
290			}
291		}
292	}
293
294	$b{$channel}--;
295	flush_mode($chan) unless ($b{$channel});
296}
297
298sub ban_fork {
299	my ($rh, $wh);
300	pipe($rh, $wh);
301	my $pid = fork();
302	unless (defined $pid) {
303		Irssi::print("%R>>%n Failed to fork() :/ -  $!");
304		close $rh; close $wh;
305		return undef;
306	} elsif ($pid) {	# parent
307		close $wh;
308		$ftag{$rh} = Irssi::input_add(fileno($rh), INPUT_READ, \&ifork, $rh);
309		Irssi::pidwait_add($pid);
310	} else {		# child
311		close $rh;
312		$parent = $wh;
313	}
314	return $pid;
315}
316
317sub ifork {
318	my $rh = shift;
319	while (<$rh>) {
320		/^error\s([^\s]+)\s(.+)/ and $b{$1}--, Irssi::print("$2"), last;
321		if (/^execute\s([^\s]+)\s([^\s]+)\s([^\s]+)\s([^\s]+)\s([^\s]+)\s(.+)/) {
322			my $serv = Irssi::server_find_tag($1);
323			ban_execute($serv->channel_find($2), $3, $4, $serv->{max_modes_in_cmd}, $5, $6);
324			last;
325		}
326	}
327	Irssi::input_remove($ftag{$rh});
328	delete $ftag{$rh};
329	close $rh;
330}
331
332sub make_ban ($$$) {
333	my ($user, $host, $bantype) = @_;
334
335	$user =~ s/^[~+\-=^]/*/;
336	if ($bantype eq "ip") {
337		$host =~ s/\.[0-9]+$/.*/;
338	} elsif ($bantype eq "class") {
339		$user = "*";
340		$host =~ s/\.[0-9]+$/.*/;
341	} elsif ($bantype eq "user") {
342		$host = "*";
343	} elsif ($bantype eq "domain") {
344		# i know -- lame
345		if ($host =~ /^.*\..*\..*\..*$/) {
346			$host =~ s/.*(\..+\..+\..+)$/*\1/;
347		} elsif ($host =~ /^.*\..*\..*$/) {
348			$host =~ s/.*(\..+\..+)$/*\1/;
349		}
350		$user = "*";
351	} elsif ($bantype eq "host") {
352		$user = "*";
353	} elsif ($bantype eq "normal") {
354#		$host =~ s/^[A-Za-z\-]*[0-9]+\./*./;
355		if ($host =~ /\d$/) {
356			$host =~ s/\.[0-9]+$/.*/;
357		} else {
358			$host =~ s/^[^.]+\./*./ if $host =~ /^.*\..*\..*$/;
359		}
360	} elsif ($bantype eq "crap") {
361		my $crap;
362		for my $c (split(//, $user)) {
363			$crap .= ((int(rand(2)))? "?" : $c);
364		}
365		$user = $crap;
366		$crap = "";
367		for my $c (split(//, $host)) {
368			$crap .= ((int(rand(2)))? "?" : $c);
369		}
370		$host = $crap;
371	}
372
373	return ("*!" . $user . "@" . $host);
374}
375
376sub mask_match ($$) {
377	my ($what, $match) = @_;
378
379	# stolen from shasta's friend.pl
380	$match =~ s/\\/\\\\/g;
381	$match =~ s/\./\\\./g;
382	$match =~ s/\*/\.\*/g;
383	$match =~ s/\!/\\\!/g;
384	$match =~ s/\?/\./g;
385	$match =~ s/\+/\\\+/g;
386	$match =~ s/\^/\\\^/g;
387	$match =~ s/\[/\\\[/g;
388
389	return ($what =~ /^$match$/i);
390}
391
392Irssi::command_bind 'ban' => \&cmd_ban;
393Irssi::settings_add_str 'misc', 'banpl_type', 'normal';
394Irssi::signal_add 'redir ban userhost' => \&userhost_red;
395