File Coverage

blib/lib/App/Bondage.pm
Criterion Covered Total %
statement 15 17 88.2
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package App::Bondage;
2             BEGIN {
3 1     1   1029 $App::Bondage::AUTHORITY = 'cpan:HINRIK';
4             }
5             BEGIN {
6 1     1   19 $App::Bondage::VERSION = '0.4.11';
7             }
8              
9 1     1   8 use strict;
  1         2  
  1         43  
10 1     1   5 use warnings FATAL => 'all';
  1         12  
  1         47  
11 1     1   5 use Carp;
  1         2  
  1         93  
12 1     1   7 use Config;
  1         3  
  1         34  
13 1     1   778 use App::Bondage::Away;
  0            
  0            
14             use App::Bondage::Client;
15             use App::Bondage::Recall;
16             use App::Bondage::State;
17             use Digest::MD5 qw(md5_hex);
18             use File::Spec::Functions qw(catdir catfile);
19             use POE qw(Filter::Line Filter::Stackable Wheel::ReadWrite Wheel::SocketFactory);
20             use POE::Filter::IRCD;
21             use POE::Component::Client::DNS;
22             use POE::Component::IRC::State;
23             use POE::Component::IRC::Plugin::AutoJoin;
24             use POE::Component::IRC::Plugin::Connector;
25             use POE::Component::IRC::Plugin::CTCP;
26             use POE::Component::IRC::Plugin::CycleEmpty;
27             use POE::Component::IRC::Plugin::Logger;
28             use POE::Component::IRC::Plugin::NickReclaim;
29             use POE::Component::IRC::Plugin::NickServID;
30             use Socket qw(inet_ntoa);
31             use YAML::XS qw(LoadFile);
32              
33             our $HOMEPAGE = 'http://search.cpan.org/perldoc?App::Bondage';
34             our $CRYPT_SALT = 'erxpnUyerCerugbaNgfhW';
35              
36             sub new {
37             my ($package, %params) = @_;
38             my $self = bless \%params, $package;
39             $self->_load_config();
40            
41             POE::Session->create(
42             object_states => [
43             $self => [ qw(_start _client_error _client_input _listener_accept _listener_failed _exit) ],
44             ],
45             );
46             return $self;
47             }
48              
49             sub _start {
50             my $self = $_[OBJECT];
51            
52             $self->{resolver} = POE::Component::Client::DNS->spawn();
53             $self->{filter} = POE::Filter::Stackable->new(
54             Filters => [
55             POE::Filter::Line->new(),
56             POE::Filter::IRCD->new()
57             ]
58             );
59            
60             while (my ($network_name, $network) = each %{ $self->{config}{networks} }) {
61             my $irc = $self->{ircs}{$network_name} = POE::Component::IRC::State->spawn(
62             LocalAddr => $network->{bind_host},
63             Server => $network->{server_host},
64             Port => $network->{server_port},
65             Password => $network->{server_pass},
66             UseSSL => $network->{use_ssl},
67             Useipv6 => $network->{use_ipv6},
68             Nick => $network->{nickname},
69             Username => $network->{username},
70             Ircname => $network->{realname},
71             AwayPoll => $network->{away_poll},
72             Flood => $network->{flood},
73             Resolver => $self->{resolver},
74             Debug => $self->{Debug},
75             plugin_debug => $self->{Debug},
76             );
77            
78             my $version;
79             {
80             no strict 'vars';
81             $version = defined $App::Bondage::VERSION
82             ? "Bondage $VERSION running on $Config{osname} $Config{osvers} -- $HOMEPAGE"
83             : "Bondage dev-git running on $Config{osname} $Config{osvers}";
84             }
85             $irc->plugin_add('CTCP', POE::Component::IRC::Plugin::CTCP->new(
86             Version => $version,
87             ));
88             $irc->plugin_add('Cycle', POE::Component::IRC::Plugin::CycleEmpty->new()) if $network->{cycle_empty};
89             $irc->plugin_add('NickReclaim', POE::Component::IRC::Plugin::NickReclaim->new());
90             $irc->plugin_add('Connector', POE::Component::IRC::Plugin::Connector->new( Delay => 120 ));
91             $irc->plugin_add('AutoJoin', POE::Component::IRC::Plugin::AutoJoin->new(
92             Channels => $network->{channels},
93             RejoinOnKick => $network->{kick_rejoin},
94             ));
95            
96             if (defined $network->{nickserv_pass}) {
97             $irc->plugin_add('NickServID', POE::Component::IRC::Plugin::NickServID->new(
98             Password => $network->{nickserv_pass}
99             ));
100             }
101            
102             if ($network->{log_public} || $network->{log_private}) {
103             my $log_dir = catdir($self->{Work_dir}, 'logs');
104             if (! -d $log_dir) {
105             mkdir $log_dir or die "Cannot create directory $log_dir; $!; aborted";
106             }
107              
108             $irc->plugin_add('Logger', POE::Component::IRC::Plugin::Logger->new(
109             Path => catdir($log_dir, $network_name),
110             Private => (defined $network->{log_private} ? $network->{log_private} : 0),
111             Public => (defined $network->{log_public} ? $network->{log_public} : 0),
112             Sort_by_date => (defined $network->{log_sortbydate} ? $network->{log_sortbydate} : 0),
113             Restricted => (defined $network->{log_restricted} ? $network->{log_restricted} : 0),
114             DCC => (defined $network->{log_dcc} ? $network->{log_dcc} : 0),
115             Notices => (defined $network->{log_notices} ? $network->{log_notices} : 0),
116             ));
117             }
118              
119             $irc->plugin_add('State', App::Bondage::State->new());
120             $irc->plugin_add('Away', App::Bondage::Away->new(
121             Message => $network->{away_msg}
122             ));
123             $irc->plugin_add('Recall', App::Bondage::Recall->new(
124             Mode => $network->{recall_mode}
125             ));
126              
127             $irc->yield('connect');
128             }
129            
130             $self->_spawn_listener();
131             $poe_kernel->sig(INT => '_exit');
132              
133             return;
134             }
135              
136             sub _client_error {
137             my ($self, $id) = @_[OBJECT, ARG3];
138             delete $self->{wheels}{$id};
139             return;
140             }
141              
142             sub _client_input {
143             my ($self, $input, $id) = @_[OBJECT, ARG0, ARG1];
144             my $info = $self->{wheels}{$id};
145            
146             if ($input->{command} =~ /(PASS)/) {
147             $info->{lc $1} = $input->{params}[0];
148             }
149             elsif ($input->{command} =~ /(NICK|USER)/) {
150             $info->{lc $1} = $input->{params}[0];
151             $info->{registered}++;
152             }
153            
154             if ($info->{registered} == 2) {
155             AUTH: {
156             last AUTH if !defined $info->{pass};
157             $info->{pass} = md5_hex($info->{pass}, $CRYPT_SALT) if length $self->{config}{password} == 32;
158             last AUTH unless $info->{pass} eq $self->{config}{password};
159             last AUTH unless my $irc = $self->{ircs}{ $info->{nick} };
160             $info->{wheel}->put("$info->{nick} NICK :$irc->nick_name");
161             $irc->plugin_add("Client_$id" => App::Bondage::Client->new( Socket => $info->{socket} ));
162             $irc->_send_event(irc_proxy_authed => $id);
163             delete $self->{wheels}{$id};
164             return;
165             }
166            
167             # wrong password or nick (network), dump the client
168             $info->{wheel}->put('ERROR :Closing Link: * [' . ( $info->{user} || 'unknown' ) . '@' . $info->{ip} . '] (Unauthorised connection)' );
169             delete $self->{wheels}{$id};
170             }
171            
172             return;
173             }
174              
175             sub _listener_accept {
176             my ($self, $socket, $peer_addr) = @_[OBJECT, ARG0, ARG1];
177             my $wheel = POE::Wheel::ReadWrite->new(
178             Handle => $socket,
179             InputFilter => $self->{filter},
180             OutputFilter => POE::Filter::Line->new(),
181             InputEvent => '_client_input',
182             ErrorEvent => '_client_error',
183             );
184              
185             my $id = $wheel->ID();
186             $self->{wheels}{$id}{wheel} = $wheel;
187             $self->{wheels}{$id}{ip} = inet_ntoa($peer_addr);
188             $self->{wheels}{$id}{registered} = 0;
189             $self->{wheels}{$id}{socket} = $socket;
190            
191             return;
192             }
193              
194             sub _listener_failed {
195             my ($self, $error) = @_[OBJECT, ARG2];
196             die "Failed to spawn listener: $error; aborted\n";
197             }
198              
199             sub _spawn_listener {
200             my ($self) = @_;
201             $self->{listener} = POE::Wheel::SocketFactory->new(
202             BindAddress => $self->{config}{listen_host},
203             BindPort => $self->{config}{listen_port},
204             SuccessEvent => '_listener_accept',
205             FailureEvent => '_listener_failed',
206             Reuse => 'yes',
207             );
208            
209             if ($self->{config}{listen_ssl}) {
210             require POE::Component::SSLify;
211             POE::Component::SSLify->import(qw(Server_SSLify SSLify_Options));
212            
213             eval { SSLify_Options('ssl.key', 'ssl.crt') };
214             chomp $@;
215             die "Unable to load SSL key ($self->{Work_dir}/ssl.key) or certificate ($self->{Work_dir}/ssl.crt): $@\n" if $@;
216            
217             eval { $self->{listener} = Server_SSLify($self->{listener}) };
218             chomp $@;
219             die "Unable to SSLify the listener: $@\n" if $@;
220             }
221             return;
222             }
223              
224             sub _load_config {
225             my ($self) = @_;
226              
227             $self->{config} = LoadFile(catfile($self->{Work_dir}, 'config.yml'));
228              
229              
230             # some sanity checks
231              
232             for my $opt (qw(listen_port password)) {
233             if (!defined $self->{config}{$opt}) {
234             die "Config option '$opt' must be defined; aborted\n";
235             }
236             }
237              
238             if (ref $self->{config}{networks} ne 'HASH'
239             || !keys %{ $self->{config}{networks} }) {
240             die "No networks defined; aborted\n";
241             }
242              
243             while (my ($network, $options) = each %{ $self->{config}{networks} }) {
244             if (!defined $options->{server_host}) {
245             die "No server_host defined for network '$network'; aborted\n";
246             }
247             }
248            
249             return;
250             }
251              
252             # die gracefully
253             sub _exit {
254             my ($kernel, $self) = @_[KERNEL, OBJECT];
255            
256             if (defined $self->{listener}) {
257             delete $self->{wheels};
258             delete $self->{listener};
259             $self->{resolver}->shutdown();
260             $kernel->signal($kernel, 'POCOIRC_SHUTDOWN', 'Caught interrupt');
261             }
262              
263             $kernel->sig_handled();
264             return;
265             }
266              
267             1;
268              
269             =encoding utf8
270              
271             =head1 NAME
272              
273             App::Bondage - A featureful IRC bouncer based on POE::Component::IRC
274              
275             =head1 SYNOPSIS
276              
277             my $bouncer = App::Bondage->new(
278             Debug => $debug,
279             Work_dir => $work_dir,
280             );
281              
282             =head1 DESCRIPTION
283              
284             Bondage is an IRC bouncer. It acts as a proxy between multiple IRC servers and
285             multiple IRC clients. It makes it easy to stay permanently connected to IRC.
286             It is mostly made up of reusable components. Very little is made from scratch
287             here. If it is, it will be made modular and reusable, probably as a
288             L plugin. This keeps the code short
289             and (hopefully) well tested by others.
290              
291             =head2 Rationale
292              
293             I wrote Bondage because no other IRC bouncer out there fit my needs. Either
294             they were missing essential features, or they were implemented in an
295             undesirable (if not buggy) way. I've tried to make Bondage stay out of your
296             way and be as transparent as possible. It's supposed to be a proxy, after all.
297              
298             =head1 FEATURES
299              
300             =head2 Easy setup
301              
302             Bondage is easy to get up and running. In the configuration file, you just
303             have to specify the port it will listen on, the password, and some IRC
304             server(s) you want Bondage to connect to. Everything else has sensible
305             defaults, though you might want to use a custom nickname and pick some
306             channels to join on connect.
307              
308             =head2 Logging
309              
310             Bondage can log both public and private messages for you. All log files
311             are saved as UTF-8.
312              
313             =head2 Stays connected
314              
315             Bondage will reconnect to IRC when it gets disconnected or the IRC server
316             stops responding.
317              
318             =head2 Recall messages
319              
320             Bondage can send you all the messages you missed since you detached, or it
321             can send you all messages received since it connected to the IRC server, or
322             neither. This feature is based on similar features found in miau,
323             dircproxy, and ctrlproxy.
324              
325             =head2 Auto-away
326              
327             Bondage will set your status to away when no clients are attached.
328              
329             =head2 Reclaim nickname
330              
331             Bondage will periodically try to change to your preferred nickname if it is
332             taken.
333              
334             =head2 Flood protection
335              
336             Bondage utilizes POE::Component::IRC's flood protection to ensure that you
337             never flood yourself off the IRC server.
338              
339             =head2 NickServ support
340              
341             Bondage can identify with NickServ for you when needed.
342              
343             =head2 Rejoins channels if kicked
344              
345             Bondage can try to rejoin a channel if you get kicked from it.
346              
347             =head2 Encrypted passwords
348              
349             Bondage supports encrypted passwords in its configuration file for added
350             security.
351              
352             =head2 SSL support
353              
354             You can connect to SSL-enabled IRC servers, and make Bondage require SSL for
355             client connections.
356              
357             =head2 IPv6 support
358              
359             Bondage can connect to IPv6 IRC servers, and also listen for client
360             connections via IPv6.
361              
362             =head2 Cycles empty channels
363              
364             Bondage can cycle (part and rejoin) channels for you when they become empty
365             in order to gain ops.
366              
367             =head2 CTCP replies
368              
369             Bondage will reply to CTCP VERSION requests when you are offline.
370              
371             =head1 CONFIGURATION
372              
373             The following options are recognized in the configuration file which is
374             called F<~/.bondage/config.yml>.
375              
376             =head2 Global options
377              
378             =head3 C
379              
380             (optional, default: I<"0.0.0.0">)
381              
382             The host that Bondage accepts connections from. This is the host you use to
383             connect to Bondage.
384              
385             =head3 C
386              
387             (required, no default)
388              
389             The port Bondage binds to.
390              
391             =head3 C
392              
393             (optional, default: I)
394              
395             Set this to true if you want Bondage to require the use of SSL for client
396             connections. You'll need to have F and F files in Bondage's
397             working directory. More information, see
398             L
399              
400             =head3 C
401              
402             (required, no default)
403              
404             The password you use to connect to Bondage. If it is 32 characters, it is
405             assumed to be encrypted (see L|bondage/"SYNOPSIS">);
406              
407             =head3 C
408              
409             (required, no default)
410              
411             This should contain a list of network names, each pointing to a list of
412             relevant options as described in the following section.
413              
414             networks:
415             freenode:
416             option1: value
417             option2: value
418             ircnet
419             option1: value
420             option2: value
421              
422             =head2 Network-specific options
423              
424             =head3 C
425              
426             (optional, default: I<"0.0.0.0">)
427              
428             The host that Bondage binds to and connects to IRC from. Useful if you have
429             multiple IPs and want to choose which one to IRC from.
430              
431             =head3 C
432              
433             (required, no default)
434              
435             The IRC server you want Bondage to connect to.
436              
437             =head3 C
438              
439             (optional, default: I<6667>)
440              
441             The port on the IRC server you want to use.
442              
443             =head3 C
444              
445             (optional, no default)
446              
447             The IRC server password, if there is one.
448              
449             =head3 C
450              
451             (optional, default: I)
452              
453             Set this to true if you want to use SSL to communicate with the IRC server.
454              
455             =head3 C
456              
457             (optional, no default)
458              
459             Your NickServ password on the IRC network, if you have one. Bondage will
460             identify with NickServ with this password on connect, and whenever you switch
461             to your original nickname.
462              
463             =head3 C
464              
465             (optional, default: your UNIX user name)
466              
467             Your IRC nick name.
468              
469             =head3 C
470              
471             (optional, default: your UNIX user name)
472              
473             Your IRC user name.
474              
475             =head3 C
476              
477             (optional, default: your UNIX real name, if any)
478              
479             Your IRC real name, or email, or whatever.
480              
481             =head3 C
482              
483             (optional, default: I)
484              
485             Set to a true value to allow flooding (disables flood protection).
486              
487             =head3 C
488              
489             (optional, no default)
490              
491             A list of all your channels and their passwords.
492              
493             channels:
494             "chan1" : ""
495             "chan2" : "password"
496             "chan3" : ""
497              
498             =head3 C
499              
500             (optional, default: I<"missed">)
501              
502             How many channel messages you want Bondage to remember, and then send to you
503             when you attach.
504              
505             B<"missed">: Bondage will only recall the channel messages you missed since
506             the last time you detached from Bondage.
507              
508             B<"none">: Bondage will not recall any channel messages.
509              
510             B<"all">: Bondage will recall all channel messages.
511              
512             B: Bondage will always recall private messages that you missed while you
513             were away, regardless of this option.
514              
515             =head3 C
516              
517             (optional, default: I)
518              
519             Set to true if you want Bondage to log all your public messages. They will be
520             saved as F<~/.bondage/logs/some_network/#some_channel.log> unless you set
521             L|/log_sortbydate> to true.
522              
523             =head3 C
524              
525             (optional, default: I)
526              
527             Set to true if you want Bondage to log all private messages. They will be saved
528             as F<~/.bondage/logs/some_network/some_nickname.log> unless you set
529             L|/log_sortbydate> to true.
530              
531             =head3 C
532              
533             (optional, default: I)
534              
535             Set to true if you want Bondage to rotate your logs. E.g. a channel log file
536             might look like F<~/.bondage/logs/some_network/#channel/2008-01-30.log>
537              
538             =head3 C
539              
540             (optional, default: I)
541              
542             Set this to true if you want Bondage to restrict the read permissions on
543             created log files/directories so other users won't be able to access them.
544              
545             =head3 C
546              
547             (optional, default: I)
548              
549             Set this to true if you want Bondage to log DCC transactions.
550              
551             =head3 C
552              
553             (optional, default: I)
554              
555             Set this to true if you want Bondage to log NOTICEs in addition to PRIVMSGs.
556              
557             =head3 C
558              
559             (optional, default: I)
560              
561             Set to true if you want Bondage to cycle (part and rejoin) opless channels
562             if they become empty.
563              
564             =head3 C
565              
566             (optional, default: I)
567              
568             Set to true if you want Bondage to try to rejoin a channel (once) if you get
569             kicked from it.
570              
571             =head3 C
572              
573             (optional, default: I)
574              
575             The interval, in seconds, in which to update information on channel members'
576             away status.
577              
578             Some IRC clients (e.g. xchat) periodically issue a C to update
579             the away status of channel members. Since Bondage caches this information and
580             replies to such requests without contacting the IRC server, clients like xchat
581             will not get up-to-date information about the away status. On the other hand,
582             this saves lots of traffic if you don't care about that functionality. But if
583             you do make use of it, set this value to, say, 300 (which is what xchat uses).
584              
585             =head1 METHODS
586              
587             =head2 C
588              
589             Arguments:
590              
591             B<'Work_dir'>, the working directory for the bouncer. Should include the
592             config file. This option is required.
593              
594             B<'Debug'>, set to 1 to enable debugging. Default is 0.
595              
596             =head1 DEPENDENCIES
597              
598             The following CPAN distributions are required:
599              
600             =over
601              
602             =item L
603              
604             =item L
605              
606             =item L
607              
608             =item L
609              
610             =item L
611              
612             =item L (if you need SSL support)
613              
614             =item L
615              
616             =item L (if you need ipv6 support)
617              
618             =back
619              
620             =head1 BUGS
621              
622             Report all bugs, feature requests, etc, here:
623             http://rt.cpan.org/Public/Dist/Display.html?Name=App%3A%3ABondage
624              
625             =head1 TODO
626              
627             Look into using L as
628             an intermediary for multiple clients.
629              
630             Keep recall messages away from prying eyes, instead of in F.
631              
632             Generate QuakeNet-specific WHO replies without contacting the IRC server.
633              
634             Add proper tests.
635              
636             =head1 AUTHOR
637              
638             Hinrik Ern SigurEsson, hinrik.sig@gmail.com
639              
640             =head1 LICENSE AND COPYRIGHT
641              
642             Copyright 2008-2009 Hinrik Ern SigurEsson
643              
644             This program is free software, you can redistribute it and/or modify
645             it under the same terms as Perl itself.
646              
647             =head1 SEE ALSO
648              
649             Other useful IRC bouncers:
650              
651             =over
652              
653             =item L
654              
655             =item L
656              
657             =item L
658              
659             =item L
660              
661             =item L
662              
663             =item L
664              
665             =item L
666              
667             =back
668              
669             =cut