瀏覽代碼

Merge pull request #6 from troglodyne/irc

Finish IRC provider
Andy Baugh 7 年之前
父節點
當前提交
93f602398e

+ 1 - 1
.gitignore

@@ -1,4 +1,4 @@
 *.bak
 *.swp
 cover_db
-.xmpptestrc
+.*testrc

+ 1 - 1
Makefile

@@ -10,4 +10,4 @@ test:
 	[ -x /usr/local/cpanel/3rdparty/bin/prove ] || prove t/*.t
 
 depend:
-	perl -MNet::XMPP -MMozilla::CA -MTest::More -MTest::Fatal -MTest::MockModule -MConfig::Simple -e 'exit 0;' || sudo cpan -i Net::XMPP Mozilla::CA Test::More Test::Fatal Test::MockModule Config::Simple
+	perl -MNet::XMPP -MMozilla::CA -MTest::More -MTest::Fatal -MTest::MockModule -MConfig::Simple -MIO::Socket::INET -MIO::Socket::SSL -e 'exit 0;' || sudo cpan -i Net::XMPP Mozilla::CA Test::More Test::Fatal Test::MockModule Config::Simple IO::Socket::INET IO::Socket::SSL

+ 3 - 1
README.md

@@ -4,7 +4,7 @@ Extra "Contact Manager" Providers for cPanel and WHM
 Current plugins:
 ================
 * XMPP  -- Stable XMPP provider (well, as stable as Net::XMPP is, anyways). See issue #2
-* IRC   -- Still WIP, don't use this, as it is completely untested and currently no-op on send.
+* IRC   -- New provider, needs more testing by users out in the wild. "Works for Me"
 * Slack -- Works presuming you have an incoming WebHook URL, much like CpanelRicky's MatterMost plugin.
 
 Installation and Use:
@@ -46,3 +46,5 @@ and write out .xmpptestrc in the toplevel directory of this git repository with
 
 You'll note these correspond to the values in the Provider's Schema module. With that set, you should spam yourself with
 a message if the t/Cpanel-iContact-Provider-XMPP.t test passes.
+
+Same goes for the IRC provider... use the same keys as in the schema module for dopeouts in its' test.

+ 8 - 5
lib/Cpanel/iContact/Provider.pm

@@ -11,11 +11,14 @@ sub new {
     my $text_body = 'HOLY CRAP THE AUTO-LAYOFF THING TRIGGERED';
     my $self = {
         'contact' => {
-            'XMPPUSERNAME'  => 'mr_t@a-team.gov',
-            'XMPPPASSWORD'  => 'bjBarracus#1',
-            'XMPPUSETLS'    => 1,
-            'XMPPTLSVERIFY' => 1,
-            'XMPPCOMPONENTNAME' => 'jibber.jabber.org'
+            'XMPPUSERNAME'      => 'mr_t@a-team.gov',
+            'XMPPPASSWORD'      => 'bjBarracus#1',
+            'XMPPUSETLS'        => 1,
+            'XMPPTLSVERIFY'     => 1,
+            'XMPPCOMPONENTNAME' => 'jibber.jabber.org',
+            'IRCSERVER'         => 'irc.bot.test',
+            'IRCPORT'           => 666,
+            'IRCNICK'           => 'DevilBot',
         },
         'args' => {
             'subject'   => 'cPanel on Drugs',

+ 130 - 20
lib/Cpanel/iContact/Provider/IRC.pm

@@ -7,29 +7,25 @@ use parent 'Cpanel::iContact::Provider';
 
 sub send {
     my ($self) = @_;
+    my @missing = grep { !defined $self->{'contact'}{$_} } qw{IRCSERVER};
+    die "Kit not complete! Missing: " . join( ", ", @missing ) if scalar( @missing );
 
     my $args_hr = $self->{'args'};
     my @errs;
 
-    my $subject_copy = $args_hr->{'subject'};
-    my $body_copy    = ${ $args_hr->{'text_body'} };
-
-    require Encode;
-    my $subject      = Encode::decode_utf8( $subject_copy, $Encode::FB_QUIET );
-    my $body         = Encode::decode_utf8( $body_copy, $Encode::FB_QUIET );
-
-    foreach my $destination ( @{ $args_hr->{'to'} } ) {
-        local $@;
-        eval {
-            my $response;
-            $self->_send(
-                'destination' => $destination,
-                'subject' => $subject,
-                'content' => $body
-            );
-        };
-        push( @errs, $@ ) if $@;
-    }
+    my $subject = $args_hr->{'subject'};
+    my $body    = ${ $args_hr->{'text_body'} };
+
+	local $@;
+	eval {
+		my $response;
+		$self->_send(
+			'destination' => $args_hr->{'to'}[0],
+			'subject'     => $subject,
+			'content'     => $body
+		);
+	};
+	push( @errs, $@ ) if $@;
 
     if (@errs) {
         die "One or more notification attempts failed. Details below:\n"
@@ -39,11 +35,125 @@ sub send {
     return 1;
 }
 
+my $conn;
 sub _send {
     my ( $self, %args ) = @_;
-    # TODO research what is installed on cP boxes to see what I can use here
+
+    if( $ENV{'AUTHOR_TESTS'} ) {
+        my $debugmsg = "# Attempting connection to $self->{'contact'}{'IRCSERVER'}:$self->{'contact'}{'IRCPORT'} as $self->{'contact'}{'IRCNICK'} in channel $args{'destination'}";
+        $debugmsg   .= " using SSL" if $self->{'contact'}{'IRCUSESSL'};
+        print $debugmsg, "\n";
+    }
+    my @message_lines = _format_message_for_irc( $args{'subject'}, $args{'content'}, $args{'destination'} );
+
+    require IO::Socket::INET;
+    require IO::Socket::SSL;
+    require Time::HiRes;
+
+    # Don't laugh, some of these notices are so long (and the server so laggy at printing) that this actually is reasonable.
+    # Usually messages are delayed as a flood limiting action.
+    local $SIG{'ALRM'} = sub { die "Timed out waiting for notification to post to IRC channel!" };
+    alarm(60);
+
+    $conn = IO::Socket::INET->new("$self->{'contact'}{'IRCSERVER'}:$self->{'contact'}{'IRCPORT'}", ) or die $!;
+    binmode( $conn, ":utf8" );
+    if( $self->{'contact'}{'IRCUSESSL'} ) {
+        print "# Upgrading connection to use SSL...\n" if $ENV{'AUTHOR_TESTS'};
+        IO::Socket::SSL->start_SSL( $conn, 'SSL_HOSTNAME' => $self->{'contact'}{'IRCSERVER'}, 'SSL_verify_mode' => 0 ) or die $IO::Socket::SSL::ERROR;
+    }
+    print "# [SENT] NICK $self->{'contact'}{'IRCNICK'}\r\n" if $ENV{'AUTHOR_TESTS'};
+    print $conn "NICK $self->{'contact'}{'IRCNICK'}\r\n";
+    print "# [SENT] USER cpsaurus * 8 :cPanel & WHM Notification Bot v0.1 (github.com/troglodyne/iContact-cPanel-Plugins)\r\n" if $ENV{'AUTHOR_TESTS'};
+    print $conn "USER cpsaurus * 8 :cPanel & WHM Notification Bot v0.1 (github.com/troglodyne/iContact-cPanel-Plugins)\r\n";
+  
+    my %got;
+    while( $conn ) {
+
+        last if !scalar(@message_lines);
+        
+        # Print it all then leave like a bad smell
+        if( $got{'366'} && $got{'332'} ) {
+            foreach my $shake_line ( @message_lines ) {
+                print "# [SENT] $shake_line" if $ENV{'AUTHOR_TESTS'};
+                print $conn $shake_line;
+            }
+            last;
+        }
+
+        my $line= readline( $conn ) || "";
+        #$line =~ s/^[^[:print:]]+$//; # Collapse blank lines
+        next if !$line;
+        print "# [GOT][" . length($line) . "] $line" if $ENV{'AUTHOR_TESTS'};
+        my @msgparts = split( ' ', $line );
+        $msgparts[1] ||= '';
+        # PING handler
+        if( $msgparts[0] eq 'PING' ) {
+            print "# [SENT] PONG $msgparts[1]\r\n" if $ENV{'AUTHOR_TESTS'};
+            print $conn "PONG $msgparts[1]\r\n";
+            next;
+        }
+        # MOTD/JOIN handler
+        if( grep { $_ eq $msgparts[1] } qw{376 422} ) {
+            print "# [SENT] JOIN $args{'destination'}\r\n" if $ENV{'AUTHOR_TESTS'};
+            print $conn "JOIN $args{'destination'}\r\n";
+            next;
+        }
+        # Channel join handler, gotta wait for NAMES and TOPIC
+        if( grep { $_ eq $msgparts[1] } qw{366 332} ) {
+            print "# [INFO] Noticed we got $msgparts[1] above. Noting that so we can know when to start spamming messages.\n" if $ENV{'AUTHOR_TESTS'};
+
+            $got{"$msgparts[1]"} = 1 ;
+            next;
+        }
+    }
+    print "# [SENT] QUIT :Done sending notification\r\n" if $ENV{'AUTHOR_TESTS'};
+    print $conn "QUIT :Done sending notification\r\n";
+    # The connection won't properly un-block until you read the response from QUIT.
+    # Unfortunately, just setting it to non-block leads to your messages not being processed.
+    # As such, just read here and let it do it's thing even though you don't actually need the data.
+    readline( $conn );
+    $conn->shutdown(2);
 
     return;
 }
 
+# https://tools.ietf.org/html/rfc2812#section-2.3
+sub _format_message_for_irc {
+    my ( $subj, $body, $chan ) = @_;
+    my @msg_lines;
+
+    my $prefix = "NOTICE $chan :";
+    my $suffix = "\r\n"; # 2 chars
+    my $msglen = 510 - length $prefix; # 512 chars total
+
+    # Subject is one line
+    while( $subj ) {
+        if( length $subj <= 510 ) {
+            push( @msg_lines, $prefix . $subj . $suffix );
+            undef $subj;
+        } else {
+            push( @msg_lines, $prefix . substr( $subj, 0, 510, "" ) . $suffix );
+        }
+    }
+
+    # Body is multiline
+    my @body_lines = split( "\n", $body );
+    foreach my $line (@body_lines) {
+        while( $line ) {
+            if( length $line <= 510 ) {
+                push( @msg_lines, $prefix . $line . $suffix );
+                undef $line;
+            } else {
+                push( @msg_lines, $prefix . substr( $line, 0, 510, "" ) . $suffix );
+            }
+        }
+    }
+
+    return @msg_lines;
+}
+
+sub DESTROY {
+    $conn->shutdown(2) if $conn;
+}
+
 1;

+ 33 - 3
lib/Cpanel/iContact/Provider/Schema/IRC.pm

@@ -5,9 +5,9 @@ use warnings;
 
 sub get_settings {
     my $help1 = <<HALP;
-<p>The IRC <em>channels</em> you wish to send cPanel & WHM notifications <em>to</em>.<br />
-For multiple channels, delimit them with commas.<br />
-Example: "#YOLO,#swag"</p>
+<p>The IRC <em>channel</em> you wish to send cPanel & WHM notifications <em>to</em>.<br />
+Multiple channels currently are not supported.<br />
+Example: "#YOLO"</p>
 HALP
     my $help2 = <<HALP;
 <p>The IRC <em>nickname</em> you wish for cPanel & WHM notifications to use.<br />
@@ -17,6 +17,16 @@ HALP
 <p>Whether or not the IRC server your Notification User is registered at supports SSL/TLS.<br />
 If set improperly, this will cause sending notifications to fail (some IRC servers <em>require</em> SSL/TLS, some <em>don't support it</em>).
 </p>
+HALP
+    my $help4 =<<HALP;
+<p>The IRC Server Address<br />
+The domain or IP your IRC server is active on.
+</p>
+HALP
+    my $help5 =<<HALP;
+<p>The IRC Server Port<br />
+The port your IRC server is active on. Defaults to 6667.
+</p>
 HALP
     return {
         'CONTACTIRC' => {
@@ -34,6 +44,15 @@ HALP
         'IRCNICK' => {
             'shadow'   => 1,
             'type'     => 'text',
+            'checkval' => sub {
+                my $value = shift();
+                $value =~ s/^\s+|\s+$//g; # Trim
+
+                # TODO get full list of "invalid characters". RFC doesn't specify.
+                die "IRC nicknames can't contain spaces!" if index( $value, " ") != -1;
+
+                return $value;
+            },
             'label' => 'IRC Notification Bot Nickname',
             'help' => $help2,
         },
@@ -42,6 +61,17 @@ HALP
             'label' => 'IRC: Use SSL/TLS?',
             'help' => $help3,
         },
+        'IRCSERVER' => {
+            'type'     => 'text',
+            'label' => 'IRC Server Address',
+            'help' => $help4,
+        },
+       'IRCPORT' => {
+            'type'     => 'text',
+            'label' => 'IRC Server Port',
+            'help' => $help5,
+        },
+
     };
 }
 

+ 47 - 0
t/Cpanel-iContact-Provider-IRC.t

@@ -0,0 +1,47 @@
+use strict;
+use warnings;
+
+use Cwd qw{abs_path};
+use File::Basename qw{dirname};
+use lib abs_path( dirname(__FILE__) . "/../lib" );
+
+use Test::More 'tests' => 3;
+use Test::Fatal;
+use IO::Socket::INET ();
+use IO::Socket::SSL  ();
+use Config::Simple   ();
+
+is( exception { require Cpanel::iContact::Provider::IRC; }, undef, 'Module at least compiles' );
+isa_ok( my $bot = Cpanel::iContact::Provider::IRC->new(), "Cpanel::iContact::Provider::IRC" );
+my $sent;
+{
+    # TODO finish unit test.
+    no warnings qw{redefine once};
+    #is( exception { $sent = $bot->send(); }, undef, 'send() did not throw' );
+}
+#ok( $sent, "...and the message appears to have actually sent." );
+
+SKIP: {
+    my $conf_file = abs_path( dirname(__FILE__) . "/../.irctestrc" );
+    skip "Skipping functional testing, needful not supplied", 1 if !$ENV{'AUTHOR_TESTS'} || !-f $conf_file;
+    my $test_conf = { Config::Simple->import_from($conf_file)->vars() };
+    my %args = (
+        'destination' => [ $test_conf->{'CONTACTIRC'} ],
+        'subject'     => 'My Super cool test notification',
+        'content'     => "This is a test of Cpanel::iContact::Provider::IRC. Please Ignore",
+    );
+
+    {
+        no warnings qw{redefine once};
+        local *Cpanel::iContact::Provider::IRC::new = sub {
+            return bless {
+                'contact' => $test_conf,
+            }, $_[0];
+        };
+        my $spammer = Cpanel::iContact::Provider::IRC->new();
+        is( exception { $spammer->_send(%args) }, undef, "Didn't fail to send notification using full functional test" );
+    }
+}
+
+# TODO error paths
+

+ 0 - 1
t/Cpanel-iContact-Provider-XMPP.t

@@ -24,7 +24,6 @@ ok( $sent, "...and the message appears to have actually sent." );
 
 SKIP: {
     my $conf_file = abs_path( dirname(__FILE__) . "/../.xmpptestrc" );
-    diag("Conf file '$conf_file' doesn't exist") if !-f $conf_file;
     skip "Skipping functional testing, needful not supplied", 1 if !$ENV{'AUTHOR_TESTS'} || !-f $conf_file;
     my $test_conf = { Config::Simple->import_from($conf_file)->vars() };
     my %args = (