File Coverage

blib/lib/POE/Component/IRC/Plugin/Proxy.pm
Criterion Covered Total %
statement 148 179 82.6
branch 35 56 62.5
condition 16 30 53.3
subroutine 24 28 85.7
pod 4 12 33.3
total 227 305 74.4


line stmt bran cond sub pod time code
1             package POE::Component::IRC::Plugin::Proxy;
2             our $AUTHORITY = 'cpan:HINRIK';
3             $POE::Component::IRC::Plugin::Proxy::VERSION = '6.92';
4 3     3   3883 use strict;
  3         8  
  3         113  
5 3     3   17 use warnings FATAL => 'all';
  3         7  
  3         159  
6 3     3   19 use Carp;
  3         9  
  3         225  
7 3     3   24 use Socket qw(inet_ntoa);
  3         7  
  3         204  
8 3         20 use POE qw(Wheel::SocketFactory Wheel::ReadWrite Filter::IRCD
9 3     3   21 Filter::Line Filter::Stackable);
  3         7  
10 3     3   2853 use POE::Component::IRC::Plugin qw(PCI_EAT_NONE);
  3         9  
  3         8014  
11              
12             sub new {
13 2     2 1 2860 my ($package) = shift;
14 2 50       25 croak "$package requires an even number of arguments" if @_ & 1;
15 2         8 my %args = @_;
16 2         14 $args{ lc $_ } = delete $args{ $_ } for keys %args;
17 2         20 return bless \%args, $package;
18             }
19              
20             sub PCI_register {
21 2     2 0 833 my ($self, $irc) = splice @_, 0, 2;
22              
23 2 50       21 if (!$irc->isa('POE::Component::IRC::State')) {
24 0         0 die __PACKAGE__ . ' requires PoCo::IRC::State or a subclass thereof';
25             }
26              
27 2         15 $irc->raw_events(1);
28 2         11 $self->{irc} = $irc;
29 2         11 $irc->plugin_register(
30             $self,
31             'SERVER',
32             qw(
33             connected
34             disconnected
35             001
36             error
37             socketerr
38             raw
39             )
40             );
41              
42 2         121 POE::Session->create(
43             object_states => [
44             $self => [qw(
45             _client_error
46             _client_flush
47             _client_input
48             _listener_accept
49             _listener_failed
50             _start
51             _shutdown
52             _spawn_listener
53             )],
54             ],
55             );
56              
57 2         345 return 1;
58             }
59              
60             sub PCI_unregister {
61 2     2 0 776 my ($self, $irc) = splice @_, 0, 2;
62 2         12 $poe_kernel->post($self->{SESSION_ID} => _shutdown => delete $self->{irc});
63 2         258 $poe_kernel->refcount_decrement($self->{SESSION_ID}, __PACKAGE__);
64 2         98 return 1;
65             }
66              
67             sub S_connected {
68 1     1 0 54 my ($self, $irc) = splice @_, 0, 2;
69 1         52 $self->{stashed} = 0;
70 1         7 $self->{stash} = [ ];
71 1         4 return PCI_EAT_NONE;
72             }
73              
74             sub S_001 {
75 1     1 0 47 my ($self, $irc) = splice @_, 0, 2;
76 1         19 $poe_kernel->post($self->{SESSION_ID} => '_shutdown');
77 1         141 $poe_kernel->post($self->{SESSION_ID} => '_spawn_listener');
78 1         104 return PCI_EAT_NONE;
79             }
80              
81             sub S_disconnected {
82 1     1 0 38 my ($self, $irc) = splice @_, 0, 2;
83 1         6 $poe_kernel->post($self->{SESSION_ID} => '_shutdown');
84 1         129 return PCI_EAT_NONE;
85             }
86              
87             sub S_socketerr {
88 0     0 0 0 my ($self, $irc) = splice @_, 0, 2;
89 0         0 $poe_kernel->post($self->{SESSION_ID} => '_shutdown');
90 0         0 return PCI_EAT_NONE;
91             }
92              
93             sub S_error {
94 1     1 0 44 my ($self, $irc) = splice @_, 0, 2;
95 1         7 $poe_kernel->post($self->{SESSION_ID} => '_shutdown');
96 1         115 return PCI_EAT_NONE;
97             }
98              
99             sub S_raw {
100 40     40 0 1854 my ($self, $irc) = splice @_, 0, 2;
101 40         72 my $line = ${ $_[0] };
  40         95  
102 40         199 my $input = $self->{irc_filter}->get( [$line] )->[0];
103              
104 40 50       1940 return PCI_EAT_NONE if $input->{command} eq 'PING';
105              
106 40         74 for my $wheel_id (keys %{ $self->{wheels} }) {
  40         143  
107 6         60 $self->_send_to_client($wheel_id, $line);
108             }
109              
110 40 100       184 return PCI_EAT_NONE if $self->{stashed};
111              
112 16 50       61 if ($input->{command} =~ /^(?:NOTICE|\d{3})$/) {
113 16         25 push @{ $self->{stash} }, $line;
  16         37  
114             }
115              
116 16 100       53 $self->{stashed} = 1 if $input->{command} =~ /^(?:376|422)$/;
117 16         58 return PCI_EAT_NONE;
118             }
119              
120             sub _send_to_client {
121 23     23   64 my ($self, $wheel_id, $line) = splice @_, 0, 3;
122 23 50       70 return if !defined $self->{wheels}->{ $wheel_id }->{wheel};
123 23 50       54 return if !$self->{wheels}->{ $wheel_id }->{reg};
124              
125 23         71 $self->{wheels}->{ $wheel_id }->{wheel}->put($line);
126 23         1151 return;
127             }
128              
129             sub _close_wheel {
130 1     1   3 my ($self, $wheel_id) = splice @_, 0, 2;
131 1 50       5 return if !defined $self->{wheels}->{ $wheel_id };
132              
133 1         12 delete $self->{wheels}->{ $wheel_id };
134 1         424 $self->{irc}->send_event(irc_proxy_close => $wheel_id);
135 1         236 return;
136             }
137              
138             sub _start {
139 2     2   543 my ($kernel, $self) = @_[KERNEL, OBJECT];
140              
141 2         9 $self->{SESSION_ID} = $_[SESSION]->ID();
142 2         15 $kernel->refcount_increment($self->{SESSION_ID}, __PACKAGE__);
143              
144 2         73 $self->{irc_filter} = POE::Filter::IRCD->new();
145             $self->{ircd_filter} = POE::Filter::Stackable->new(
146             Filters => [
147             POE::Filter::Line->new(),
148             $self->{irc_filter},
149 2         48 ],
150             );
151              
152 2 50       148 if ($self->{irc}->connected()) {
153 0         0 $kernel->yield('_spawn_listener');
154             }
155 2         10 return;
156             }
157              
158             sub _spawn_listener {
159 1     1   99 my $self = $_[OBJECT];
160              
161             $self->{listener} = POE::Wheel::SocketFactory->new(
162             BindAddress => $self->{bindaddress} || 'localhost',
163 1   50     17 BindPort => $self->{bindport} || 0,
      50        
164             SuccessEvent => '_listener_accept',
165             FailureEvent => '_listener_failed',
166             Reuse => 'yes',
167             );
168              
169 1 50       750 if (!$self->{listener}) {
170 0         0 my $irc = $self->{irc};
171 0         0 $irc->plugin_del($self);
172 0         0 return;
173             }
174              
175 1         4 $self->{irc}->send_event(irc_proxy_up => $self->{listener}->getsockname());
176 1         148 return;
177             }
178              
179             sub _listener_accept {
180 1     1   333 my ($self, $socket, $peeradr, $peerport) = @_[OBJECT, ARG0 .. ARG2];
181              
182             my $wheel = POE::Wheel::ReadWrite->new(
183             Handle => $socket,
184             InputFilter => $self->{ircd_filter},
185 1         15 OutputFilter => POE::Filter::Line->new(),
186             InputEvent => '_client_input',
187             ErrorEvent => '_client_error',
188             FlushedEvent => '_client_flush',
189             );
190              
191 1 50       567 if ($wheel) {
192 1         6 my $wheel_id = $wheel->ID();
193 1         8 $self->{wheels}->{ $wheel_id }->{wheel} = $wheel;
194 1         5 $self->{wheels}->{ $wheel_id }->{port} = $peerport;
195 1         11 $self->{wheels}->{ $wheel_id }->{peer} = inet_ntoa( $peeradr );
196 1         4 $self->{wheels}->{ $wheel_id }->{start} = time;
197 1         5 $self->{wheels}->{ $wheel_id }->{reg} = 0;
198 1         3 $self->{wheels}->{ $wheel_id }->{register} = 0;
199 1         7 $self->{irc}->send_event(irc_proxy_connect => $wheel_id);
200             }
201             else {
202 0         0 $self->{irc}->send_event(irc_proxy_rw_fail => inet_ntoa( $peeradr ) => $peerport);
203             }
204              
205 1         145 return;
206             }
207              
208             sub _listener_failed {
209 0     0   0 delete ( $_[OBJECT]->{listener} );
210 0         0 return;
211             }
212              
213             sub _client_flush {
214 5     5   4387 my ($self, $wheel_id) = @_[OBJECT, ARG0];
215              
216 5 50 33     48 return if !defined $self->{wheels}->{ $wheel_id } || !$self->{wheels}->{ $wheel_id }->{quiting};
217 0         0 $self->_close_wheel($wheel_id);
218 0         0 return;
219             }
220              
221             # this code needs refactoring
222             ## no critic (Subroutines::ProhibitExcessComplexity)
223             sub _client_input {
224 12     12   4498 my ($self, $input, $wheel_id) = @_[OBJECT, ARG0, ARG1];
225 12         36 my ($irc, $wheels) = ($self->{irc}, $self->{wheels});
226              
227 12 50       39 return if $wheels->{$wheel_id}{quiting};
228              
229 12 100       33 if ($input->{command} eq 'QUIT') {
230 1         7 $self->_close_wheel($wheel_id);
231 1         21 return;
232             }
233              
234 11 100 66     39 if ($input->{command} eq 'PASS' && $wheels->{$wheel_id}{reg} < 2) {
235 1         3 $wheels->{$wheel_id}{pass} = $input->{params}[0];
236             }
237              
238 11 100 66     33 if ($input->{command} eq 'NICK' && $wheels->{$wheel_id}{reg} < 2) {
239 1         5 $wheels->{$wheel_id}{nick} = $input->{params}[0];
240 1         3 $wheels->{$wheel_id}{register}++;
241             }
242              
243 11 100 66     32 if ($input->{command} eq 'USER' && $wheels->{$wheel_id}{reg} < 2) {
244 1         5 $wheels->{$wheel_id}{user} = $input->{params}[0];
245 1         2 $wheels->{$wheel_id}{register}++;
246             }
247              
248 11 100 100     43 if (!$wheels->{$wheel_id}{reg} && $wheels->{$wheel_id}{register} >= 2) {
249 1         7 my $password = delete $wheels->{$wheel_id}{pass};
250 1         5 $wheels->{$wheel_id}{reg} = 1;
251              
252 1 50 33     9 if (!$password || $password ne $self->{password}) {
253             $self->_send_to_client($wheel_id,
254             'ERROR :Closing Link: * ['
255             . ($wheels->{$wheel_id}{user} || 'unknown')
256             . '@' . $wheels->{$wheel_id}{peer}
257 0   0     0 . '] (Unauthorised connection)'
258             );
259 0         0 $wheels->{$wheel_id}{quiting}++;
260 0         0 return;
261             }
262              
263 1         5 my $nickname = $irc->nick_name();
264 1         6 my $fullnick = $irc->nick_long_form($nickname);
265 1 50       7 if ($nickname ne $wheels->{$wheel_id}{nick}) {
266 0         0 $self->_send_to_client($wheel_id, "$wheels->{$wheel_id}{nick} NICK :$nickname");
267             }
268              
269 1         2 for my $line (@{ $self->{stash} }) {
  1         130  
270 16         40 $self->_send_to_client($wheel_id, $line);
271             }
272              
273 1         7 for my $channel ($irc->nick_channels($nickname)) {
274 1         8 $self->_send_to_client($wheel_id, ":$fullnick JOIN $channel");
275 1         6 $irc->yield(names => $channel);
276 1         125 $irc->yield(topic => $channel);
277             }
278              
279 1         110 $irc->send_event(irc_proxy_authed => $wheel_id);
280 1         111 return;
281             }
282              
283 10 100       43 return if !$wheels->{$wheel_id}{reg};
284              
285 4 50       15 if ($input->{command} =~ /^(?:NICK|USER|PASS)$/) {
286 0         0 return;
287             }
288              
289 4 50       13 if ($input->{command} eq 'PING') {
290 0         0 $self->_send_to_client($wheel_id, "PONG $input->{params}[0]");
291 0         0 return;
292             }
293              
294 4 50 33     15 if ($input->{command} eq 'PONG' and $input->{params}[0] =~ /^[0-9]+$/) {
295 0         0 $wheels->{$wheel_id}{lag} = time() - $input->{params}[0];
296 0         0 return;
297             }
298              
299 4         19 $irc->yield(quote => $input->{raw_line});
300 4         538 return;
301             }
302              
303             sub _client_error {
304 0     0   0 my ($self, $wheel_id) = @_[OBJECT, ARG3];
305              
306 0         0 $self->_close_wheel($wheel_id);
307 0         0 return;
308             }
309              
310             sub _shutdown {
311 5     5   3274 my $self = $_[OBJECT];
312 5   66     30 my $irc = $self->{irc} || $_[ARG0];
313              
314 5         24 my $mysockaddr = $self->getsockname();
315 5         41 delete $self->{listener};
316              
317 5         276 for my $wheel_id ( $self->list_wheels() ) {
318 0         0 $self->_close_wheel( $wheel_id );
319             }
320 5         13 delete $self->{wheels};
321 5         29 $irc->send_event(irc_proxy_down => $mysockaddr);
322              
323 5         643 return;
324             }
325              
326             sub getsockname {
327 5     5 1 11 my ($self) = @_;
328 5 100       39 return if !$self->{listener};
329 1         11 return $self->{listener}->getsockname();
330             }
331              
332             sub list_wheels {
333 5     5 1 19 my ($self) = @_;
334 5         10 return keys %{ $self->{wheels} };
  5         28  
335             }
336              
337             sub wheel_info {
338 0     0 1   my ($self, $wheel_id) = @_;
339 0 0         return if !defined $self->{wheels}->{ $wheel_id };
340 0 0         return $self->{wheels}->{ $wheel_id }->{start} if !wantarray;
341 0           return map { $self->{wheels}->{ $wheel_id }->{$_} } qw(peer port start lag);
  0            
342             }
343              
344             1;
345              
346             =encoding utf8
347              
348             =head1 NAME
349              
350             POE::Component::IRC::Plugin::Proxy - A PoCo-IRC plugin that provides a
351             lightweight IRC proxy/bouncer
352              
353             =head1 SYNOPSIS
354              
355             use strict;
356             use warnings;
357             use POE qw(Component::IRC::State Component::IRC::Plugin::Proxy Component::IRC::Plugin::Connector);
358              
359             my $irc = POE::Component::IRC::State->spawn();
360              
361             POE::Session->create(
362             package_states => [
363             main => [ qw(_start) ],
364             ],
365             heap => { irc => $irc },
366             );
367              
368             $poe_kernel->run();
369              
370             sub _start {
371             my ($kernel, $heap) = @_[KERNEL, HEAP];
372             $heap->{irc}->yield( register => 'all' );
373             $heap->{proxy} = POE::Component::IRC::Plugin::Proxy->new( bindport => 6969, password => "m00m00" );
374             $heap->{irc}->plugin_add( 'Connector' => POE::Component::IRC::Plugin::Connector->new() );
375             $heap->{irc}->plugin_add( 'Proxy' => $heap->{proxy} );
376             $heap->{irc}->yield ( connect => { Nick => 'testbot', Server => 'someserver.com' } );
377             return;
378             }
379              
380             =head1 DESCRIPTION
381              
382             POE::Component::IRC::Plugin::Proxy is a L
383             plugin that provides lightweight IRC proxy/bouncer server to your
384             L bots. It enables multiple IRC
385             clients to be hidden behind a single IRC client-server connection.
386              
387             Spawn a L session and add in a
388             POE::Component::IRC::Plugin::Proxy plugin object, specifying a bindport and a
389             password the connecting IRC clients have to use. When the component is
390             connected to an IRC network a listening port is opened by the plugin for
391             multiple IRC clients to connect.
392              
393             Neat, huh? >;o)
394              
395             This plugin will activate L's raw
396             events (L|POE::Component::IRC/irc_raw>) by calling
397             C<< $irc->raw_events(1) >>.
398              
399             This plugin requires the IRC component to be
400             L or a subclass thereof.
401              
402             =head1 METHODS
403              
404             =head2 C
405              
406             Takes a number of arguments:
407              
408             B<'password'>, the password to require from connecting clients;
409              
410             B<'bindaddress'>, a local address to bind the listener to, default is 'localhost';
411              
412             B<'bindport'>, what port to bind to, default is 0, ie. randomly allocated by OS;
413              
414             Returns an object suitable for passing to
415             L's C method.
416              
417             =head2 C
418              
419             Takes no arguments. Accesses the listeners C method. See
420             L for details of the
421             return value;
422              
423             =head2 C
424              
425             Takes no arguments. Returns a list of wheel ids of the current connected clients.
426              
427             =head2 C
428              
429             Takes one parameter, a wheel ID to query. Returns undef if an invalid wheel id
430             is passed. In a scalar context returns the time that the client connected in
431             unix time. In a list context returns a list consisting of the peer address,
432             port, tthe connect time and the lag in seconds for that connection.
433              
434             =head1 OUTPUT EVENTS
435              
436             The plugin emits the following L
437             events:
438              
439             =head2 C
440              
441             Emitted when the listener is successfully started. C is the result of the
442             listener C.
443              
444             =head2 C
445              
446             Emitted when a client connects to the listener. C is the wheel ID of the
447             client.
448              
449             =head2 C
450              
451             Emitted when the L fails on a
452             connection. C is the wheel ID of the client.
453              
454             =head2 C
455              
456             Emitted when a connecting client successfully negotiates an IRC session with
457             the plugin. C is the wheel ID of the client.
458              
459             =head2 C
460              
461             Emitted when a connected client disconnects. C is the wheel ID of the
462             client.
463              
464             =head2 C
465              
466             Emitted when the listener is successfully shutdown. C is the result of the
467             listener C.
468              
469             =head1 QUIRKS
470              
471             Connecting IRC clients will not be able to change nickname. This is a feature.
472              
473             =head1 AUTHOR
474              
475             Chris 'BinGOs' Williams
476              
477             =head1 SEE ALSO
478              
479             L
480              
481             L
482              
483             =cut