File Coverage

blib/lib/POEx/IRC/Backend.pm
Criterion Covered Total %
statement 185 276 67.0
branch 65 138 47.1
condition 17 59 28.8
subroutine 38 51 74.5
pod 11 13 84.6
total 316 537 58.8


line stmt bran cond sub pod time code
1             package POEx::IRC::Backend;
2             $POEx::IRC::Backend::VERSION = '0.030001';
3 2     2   136514 use strictures 2;
  2         3670  
  2         111  
4              
5 2     2   484 use Carp;
  2         4  
  2         195  
6 2     2   14 use Scalar::Util 'blessed';
  2         5  
  2         142  
7              
8 2     2   1610 use IRC::Message::Object ();
  2         499361  
  2         85  
9              
10 2     2   1110 use Net::IP::Minimal 'ip_is_ipv6';
  2         1276  
  2         163  
11              
12 2         23 use POE qw/
13             Wheel::ReadWrite
14             Wheel::SocketFactory
15              
16             Filter::Stackable
17             Filter::IRCv3
18             Filter::Line
19 2     2   12 /;
  2         3  
20              
21 2         205 use Socket qw/
22             AF_INET AF_INET6
23             pack_sockaddr_in
24              
25             getnameinfo
26             NI_NUMERICHOST
27             NI_NUMERICSERV
28             NIx_NOSERV
29 2     2   33625 /;
  2         12  
30              
31 2     2   14 use Types::Standard -all;
  2         4  
  2         32  
32 2     2   65269 use Types::TypeTiny -all;
  2         6  
  2         15  
33              
34 2     2   2967 use Try::Tiny;
  2         2295  
  2         144  
35              
36 2     2   905 use POEx::IRC::Backend::Connect;
  2         10  
  2         116  
37 2     2   927 use POEx::IRC::Backend::Connector;
  2         8  
  2         72  
38 2     2   733 use POEx::IRC::Backend::Listener;
  2         5  
  2         369  
39              
40              
41 4     4 0 29 sub RUNNING_IN_HELL () { $^O =~ /(cygwin|MSWin32)/ }
42              
43             sub get_unpacked_addr {
44 10     10 0 77 my ($sock_packed, %params) = @_;
45 10 100       116 my ($err, $addr, $port) = getnameinfo $sock_packed,
46             NI_NUMERICHOST | NI_NUMERICSERV,
47             ( $params{noserv} ? NIx_NOSERV : () );
48 10 50       32 croak $err if $err;
49 10 100       41 $params{noserv} ? $addr : ($addr, $port)
50             }
51              
52              
53 2     2   11 use Moo 2;
  2         50  
  2         15  
