http://qs321.pair.com?node_id=11124132

Clive89 has asked for the wisdom of the Perl Monks concerning the following question:

Hello Perl Monks,

i'm new to perl and trying to build a webserver that executes commands on multiple remote devices. I'm using Expect.pm to establish the ssh connection, send the commands and retrieve the results. For each connection a subprocess is started as required (tainted). The code works as expected when executed in the console but when the sub is invoked via the html interface, the Expect.pm subroutines do not retrieve the response from the remote device. I'm assuming some right issue, but i don't know how to address it ... Any help is appreciated!

Friendly Regards,

Phil

Remote Class - builds upon Expect.pm to establish a ssh connection.

#!/usr/bin/perl -wT use lib "/daten/http/toolbox_dev/cgi-bin/bambi/pw/server"; # add +source folder to path. use warnings; use strict; use config; # load + configuration. use Expect; # load + expect routines. use process; # load + multi process routines. use tools; # use Data::Dumper; # ###################################### # The Remote Class is used to execute commands on hosts. ###################################### package Remote; my $user = "XXX"; my $pass = "XXX"; my $prompt = '^\w+#'; # regular expression that matches the +command prompt (currently rough). my $timeout = 10; # seconds before the connection is abo +rted. # start a ssh connection and handle login dialog. # :param 0: host to connect to. # :return: established ssh connection. sub open_ssh { my ($host) = @_; my $ssh_cmd = "ssh ".$user."@".$host; Debug::dlog("connecting to $host"); my $exp = Expect->spawn($ssh_cmd) or return; Debug::dlog("$exp spawned ssh"); $exp->restart_timeout_upon_receive(1); $exp->log_stdout(0); $exp->expect($timeout, ["yes/no", \&send_yes, $exp], ["assword", \&send_pass, $exp], [$prompt]); return $exp; } # send the user name to a host. # :param 0: established ssh connection. # :return: sub send_user { my ($exp) = @_; $exp->send("$user\n"); Debug::dlog("$exp send user"); $exp->exp_continue; } # send the password to the host. # :param 0: established ssh connection. # :return: sub send_pass { my ($exp) = @_; $exp->send("$pass\n"); Debug::dlog("$exp send password"); $exp->exp_continue; } # send yes to the host. # :param 0: established ssh connection. # :return: sub send_yes { my ($exp) = @_; $exp->send("yes\n"); Debug::dlog("$exp send yes"); $exp->exp_continue; } # execute a command on the host and retrieve its response.. # :param 0: established ssh connection. # :param 1: command to execute. # :return: response of the host. sub exec_cmd { my ($exp, $cmd) = @_; $exp->send("$cmd\n"); await_prompt($exp); my $response = $exp->before(); Debug::dlog("$exp executed $cmd, retrieved ".length($response)." c +hars"); return $response; } # wait for acknowlegment of the host (currently rough) # :param 0: established ssh connection. # :return: sub await_prompt { my ($exp) = @_; $exp->expect($timeout, [$prompt]); } # disconnect from the host. # :param 0: established ssh connection. # :return: sub quit { my ($exp) = @_; $exp->send("exit\n"); $exp->soft_close(); Debug::dlog("$exp disconnected"); } # executes multiple commands on a host and retrieve the responses. # :param 0: array of commands to execute. # :param 1: host to connect to. # :return: dictionary with the commands as keys and the responses + as values. sub exec_on { my ($cmds, $host) = @_; my $ssh = open_ssh($host); if($ssh) { my %cmd_resp = (); foreach my $cmd (@$cmds) { $cmd_resp{$cmd} = exec_cmd($ssh, $cmd); } quit($ssh); return \%cmd_resp; } else { Debug::dlog("failed to connect to $host"); return "Couldn't start SSH"; } } # executes multiple commands on multiple hosts and retrieve the respon +ses. # :param 0: array of commands to execute. # :param 1: array of hosts to connect to. # :return: dictionary with the host names as keys and the command + dictionary as value. sub exec_on_each { my ($cmds, $hosts) = @_; my %host_resp = (); foreach my $host (@$hosts) { $host_resp{$host} = exec_on($cmds, $host); } return \%host_resp; }

Dispatcher Class - starts a subprocess and retrieves the return of a function via a pipe.

