# ho_tfind.pl # $Id: ho_tfind.pl,v 1.18 2005/03/27 13:13:34 jvunder Exp $ # # Provides extended search functionality for the /TRACE command. # # Part of the Hybrid Oper Script Collection. # # Based on BlackJac's /TFIND and morrow's stat.pl script. # # Known bugs: # * If the output window is closed halfway, the script crashes. use strict; use vars qw($VERSION %IRSSI $SCRIPT_NAME); use Irssi; use Irssi::Irc; use HOSC::again; use HOSC::again 'HOSC::Base'; use HOSC::again 'HOSC::Tools'; import HOSC::Tools qw(test_regexps get_equality glob_to_regexp); use Getopt::Long; $SCRIPT_NAME = "Trace Find"; ($VERSION) = '$Revision: 1.18 $' =~ / (\d+\.\d+) /; %IRSSI = ( authors => 'Garion', contact => 'garion@irssi.org', name => 'ho_tfind', description => 'Provides extended search functionality for the /TRACE command.', license => 'Public Domain', url => 'http://www.garion.org/irssi/hosc/', changed => '04 April 2004 12:34:38', ); my ($stats, $args); my @found_clients; # Storage place for found clients in case of sorting my @cache; my $cache_time; my $cache_tag; # the network tag for which the cache is active. # --------------------------------------------------------------------- # /TFIND sub cmd_tfind { my ($arguments, $server, $item) = @_; $args = parse_arguments($arguments); if ($args->{help}) { Irssi::command_runsub ('tfind', 'help', $server, $item); return; } if ($args->{error} || length $arguments == 0) { return print_usage(); } if (!$server) { return ho_print_error("No server in this window."); } $stats->{num_clients} = 0; $stats->{num_found} = 0; $stats->{window} = Irssi::active_win(); @found_clients = (); if ($stats->{busy}) { ho_print_error("Sorry, already performing a TFIND. Please wait."); return; } if ($args->{equality} && $args->{equality} !~ /^(n|nu|nr|ur|nur)$/) { ho_print_error("TFIND: Invalid equality " . $args->{equality} . "."); return; } for my $property (qw[ rnick ruser rhost rgecos ]) { if (defined $args->{$property} && !test_regexps($args->{$property})) { return ho_print_error("TFIND: Invalid regexp in $property."); } } my $search_param_text = get_param_text($args); ho_print("Searching TRACE output with params $search_param_text"); my $use_cache = Irssi::settings_get_bool('ho_tfind_use_cache'); if ($use_cache && !$args->{nocache}) { my $cache_expiry_time = Irssi::settings_get_int('ho_tfind_cache_expiry_time'); my $cache_age = time() - $cache_time; if (!$use_cache) { ho_print_warning('Cache is not enabled. Use "/set ' . 'ho_tfind_use_cache ON" to enable it.'); } elsif (@cache == 0) { ho_print_warning('Cache empty. Not using it.'); } elsif ($cache_age > $cache_expiry_time) { ho_print_warning('Cache expired. Not using it.'); } elsif ($cache_tag ne $server->{tag}) { ho_print_warning('Cache contents are for diffent tag. Replacing cache.'); undef @cache; } else { # Phew, can use the cache. ho_print("Using cache: " . scalar @cache . " clients. ". "Age is $cache_age/$cache_expiry_time."); trace_from_cache($server); return; } } $server->redirect_event( 'command cmd_tfind', 0, '', (split(/\s+/, $arguments) > 2), undef, { "event 203", "redir event_trace_line", "event 204", "redir event_trace_line", "event 205", "redir event_trace_line", "event 709", "redir event_trace_line", "event 206", "redir event_stop", "event 207", "redir event_stop", "event 208", "redir event_stop", "event 209", "redir event_stop", "event 262", "redir event_trace_end", "event 421", "redir event_unknown_command", } ); $stats->{busy} = 1; $cache_tag = $server->{tag}; undef @cache; $cache_time = time; if ($args->{etrace} || $server->{version} =~ /ircd-ratbox/) { $server->send_raw("ETRACE"); } else { $server->send_raw("TRACE"); } } # --------------------------------------------------------------------- sub cmd_tfind_help { my ($arguments, $server, $item) = @_; print_help(); } # --------------------------------------------------------------------- sub parse_arguments { my ($arguments) = @_; my $opt; # Irssi works with -argument, Getopt::Long expects --argument. Fix. $arguments =~ s/-/--/g; # Smart splitting of $arguments: understands "multi word argument". local @ARGV; my @tempargv = split / /, $arguments; my ($in_multi_token, $delimiter, $index) = (0, undef, 0); while (@tempargv) { my $token = shift @tempargv; if ($in_multi_token) { if ($token =~ /$delimiter$/) { # End of multi token argument $token =~ s/$delimiter$//; $ARGV[$index] .= " " . $token; $in_multi_token = 0; $index++; } else { # Continue multi token argument $ARGV[$index] .= " " . $token; } } elsif ($token =~ /^(['"])/ && $token !~ /['"]$/) { # New multi token argument $delimiter = $1; $token =~ s/^['"]//; $ARGV[$index] = $token; $in_multi_token = 1; } else { # Single token argument $ARGV[$index] = $token; $index++; } } # Prevent GetOptions frow screwing up the layout in case of errors. # Thanks xmath. :) local $SIG{__WARN__} = sub { my ($msg) = @_; $msg = '/TFIND: ' . $msg; ho_print_error($msg); }; my $res = GetOptions( 'nocache' => \$opt->{nocache}, 'sort=s' => \$opt->{sort}, 'nick=s' => \$opt->{nick}, 'user=s' => \$opt->{user}, 'host=s' => \$opt->{host}, 'ip=s' => \$opt->{ip}, 'gecos=s' => \$opt->{gecos}, 'rnick=s' => \$opt->{rnick}, 'ruser=s' => \$opt->{ruser}, 'rhost=s' => \$opt->{rhost}, 'rip=s' => \$opt->{rip}, 'rgecos=s' => \$opt->{rgecos}, 'equality=s'=> \$opt->{equality}, 'spoof' => \$opt->{spoof}, 'nospoof' => \$opt->{nospoof}, 'oper' => \$opt->{oper}, 'nooper' => \$opt->{nooper}, 'etrace' => \$opt->{etrace}, 'help' => \$opt->{help}, '4' => \$opt->{ipv4}, '6' => \$opt->{ipv6}, 'rawcmd=s' => \$opt->{rawcmd}, 'count' => \$opt->{count}, ); $opt->{error} = 1 unless $res; for my $arg (@ARGV) { # If any args left, read them. if (lc $arg eq "help") { $opt->{help} = 1; } else { $opt->{error} = 1; } } # Change glob patterns into regexp patterns. for my $var (qw[ nick user host gecos ip ]) { if (defined $opt->{$var}) { # Store the original glob request for displaying later on. $opt->{"glob$var"} = $opt->{$var}; # Convert glob to regexp for matching later on. $opt->{$var} = glob_to_regexp($opt->{$var}); } } # Now restore any -- in the values back to -. for my $key (keys %$opt) { $opt->{$key} =~ s/--/-/g; } return $opt; } # --------------------------------------------------------------------- sub get_param_text { my ($args) = @_; my $text; for my $var (qw[ nick user host gecos ip ]) { if (defined $args->{$var} && length $args->{$var}) { $text .= "($var is " . $args->{"glob$var"} . ") "; } if (defined $args->{"r$var"} && length $args->{"r$var"}) { $text .= "($var regexp " . $args->{"r$var"} . ") "; } } $text .= "(spoof) " if $args->{spoof}; $text .= "(not spoof) " if $args->{nospoof}; $text .= "(oper) " if $args->{oper}; $text .= "(not oper) " if $args->{nooper}; $text .= "(only ipv4) " if $args->{ipv4}; $text .= "(only ipv6) " if $args->{ipv6}; $text .= "(equality " . $args->{equality} . ") " if $args->{equality}; $text .= "using ETRACE " if $args->{etrace}; $text =~ s/\) \(/) and (/g; $text =~ s/ $//; return $text; } # --------------------------------------------------------------------- # Catching and processing output of the ircd. sub event_trace_line { my ($server, $data, $nick, $address) = @_; my ($ownnick, $line) = $data =~ /^(\S*)\s+(.*)$/; my $details = get_trace_line_details($line); return if $details->{crap}; if (Irssi::settings_get_bool('ho_tfind_use_cache')) { push @cache, $details; } process_line($details, $server); } # --------------------------------------------------------------------- sub trace_from_cache { my ($server) = @_; for my $details (@cache) { process_line($details, $server); } trace_end(); } # --------------------------------------------------------------------- # Processes one line of TRACE output, whether retrieved from TRACE output # or from the cache. sub process_line { my ($details, $server) = @_; $stats->{num_clients}++; for my $check (qw[ nick user host gecos ip ]) { # Glob check return if defined $args->{$check} && length $args->{$check} && $details->{$check} !~ /$args->{$check}/i; # Regexp check return if defined $args->{"r$check"} && length $args->{"r$check"} && $details->{$check} !~ /$args->{"r$check"}/; } return if $args->{spoof} && $details->{ip} ne "255.255.255.255"; return if $args->{nospoof} && $details->{ip} eq "255.255.255.255"; return if $args->{oper} && !$details->{is_oper}; return if $args->{nooper} && $details->{is_oper}; return if $args->{ipv4} && !$details->{ipv4}; return if $args->{ipv6} && !$details->{ipv6}; if ($args->{equality}) { my $eq = get_equality($details->{nick}, $details->{user}, $details->{gecos}); return if $eq ne $args->{equality}; } if (defined $args->{rawcmd}) { execute_raw_command($details, $server); } elsif (defined $args->{sort}) { push @found_clients, $details; } else { if ($args->{'count'}) { # Don't print the client, only list the number of clients found # at the end. } else { print_client($details); } } $stats->{num_found}++; } # --------------------------------------------------------------------- sub print_client { my ($details) = @_; my $format = 'ho_tfind_line'; $format = 'ho_tfind_line_v6' if $details->{ipv6}; $stats->{window}->printformat(MSGLEVEL_CRAP, $format, $details->{nick}, $details->{user}, $details->{host}, $details->{gecos}, $details->{ip}); } # --------------------------------------------------------------------- sub execute_raw_command { my ($details, $server) = @_; my $cmd = $args->{rawcmd}; for (qw[ nick user host gecos ip ]) { $cmd =~ s/%$_%/$details->{$_}/g; } $server->send_raw_now($cmd); } # --------------------------------------------------------------------- # Processes a single /TRACE output line and returns a hashref with the # relevant data. sub get_trace_line_details { my ($line) = @_; my $details; # TRACE if ($line =~ /(User|Oper)\s+(\S+)\s+(\S+)\[([^@]+)@(\S+)\]\s+\(([^)]+)\)/) { $details->{is_user} = 1 if $1 eq "User"; $details->{is_oper} = 1 if $1 eq "Oper"; $details->{class} = $2; $details->{nick} = $3; $details->{user} = $4; $details->{host} = $5; $details->{ip} = $6; $details->{ipv4} = 1 if $details->{ip} !~ /:/; $details->{ipv6} = 1 if $details->{ip} =~ /:/; return $details; } # ETRACE if ($line =~ /(User|Oper)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+:(.*)$/) { $details->{is_user} = 1 if $1 eq "User"; $details->{is_oper} = 1 if $1 eq "Oper"; $details->{class} = $2; $details->{nick} = $3; $details->{user} = $4; $details->{host} = $5; $details->{ip} = $6; $details->{gecos} = $7; $details->{ipv4} = 1 if $details->{ip} !~ /:/; $details->{ipv6} = 1 if $details->{ip} =~ /:/; return $details; } $details->{crap} = 1; return $details; } # --------------------------------------------------------------------- sub signal_stop { my ($server, $data, $nick, $address) = @_; Irssi::signal_stop(); } # --------------------------------------------------------------------- sub event_trace_end { my ($server, $data, $nick, $address) = @_; trace_end(); } # --------------------------------------------------------------------- sub trace_end { if (defined $args->{sort}) { my @sorted_clients = sort_clients(@found_clients); print_client($_) for @sorted_clients; } ho_print("Found " . $stats->{num_found} . " match" . ($stats->{num_found} == 1 ? "" : "es") . " in " . $stats->{num_clients} . " client" . ($stats->{num_clients} == 1 ? "" : "s") . "."); $stats->{busy} = 0; } # --------------------------------------------------------------------- sub sort_clients { my @clients = @_; my %sort_params = ( n => { field => 'nick', order => 'normal', }, N => { field => 'nick', order => 'reverse', }, u => { field => 'user', order => 'normal', }, U => { field => 'user', order => 'reverse', }, h => { field => 'host', order => 'normal', }, H => { field => 'host', order => 'reverse', }, g => { field => 'gecos', order => 'normal', }, G => { field => 'gecos', order => 'reverse', }, ); if (exists $sort_params{$args->{sort}}) { return map { $_->[0] } sort { text_compare( $a->[1], $b->[1], $sort_params{$args->{sort}}->{order}, Irssi::settings_get_bool('ho_tfind_sort_case_sensitive') ) } map { [ $_, $_->{ $sort_params{ $args->{sort} }->{field} } ] } @clients; } elsif ($args->{sort} eq 'i') { return map { $_->[0] } sort { ip_compare($a->[1], $b->[1]) } map { [ $_, $_->{ip} ] } @clients; } return @clients; } # --------------------------------------------------------------------- sub text_compare { my ($first, $second, $reverse, $case_sensitive) = @_; if ($reverse eq 'normal') { if ($case_sensitive) { return ($first cmp $second); } else { return ((lc $first) cmp (lc $second)); } } else { if ($case_sensitive) { return ((reverse $first) cmp (reverse $second)); } else { return ((reverse lc $first) cmp (reverse lc $second)); } } } # --------------------------------------------------------------------- # This is not an exact ip comparison, but it's Good Enough[tm]. # It treats the ip as a number and sorts that. sub ip_compare { my ($first, $second) = @_; if ($first =~ /\./) { return -1 if $second =~ /:/; return $first <=> $second; } else { return 1 if $second =~ /\./; return $first <=> $second; } } # --------------------------------------------------------------------- sub event_unknown_command { my ($server, $data, $nick, $address) = @_; ho_print_error("This server does not support ETRACE."); $stats->{busy} = 0; } # --------------------------------------------------------------------- # Initialisation ho_print_init_begin($SCRIPT_NAME); Irssi::settings_add_bool('ho', 'ho_tfind_use_cache', 0); Irssi::settings_add_int('ho', 'ho_tfind_cache_expiry_time', 60); Irssi::settings_add_bool('ho', 'ho_tfind_sort_case_sensitive', 0); Irssi::command_bind('tfind', 'cmd_tfind'); Irssi::command_bind('tfind help', 'cmd_tfind_help'); Irssi::signal_add({ "redir event_trace_line" => \&event_trace_line, "redir event_trace_end" => \&event_trace_end, "redir event_stop" => \&signal_stop, "redir event_unknown_command" => \&event_unknown_command, }); # ok this is IMO realy ugly, if anyone has a suggestion please let me know Irssi::Irc::Server::redirect_register("command cmd_tfind", 0, 0, { "event 203" => 1, # RPL_TRACEUNKNOWN "event 204" => 1, # RPL_TRACEOPERATOR "event 205" => 1, # RPL_TRACEUSER "event 206" => 1, # RPL_TRACESERVER "event 207" => 1, # RPL_TRACESERVICE "event 208" => 1, # RPL_TRACENEWTYPE "event 209" => 1, # RPL_TRACECLASS "event 709" => 1, # ratbox ETRACE output }, { "event 219" => 1, # end of stats "event 262" => 1, # end of trace "event 263" => 1, # tryagain "event 401" => 1, # no such server "event 421" => 1, # unknow command (missing ETRACE) }, undef, ); # Register format. # nick, user, host, gecos, ip Irssi::theme_register([ 'ho_tfind_line', '{nick $[-9]0}{comment %g$[!15]4}{chanhost_hilight $[-11]1@$[!38]2}{comment $3}', 'ho_tfind_line_v6', '{nick $[-9]0}{comment %g$[!24]4}{chanhost_hilight $[-11]1@$[!38]2}{comment $3}', ]); ho_print_init_end($SCRIPT_NAME); # --------------------------------------------------------------------- # Help. sub print_usage { ho_print_help('section', 'Syntax'); ho_print_help('syntax', '/TFIND [-]HELP'); ho_print_help('syntax', "/TFIND - [] - [] .."); #ho_print_help('syntax', "/TFIND - [] [action [arguments]]\n"); #ho_print_help("Use /TFIND -help for help\n"); } sub print_help { ho_print_help('head', $SCRIPT_NAME); print_usage(); ho_print_help('section', 'Introduction'); ho_print_help("Script to search through /TRACE output.\n"); ho_print_help("Glob search has * and ? as wildcards.\n"); ho_print_help('This script has a cache built in, which is disabled by '. 'default. It can be enabled by setting ho_tfind_use_cache to ON. '. 'When a TRACE is sent to the server and caching is enabled, the '. 'TRACE output is stored internally. If another /TFIND is done '. 'shortly after the previous one, the cache data is used instead of '. "sending another TRACE to the server.\n"); ho_print_help('section', 'Arguments'); ho_print_help('argument', '-nocache', 'Does not use cache, even if available.'); my @args = ( [qw(nick Glob nickname)], [qw(user Glob username)], [qw(host Glob hostname)], [qw(ip Glob ip)], [qw(gecos Glob gecos)], [qw(rnick Regexp nickname)], [qw(ruser Regexp username)], [qw(rhost Regexp hostname)], [qw(rip Regexp ip)], [qw(rgecos Regexp gecos)], ); for my $arg (@args) { ho_print_help('argument', '-' . $arg->[0] . ' ', $arg->[1] . ' searches for in the ' . $arg->[2] . '.'); } ho_print_help('argument', '-4', 'Searches for ipv4 clients.'); ho_print_help('argument', '-6', 'Searches for ipv6 clients.'); ho_print_help('argument', '-oper', 'Searches for opers.'); ho_print_help('argument', '-nooper', 'Excludes opers.'); ho_print_help('argument', '-spoof', 'Searches for spoofs.'); ho_print_help('argument', '-nospoof', 'Excludes spoofs.'); ho_print_help('argument', '-equality ', 'Requires this equality. See below.'); ho_print_help('argument', '-sort ', 'Sorts by criterium. See below.'); ho_print_help('argument', '-rawcmd ""', 'Executes the given command. See below.'); ho_print_help('argument', '-etrace', 'Use ETRACE instead of TRACE.'); ho_print_help('section', 'Settings'); ho_print_help('setting', 'ho_tfind_use_cache', 'Boolean indicating whether to cache /TRACE output.'); ho_print_help('setting', 'ho_tfind_cache_expiry_time', 'Time, in seconds, after which the cache becomes invalid.'); ho_print_help('setting', 'ho_tfind_sort_case_sensitive', 'Whether output sorting is case sensitive.'); # ho_print_help('setting', 'ho_tfind_kline_time', # 'Time (in minutes) for -kline option. [not functional yet]'); # ho_print_help('setting', 'ho_tfind_kline_reason', # 'Reason for -kline option. [not functional yet]'); # ho_print_help('setting', 'ho_tfind_log_file', # 'File to log found clients to. [not functional yet]'); ho_print_help('section', 'Equality'); ho_print_help("Equality is a term which describes the relationship ". "between a client's nick, user and realname (gecos). The following ". "equalities exist:"); ho_print_help(" n - all three are different"); ho_print_help(" nu - nick equals username, realname is different"); ho_print_help(" nr - nick equals realname, username is different"); ho_print_help(" ur - username equals realname, nick is different"); ho_print_help(" nur - all three are equal\n"); ho_print_help('section', 'Sorting'); ho_print_help("The output of clients can be sorted using the -sort ". "option. The following search criteria are allowed:"); ho_print_help(" n - sort by nick"); ho_print_help(" N - sort by reversed nick"); ho_print_help(" u - sort by username"); ho_print_help(" U - sort by reversed username"); ho_print_help(" h - sort by host"); ho_print_help(" H - sort by reversed host"); ho_print_help(" g - sort by gecos"); ho_print_help(" G - sort by reversed gecos"); ho_print_help(" i - sort by ip"); ho_print_help("By default, sorting is done case insensitive. To " . "change this, use the setting ho_tfind_sort_case_sensitive.\n"); ho_print_help('section', 'Raw command'); ho_print_help("Using the -rawcmd option, you can make this script " . "execute a raw IRCD command on all the found clients. Do not " . "forget to put the double quotes around the command."); ho_print_help("To make this feature actually useful, several " . "strings are automatically replaced by the found client's " . "properties before the raw command is sent. These are:"); ho_print_help(" %nick% - the client's nick"); ho_print_help(" %user% - the client's username"); ho_print_help(" %host% - the client's hostname"); ho_print_help(" %ip% - the client's ip"); ho_print_help(" %gecos% - the client's gecos"); ho_print_help("Remember: do not forget the quotes around the command!\n"); ho_print_help('section', 'Examples'); ho_print_help('argument', '/tfind -spoof -nooper -sort H', 'Finds all spoofed, non-opered clients, sorted by their '. 'reversed hostname.'); ho_print_help('argument', '/tfind -rnick ^\[..+\].?[0-9]+$ -equality nu', 'Finds all clients with a [abc]-123 kind of nickname, whose ' . 'nickname is equal to their username.'); ho_print_help('argument', '/tfind -gecos "w3 rul3 j00r 4ss" -rawcmd '. '"PRIVMSG %nick% :.die"', 'Finds all clients with the "we rule" '. 'gecos, and sends them a message ".die".'); ho_print_help('argument', '/tfind -rnick [A-Z]{4} -rawcmd '. '"DLINE %ip% :drone"', 'Places a D-line on the ip of each client ' . 'with at least 4 consecutive uppercase letters in their nick.'); }