patch from Joe Atzberger to provide support for using SSh keys, including new key...
authorerickson <erickson@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 22 Dec 2009 19:48:40 +0000 (19:48 +0000)
committererickson <erickson@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 22 Dec 2009 19:48:40 +0000 (19:48 +0000)
git-svn-id: svn://svn.open-ils.org/ILS/trunk@15226 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/src/perlmods/OpenILS/Application/Trigger/Reactor/SendFile.pm

index 6a89da6..70f343a 100644 (file)
@@ -7,6 +7,7 @@ use OpenSRF::Utils::Logger qw/:logger/;
 
 use Data::Dumper;
 use Net::uFTP;
+use Net::SSH2;      # because uFTP doesn't handle SSH keys (yet?)
 use File::Temp;
 
 $Data::Dumper::Indent = 0;
@@ -14,6 +15,8 @@ $Data::Dumper::Indent = 0;
 use strict;
 use warnings;
 
+our %keyfiles = ();
+
 sub ABOUT {
     return <<ABOUT;
 
@@ -26,6 +29,8 @@ by the Event Definition through event parameters:
    ~ remote_user
    ~ remote_password
    ~ remote_account
+   ~ ssh_privatekey
+   ~ ssh_publickey
    ~ type (FTP, SFTP or SCP -- default FTP)
    ~ port
    ~ debug
@@ -48,9 +53,90 @@ Note that specifying a password will require you to specify a user.
 Similarly, specifying an account requires both user and password.
 That is, there are no assumed defaults when the latter arguments are used.
 
+SSH KEYS:
+
+The use of ssh keys is preferred. 
+
+The reactor attempts to use SSH keys where they are specified or otherwise found
+in the runtime environment.  If only one key is specified, we attempt to derive
+the corresponding filename based on the ssh-keygen defaults.  If either key is
+specified, but both are not found (and readable) then the result is failure.  If
+no key is specified, but keys are found, the key-based connections will be attempted,
+but failure will be non-fatal.
+
 ABOUT
 }
 
