Koha-clms/Koha/SFTP/Server.pm

447 lines
13 KiB
Perl

package Koha::SFTP::Server;
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Koha::Database;
use Koha::Exceptions::Object;
use Koha::Encryption;
use Koha::Logger;
use Koha::SFTP::Servers;
use Try::Tiny qw( catch try );
use Mojo::JSON;
use Net::SFTP::Foreign;
use Net::FTP;
use File::Spec;
use IO::File;
use base qw(Koha::Object);
=head1 NAME
Koha::SFTP::Server - Koha SFTP Server Object class
=head1 API
=head2 Class methods
=head3 store
$server->store;
Overloaded store method that ensures directory paths end with a forward slash.
=cut
sub store {
my ($self) = @_;
for my $dir_field (qw( download_directory upload_directory )) {
my $dir = $self->$dir_field;
next unless $dir && $dir ne '';
$self->$dir_field( $dir . '/' ) unless substr( $dir, -1 ) eq '/';
}
return $self->SUPER::store;
}
=head3 to_api
my $json = $sftp_server->to_api;
Returns a JSON representation of the object suitable for API output,
excluding sensitive data.
=cut
sub to_api {
my ( $self, $params ) = @_;
my $json_sftp = $self->SUPER::to_api($params) or return;
delete $json_sftp->{password};
delete $json_sftp->{key_file};
return $json_sftp;
}
=head3 to_api_mapping
This method returns the mapping for representing a Koha::SFTP::Server object
on the API.
=cut
sub to_api_mapping {
return { id => 'sftp_server_id' };
}
=head3 plain_text_password
my $password = $server->plain_text_password;
Returns the decrypted plaintext password.
=cut
sub plain_text_password {
my ($self) = @_;
return unless $self->password;
return Koha::Encryption->new->decrypt_hex( $self->password );
}
=head3 plain_text_key
my $key = $server->plain_text_key;
Returns the decrypted plaintext key file.
=cut
sub plain_text_key {
my ($self) = @_;
return unless $self->key_file;
return Koha::Encryption->new->decrypt_hex( $self->key_file ) . "\n";
}
=head3 write_key_file
my $success = $server->write_key_file;
Writes the keyfile from the db into a file.
Returns 1 on success, undef on failure.
=cut
sub write_key_file {
my ($self) = @_;
my $upload_path = C4::Context->config('upload_path') or return;
my $logger = Koha::Logger->get;
my $key_path = File::Spec->catdir( $upload_path, 'ssh_keys' );
my $key_file = File::Spec->catfile( $key_path, 'id_ssh_' . $self->id );
mkdir $key_path unless -d $key_path;
unlink $key_file if -f $key_file;
my $fh = IO::File->new( $key_file, 'w' ) or return;
try {
chmod 0600, $key_file if -f $key_file;
print $fh $self->plain_text_key;
close $fh or $logger->warn("Failed to close key file: $!");
return 1;
} catch {
$logger->warn("Error writing key file: $_");
close $fh;
return;
};
}
=head3 locate_key_file
my $path = $server->locate_key_file;
Returns the keyfile's path if it exists, undef otherwise.
=cut
sub locate_key_file {
my ($self) = @_;
my $upload_path = C4::Context->config('upload_path') or return;
my $key_file = File::Spec->catfile(
$upload_path,
'ssh_keys',
'id_ssh_' . $self->id
);
return ( -f $key_file ) ? $key_file : undef;
}
=head3 update_password
my $success = $server->update_password;
Update the server's encrypted password.
=cut
sub update_password {
my ( $self, $value ) = @_;
return unless defined $value;
$self->password( Koha::Encryption->new->encrypt_hex($value) );
return $self->SUPER::store;
}
=head3 update_key_file
my $success = $server->update_key_file($new_key_file);
Update the server's encrypted key file.
=cut
sub update_key_file {
my ( $self, $value ) = @_;
return unless defined $value;
$self->key_file( Koha::Encryption->new->encrypt_hex( _dos2unix($value) ) );
return $self->SUPER::store;
}
=head3 update_status
my $success = $server->update_status($new_status);
Update the server's status
=cut
sub update_status {
my ( $self, $value ) = @_;
return unless defined $value;
return $self->set( { status => $value } )->store;
}
=head3 test_conn
my ($success, $results) = $server->test_conn;
Tests connection to the server. Returns success flag and detailed test results.
=cut
sub test_conn {
my ($self) = @_;
my $default_result = {
passed => Mojo::JSON->false,
err => undef,
msg => undef,
};
my $sftp_test_results;
if ( $self->transport eq 'sftp' ) {
$sftp_test_results->{'1_sftp_conn'} = {%$default_result};
my $sftp = Net::SFTP::Foreign->new(
host => $self->host,
user => $self->user_name,
password => $self->plain_text_password,
key_path => $self->locate_key_file,
port => $self->port,
timeout => 10,
more => [
qw(-vv),
qw(-o StrictHostKeyChecking=no),
],
);
unless ( $sftp->error ) {
$sftp_test_results->{'1_sftp_conn'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'1_sftp_conn'}->{'msg'} = $sftp->status;
} else {
$sftp_test_results->{'1_sftp_conn'}->{'err'} = $sftp->error;
}
unless ( $sftp->error ) {
unless ( $self->download_directory eq '' ) {
$sftp_test_results->{'2a_sftp_cwd_dl'} = {%$default_result};
$sftp->setcwd( $self->download_directory );
unless ( $sftp->error ) {
$sftp_test_results->{'2a_sftp_cwd_dl'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'2a_sftp_cwd_dl'}->{'msg'} = $sftp->status;
} else {
$sftp_test_results->{'2a_sftp_cwd_dl'}->{'err'} = $sftp->error;
}
}
$sftp_test_results->{'2b_sftp_ls_dl'} = {%$default_result};
$sftp->ls();
unless ( $sftp->error ) {
$sftp_test_results->{'2b_sftp_ls_dl'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'2b_sftp_ls_dl'}->{'msg'} = $sftp->status;
} else {
$sftp_test_results->{'2b_sftp_ls_dl'}->{'err'} = $sftp->error;
}
unless ( $self->upload_directory eq '' ) {
$sftp_test_results->{'2c_sftp_cwd_ul'} = {%$default_result};
$sftp->setcwd( $self->upload_directory );
unless ( $sftp->error ) {
$sftp_test_results->{'2c_sftp_cwd_ul'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'2c_sftp_cwd_ul'}->{'msg'} = $sftp->status;
} else {
$sftp_test_results->{'2c_sftp_cwd_ul'}->{'err'} = $sftp->error;
}
}
$sftp_test_results->{'2d_sftp_ls_ul'} = {%$default_result};
$sftp->ls( $self->upload_directory );
unless ( $sftp->error ) {
$sftp_test_results->{'2d_sftp_ls_ul'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'2d_sftp_ls_ul'}->{'msg'} = $sftp->status;
} else {
$sftp_test_results->{'2d_sftp_ls_ul'}->{'err'} = $sftp->error;
}
$sftp_test_results->{'3_sftp_write'} = {%$default_result};
open my $fh, '<', \"Hello, world!\n";
close $fh if ( $sftp->put( $fh, $self->upload_directory . '.koha_test_file' ) );
unless ( $sftp->error ) {
$sftp_test_results->{'3_sftp_write'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'3_sftp_write'}->{'msg'} = $sftp->status;
} else {
$sftp_test_results->{'3_sftp_write'}->{'err'} = $sftp->error;
}
$sftp_test_results->{'4_sftp_del'} = {%$default_result};
$sftp->remove( $self->upload_directory . '.koha_test_file' );
unless ( $sftp->error ) {
$sftp_test_results->{'4_sftp_del'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'4_sftp_del'}->{'msg'} = $sftp->status;
} else {
$sftp_test_results->{'4_sftp_del'}->{'err'} = $sftp->error;
}
}
} elsif ( $self->transport eq 'ftp' ) {
$sftp_test_results->{'1_ftp_conn'} = {%$default_result};
my $ftp = Net::FTP->new(
$self->host,
Port => $self->port,
Timeout => 10,
Passive => ( scalar $self->passive ) ? 1 : 0,
);
if ($ftp) {
$sftp_test_results->{'1_ftp_conn'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'1_ftp_conn'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'1_ftp_conn'}->{'err'} = 'cannot connect to ' . $self->host . ': ' . $@;
}
if ($ftp) {
$sftp_test_results->{'2_ftp_login'} = {%$default_result};
my $login = $ftp->login(
$self->user_name,
$self->plain_text_password,
);
if ($login) {
$sftp_test_results->{'2_ftp_login'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'2_ftp_login'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'2_ftp_login'}->{'err'} = $ftp->message;
}
unless ( $self->download_directory eq '' ) {
$sftp_test_results->{'3a_ftp_cwd_dl'} = {%$default_result};
my $cwd_dl = $ftp->cwd( $self->download_directory );
if ($cwd_dl) {
$sftp_test_results->{'3a_ftp_cwd_dl'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'3a_ftp_cwd_dl'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'3a_ftp_cwd_dl'}->{'err'} = $ftp->message;
}
}
$sftp_test_results->{'3b_ftp_ls_dl'} = {%$default_result};
my $ls_dl = $ftp->ls();
if ($ls_dl) {
$sftp_test_results->{'3b_ftp_ls_dl'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'3b_ftp_ls_dl'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'3b_ftp_ls_dl'}->{'err'} = $ftp->message;
}
unless ( $self->upload_directory eq '' ) {
$sftp_test_results->{'3c_ftp_cwd_ul'} = {%$default_result};
my $cwd_ul = $ftp->cwd( $self->upload_directory );
if ($cwd_ul) {
$sftp_test_results->{'3c_ftp_cwd_ul'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'3c_ftp_cwd_ul'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'3c_ftp_cwd_ul'}->{'err'} = $ftp->message;
}
}
$sftp_test_results->{'3d_ftp_ls_ul'} = {%$default_result};
my $ls_ul = $ftp->ls();
if ($ls_ul) {
$sftp_test_results->{'3d_ftp_ls_ul'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'3d_ftp_ls_ul'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'3d_ftp_ls_ul'}->{'err'} = $ftp->message;
}
$sftp_test_results->{'4_ftp_write'} = {%$default_result};
open my $fh, '<', \"Hello, world!\n";
close $fh if ( my $put = $ftp->put( $fh, $self->upload_directory . '.koha_test_file' ) );
if ($put) {
$sftp_test_results->{'4_ftp_write'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'4_ftp_write'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'4_ftp_write'}->{'err'} = $ftp->message;
}
$sftp_test_results->{'5_ftp_del'} = {%$default_result};
my $delete = $ftp->delete( $self->upload_directory . '.koha_test_file' );
if ($delete) {
$sftp_test_results->{'5_ftp_del'}->{'passed'} = Mojo::JSON->true;
$sftp_test_results->{'5_ftp_del'}->{'msg'} = $ftp->message;
} else {
$sftp_test_results->{'5_ftp_del'}->{'err'} = $ftp->message;
}
}
}
$self->update_status('tests_ok');
foreach my $val ( values %$sftp_test_results ) {
if ( defined $val->{'err'} ) {
$self->update_status('tests_failed');
}
}
return ( 1, $sftp_test_results );
}
=head2 Internal methods
=head3 _dos2unix
Return a CR-free string from an input
=cut
sub _dos2unix {
my $dosStr = shift;
return $dosStr =~ s/\015\012/\012/gr;
}
=head3 _type
Return type of Object relating to Schema ResultSet
=cut
sub _type {
return 'SftpServer';
}
1;