###################################################################
# DroneBL RPC2 query for irssi
# This script allows querying the DroneBL using RPC2 calls, which
# will allow queries including wildcards and ranges. Examples:
# /dronebl 127.0.0.?
# /dronebl 10.0.*
# /dronebl 192.168.[1-20].*
#
# It also allows addition of ip addresses via "add $type $ipaddr":
# /dronebl add 1 10.10.2.20
#
# To see a list of types, issue:
# /dronebl types
#
# For more information:
# /dronebl help
#
# Requires LWP (libwww-perl -- which you probably already have),
# and Text::TabularDisplay (available via CPAN or your vendor).
#
# You pretty much have to install LWP via cpan(1) or your vendor's
# package distribution system (such as apt-get(1)), but you can
# download Text::TabularDisplay, untar, and mkdir
# ~/.irssi/scripts/Text; cp TabularDisplay.pm ~/.irssi/scripts/Text
#
# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# http://sam.zoy.org/wtfpl/COPYING for more details.
###################################################################
use vars qw($VERSION %IRSSI);
use strict;
use Irssi qw(command_bind settings_get_str settings_add_str settings_get_bool settings_add_bool);
use LWP::UserAgent;
use HTTP::Request::Common;
use Text::ParseWords 'shellwords';
use Text::TabularDisplay;
$VERSION = '0.5';
%IRSSI = (
authors => 'Steve Church (rojo), Jmax',
contact => 'irc.atheme.org on #dronebl',
name => 'DroneBL RPC2 query / host submission',
description => 'query (and modify) the DroneBL with wildcards via irssi',
license => 'WTFPLv2',
url => 'http://headcandy.org/rojo/',
changed => $VERSION,
modules => 'LWP::UserAgent HTTP::Request Text::TabularDisplay',
commands => 'dronebl'
);
settings_add_str('DroneBL', 'dronebl_rpckey', '');
settings_add_str('DroneBL', 'dronebl_columns', 'listed ip type timestamp');
settings_add_bool('DroneBL', 'dronebl_show_only_active', 0);
settings_add_bool('DroneBL', 'dronebl_show_type_names', 0);
my $DroneBL = "https://dronebl.org/RPC2";
my $classes_page = "https://dronebl.org/classes?format=txt";
my $expired_after = 86400*90; # 90 days
my $userAgent = LWP::UserAgent->new(agent => 'perl post');
my %classes;
if (!settings_get_str('dronebl_rpckey')) {
dronebl_nag(0);
}
command_bind dronebl => \&dronebl;
sub dronebl {
my ($data, $server, $channel) = @_;
my @args = shellwords($data);
if ($args[0] eq 'help') {
dronebl_help($channel);
return;
} elsif ($args[0] eq 'types') {
dronebl_update_classes($channel);
dronebl_print('Types: ');
my @types = sort { $a <=> $b } keys %classes;
foreach my $type (@types) {
dronebl_print( sprintf('%3s', $type) . ': ' . $classes{$type} );
}
return;
} elsif ($args[0] eq 'add' || $args[0] eq 'submit') {
my ($type, $ipaddr) = @args[1..2];
if (!$type or $ipaddr !~ /^[0-9\.]+$/) {
dronebl_print('Need type and IP address');
return;
}
my ($matches, $active, $expired) = dronebl_lookup($ipaddr);
if ($matches && $active && !$expired) {
dronebl_print("$ipaddr: Already listed");
} else {
my ($line) = dronebl_request($channel, "" );
dronebl_print("$ipaddr: Success adding") if $line;
}
} elsif ($args[0] && $args[0] =~ /[0-9\.\?\*\%_\[\]-]+/) {
my @clean;
foreach (@args) { push @clean, $_ if /^[0-9\.\?\*\%_\[\]-]+$/; }
my ($matches, $active, $expired, @results) = dronebl_lookup($channel, @clean);
my @columns = split(/\s+/, settings_get_str('dronebl_columns'));
my $table = Text::TabularDisplay->new(@columns);
if ($matches) {
foreach my $result (@results) {
if ($result->{listed} || !settings_get_bool('dronebl_show_only_active')) {
$result->{listed} = ($result->{listed}) ? 'yes' : 'no';
$result->{listed} .= ' (exp)' if $result->{expired};
$result->{timestamp} = scalar gmtime($result->{timestamp});
if (settings_get_bool('dronebl_show_type_names')) {
my $type_name = dronebl_class($channel, $result->{type});
$result->{type} .= " ($type_name)" if $type_name;
}
my @row;
foreach my $column (@columns) {
push @row, $result->{$column};
}
$table->add(@row);
}
}
my @table = split /\n/, $table->render;
dronebl_print("%8%#" . $_ . "%#%8", $channel) foreach @table;
dronebl_print("%W%Nquery: $data | matches: $matches | active: $active | expired: $expired", $channel);
} else {
dronebl_print("%W%Nquery: $data | no match(es)");
}
} else {
dronebl_help($channel);
}
}
sub dronebl_lookup {
my ($channel, @queries) = @_;
$_ = "" foreach @queries;
my @lines = dronebl_request($channel, @queries) or return;
my ($matches, $active, $expired) = (0, 0, 0);
my @results;
foreach my $line (@lines) {
if ($line =~ /\n\n";
$message .= "\t$_\n" foreach @reqs;
$message .= "\n";
my $response = $userAgent->request(POST $DroneBL, Content_Type => 'text/xml', Content => $message);
if ($response->is_success) {
my $xml = $response->as_string;
my @lines = split(/\n/, $xml);
if ($xml =~ /error/) {
my ($error, $error_message, $error_query);
foreach my $line (@lines) {
if ($line =~ //) { $error = $line; }
if ($line =~ //) { $error_message = $line; }
if ($line =~ //) { $error_query = $line; }
}
s{\s*?[^>]+>}{}g for ($error, $error_message, $error_query);
$error_query =~ s/%/%%/g;
dronebl_print("The server returned an error message ($error: $error_message)", $channel);
dronebl_print("Extended information: $error_query", $channel) if $error_query;
return;
}
return @lines;
}
else {
dronebl_print($response->error_as_HTML, $channel);
return;
}
}
sub dronebl_print {
my ($data, $channel) = @_;
if ($channel && $channel->{type} eq "CHANNEL") {
$channel->print($data, MSGLEVEL_CLIENTCRAP);
}
else {
print CLIENTCRAP $data;
}
}
sub dronebl_help {
my ($channel) = @_;
dronebl_print(<request(GET $classes_page);
if ($response->is_success) {
foreach my $line (split(/\n/, $response->as_string)) {
if (grep(/\t/, $line)) {
my @parms = split(/\t/, $line);
$classes{@parms[0]} = @parms[1];
}
}
}
else { return; }
}
return 1;
}