+sub plausible_dirs {
+    # returns plausible locations of a .ssh subdir where SSH keys might be stashed
+    # NOTE: these would need to be properly genericized w/ Makefule vars
+    # in order to support Debian packaging and multiple EG's on one box.
+    # Until that happens, we just rely on $HOME
+
+    my @bases = (
+       # '/openils/conf',     # __EG_CONFIG_DIR__
+    );
+    ($ENV{HOME}) and unshift @bases, $ENV{HOME};
+
+    return grep {-d $_} map {"$_/.ssh"} @bases;
+}
+
+sub get_keyfiles {
+    # populates %keyfiles hash
+    # %keyfiles maps SSH_PRIVATEKEY => SSH_PUBLICKEY
+    my $force = (@_ ? shift : 0);
+    return %keyfiles if (%keyfiles and not $force);   # caching
+    $logger->info("Checking for SSH keyfiles" . ($force ? ' (ignoring cache)' : ''));
+    %keyfiles = ();  # reset to empty
+    my @dirs = plausible_dirs();
+    $logger->debug(scalar(@dirs) . " plausible dirs: " . join(', ', @dirs));
+    foreach my $dir (@dirs) {
+        foreach my $key (qw/rsa dsa/) {
+            my $private = "$dir/id_$key";
+            my $public  = "$dir/id_$key.pub";
+            unless (-r $private) {
+                $logger->debug("Key '$private' cannot be read: $!");
+                next;
+            }
+            unless (-r $public) {
+                $logger->debug("Key '$public' cannot be read: $!");
+                next;
+            }
+            $keyfiles{$private} = $public;
+        }
+    }
+    return %keyfiles;
+}
+
+sub param_keys {
+    my $params = shift;
+    my %keys = ();
+    if ($params->{ssh_publickey } and not $params->{ssh_privatekey}) {
+        $params->{ssh_privatekey} = $params->{ssh_publickey};        # try to guess missing private key name
+        unless ($params->{ssh_privatekey} =~ s/\.pub$// and -r $params->{ssh_privatekey}) {
+            $logger->error("No ssh_privatekey specified or found to pair with " . $params->{ssh_publickey});
+            return;
+        }
+    }
+    if ($params->{ssh_privatekey} and not $params->{ssh_publickey }) {
+        $params->{ssh_publickey}  = $params->{ssh_privatekey} . '.pub'; # guess missing public key name
+        unless (-r $params->{ssh_publickey}) {
+            $logger->error("No ssh_publickey specified or found to pair with " . $params->{ssh_privatekey});
+            return;
+        }
+    }
+
+    # so now, we have either both ssh_p*key params or neither
+    foreach (qw/ssh_publickey ssh_privatekey/) {
+        unless (-r $params->{$_}) {
+            $logger->error("$_ '" . $params->{$_} . "' cannot be read: $!");
+            return;                 # quit w/ error if we fail on any user-specified key
+        }
+        $keys{$params->{ssh_privatekey}} = $params->{ssh_publickey};
+    }
+    return %keys;
+}
+
 sub handler {
     my $self = shift;
     my $env  = shift;
@@ -62,19 +148,67 @@ sub handler {
         return;
     }
 
-    my %options = ();
-    foreach (qw/debug type port/) {
-        $options{$_} = $params->{$_} if $params->{$_};
+    my $text = $self->run_TT($env) or return;
+    my $tmp  = File::Temp->new();    # magical self-destructing tempfile
+    print $tmp $text;
+    $logger->info("SendFile Reactor: using tempfile $tmp");
+
+    my %keys     = ();
+    my $specific = 0;
+    my @put_args = ($tmp->filename);      # same for scp_put and uFTP put
+    push @put_args, $params->{remote_file} if $params->{remote_file};     # user can specify remote_file name, optionally
+
+    unless ($params->{type} and $params->{type} eq 'FTP') {
+        if ($params->{ssh_publickey} || $params->{ssh_privatekey}) {
+            $specific = 1;
+            %keys = param_keys($params) or return;  # we got one or both params, but they didn't pan out
+        } else {
+            %keys = get_keyfiles();     # optional "force" arg could be used here to empty cache
+        }
     }
-    my $ftp = Net::uFTP->new($host, %options);
 
+    if (%keys) {
+        my $ssh2 = Net::SSH2->new();
+        unless($ssh2->connect($host)) {
+            $logger->warn("SSH2 connect FAILED: $!" . join(" ", $ssh2->error));
+            $specific and return;
+            %keys = ();     # forget the keys, we cannot connect
+        }
+        foreach (keys %keys) {
+            my %auth_args = (
+                privatekey => $_,
+                publickey  => $keys{$_},
+                rank => [qw/ publickey hostbased password /],
+            );
+            $params->{remote_user    } and $auth_args{username} = $params->{remote_user    };
+            $params->{remote_password} and $auth_args{password} = $params->{remote_password};
+            $params->{remote_host    } and $auth_args{hostname} = $params->{remote_host    };
+
+            if ($ssh2->auth(%auth_args)) {
+                if ($ssh2->scp_put(@put_args)) {
+                    $logger->info("SendFile Reactor: successfully sent ${host} " . join(' --> ', @put_args));
+                    return 1;
+                } else {
+                    $logger->error("SendFile Reactor: put to $host failed with error: $!");
+                    return;
+                }
+            } elsif ($specific) {
+                $logger->error("Abort reactor: ssh2->auth FAILED for $host using $_: $!");
+                return;
+            } else {
+                $logger->notice("Unsuccessful keypair: ssh2->auth FAILED for $host using $_: $!");
+            }
+        }
+    }
     # my $conf = OpenSRF::Utils::SettingsClient->new;
     # $$env{something_hardcoded} = $conf->config_value('category', 'whatever');
 
-    my $text = $self->run_TT($env) or return;
-    my $tmp  = File::Temp->new();    # magical self-destructing tempfile
-    print $tmp $text;
-    $logger->info("SendFile Reactor: using tempfile $tmp");
+    # Try w/ non-key uFTP methods
+    my %options = ();
+    foreach (qw/debug type port/) {
+        $options{$_} = $params->{$_} if $params->{$_};
+    }
+    my $ftp = Net::uFTP->new($host, %options);
 
     my @login_args = ();
     foreach (qw/remote_user remote_password remote_account/) {
@@ -85,16 +219,14 @@ sub handler {
         return;
     }
 
-    my @put_args = ($tmp);
-    push @put_args, $params->{remote_file} if $params->{remote_file};     # user can specify remote_file name, optionally
     my $filename = $ftp->put(@put_args);
     if ($filename) {
-        $logger->info("SendFile Reactor: successfully sent ${host} $filename");
+        $logger->info("SendFile Reactor: successfully sent ${host} $tmp --> $filename");
         return 1;
+    } else {
+        $logger->error("SendFile Reactor: put to $host failed with error: $!");
+        return;
     }
-
-    $logger->error("SendFile Reactor: put to $host failed with error: $!");
-    return;
 }
 
 1;