1 package OpenILS::Utils::RemoteAccount;
3 # use OpenSRF::Utils::SettingsClient;
4 use OpenSRF::Utils::Logger qw/:logger/;
8 use Net::SSH2; # because uFTP doesn't handle SSH keys (yet?)
11 $Data::Dumper::Indent = 0;
25 remote_password => undef,
26 remote_account => undef,
28 ssh_privatekey => undef,
29 ssh_publickey => undef,
43 The Remote Account module attempts to transfer a file to/from a remote server.
44 Net::uFTP is used, encapsulating the available options of SCP, FTP and SFTP.
46 All information is expected to be gathered by the Event Definition through event parameters:
47 ~ remote_host (required)
53 ~ type (FTP, SFTP or SCP -- default FTP)
57 The latter three are optionally passed to the Net::uFTP constructor.
59 Note: none of the parameters are actually required, except remote_host.
60 That is because remote_user, remote_password and remote_account can all be
61 extrapolated from other sources, as the Net::FTP docs describe:
63 If no arguments are given then Net::FTP uses the Net::Netrc package
64 to lookup the login information for the connected host.
66 If no information is found then a login of anonymous is used.
68 If no password is given and the login is anonymous then anonymous@
69 will be used for password.
71 Note that specifying a password will require you to specify a user.
72 Similarly, specifying an account requires both user and password.
73 That is, there are no assumed defaults when the latter arguments are used.
77 The use of ssh keys is preferred.
79 We attempt to use SSH keys where they are specified or otherwise found
80 in the runtime environment. If only one key is specified, we attempt to derive
81 the corresponding filename based on the ssh-keygen defaults. If either key is
82 specified, but both are not found (and readable) then the result is failure. If
83 no key is specified, but keys are found, the key-based connections will be attempted,
84 but failure will be non-fatal.
89 # returns plausible locations of a .ssh subdir where SSH keys might be stashed
90 # NOTE: these would need to be properly genericized w/ Makefule vars
91 # in order to support Debian packaging and multiple EG's on one box.
92 # Until that happens, we just rely on $HOME
95 # '/openils/conf', # __EG_CONFIG_DIR__
97 ($ENV{HOME}) and unshift @bases, $ENV{HOME};
99 return grep {-d $_} map {"$_/.ssh"} @bases;
103 # populates %keyfiles hash
104 # %keyfiles maps SSH_PRIVATEKEY => SSH_PUBLICKEY
106 my $force = (@_ ? shift : 0);
107 return %keyfiles if (%keyfiles and not $force); # caching
108 $logger->info("Checking for SSH keyfiles" . ($force ? ' (ignoring cache)' : ''));
109 %keyfiles = (); # reset to empty
110 my @dirs = plausible_dirs();
111 $logger->debug(scalar(@dirs) . " plausible dirs: " . join(', ', @dirs));
112 foreach my $dir (@dirs) {
113 foreach my $key (qw/rsa dsa/) {
114 my $private = "$dir/id_$key";
115 my $public = "$dir/id_$key.pub";
116 unless (-r $private) {
117 $logger->debug("Key '$private' cannot be read: $!");
120 unless (-r $public) {
121 $logger->debug("Key '$public' cannot be read: $!");
124 $keyfiles{$private} = $public;
133 if ($self->ssh_publickey and not $self->ssh_privatekey) {
134 my $private = $self->ssh_publickey;
135 unless ($private and $private =~ s/\.pub$// and -r $self->ssh_privatekey) { # try to guess missing private key name
136 $logger->error("No ssh_privatekey specified or found to pair with " . $self->ssh_publickey);
139 $self->ssh_privatekey($private);
141 if ($self->ssh_privatekey and not $self->ssh_publickey) {
142 my $pub = $self->ssh_privatekey . '.pub'; # guess missing public key name
144 $logger->error("No ssh_publickey specified or found to pair with " . $self->ssh_privatekey);
147 $self->ssh_publickey($pub);
150 # so now, we have either both ssh_p*keys params or neither
151 foreach (qw/ssh_publickey ssh_privatekey/) {
152 unless (-r $self->{$_}) {
153 $logger->error("$_ '" . $self->{$_} . "' cannot be read: $!");
154 return; # quit w/ error if we fail on any user-specified key
157 $keys{$self->ssh_privatekey} = $self->ssh_publickey;
163 my $text = shift || $self->content || '';
164 my $tmp = File::Temp->new(); # magical self-destructing tempfile
165 # print $tmp "THIS IS TEXT\n";
166 print $tmp $text or $logger->error(__PACKAGE__ . " : could not write to tempfile '$tmp'");
168 $self->tempfile($tmp); # save the object
169 $self->localfile($tmp->filename); # save the filename
170 $logger->info(__PACKAGE__ . " : using tempfile $tmp");
171 return $self->localfile; # return the filename
178 $self->init($params); # secondary init
185 unless (defined $self->content or $self->localfile) { # content can be emptystring
186 $logger->error($self->error("No content or localfile specified -- nothing to send"));
190 # tricky subtlety: we want to use the most recently specified options
191 # with priority order: filename, content, old filename, old content.
193 # The $params->{x} will already match $self->x after the init above,
194 # so the checks using $params below are for whether the value was specified NOW (via put()) or not.
196 # if we got a new localfile value, we use it
197 # else if the content is new to this call, build a new tempfile w/ it,
198 # else use existing localfile,
199 # else build new tempfile w/ content already specified via new()
201 return $params->{localfile} || (
202 (defined $params->{content}) ?
203 $self->new_tempfile($self->content) : # $self->content is same value as $params->{content}
204 ($self->localfile || $self->new_tempfile($self->content))
212 $self->init($params); # secondary init
214 my $localfile = $self->outbound_file($params) or return;
217 $self->{put_args} = [$localfile]; # same for scp_put and uFTP put
219 push @{$self->{put_args}}, $self->remote_file if $self->remote_file; # user can specify remote_file name, optionally
221 unless ($self->type and $self->type eq 'FTP') {
222 if ($self->ssh_publickey || $self->ssh_privatekey) {
224 %keys = $self->param_keys() or return; # we got one or both params, but they didn't pan out
226 %keys = get_keyfiles(); # optional "force" arg could be used here to empty cache
231 $try = $self->put_ssh2(%keys) if (%keys);
232 return $try if $try; # if we had keys and they worked, we're done
234 # Otherwise, try w/ non-key uFTP methods
235 return $self->put_uftp;
242 $logger->info("*** attempting put with ssh keys");
243 my $ssh2 = Net::SSH2->new();
244 unless($ssh2->connect($self->remote_host)) {
245 $logger->warn($self->error("SSH2 connect FAILED: $!" . join(" ", $ssh2->error)));
246 $self->specific and return; # user told us what key(s) she wanted, and it failed.
247 %keys = (); # forget the keys, we cannot connect
249 foreach (keys %keys) {
252 publickey => $keys{$_},
253 rank => [qw/ publickey hostbased password /],
255 $self->remote_user and $auth_args{username} = $self->remote_user ;
256 $self->remote_password and $auth_args{password} = $self->remote_password;
257 $self->remote_host and $auth_args{hostname} = $self->remote_host ;
259 if ($ssh2->auth(%auth_args)) {
260 if ($ssh2->scp_put( @{$self->{put_args}} )) {
261 $logger->info(sprintf __PACKAGE__ . " : successfully sent %s %s", $self->remote_host, join(' --> ', @{$self->{put_args}} ));
264 $logger->error($self->error(sprintf __PACKAGE__ . " : put to %s failed with error: $!", $self->remote_host));
267 } elsif ($self->specific) {
268 $logger->error($self->error(sprintf "Abort: ssh2->auth FAILED for %s using %s: $!", $self->remote_host, $_));
271 $logger->notice($self->error(sprintf "Unsuccessful keypair: ssh2->auth FAILED for %s using %s: $!", $self->remote_host, $_));
279 foreach (qw/debug type port/) {
280 $options{$_} = $self->{$_} if $self->{$_};
282 # TODO: eval wrapper, set $self->error($!) on failure
283 my $ftp = Net::uFTP->new($self->remote_host, %options) or return;
286 foreach (qw/remote_user remote_password remote_account/) {
287 push @login_args, $self->{$_} if $self->{$_};
289 unless ($ftp->login(@login_args)) {
290 $logger->error(__PACKAGE__ . ' : ' . $self->error("failed login to " . $self->remote_host . " w/ args(" . join(',', @login_args) . ')'));
298 my $ftp = $self->uftp or return;
299 my $filename = $ftp->put(@{$self->{put_args}});
301 $logger->info(__PACKAGE__ . " : successfully sent $self->remote_host $self->localfile --> $filename");
304 $logger->error(__PACKAGE__ . ' : ' . $self->error("put to " . $self->remote_host . " failed with error: $!"));
312 my @required = @_; # qw(remote_host) ; # nothing required now
314 foreach (keys %{$self->{_permitted}}) {
315 $self->{$_} = $params->{$_} if defined $params->{$_};
318 foreach (@required) {
319 unless ($self->{$_}) {
320 $logger->error("Required parameter $_ not specified");
329 my( $class, %args ) = @_;
330 my $self = { _permitted => \%fields, %fields };
334 $self->init(\%args); # or croak "Initialization error caused by bad args";
339 # in order to create, we must first ...
344 my $class = ref($self) or croak "$self is not an object";
345 my $name = $AUTOLOAD;
347 $name =~ s/.*://; # strip leading package stuff
349 unless (exists $self->{_permitted}->{$name}) {
350 croak "Cannot access '$name' field of class '$class'";
354 return $self->{$name} = shift;
356 return $self->{$name};