54             with 'POEx::IRC::Backend::Role::CheckAvail';
55              
56              
57             has session_id => (
58             init_arg => undef,
59             lazy => 1,
60             is => 'ro',
61             writer => '_set_session_id',
62             );
63              
64             has controller => (
65             ## Session ID for controller session
66             ## Typically set by 'register' event, though it doesn't have to be:
67             lazy => 1,
68             is => 'ro',
69             writer => '_set_controller',
70             predicate => 'has_controller',
71             );
72              
73             has filter_irc => (
74             lazy => 1,
75             isa => InstanceOf['POE::Filter'],
76             is => 'ro',
77             default => sub { POE::Filter::IRCv3->new },
78             );
79              
80             has filter_line => (
81             lazy => 1,
82             isa => InstanceOf['POE::Filter'],
83             is => 'ro',
84             default => sub {
85             POE::Filter::Line->new(
86             InputRegexp => '\015?\012',
87             OutputLiteral => "\015\012",
88             )
89             },
90             );
91              
92             has filter => (
93             lazy => 1,
94             isa => InstanceOf['POE::Filter'],
95             is => 'ro',
96             default => sub {
97             my ($self) = @_;
98             POE::Filter::Stackable->new(
99             Filters => [ $self->filter_line, $self->filter_irc ],
100             );
101             },
102             );
103              
104             has listeners => (
105             ## POEx::IRC::Backend::Listener objs
106             ## These are listeners for a particular port.
107             init_arg => undef,
108             is => 'ro',
109             isa => HashRef,
110             writer => '_set_listeners',
111             default => sub { +{} },
112             );
113              
114             has connectors => (
115             ## POEx::IRC::Backend::Connector objs
116             ## These are outgoing (peer) connectors.
117             init_arg => undef,
118             is => 'ro',
119             isa => HashRef,
120             writer => '_set_connectors',
121             default => sub { +{} },
122             );
123              
124             has wheels => (
125             ## POEx::IRC::Backend::Connect objs
126             ## These are our connected wheels.
127             init_arg => undef,
128             is => 'ro',
129             isa => HashRef,
130             writer => '_set_wheels',
131             default => sub { +{} },
132             );
133              
134             has ssl_context => (
135             lazy => 1,
136             is => 'ro',
137             writer => '_set_ssl_context',
138             default => sub { undef },
139             );
140              
141              
142             sub spawn {
143 2     2 1 995 my ($class, %args) = @_;
144 2         5 my $ssl_opts = delete $args{ssl_opts};
145 2 50       13 my $self = blessed $class ? $class : $class->new(%args);
146              
147 2 50       63 POE::Session->create(
148             object_states => [
149             $self => {
150             _start => '_start',
151             _stop => '_stop',
152              
153             register => '_register_controller',
154             shutdown => '_shutdown',
155             create_connector => '_create_connector',
156             create_listener => '_create_listener',
157             remove_listener => '_remove_listener',
158             send => '_send',
159              
160             _accept_conn_v4 => '_accept_conn',
161             _accept_conn_v6 => '_accept_conn',
162             _accept_fail => '_accept_fail',
163             _idle_alarm => '_idle_alarm',
164              
165             _connector_up_v4 => '_connector_up',
166             _connector_up_v6 => '_connector_up',
167             _connector_failed => '_connector_failed',
168              
169             _ircsock_input => '_ircsock_input',
170             _ircsock_error => '_ircsock_error',
171             _ircsock_flushed => '_ircsock_flushed',
172             },
173             ],
174             ) or confess "Failed to spawn POE::Session";
175              
176 2 100       303 if (defined $ssl_opts) {
177 1 50       5 confess "expected ssl_opts to be an ARRAY but got $ssl_opts"
178             unless ref $ssl_opts eq 'ARRAY';
179 1         2 my $ssl_err;
180             try {
181 1 50   1   53 die "Failed to load POE::Component::SSLify" unless $self->has_ssl_support;
182 1         6 $self->_set_ssl_context(
183             POE::Component::SSLify::SSLify_ContextCreate( @$ssl_opts )
184             );
185 1         1110 1
186             } catch {
187 0     0   0 $ssl_err = $_;
188             undef
189 1 50       19 } or confess "SSLify failure: $ssl_err";
  0         0  
190             }
191              
192             $self
193 2         22 }
194              
195             sub _start {
196 2     2   547 my ($kernel, $self) = @_[KERNEL, OBJECT];
197 2         9 $self->_set_session_id( $_[SESSION]->ID );
198 2         20 $kernel->refcount_increment( $self->session_id, "IRCD Running" );
199             }
200              
201 2     2   1545 sub _stop {}
202              
203             sub shutdown {
204 1     1 1 429 my $self = shift;
205 1         6 $poe_kernel->post( $self->session_id => shutdown => @_ )
206             }
207              
208             sub _shutdown {
209 2     2   946 my ($kernel, $self) = @_[KERNEL, OBJECT];
210              
211 2         14 $kernel->refcount_decrement( $self->session_id => "IRCD Running" );
212 2         76 $kernel->refcount_decrement( $self->controller => "IRCD Running" );
213              
214             ## _disconnected should also clear our alarms.
215 2         40 $self->_disconnected($_, "Server shutdown") for keys %{ $self->wheels };
  2         19  
216              
217 2         7 for my $attr (map {; '_set_'.$_ } qw/ listeners connectors wheels /) {
  6         13  
218 6         2111 $self->$attr(+{})
219             }
220             }
221              
222             sub _register_controller {
223 2     2   556 my ($kernel, $self) = @_[KERNEL, OBJECT];
224              
225 2 50       10 $kernel->refcount_decrement( $self->controller => "IRCD Running" )
226             if $self->has_controller;
227 2         7 $self->_set_controller( $_[SENDER]->ID );
228 2         21 $kernel->refcount_increment( $self->controller => "IRCD Running" );
229              
230 2         44 $kernel->post( $self->controller => ircsock_registered => $self );
231             }
232              
233             sub _idle_alarm {
234 0     0   0 my ($kernel, $self, $w_id) = @_[KERNEL, OBJECT, ARG0];
235 0   0     0 my $this_conn = $self->wheels->{$w_id} || return;
236              
237 0         0 $kernel->post( $self->controller => ircsock_connection_idle => $this_conn );
238              
239 0         0 $this_conn->alarm_id(
240             $kernel->delay_set( _idle_alarm => $this_conn->idle, $w_id )
241             );
242             }
243              
244             sub create_listener {
245 2     2 1 175 my $self = shift;
246 2         11 $poe_kernel->post( $self->session_id => create_listener => @_ );
247 2         133 $self
248             }
249              
250             sub _create_listener {
251 2     2   266 my ($kernel, $self, %args) = @_[KERNEL, OBJECT, ARG0 .. $#_];
252 2         17 $args{lc $_} = delete $args{$_} for keys %args;
253              
254 2   50     11 my $bindaddr = delete $args{bindaddr} || '0.0.0.0';
255 2   50     13 my $bindport = delete $args{port} || 0;
256              
257 2 50 33     16 my $protocol = ( delete $args{ipv6} || ip_is_ipv6($bindaddr) ) ? 6 : 4;
258              
259 2 50       47 my $wheel = POE::Wheel::SocketFactory->new(
    50          
260             SocketDomain => ($protocol == 6 ? AF_INET6 : AF_INET),
261             BindAddress => $bindaddr,
262             BindPort => $bindport,
263             SuccessEvent =>
264             ( $protocol == 6 ? '_accept_conn_v6' : '_accept_conn_v4' ),
265             FailureEvent => '_accept_fail',
266             Reuse => 1,
267             );
268              
269 2         887 my $id = $wheel->ID;
270              
271 2 50 50     47 my $listener = POEx::IRC::Backend::Listener->new(
      100        
272             protocol => $protocol,
273             wheel => $wheel,
274             addr => $bindaddr,
275             port => $bindport,
276             idle => ( delete($args{idle}) || 180 ),
277             ssl => ( delete($args{ssl}) || 0 ),
278             ( keys %args ? (args => \%args) : () ),
279             );
280              
281 2         1107 $self->listeners->{$id} = $listener;
282              
283             ## Real bound port/addr
284 2         10 my (undef, $port) = get_unpacked_addr( $wheel->getsockname );
285 2 50       17 $listener->set_port($port) if $port;
286              
287 2         54 $kernel->post( $self->controller => ircsock_listener_created => $listener )
288             }
289              
290             sub remove_listener {
291 2     2 1 4264 my $self = shift;
292 2         16 $poe_kernel->post( $self->session_id => remove_listener => @_ );
293 2         139 $self
294             }
295              
296             sub _remove_listener {
297 2     2   222 my ($kernel, $self) = @_[KERNEL, OBJECT];
298 2         12 my %args = @_[ARG0 .. $#_];
299 2         20 $args{lc $_} = delete $args{$_} for keys %args;
300              
301 2 50 33     13 if (defined $args{listener} && $self->listeners->{ $args{listener} }) {
302 0         0 my $listener = delete $self->listeners->{ $args{listener} };
303 0         0 $listener->clear_wheel;
304 0         0 $kernel->post( $self->controller =>
305             ircsock_listener_removed => $listener
306             );
307             return
308 0         0 }
309              
310 2         3 my @removed;
311              
312 2         3 LISTENER: for my $id (keys %{ $self->listeners }) {
  2         13  
313 2         11 my $listener = $self->listeners->{$id};
314 2 50 33     44 if (defined $args{port} && defined $args{addr}) {
    50 33        
    0 0        
315 0 0 0     0 if ($args{addr} eq $listener->addr && $args{port} eq $listener->port) {
316 0         0 delete $self->listeners->{$id};
317 0         0 push @removed, $listener;
318             next LISTENER
319 0         0 }
320             } elsif (defined $args{addr} && $args{addr} eq $listener->addr) {
321 2         7 delete $self->listeners->{$id};
322 2         6 push @removed, $listener;
323             } elsif (defined $args{port} && $args{port} eq $listener->port) {
324 0         0 delete $self->listeners->{$id};
325 0         0 push @removed, $listener;
326             }
327             }
328              
329 2         5 for my $listener (@removed) {
330 2         10 $listener->clear_wheel;
331 2         1150 $kernel->post( $self->controller =>
332             ircsock_listener_removed => $listener
333             );
334             }
335             }
336              
337             sub _accept_fail {
338 0     0   0 my ($kernel, $self) = @_[KERNEL, OBJECT];
339 0         0 my ($op, $errnum, $errstr, $listener_id) = @_[ARG0 .. ARG3];
340              
341 0         0 my $listener = delete $self->listeners->{$listener_id};
342 0 0       0 if ($listener) {
343 0         0 $listener->clear_wheel;
344 0         0 $kernel->post( $self->controller =>
345             ircsock_listener_failure => $listener, $op, $errnum, $errstr
346             );
347             }
348             }
349              
350             sub _accept_conn {
351             ## Accepted connection to a listener.
352 2     2   530 my ($self, $sock, $p_addr, $p_port, $listener_id) = @_[OBJECT, ARG0 .. ARG3];
353              
354 2 50       18 my ($protocol, $un_p_addr) = $_[STATE] eq '_accept_conn_v6' ?
355             ( 6, $p_addr )
356             : ( 4,
357             get_unpacked_addr( pack_sockaddr_in($p_port, $p_addr), noserv => 1 )
358             )
359             ;
360              
361 2         11 my $listener = $self->listeners->{$listener_id};
362 2         10 my $using_ssl = $listener->ssl;
363 2 100       11 if ($using_ssl) {
364             try {
365 1 50   1   45 die "Failed to load POE::Component::SSLify" unless $self->has_ssl_support;
366 1         5 $sock = POE::Component::SSLify::Server_SSLify($sock, $self->ssl_context);
367             } catch {
368 0     0   0 warn "Could not SSLify (server) socket: $_";
369             undef
370 1 50       10 } or return;
  0         0  
371             }
372              
373 2         560 my $wheel = POE::Wheel::ReadWrite->new(
374             Handle => $sock,
375             Filter => $self->filter,
376             InputEvent => '_ircsock_input',
377             ErrorEvent => '_ircsock_error',
378             FlushedEvent => '_ircsock_flushed',
379             );
380              
381 2 50       1096 unless ($wheel) {
382 0         0 warn "Wheel creation failure in _accept_conn";
383             return
384 0         0 }
385              
386 2 100       18 my ($sockaddr, $sockport) = get_unpacked_addr(
387             getsockname(
388             $using_ssl ? POE::Component::SSLify::SSLify_GetSocket($sock) : $sock
389             )
390             );
391              
392 2         13 my $w_id = $wheel->ID;
393 2 50       33 my $this_conn = $self->wheels->{$w_id} =
394             POEx::IRC::Backend::Connect->new(
395             ($listener->has_args ? (args => $listener->args) : () ),
396             protocol => $protocol,
397             wheel => $wheel,
398             peeraddr => $un_p_addr,
399             peerport => $p_port,
400             sockaddr => $sockaddr,
401             sockport => $sockport,
402             seen => time,
403             idle => $listener->idle,
404             ssl => $using_ssl,
405             );
406              
407 2         198 $this_conn->alarm_id(
408             $poe_kernel->delay_set( _idle_alarm => $this_conn->idle, $w_id )
409             );
410              
411 2         2276 $poe_kernel->post( $self->controller =>
412             ircsock_listener_open => $this_conn, $listener
413             );
414             }
415              
416             sub create_connector {
417 2     2 1 1765 my $self = shift;
418 2         17 $poe_kernel->post( $self->session_id => create_connector => @_ );
419 2         172 $self
420             }
421              
422             sub _create_connector {
423             ## Connector; try to spawn socket <-> remote peer
424             ## remoteaddr =>
425             ## remoteport =>
426             ## [optional]
427             ## bindaddr =>
428             ## ipv6 =>
429             ## ssl =>
430             ## ... other args get added to ->args()
431 2     2   197 my (undef, $self) = @_[KERNEL, OBJECT];
432 2         14 my %args = @_[ARG0 .. $#_];
433              
434 2         20 $args{lc $_} = delete $args{$_} for keys %args;
435              
436 2         5 my $remote_addr = delete $args{remoteaddr};
437 2         4 my $remote_port = delete $args{remoteport};
438              
439 2 50 33     15 die "create_connector expects a remoteaddr and remoteport\n"
440             unless defined $remote_addr and defined $remote_port;
441              
442 2 50 33     19 my $protocol =
    50          
    50          
443             delete $args{ipv6} ? 6
444             : ip_is_ipv6($remote_addr) ? 6
445             : ( $args{bindaddr} && ip_is_ipv6($args{bindaddr}) ) ? 6
446             : 4;
447              
448 2 50       80 my $wheel = POE::Wheel::SocketFactory->new(
    50          
    50          
449             SocketDomain => ($protocol == 6 ? AF_INET6 : AF_INET),
450             SocketProtocol => 'tcp',
451              
452             RemoteAddress => $remote_addr,
453             RemotePort => $remote_port,
454              
455             FailureEvent => '_connector_failed',
456             SuccessEvent =>
457             ( $protocol == 6 ? '_connector_up_v6' : '_connector_up_v4' ),
458              
459             (
460             defined $args{bindaddr} ?
461             ( BindAddress => delete $args{bindaddr} ) : ()
462             ),
463             );
464              
465 2         956 my $id = $wheel->ID;
466              
467 2 100       42 $self->connectors->{$id} = POEx::IRC::Backend::Connector->new(
    50          
    50          
468             wheel => $wheel,
469             addr => $remote_addr,
470             port => $remote_port,
471             protocol => $protocol,
472              
473             (defined $args{ssl} ?
474             (ssl => delete $args{ssl}) : () ),
475              
476             (defined $args{bindaddr} ?
477             (bindaddr => delete $args{bindaddr}) : () ),
478              
479             ## Attach any extra args to Connector->args()
480             (keys %args ?
481             (args => \%args) : () ),
482             );
483             }
484              
485              
486             sub _connector_up {
487 2     2   916 my ($kernel, $self, $sock, $p_addr, $p_port, $c_id)
488             = @_[KERNEL, OBJECT, ARG0 .. ARG3];
489              
490 2         2 my ($protocol, $un_p_addr);
491 2 50       10 if ($_[STATE] eq '_connector_up_v6') {
492 0         0 $protocol = 6;
493 0         0 $un_p_addr = $p_addr;
494             } else {
495 2         4 $protocol = 4;
496 2         14 $un_p_addr = get_unpacked_addr(
497             pack_sockaddr_in($p_port, $p_addr), noserv => 1
498             );
499             }
500              
501             ## No need to try to connect out any more; remove from connectors pool:
502 2         19 my $ct = delete $self->connectors->{$c_id};
503              
504 2         4 my $using_ssl;
505 2 100       12 if ( $ct->ssl ) {
506             try {
507 1 50   1   43 die "Failed to load POE::Component::SSLify" unless $self->has_ssl_support;
508 1         27 $sock = POE::Component::SSLify::Client_SSLify(
509             $sock, undef, undef, $self->ssl_context
510             );
511 1         268 $using_ssl = 1
512             } catch {
513 0     0   0 warn "Could not SSLify (client) socket: $_\n";
514             undef
515 1 50       10 } or return;
  0         0  
516             }
517              
518 2         73 my $wheel = POE::Wheel::ReadWrite->new(
519             Handle => $sock,
520             InputEvent => '_ircsock_input',
521             ErrorEvent => '_ircsock_error',
522             FlushedEvent => '_ircsock_flushed',
523             Filter => POE::Filter::Stackable->new(
524             Filters => [ $self->filter ],
525             )
526             );
527              
528 2 50       619 unless ($wheel) {
529 0         0 warn "Wheel creation failure in _connector_up";
530             return
531 0         0 }
532              
533 2 100       18 my ($sockaddr, $sockport) = get_unpacked_addr(
534             getsockname(
535             $using_ssl ? POE::Component::SSLify::SSLify_GetSocket($sock) : $sock
536             )
537             );
538              
539 2 50       19 my $this_conn = POEx::IRC::Backend::Connect->new(
540             ($ct->has_args ? (args => $ct->args) : () ),
541             protocol => $protocol,
542             wheel => $wheel,
543             peeraddr => $un_p_addr,
544             peerport => $p_port,
545             sockaddr => $sockaddr,
546             sockport => $sockport,
547             seen => time,
548             ssl => $using_ssl,
549             );
550              
551 2         107 $self->wheels->{ $wheel->ID } = $this_conn;
552              
553 2         15 $kernel->post( $self->controller =>
554             ircsock_connector_open => $this_conn
555             );
556             }
557              
558             sub _connector_failed {
559 0     0   0 my ($kernel, $self) = @_[KERNEL, OBJECT];
560 0         0 my ($op, $errno, $errstr, $c_id) = @_[ARG0 .. ARG3];
561              
562 0         0 my $ct = delete $self->connectors->{$c_id};
563 0         0 $ct->clear_wheel;
564              
565 0         0 $kernel->post( $self->controller =>
566             ircsock_connector_failure => $ct, $op, $errno, $errstr
567             );
568             }
569              
570             ## _ircsock_* handlers talk to endpoints via listeners/connectors
571              
572             sub _ircsock_input {
573             # ($input, $w_id) = @_[ARG0, ARG1];
574 6     6   5825 my $this_conn = $_[OBJECT]->wheels->{ $_[ARG1] };
575 6         111 $this_conn->seen( time );
576 6 100       1020 $poe_kernel->delay_adjust( $this_conn->alarm_id, $this_conn->idle )
577             if $this_conn->has_alarm_id;
578              
579 6         152 $poe_kernel->post( $_[OBJECT]->controller =>
580 6         556 ircsock_input => $this_conn, IRC::Message::Object->new(%{ $_[ARG0] })
581             );
582             }
583              
584             sub _ircsock_error {
585             ## Lost someone.
586 0     0   0 my (undef, $self) = @_[KERNEL, OBJECT];
587 0         0 my ($errstr, $w_id) = @_[ARG2, ARG3];
588              
589 0   0     0 my $this_conn = $self->wheels->{$w_id} || return;
590              
591 0   0     0 $self->_disconnected(
592             $w_id,
593             $errstr || $this_conn->is_disconnecting
594             );
595             }
596              
597             sub _ircsock_flushed {
598             ## Socket's been flushed; we may have something to do.
599 4     4   4943 my ($self, $w_id) = @_[OBJECT, ARG0];
600              
601 4   50     25 my $this_conn = $self->wheels->{$w_id} || return;
602              
603 4 50       68 if ($this_conn->is_disconnecting) {
604 0         0 return $self->_disconnected( $w_id, $this_conn->is_disconnecting )
605             }
606              
607 4 50       1360 if ($this_conn->is_pending_compress) {
608 0         0 return $self->set_compressed_link_now($w_id)
609             }
610             }
611              
612             sub _send {
613             ## POE bridge to send()
614 0     0   0 $_[OBJECT]->send(@_[ARG0 .. $#_ ]);
615             }
616              
617             ## Methods.
618              
619             sub send {
620             ## ->send(HASH, ID [, ID .. ])
621 6     6 1 13536 my ($self, $out, @ids) = @_;
622              
623 6 100 66     58 if (blessed $out && $out->isa('IRC::Message::Object')) {
624             # breaks encapsulation for performance reasons:
625             $out = +{
626             command => $out->command,
627             (
628 4 100       18 map {; exists $out->{$_} ? ($_ => $out->{$_}) : () }
  16         47  
629             qw/ colonify prefix params tags /
630             ),
631             };
632             }
633              
634             confess
635 6 50 33     36 "send() takes a HASH or IRC::Message::Object and a list of connection IDs"
636             unless ref $out eq 'HASH' and @ids;
637              
638 6         9 TARGET: for my $target (@ids) {
639             # FIXME tests/docs wrt passing in Connect objs
640 6 100       24 $target = $target->wheel_id if blessed $target;
641 6   50     104 ($self->wheels->{$target} || next TARGET)->wheel->put($out)
642             }
643              
644             $self
645 6         619 }
646              
647             sub disconnect {
648             # Mark a wheel for disconnection at next flush.
649 0     0 1 0 my ($self, $w_id, $str) = @_;
650 0 0       0 $w_id = $w_id->wheel_id if blessed $w_id;
651 0 0       0 confess "disconnect() needs an (extant) wheel ID or Connect object"
652             unless defined $w_id;
653              
654             # Application code should probably check $conn->has_wheel before trying to
655             # call a ->disconnect, but if not, it's hard to determine if we were passed
656             # junk or just racing against an already-gone wheel:
657 0 0       0 unless (defined $self->wheels->{$w_id}) {
658 0         0 carp "Attempting to disconnect() unknown wheel '$w_id'\n",
659             " This warning may be spurious. Your wheel may have died of natural causes.\n",
660             " Try checking '\$conn->has_wheel' before calling disconnect()." ;
661             return
662 0         0 }
663              
664 0   0     0 $self->wheels->{$w_id}->is_disconnecting($str // "Client disconnect");
665 0         0 $self
666             }
667              
668             sub disconnect_now {
669 0     0 1 0 my ($self, $w_id, $str) = @_;
670 0 0       0 $w_id = $w_id->wheel_id if blessed $w_id;
671 0 0       0 confess "disconnect_now needs an (extant) wheel ID or Connect object"
672             unless defined $w_id;
673              
674 0 0       0 unless (defined $self->wheels->{$w_id}) {
675 0         0 carp "Attempting to disconnect() unknown wheel '$w_id'\n",
676             " This warning may be spurious. Your wheel may have died of natural causes.\n",
677             " Try checking '\$conn->has_wheel' before calling disconnect()." ;
678             return
679 0         0 }
680              
681 0   0     0 $self->_disconnected($w_id, $str // "Client disconnect");
682 0         0 $self
683             }
684              
685             sub _disconnected {
686             ## Wheel needs cleanup.
687 4     4   7 my ($self, $w_id, $str) = @_;
688 4 50 33     28 return unless $w_id and $self->wheels->{$w_id};
689              
690 4         11 my $this_conn = delete $self->wheels->{$w_id};
691              
692             ## Idle timer cleanup
693 4 100       60 $poe_kernel->alarm_remove( $this_conn->alarm_id )
694             if $this_conn->has_alarm_id;
695              
696 4 50       192 if (RUNNING_IN_HELL) {
697 0         0 $this_conn->wheel->shutdown_input;
698 0         0 $this_conn->wheel->shutdown_output;
699             }
700              
701             ## Higher layers may still have a $conn object bouncing about.
702             ## They should check ->has_wheel to determine if the Connect obj
703             ## has been disconnected (no longer has a wheel).
704 4         15 $this_conn->clear_wheel;
705              
706 4         1273 $poe_kernel->post( $self->controller =>
707             ircsock_disconnect => $this_conn, $str
708             );
709              
710 4         248 1
711             }
712              
713             sub set_compressed_link {
714 0     0 1   my ($self, $w_id) = @_;
715 0 0         confess "set_compressed_link() needs a wheel ID"
716             unless defined $w_id;
717              
718 0 0         confess "Failed to load POE::Filter::Zlib::Stream"
719             unless $self->has_zlib_support;
720              
721 0 0         unless ($self->wheels->{$w_id}) {
722 0           carp "set_compressed_link for nonexistant wheel '$w_id'";
723             return
724 0           }
725              
726 0           $self->wheels->{$w_id}->is_pending_compress(1);
727              
728 0           $self
729             }
730              
731             sub set_compressed_link_now {
732 0     0 1   my ($self, $w_id) = @_;
733 0 0         confess "set_compressed_link() needs a wheel ID"
734             unless defined $w_id;
735            
736 0           my $this_conn = $self->wheels->{$w_id};
737 0 0         unless (defined $this_conn) {
738 0           carp "set_compressed_link_now for nonexistant wheel '$w_id'";
739             return
740 0           }
741              
742 0 0         confess "Failed to load POE::Filter::Zlib::Stream"
743             unless $self->has_zlib_support;
744              
745 0           $this_conn->wheel->get_input_filter->unshift(
746             POE::Filter::Zlib::Stream->new
747             );
748              
749 0           $this_conn->is_pending_compress(0);
750 0           $this_conn->set_compressed(1);
751              
752 0           $poe_kernel->post( $self->controller =>
753             ircsock_compressed => $this_conn
754             );
755              
756 0           $self
757             }
758              
759             sub unset_compressed_link {
760 0     0 1   my ($self, $w_id) = @_;
761 0 0         confess "unset_compressed_link() needs a wheel ID"
762             unless defined $w_id;
763              
764 0           my $this_conn = $self->wheels->{$w_id};
765 0 0 0       unless (defined $this_conn && $this_conn->compressed) {
766 0           carp
767             "unset_compressed_link on uncompressed or nonexistant wheel '$w_id'";
768             return
769 0           }
770              
771 0           $this_conn->wheel->get_input_filter->shift;
772 0           $this_conn->set_compressed(0);
773              
774 0           $self
775             }
776              
777             ## FIXME listener connect ip blacklist?
778              
779 2     2   6978 no warnings 'void';
  2         4  
  2         296  
780             print
781             qq[ pretend for a moment that I'm stuck with mysql\n],
782             qq[ ok, fetching my laughing hat and monocle\n],
783             unless caller; 1;
784              
785              
786             =pod
787              
788             =for Pod::Coverage has_\w+ RUNNING_IN_HELL get_unpacked_addr
789              
790             =head1 NAME
791              
792             POEx::IRC::Backend - IRC client or server backend
793              
794             =head1 SYNOPSIS
795              
796             use POE;
797             use POEx::IRC::Backend;
798              
799             POE::Session->create(
800             package_states => [
801             main => [ qw/
802             _start
803             ircsock_registered
804             ircsock_input
805             / ],
806             ],
807             );
808              
809             sub _start {
810             # Spawn a Backend and register as the controlling session:
811             my $backend = POEx::IRC::Backend->spawn;
812             $_[HEAP]->{backend} = $backend;
813             $_[KERNEL]->post( $backend->session_id, 'register' );
814             }
815              
816             sub ircsock_registered {
817             my $backend = $_[HEAP]->{backend};
818              
819             # Listen for incoming IRC traffic:
820             $backend->create_listener(
821             bindaddr => $addr,
822             port => $port,
823             );
824              
825             # Connect to a remote endpoint:
826             $backend->create_connector(
827             remoteaddr => $remote,
828             remoteport => $remoteport,
829             # Optional:
830             bindaddr => $bindaddr,
831             ipv6 => 1,
832             ssl => 1,
833             );
834             }
835              
836             # Handle and dispatch incoming IRC events:
837             sub ircsock_input {
838             # POEx::IRC::Backend::Connect obj:
839             my $this_conn = $_[ARG0];
840              
841             # IRC::Message::Object obj:
842             my $input_obj = $_[ARG1];
843              
844             my $cmd = $input_obj->command;
845              
846             # ... dispatch, etc ...
847             }
848              
849             =head1 DESCRIPTION
850              
851             A L IRC socket handler that can be used (by client or server
852             implementations) to speak the IRC protocol to endpoints via
853             L objects.
854              
855             Inspired by L & L.
856              
857             This is a very low-level interface to IRC sockets; the goal is to provide all
858             the necessary scaffolding to develop stateless or stateful IRC clients and
859             daemons. See L for an experimental IRC client library
860             using this backend (and the L section of this documentation for
861             related tools).
862              
863             =head2 Attributes
864              
865             =head3 controller
866              
867             Retrieve the L ID for the backend's registered controller.
868              
869             Predicate: B
870              
871             =head3 connectors
872              
873             A HASH of active Connector objects, keyed on their wheel ID.
874              
875             =head3 filter
876              
877             A L instance consisting of the current L
878             stacked with L (at the time the attribute is built).
879              
880             =head3 filter_irc
881              
882             A L instance with B disabled, by default (this
883             behavior changed in v0.27.2).
884              
885             A server-side Backend may want a colonifying filter:
886              
887             my $backend = POEx::IRC::Backend->new(
888             filter_irc => POE::Filter::IRCv3->new(colonify => 1),
889             ...
890             );
891              
892             =head3 filter_line
893              
894             A L instance.
895              
896             =head3 listeners
897              
898             HASH of active Listener objects, keyed on their wheel ID.
899              
900             =head3 session_id
901              
902             Returns the backend's session ID.
903              
904             =head3 ssl_context
905              
906             Returns the L Context object, if we have one (or C if
907             not); the context is set up by L if C are specified.
908              
909             =head3 wheels
910              
911             HASH of actively connected wheels, keyed on their wheel ID.
912              
913              
914             =head2 Methods
915              
916             =head3 spawn
917              
918             my $backend = POEx::IRC::Backend->spawn(
919             ## Optional, needed for SSL-ified server-side sockets
920             ssl_opts => [
921             'server.key',
922             'server.cert',
923             ],
924             );
925              
926             Creates the backend's L.
927              
928             The C ARRAY is passed directly to
929             L, if present. As of C,
930             each Backend gets its own L context object (rather than sharing
931             the global context). See L & L.
932              
933             =head3 create_connector
934              
935             $backend->create_connector(
936             remoteaddr => $addr,
937             remoteport => $addr,
938             ## Optional:
939             bindaddr => $local_addr,
940             ipv6 => 1,
941             ssl => 1,
942             ## Unrecognized opts are stored in the Connector's 'args' hash:
943             tag => 'foo',
944             );
945              
946             Attempts to create a L that
947             holds a L connector wheel; connectors will
948             attempt to establish an outgoing connection immediately.
949              
950             Unrecognized options are stored in the L's
951             C HASH-type attribute; this is passed to successfully created
952             L instances (as of C). Note that the
953             reference is shared, not copied.
954              
955             =head3 create_listener
956              
957             $backend->create_listener(
958             bindaddr => $addr,
959             port => $port,
960             ## Optional:
961             ipv6 => 1,
962             ssl => 1,
963             idle => $seconds,
964             );
965              
966             Attempts to create a L
967             that holds a L listener wheel.
968              
969             Unrecognized arguments will be added to the Listener object's C
970             attribute, which is then passed on to L objects
971             created by incoming connections to that listener, similar to the behavior
972             described in L (as of C).
973              
974             =head3 remove_listener
975              
976             $backend->remove_listener(
977             listener => $listener_id,
978             );
979              
980             ## or via addr, port, or combination thereof:
981             $backend->remove_listener(
982             addr => '127.0.0.1',
983             port => 6667,
984             );
985              
986             Removes a listener and clears its B attribute; the socket shuts down
987             when the L wheel goes out of scope.
988              
989             =head3 disconnect
990              
991             $backend->disconnect($wheel_id, $disconnect_string);
992              
993             Given a L or its C, mark the specified
994             wheel for disconnection.
995              
996             This method will warn if the given C cannot be found, which may be
997             due to the connection disappearing prior to calling C.
998              
999             You can avoid spurious warnings by checking if the
1000             L still has an active wheel attached:
1001              
1002             if ($this_conn->has_wheel) {
1003             $backend->disconnect( $this_conn )
1004             }
1005              
1006             Note that disconnection typically happens after a buffer flush; if your
1007             software does not perform entirely like a traditional platform (server
1008             implementations will typically send C<< ERROR: Closing Link >> or similar to
1009             clients marked for disconnection, which will trigger a buffer flush) you may
1010             currently experience "late" disconnects. See L.
1011              
1012             =head3 disconnect_now
1013              
1014             Like L, but attempt to destroy the wheel immediately (without
1015             waiting for a buffer flush).
1016              
1017             =head3 send
1018              
1019             $backend->send(
1020             {
1021             prefix => $prefix,
1022             params => [ @params ],
1023             command => $cmd,
1024             },
1025             @connect_ids
1026             );
1027              
1028             use IRC::Message::Object 'ircmsg';
1029             my $msg = ircmsg(
1030             command => 'PRIVMSG',
1031             params => [ $chan, $string ],
1032             );
1033             $backend->send( $msg, $connect_obj );
1034              
1035             Feeds L and sends the resultant raw IRC
1036             line to the specified connection wheel ID(s) or L
1037             object(s).
1038              
1039             Accepts either an L or a HASH compatible with
1040             L -- look there for details.
1041              
1042             Note that unroutable (target connection IDs with no matching live
1043             wheel) messages are silently dropped. You can check L yourself before
1044             sending if this behavior is unwanted:
1045              
1046             for my $target (@connect_ids) {
1047             unless (exists $backend->wheels->{$target}) {
1048             warn "Cannot send to nonexistant target '$target'";
1049             next
1050             }
1051             $backend->send(
1052             { prefix => $prefix, params => [ @params ], command => $cmd },
1053             $target
1054             );
1055             }
1056              
1057             =head3 has_ssl_support
1058              
1059             Returns true if L was successfully loaded.
1060              
1061             =head3 has_zlib_support
1062              
1063             Returns true if L was successfully loaded.
1064              
1065             =head3 set_compressed_link
1066              
1067             $backend->set_compressed_link( $conn_id );
1068              
1069             Mark a specified connection wheel ID as pending compression;
1070             L will be added to the filter stack when the
1071             next flush event arrives.
1072              
1073             This method will die unless L is true.
1074              
1075             =head3 set_compressed_link_now
1076              
1077             $backend->set_compressed_link_now( $conn_id );
1078              
1079             Add a L to the connection's filter stack
1080             immediately, rather than upon next flush event.
1081              
1082             This method will die unless L is true.
1083              
1084             =head3 unset_compressed_link
1085              
1086             $backend->unset_compressed_link( $conn_id );
1087              
1088             Remove L from the connection's filter stack.
1089              
1090              
1091             =head2 Received events
1092              
1093             =head3 register
1094              
1095             $poe_kernel->post( $backend->session_id,
1096             'register'
1097             );
1098              
1099             Register the sender session as the backend's controller session. The last
1100             session to send 'register' is the session that receives notification
1101             events from the backend component.
1102              
1103             =head3 create_connector
1104              
1105             Event interface to I -- see L
1106              
1107             =head3 create_listener
1108              
1109             Event interface to I -- see L
1110              
1111             =head3 remove_listener
1112              
1113             Event interface to I -- see L
1114              
1115             =head3 send
1116              
1117             Event interface to I -- see L
1118              
1119             =head3 shutdown
1120              
1121             Disconnect all wheels and clean up.
1122              
1123              
1124             =head2 Dispatched events
1125              
1126             These events are dispatched to the controller session; see L.
1127              
1128             =head3 ircsock_compressed
1129              
1130             Dispatched when a connection wheel has had a compression filter added.
1131              
1132             C<$_[ARG0]> is the connection's
1133             L
1134              
1135             =head3 ircsock_connection_idle
1136              
1137             Dispatched when a connection wheel has had no input for longer than
1138             specified idle time (see L regarding idle times).
1139              
1140             Currently these events are only issued for incoming Connects accepted on a
1141             Listener, not outgoing Connects created by a Connector.
1142              
1143             C<$_[ARG0]> is the connection's
1144             L
1145              
1146             See also: L
1147              
1148             =head3 ircsock_connector_failure
1149              
1150             Dispatched when a Connector has failed due to some sort of socket error.
1151              
1152             C<$_[ARG0]> is the connection's
1153             L with wheel() cleared.
1154              
1155             C<@_[ARG1 .. ARG3]> contain the socket error details reported by
1156             L; operation, errno, and errstr, respectively.
1157              
1158             =head3 ircsock_connector_open
1159              
1160             Dispatched when a Connector has established a connection to a peer.
1161              
1162             C<$_[ARG0]> is the L for the
1163             connection.
1164              
1165             =head3 ircsock_disconnect
1166              
1167             Dispatched when a connection wheel has been cleared.
1168              
1169             C<$_[ARG0]> is the connection's L
1170             with wheel() cleared.
1171              
1172             =head3 ircsock_input
1173              
1174             Dispatched when there is some IRC input from a connection wheel.
1175              
1176             C<$_[ARG0]> is the connection's
1177             L.
1178              
1179             C<$_[ARG1]> is an L.
1180              
1181             =head3 ircsock_listener_created
1182              
1183             Dispatched when a L has been
1184             created.
1185              
1186             C<$_[ARG0]> is the L instance;
1187             the instance's port() is altered based on getsockname() details after
1188             socket creation and before dispatching this event.
1189              
1190             =head3 ircsock_listener_failure
1191              
1192             Dispatched when a Listener has failed due to some sort of socket error.
1193              
1194             C<$_[ARG0]> is the L object.
1195              
1196             C<@_[ARG1 .. ARG3]> contain the socket error details reported by
1197             L; operation, errno, and errstr, respectively.
1198              
1199             =head3 ircsock_listener_open
1200              
1201             Dispatched when a listener accepts a connection.
1202              
1203             C<$_[ARG0]> is the connection's L
1204              
1205             C<$_[ARG1]> is the connection's L
1206              
1207             =head3 ircsock_listener_removed
1208              
1209             Dispatched when a Listener has been removed.
1210              
1211             C<$_[ARG0]> is the L object.
1212              
1213             =head3 ircsock_registered
1214              
1215             Dispatched when a L event has been successfully received, as a
1216             means of acknowledging the controlling session.
1217              
1218             C<$_[ARG0]> is the Backend's C<$self> object.
1219              
1220             =head1 BUGS
1221              
1222             Probably lots. Please report them via RT, e-mail, IRC
1223             (C), or GitHub
1224             (L).
1225              
1226             =head1 SEE ALSO
1227              
1228             L
1229              
1230             L
1231              
1232             L
1233              
1234             L
1235              
1236             L
1237              
1238             L
1239              
1240             L for an experimental IRC client library using this
1241             backend.
1242              
1243             L for an irssi-based
1244             bouncer/proxy system using this backend.
1245              
1246             L and L for documentation regarding
1247             IRC message parsing.
1248              
1249             L for an extensive set of IRC-related utilities.
1250              
1251             L if you're looking for a mature, fully-featured IRC
1252             client library.
1253              
1254             =head1 AUTHOR
1255              
1256             Jon Portnoy
1257              
1258             Inspiration derived from L and
1259             L by BINGOS, HINRIK et al.
1260              
1261             =cut
1262