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