File Coverage

blib/lib/ThreatNet/Bot/AmmoBot.pm
Criterion Covered Total %
statement 54 111 48.6
branch 14 60 23.3
condition 4 11 36.3
subroutine 15 27 55.5
pod 8 8 100.0
total 95 217 43.7


line stmt bran cond sub pod time code
1             package ThreatNet::Bot::AmmoBot;
2              
3             =pod
4              
5             =head1 NAME
6              
7             ThreatNet::Bot::AmmoBot - Tail threat messages from a file to ThreatNet
8              
9             =head1 DESCRIPTION
10              
11             C is the basic foot soldier of the ThreatNet
12             bot ecosystem, fetching ammunition and bringing it to the channel.
13              
14             It connects to a single ThreatNet channel, and then tails one or more
15             files scanning for threat messages while following the basic channel
16             rules.
17              
18             When it sees a L-compatible message appear
19             at the end of the file, it will report it to the channel (subject to
20             the appropriate channel rules).
21              
22             Its main purpose is to make it as easy as possible to connect any system
23             capable of writing a log file to ThreatNet. If an application can be
24             configured or coded to spit out the appropriately formatted messages to
25             a file, then C will patiently watch for them and then haul them
26             off to the channel for you (so you don't have to).
27              
28             It the data can be extracted from an existing file format, then a
29             C property can be set which will specify a class to be used
30             as a customer L for the event stream.
31              
32             =head1 METHODS
33              
34             =cut
35              
36 2     2   24222 use strict;
  2         6  
  2         91  
37 2     2   958 use Params::Util '_INSTANCE';
  2         5437  
  2         131  
38 2         17 use POE 'Wheel::FollowTail',
39             'Component::IRC',
40 2     2   2458 'Component::IRC::Plugin::Connector';
  2         115719  
41 2     2   648222 use ThreatNet::Message::IPv4 ();
  2         9  
  2         40  
42 2     2   625 use ThreatNet::Filter::Chain ();
  2         4  
  2         35  
43 2     2   475 use ThreatNet::Filter::Network ();
  2         4  
  2         39  
44 2     2   509 use ThreatNet::Filter::ThreatCache ();
  2         4  
  2         37  
45              
46 2     2   9 use vars qw{$VERSION};
  2         3  
  2         81  
47             BEGIN {
48 2     2   2957 $VERSION = '0.20';
49             }
50              
51              
52              
53              
54              
55             #####################################################################
56             # Constructor and Accessors
57              
58             =pod
59              
60             =head2 new %args
61              
62             The isn't really any big reason that you would be wanting to instantiate
63             a C yourself, but if it comes to that you do
64             it by simply passing a list of the appropriate arguments to the C
65             method.
66              
67             # Create the ammobot
68             my $Bot = ThreatNet::Bot::AmmoBot->new( %args );
69            
70             # Run the ammobot
71             $Bot->run;
72              
73             =cut
74              
75             sub new {
76 1     1 1 22 my ($class, %args) = @_;
77              
78             # Check the args
79 1 50       4 $args{Nick} or die "Did not specify a nickname";
80 1 50       4 $args{Channel} or die "Did not specify a channel";
81 1 50       10 $args{Channel} =~ /^\#\w+$/
82             or die "Invalid channel specification";
83 1 50       3 $args{Server} or die "Did not specify a server";
84 1   50     8 $args{Port} ||= 6667;
85 1   33     5 $args{Username} ||= $args{Nick};
86 1   33     10 $args{Ircname} ||= $args{Nick};
87 1         4 $args{Tails} = {};
88              
89             # Create the IRC client
90 1 50       9 unless ( _INSTANCE($args{IRC}, 'POE::Component::IRC') ) {
91 1 50       13 $args{IRC} = POE::Component::IRC->spawn
92             or die "Failed to create new IRC server: $!";
93             }
94              
95             # Create the empty object
96 1         5587 my $self = bless {
97             running => '',
98             args => \%args,
99             }, $class;
100              
101 1         5 $self;
102             }
103              
104             =pod
105              
106             =head2 args
107              
108             The C accessor returns the argument hash.
109              
110             =cut
111              
112 1     1 1 453 sub args { $_[0]->{args} }
113              
114             =pod
115              
116             =head2 tails
117              
118             The C accessor returns the C of C objects
119             indexed by file name.
120              
121             =cut
122              
123 4     4 1 32 sub tails { $_[0]->{args}->{Tails} }
124              
125             =pod
126              
127             =head2 running
128              
129             The C accessor returns true if the bot is currently
130             running, or false if the bot has not yet started.
131              
132             =cut
133              
134 2     2 1 10 sub running { $_[0]->{running} }
135              
136             =pod
137              
138             =head2 Session
139              
140             Once the bot has started, the C accessor provides direct access
141             to the L object for the bot.
142              
143             =cut
144              
145 0     0 1 0 sub Session { $_[0]->{Session} }
146              
147             =pod
148              
149             =head2 files
150              
151             The C accessor returns a list of the files the bot is tailing
152             (or will be tailing), or in scalar context returns the number of files.
153              
154             =cut
155              
156             sub files {
157 1     1 1 4 my $self = shift;
158             wantarray
159 0         0 ? (sort keys %{$self->tails})
  1         3  
160 1 50       5 : scalar(keys %{$self->tails});
161             }
162              
163             =pod
164              
165             =head2 add_file $file [, Filter => $POEFilter ]
166              
167             Once you have created the Bot object, the C method is used to
168             add the list of files that the bot will be tailing.
169              
170             It takes as argument a file name, followed by a number of key/value
171             parameters.
172              
173             For the time being, the only available param is C<"Filter">. The filter
174             param provides a class name. The class will be loaded if needed, and
175             then a new default object of it created and used as a custom
176             L for the file.
177              
178             =cut
179              
180             sub add_file {
181 1     1 1 3 my $self = shift;
182 1 50       3 $self->running and die "Cannot add files once the bot is running";
183 1 50 33     65 my $file = ($_[0] and ( -p $_[0] or -f $_[0] ) and -r $_[0]) ? shift
184             : die "Invalid file '$_[0]'";
185 1 50       4 if ( $self->tails->{$file} ) {
186 0         0 die "File '$file' already attached to bot";
187             }
188              
189             # Create the basic FollowTail params
190 1         5 my %args = @_;
191 1         8 my %Params = (
192             Filename => $file,
193             PollInterval => 1,
194             InputEvent => 'tail_input',
195             ErrorEvent => 'tail_error',
196             );
197              
198             # Add the optional params if needed
199 1 50       10 if ( _INSTANCE($args{Driver}, 'POE::Driver') ) {
    50          
200 0         0 $Params{Driver} = $args{Driver};
201             } elsif ( $args{Driver} ) {
202 0         0 die "Driver param was not a valid POE::Driver";
203             }
204 1 50       10 if ( _INSTANCE($args{Filter}, 'POE::Filter') ) {
    50          
205 0         0 $Params{Filter} = $args{Filter};
206             } elsif ( $args{Filter} ) {
207 0         0 die "Filter param was not a valid POE::Filter";
208             }
209              
210             # Save the FollowTail params
211 1         4 $self->tails->{$file} = \%Params;
212              
213 1         6 1;
214             }
215              
216             =pod
217              
218             =head2 run
219              
220             Once the bot has been created, and all of the files have been added, the
221             C method is used to start the bot, and connect to the files and the
222             IRC server.
223              
224             The method dies if the bot has not had any files added.
225              
226             =cut
227              
228             sub run {
229 0     0 1   my $self = shift;
230 0 0         unless ( $self->files ) {
231 0           die "Refusing to start, no files added";
232             }
233              
234             # Create the Session
235 0           $self->{Session} = POE::Session->create(
236             inline_states => {
237             _start => \&_start,
238             stop => \&_stop,
239              
240             tail_input => \&_tail_input,
241             tail_error => \&_tail_error,
242              
243             irc_001 => \&_irc_001,
244             irc_socketerr => \&_irc_socketerr,
245             irc_disconnected => \&_irc_disconnected,
246             irc_public => \&_irc_public,
247              
248             threat_receive => \&_threat_receive,
249             threat_send => \&_threat_send,
250             },
251             args => [ $self->args ],
252             );
253              
254 0           $self->{running} = 1;
255 0           POE::Kernel->run;
256             }
257              
258              
259              
260              
261              
262             #####################################################################
263             # POE Event Handlers
264              
265             # Add a file
266             # Called when the Kernel fires up
267             sub _start {
268 0     0     %{$_[HEAP]} = %{$_[ARG0]};
  0            
  0            
269              
270             # Create the main message i/o filter
271 0 0         $_[HEAP]->{ThreatCache} = ThreatNet::Filter::ThreatCache->new
272             or die "Failed to create ThreatCache Filter";
273 0 0         $_[HEAP]->{Filter} = ThreatNet::Filter::Chain->new(
274             ThreatNet::Filter::Network->new( discard => 'rfc3330' ),
275             $_[HEAP]->{ThreatCache},
276             ) or die "Failed to create Message I/O Filter";
277              
278             # Register for events and connect to the server
279 0           $_[HEAP]->{IRC}->yield( register => 'all' );
280 0           $_[HEAP]->{IRC}->plugin_add(
281             'Connector' => POE::Component::IRC::Plugin::Connector->new( delay => 60 )
282             );
283 0 0         $_[HEAP]->{IRC}->yield( connect => {
    0          
284             Nick => $_[HEAP]->{Nick},
285             Server => $_[HEAP]->{Server},
286             Port => $_[HEAP]->{Port},
287             $_[HEAP]->{Flood}
288             ? (Flood => 1)
289             : (),
290             $_[HEAP]->{ServerPassword}
291             ? (Password => $_[HEAP]->{ServerPassword})
292             : (),
293             Username => $_[HEAP]->{Username},
294             Ircname => $_[HEAP]->{Ircname},
295             } );
296              
297             # Initialize the tails
298 0           my $Tails = $_[HEAP]->{Tails};
299 0           foreach my $key ( sort keys %$Tails ) {
300 0 0         $Tails->{$key} = POE::Wheel::FollowTail->new( %{$Tails->{$key}} )
  0            
301             or die "Failed to create FollowTail for $key";
302             }
303             }
304              
305             sub _stop {
306             # Stop tailing the files
307 0     0     delete $_[HEAP]->{Tails};
308              
309             # Disconnect from IRC
310 0 0         if ( $_[HEAP]->{IRC} ) {
311 0 0         if ( $_[HEAP]->{IRC}->connected ) {
312 0           $_[HEAP]->{IRC}->yield( quit => 'Controlled shutdown' );
313             }
314 0           delete $_[HEAP]->{IRC};
315             }
316              
317 0           1;
318             }
319              
320              
321              
322              
323              
324             #####################################################################
325             # The Tailing of the File
326              
327             sub _tail_input {
328 0     0     my $input = $_[ARG0];
329 0           chomp $input;
330              
331             # Does the input line form a valid message?
332 0 0         my $Message = ThreatNet::Message::IPv4->new( $input ) or return;
333              
334             # Send the Message to the channel (or not, for now)
335 0           $_[KERNEL]->yield( threat_send => $Message );
336             }
337              
338             sub _tail_error {
339 0     0     $_[KERNEL]->yield( stop => 1 );
340             }
341              
342              
343              
344              
345              
346             #####################################################################
347             # IRC Events
348              
349             # Connected
350             sub _irc_001 {
351 0     0     $_[HEAP]->{IRC}->yield( join => $_[HEAP]->{Channel} );
352             }
353              
354             # Failed to connect
355             sub _irc_socketerr {
356 0     0     $_[KERNEL]->yield( stop => 1 );
357             }
358              
359             # We were disconnected
360             ### FIXME - Make this reconnect
361             sub _irc_disconnected {
362 0 0   0     if ( $_[HEAP]->{IRC} ) {
363 0           $_[KERNEL]->yield( stop => 1 );
364             } else {
365             # Already shutting down, do nothing
366             }
367             }
368              
369             # Normal channel message
370             sub _irc_public {
371 0     0     my ($who, $where, $msg) = @_[ARG0, ARG1, ARG2];
372              
373             # Is this a ThreatNet message?
374 0           my $Message = ThreatNet::Message::IPv4->new($msg);
375 0 0         if ( $Message ) {
376             # Pass the message through the channel i/o filter
377 0 0         $_[HEAP]->{Filter}->keep($Message) or return;
378              
379             # Hand off to the threat_receive message
380 0           return $_[KERNEL]->yield( threat_receive => $Message );
381             }
382              
383             # Is this an addressed message?
384 0           my $Nick = $_[HEAP]->{Nick};
385 0 0         return unless $msg =~ /^$Nick(?::|,)?\s+(\w+)\b/;
386 0           my $command = lc $1;
387 0 0         return unless lc($1) eq 'status';
388              
389             # Generate stats
390 0           my $stats = $_[HEAP]->{ThreatCache}->stats;
391 0           my $message = "Online $stats->{time_running} seconds. $stats->{seen} events at $stats->{rate_seen}/s with $stats->{kept} kept and $stats->{size} currently in the ThreatCache. $stats->{percent_discard} synced with the channel";
392 0           $_[HEAP]->{IRC}->yield( privmsg => $_[HEAP]->{Channel}, $message );
393             }
394              
395              
396              
397              
398              
399             #####################################################################
400             # ThreatNet Events
401              
402             # We just do nothing normally
403             sub _threat_receive {
404 0     0     1;
405             }
406              
407             sub _threat_send {
408 0     0     my $Message = $_[ARG0];
409              
410             # Pass it through the filter
411 0 0         $_[HEAP]->{Filter}->keep($Message) or return;
412              
413             # Occasionally the IRC object is missing.
414             # I'm not entirely sure why this is the case, but it
415             # isn't very expensive to just check, and drop any
416             # messages if it's not there.
417 0 0         return unless $_[HEAP]->{IRC};
418              
419             # Send the message immediately
420 0           $_[HEAP]->{IRC}->yield( privmsg => $_[HEAP]->{Channel}, $Message->message );
421             }
422              
423             1;
424              
425             =pod
426              
427             =head1 TO DO
428              
429             - Add support for additional outbound filters
430              
431             =head1 SUPPORT
432              
433             All bugs should be filed via the bug tracker at
434              
435             L
436              
437             For other issues, or commercial enhancement and support, contact the author
438              
439             =head1 AUTHORS
440              
441             Adam Kennedy Eadamk@cpan.orgE
442              
443             =head1 SEE ALSO
444              
445             L, L
446              
447             =head1 COPYRIGHT
448              
449             Copyright (c) 2005 Adam Kennedy. All rights reserved.
450             This program is free software; you can redistribute
451             it and/or modify it under the same terms as Perl itself.
452              
453             The full text of the license can be found in the
454             LICENSE file included with this module.
455              
456             =cut