This is an example of how to execute a remote command using Dancer, Net::SSH2 and fork. It is NOT secure at all, it is just to have an idea and to get some lines of code to implement a function to execute a remote command.
How to send the public key to the remote servers?
To allow the remote server accept the connection, it is required this server knows and authorize the key. This is done by send the public key to the remote host execute this command:
cat ~/.ssh/id_rsa.pub | ssh $user@$server 'cat >> ~/.ssh/authorized_keys'Or this one:
ssh-copy-id -i ~/.ssh/id_rsa.pub $user@$server
There are some comments about this code:
- The password variable may be optional.
- Due to access restriction to the private key I copy it to a directory where the application can read it. Make sure it is only accessible from the dancer application.
- May be good idea to have a function to parse the command and make sure it does not have malicious commands such as 'rm'.
- I tried this code with threads but there are modules not thread-safe so I recommend to use fork instead.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
use strict; | |
use warnings; | |
use Dancer; | |
use Net::SSH2; | |
sub execCommand ($$) { | |
my ( $ssh2, $cmd ) = @_; | |
my %args=( | |
timeout => 1_000, # polling timeout | |
bufsize => 10_240, # read buffer size when polling | |
); | |
$ssh2->blocking(1); #needed for ssh->channel | |
my $chan=$ssh2->channel(); # create SSH2 channel | |
if ($ssh2->error()) { | |
return (undef, undef, 100); | |
} | |
# exec $cmd (caveat: only one command line can be executed over this channel. No "ls -l;whoami" combo. Use ssh->shell instead. | |
unless ($chan->exec($cmd)) { | |
return (undef, undef, 500); | |
} | |
# defin polling context: will poll stdout (in) and stderr (ext) | |
my @poll = ( { handle => $chan, events => ['in','ext'] } ); | |
my %std=(); # hash of strings. store stdout/stderr results | |
$ssh2->blocking( 0 ); # needed for channel->poll | |
while(!$chan->eof) { # there still something to read from channel | |
$ssh2->poll( $args{'timeout'}, [ @poll ] ); # if any event, it will be store into $poll; | |
my( $n, $buf ); # number of bytes read (n) into buffer (buf) | |
foreach my $poll ( @poll ) { # for each event | |
foreach my $ev ( qw( in ext ) ) { #for each stdout/stderr | |
next unless $poll->{revents}{$ev}; | |
#there are something to read here, into $std{$ev} hash | |
if( $n = $chan->read( $buf, $args{'bufsize'}, $ev eq 'ext' ) ) { #got n byte into buf for stdout ($ev='in') or stderr ($ev='ext') | |
$std{$ev}.=$buf; | |
} | |
} #done foreach | |
} | |
} | |
$chan->wait_closed(); #not really needed but cleaner | |
my $exit_code=$chan->exit_status(); | |
$chan->close(); #not really needed but cleaner | |
$ssh2->blocking(1); # set it back for sanity (future calls) | |
return ($std{'in'},$std{'ext'},$exit_code); | |
} | |
sub execute ($$$$) { | |
my ($ip, $username, $password, $cmd) = @_; | |
my $pid = fork(); | |
if ($pid) { | |
# This is the parent (DANCER) | |
debug "Process started with PID $pid\n"; | |
} elsif ( $pid == 0 ) { | |
# This is the child | |
my $ssh2 = Net::SSH2->new(); | |
$ssh2->connect( $ip ) or debug("Cannot connect to $ip"); | |
my $publicKeyFile = './id_rsa.pub'; # path(setting('appdir'), 'db', 'id_rsa.pub'); # I prefer to copy the public key in your app dir due to permissions issues | |
my $privateKeyFile = './id_rsa'; # path(setting('appdir'), 'db', 'id_rsa'); # I prefer to copy the private key in your app dir due to permissions issues | |
if ( $ssh2->auth_publickey( $username, $publicKeyFile, $privateKeyFile, $password ) ) { | |
my ($stdout, $stderr, $exitcode) = execCommand($ssh2, $cmd); | |
} else { | |
debug "Could not authenticate to $ip with $username"; | |
} | |
$ssh2->disconnect(); | |
} else { | |
debug "Could not fork: $!\n"; | |
} | |
} | |
set logger => "console"; | |
set log => "core"; | |
set show_errors => 1; | |
get '/uptime/:ip' => sub { | |
my $username = "the username"; | |
my $password = "the password"; | |
execute(param('ip'), $username, $password, "uptime > /tmp/dancer_example.txt"); | |
return 'uptime is running'; | |
}; | |
dance; | |
true; |
I enjoy what you guys are usually up too. This sort of clever work and coverage! Keep up the excellent works guys I've included you guys to my blogroll.
ReplyDelete