#!/usr/bin/perl -wT ###################################### # The Dispatcher Class can execute a function in a sub process. # Pipes are used for IPC. The result of the function is serialized # in order to be transmitted as a string message. ###################################### package Dispatcher; use lib "/daten/http/toolbox_dev/cgi-bin/bambi/pw/server"; # add +source folder to path. use strict; use warnings; use Data::Dumper; # seri +alization routines use tools; # $Data::Dumper::Indent = 0; # $Data::Dumper::Maxdepth = 3; # $Data::Dumper::Purity = 0; # $Data::Dumper::Deepcopy = 1; # @Dispatcher::workers = (); # array that holds all worker +objects currently running. @Dispatcher::jobs = (); # array that holds all jobs to execute +. # appends multiple jobs to the dispatchers query. # :param 0: array of jobs. # :return: sub query_jobs { my ($jobs) = @_; push(@Dispatcher::jobs, @$jobs); } # appends a job to the dispatchers query. # :param 0: job # :return: sub query_job { my ($job) = @_; push(@Dispatcher::jobs, $job); } # executes all jobs in the query. # :return: the data returned from the workers. sub execute_jobs { foreach my $job (@Dispatcher::jobs) { assign_job($job); # assign a worker process to t +he job. } @Dispatcher::jobs = (); # clear list of jobs. return join_jobs(); } # assigns a job to a worker process. # :param 0: job. # :return: sub assign_job { my ($job) = @_; my $pid = open my $pipe => "-|"; # create worke +r process and connect it using a pipe. Debug::dlog("spawned process : $pid"); die "Failed to fork: $!" unless defined $pid; # check that c +reation was successfull. my $routine = $job->get_routine(); # get routine +to execute. my $params = $job->get_params(); # get paramete +rs to pass. unless ($pid) { # the followin +g code is only executed in the worker process: $ENV{"PATH"} = "/usr/bin"; # delete conte +xt. my $return = $routine->(@$params); # execute rout +ine. my $dump = Dumper($return); # serialize th +e returned object. print($dump); # print data s +tring to pipe. Debug::dlog("process ".$$." dumped ".length($dump)." chars"); exit; # terminate pr +ocess. } else { # only in pare +nt process: my $worker = new Worker($pipe, $pid); # construct ne +w worker object. push(@Dispatcher::workers, $worker); # save worker +object. } } # waits till all workers are finished and returns the generated data. # :return: data returned from workers. sub join_jobs { my @output; foreach my $worker (@Dispatcher::workers) { waitpid($worker->get_pid(), 0); + # wait till worker is done. Debug::dlog("process ".$worker->get_pid()." exited"); my $data = receive_data($worker->get_pipe()); + # receive the data. Debug::dlog("received $data from process ".$worker->get_pid()) +; push(@output, $data); + # save data. } @Dispatcher::workers = (); + # clear list of workers. return @output; } # receives the serialized data and recreate the original data structur +e. # :param 0: pipe to receive from. # :return: the original data structure. sub receive_data { my ($pipe) = @_; my @lines = <$pipe>; my $data_str = join('', @lines); my $sec_str = Tools::untaint($data_str); Debug::dlog("received ".length($sec_str)." chars");; my $VAR1 = ""; eval $sec_str; return $VAR1; } ###################################### # The Job class encapsulates a routine and its parameters ###################################### package Job; # the constructor is passed the routine reference and the parameter ar +ray. # :param 0: the object name (passed automatically). # :param 1: the routine to execute. # :param 2: the parameters to pass to it. # :return: job object. sub new { my $class = shift; my $self = { _routine => shift, _params => shift }; bless $self, $class; return $self; } # returns the routine reference. # :return: routine reference. sub get_routine { my ($self) = @_; return $self->{_routine}; } # returns the parameter array. # :return: parameters array. sub get_params { my ($self) = @_; return $self->{_params}; } ###################################### # The Worker class holds the data of the spawned subprocess. ###################################### package Worker; # the constructor is passed the pipe reference and the pid. # :param 0: the object name (passed automatically). # :param 1: the pipe object. # :param 2 the pid of the corresponding prcoess. # :return: worker object. sub new { my $class = shift; my $self = { _pipe => shift, _pid => shift }; bless $self, $class; return $self; } # returns the pipe reference. # :return: pipe. sub get_pipe { my ($self) = @_; return $self->{_pipe}; } # returns the pid. # :return: pid. sub get_pid { my ($self) = @_; return $self->{_pid}; }

Replies are listed 'Best First'.
Re: Expect receives no output, when requested via HTML
by Corion (Patriarch) on Nov 24, 2020 at 13:09 UTC
    ... but when the sub is invoked via the html interface, ...

    As a first step, I would assume that the user your web server launches your program as is not the user that you're using on the console, or that it has less permissions. Also, it won't have an interactive terminal connected, so ssh will not interactively ask for user/passwords (maybe).

    To test this "interactivity" idea, consider launching your program with STDIN redirected:

    /path/to/program </dev/null

    If that also fails, or in any case, I recommend moving away from using passwords with SSH and instead using a (passwordless) key. This is more or less the same security, but at least allows you to separate the permission to read the source code of your program from the permission to read the file allowing the remote access.

    In any case, you should limit the key on the remote machine to the specific set of commands your program will be issuing.

      If have executed the following code with input redirected to dev/null

      my @cmds = ("sh int des"); # define comma +nds. my @hosts = ("XXX", "XXX"); # define hosts. my @params = (\@cmds, \@hosts); # construct pa +rameter array (currenlty mandatory for execution in a sub process). my $job = new Job(\&exec_on_each, \@params); # define job Dispatcher::query_job($job); # add jobs to +the dispatchers queue. my @out = Dispatcher::execute_jobs(); # execute all +jobs and retieve the result. print Tools::to_string(\@out); # print the re +sult in the console.
      Surprisingly this returns the result as expected. I think the user that executes the script via the website has insufficent rights to start SSH.

        You launch ssh without an absolute path name.

        Have you made sure that $ENV{PATH} is the same when run via the web server as when run via the console?

        Have you inspected the error message you get? $exp->error() might provide more information...

      Thanks for your reply! I think i understand the issue now. When there is no terminal, there can't be any interaction. Unfortunately i can not establish a passwordless connection (for reasons i would like to ommit). I will test as you advised. I just wonder, even if i connect with a key, will i be able to retrieve the devices response without any terminal? Is there another library for perl, that works without a terminal?

      Friendly Regards,

      Phil

        Personally, for interactive stuff, I would look at Net::SSH::Any or any of the modules that Net::SSH::Any uses. There you can conveniently send commands/input over a channel without needing Expect at all.

        Alternatively, you could look at IPC::Run for opening a child process (in your case, the ssh connection) for reading and writing, but this goes close to reimplementing Expect yourself and won't solve the password issue.