summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorFlorian Schlichting <fschlich@zedat.fu-berlin.de>2012-02-22 22:32:16 +0100
committerFlorian Schlichting <fschlich@zedat.fu-berlin.de>2012-02-22 22:32:16 +0100
commit92f4e92ad208c352c84c30fcacfca98128f4e4b7 (patch)
tree31ce5acbe9c781184d47b4f2b418b91f1327f0c3 /lib
Import original source of Net-OpenSSH 0.57
Diffstat (limited to 'lib')
-rw-r--r--lib/Net/OpenSSH.pm4128
-rw-r--r--lib/Net/OpenSSH/Constants.pm70
2 files changed, 4198 insertions, 0 deletions
diff --git a/lib/Net/OpenSSH.pm b/lib/Net/OpenSSH.pm
new file mode 100644
index 0000000..2378199
--- /dev/null
+++ b/lib/Net/OpenSSH.pm
@@ -0,0 +1,4128 @@
+package Net::OpenSSH;
+
+our $VERSION = '0.57';
+
+use strict;
+use warnings;
+
+our $debug ||= 0;
+
+our $FACTORY;
+
+use Carp qw(carp croak);
+use POSIX qw(:sys_wait_h);
+use Socket;
+use File::Spec;
+use Cwd ();
+use Scalar::Util ();
+use Errno ();
+use Net::OpenSSH::Constants qw(:error);
+
+my $thread_generation = 0;
+
+sub CLONE { $thread_generation++ };
+
+sub _debug { print STDERR '# ', (map { defined($_) ? $_ : '<undef>' } @_), "\n" }
+
+sub _debug_dump {
+ require Data::Dumper;
+ local $Data::Dumper::Terse = 1;
+ local $Data::Dumper::Indent = 0;
+ my $head = shift;
+ _debug("$head: ", Data::Dumper::Dumper(@_));
+}
+
+sub _hexdump {
+ no warnings qw(uninitialized);
+ my $data = shift;
+ while ($data =~ /(.{1,32})/smg) {
+ my $line=$1;
+ my @c= (( map { sprintf "%02x",$_ } unpack('C*', $line)),
+ ((" ") x 32))[0..31];
+ $line=~s/(.)/ my $c=$1; unpack("c",$c)>=32 ? $c : '.' /egms;
+ print STDERR "#> ", join(" ", @c, '|', $line), "\n";
+ }
+}
+
+{
+ my %good;
+
+ sub _sub_options {
+ my $sub = shift;
+ $good{__PACKAGE__ . "::$sub"} = { map { $_ => 1 } @_ };
+ }
+
+ sub _croak_bad_options (\%) {
+ my $opts = shift;
+ if (%$opts) {
+ my $sub = (caller 1)[3];
+ my $good = $good{$sub};
+ my @keys = ( $good ? grep !$good->{$_}, keys %$opts : keys %$opts);
+ if (@keys) {
+ croak "Invalid or bad combination of options ('" . CORE::join("', '", @keys) . "')";
+ }
+ }
+ }
+}
+
+sub _tcroak {
+ if (${^TAINT} > 0) {
+ push @_, " while running with -T switch";
+ goto &croak;
+ }
+ if (${^TAINT} < 0) {
+ push @_, " while running with -t switch";
+ goto &carp;
+ }
+}
+
+sub _catch_tainted_args {
+ my $i;
+ for (@_) {
+ next unless $i++;
+ if (Scalar::Util::tainted($_)) {
+ my (undef, undef, undef, $subn) = caller 1;
+ my $msg = ( $subn =~ /::([a-z]\w*)$/
+ ? "Insecure argument '$_' on '$1' method call"
+ : "Insecure argument '$_' on method call" );
+ _tcroak($msg);
+ }
+ elsif (ref($_) eq 'HASH') {
+ for (grep Scalar::Util::tainted($_), values %$_) {
+ my (undef, undef, undef, $subn) = caller 1;
+ my $msg = ( $subn =~ /::([a-z]\w*)$/
+ ? "Insecure argument on '$1' method call"
+ : "Insecure argument on method call" );
+ _tcroak($msg);
+ }
+ }
+ }
+}
+
+sub _set_error {
+ my $self = shift;
+ my $code = shift || 0;
+ my $err = $self->{_error} = ( $code
+ ? Scalar::Util::dualvar($code, join(': ', @{$self->{_error_prefix}},
+ (@_ ? @_ : "Unknown error $code")))
+ : 0 );
+ $debug and $debug & 1 and _debug "set_error($code - $err)";
+ return $err
+}
+
+my $check_eval_re = do {
+ my $path = quotemeta $INC{"Net/OpenSSH.pm"};
+ qr/at $path line \d+.$/
+};
+
+sub _check_eval_ok {
+ my ($self, $code) = @_;
+ if ($@) {
+ my $err = $@;
+ $err =~ s/$check_eval_re//;
+ $self->_set_error($code, $err);
+ return;
+ }
+ 1
+}
+
+sub _or_set_error {
+ my $self = shift;
+ $self->{_error} or $self->_set_error(@_);
+}
+
+sub _first_defined { defined && return $_ for @_; return }
+
+my $obfuscate = sub {
+ # just for the casual observer...
+ my $txt = shift;
+ $txt =~ s/(.)/chr(ord($1) ^ 47)/ges
+ if defined $txt;
+ $txt;
+};
+
+my $deobfuscate = $obfuscate;
+
+# regexp from Regexp::IPv6
+my $IPv6_re = qr((?-xism::(?::[0-9a-fA-F]{1,4}){0,5}(?:(?::[0-9a-fA-F]{1,4}){1,2}|:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})))|[0-9a-fA-F]{1,4}:(?:[0-9a-fA-F]{1,4}:(?:[0-9a-fA-F]{1,4}:(?:[0-9a-fA-F]{1,4}:(?:[0-9a-fA-F]{1,4}:(?:[0-9a-fA-F]{1,4}:(?:[0-9a-fA-F]{1,4}:(?:[0-9a-fA-F]{1,4}|:)|(?::(?:[0-9a-fA-F]{1,4})?|(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))))|:(?:(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4})?|))|(?::(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|:[0-9a-fA-F]{1,4}(?::(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[0-9a-fA-F]{1,4}){0,2})|:))|(?:(?::[0-9a-fA-F]{1,4}){0,2}(?::(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[0-9a-fA-F]{1,4}){1,2})|:))|(?:(?::[0-9a-fA-F]{1,4}){0,3}(?::(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[0-9a-fA-F]{1,4}){1,2})|:))|(?:(?::[0-9a-fA-F]{1,4}){0,4}(?::(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))|(?::[0-9a-fA-F]{1,4}){1,2})|:))));
+
+sub parse_connection_opts {
+ my ($class, $opts) = @_;
+ my ($user, $passwd, $ipv6, $host, $port, $host_squared);
+
+ my $target = delete $opts->{host};
+ defined $target or croak "mandatory host argument missing";
+
+ ($user, $passwd, $ipv6, $host, $port) =
+ $target =~ m{^
+ \s* # space
+ (?:
+ ([^\@:]+) # username
+ (?::(.*))? # : password
+ \@ # @
+ )?
+ (?: # host
+ ( # IPv6...
+ \[$IPv6_re\] # [IPv6]
+ | # or
+ $IPv6_re # IPv6
+ )
+ | # or
+ ([^\[\]\@:]+) # hostname / ipv4
+ )
+ (?::([^\@:]+))? # port
+ \s* # space
+ $}ix
+ or croak "bad host/target '$target' specification";
+
+ if (defined $ipv6) {
+ ($host) = $ipv6 =~ /^\[?(.*?)\]?$/;
+ $host_squared = "[$host]";
+ }
+ else {
+ $host_squared = $host;
+ }
+
+ $user = delete $opts->{user} unless defined $user;
+ $port = delete $opts->{port} unless defined $port;
+ $passwd = delete $opts->{passwd} unless defined $passwd;
+ $passwd = delete $opts->{password} unless defined $passwd;
+
+ wantarray and return ($host, $port, $user, $passwd, $host_squared);
+
+ my %r = ( user => $user,
+ password => $passwd,
+ host => $host,
+ host_squared => $host_squared,
+ port => $port );
+ $r{ipv6} = 1 if defined $ipv6;
+ return \%r;
+}
+
+sub new {
+ ${^TAINT} and &_catch_tainted_args;
+
+ my $class = shift;
+ @_ & 1 and unshift @_, 'host';
+
+ return $FACTORY->($class, @_) if defined $FACTORY;
+
+ my %opts = @_;
+
+ my $external_master = delete $opts{external_master};
+ # reuse_master is an obsolete alias:
+ $external_master = delete $opts{reuse_master} unless defined $external_master;
+
+ if (not defined $opts{host} and defined $external_master) {
+ $opts{host} = 'UNKNOWN';
+ }
+
+ my ($host, $port, $user, $passwd, $host_squared) = $class->parse_connection_opts(\%opts);
+
+ my ($passphrase, $key_path, $login_handler);
+
+ unless (defined $passwd) {
+ $key_path = delete $opts{key_path};
+ $passwd = delete $opts{passphrase};
+ if (defined $passwd) {
+ $passphrase = 1;
+ }
+ else {
+ $login_handler = delete $opts{login_handler};
+ }
+ }
+
+ my $batch_mode = delete $opts{batch_mode};
+ my $ctl_path = delete $opts{ctl_path};
+ my $ctl_dir = delete $opts{ctl_dir};
+ my $proxy_command = delete $opts{proxy_command};
+ my $gateway = delete $opts{gateway} unless defined $proxy_command;
+ my $ssh_cmd = _first_defined delete $opts{ssh_cmd}, 'ssh';
+ my $rsync_cmd = _first_defined delete $opts{rsync_cmd}, 'rsync';
+ my $scp_cmd = delete $opts{scp_cmd};
+ my $timeout = delete $opts{timeout};
+ my $kill_ssh_on_timeout = delete $opts{kill_ssh_on_timeout};
+ my $strict_mode = _first_defined delete $opts{strict_mode}, 1;
+ my $async = delete $opts{async};
+ my $target_os = _first_defined delete $opts{target_os}, 'unix';
+ my $expand_vars = delete $opts{expand_vars};
+ my $vars = _first_defined delete $opts{vars}, {};
+ my $default_encoding = delete $opts{default_encoding};
+ my $default_stream_encoding =
+ _first_defined delete $opts{default_stream_encoding}, $default_encoding;
+ my $default_argument_encoding =
+ _first_defined delete $opts{default_argument_encoding}, $default_encoding;
+
+ my ($master_opts, @master_opts,
+ $master_stdout_fh, $master_stderr_fh,
+ $master_stdout_discard, $master_stderr_discard);
+ unless ($external_master) {
+ ($master_stdout_fh = delete $opts{master_stdout_fh} or
+ $master_stdout_discard = delete $opts{master_stdout_discard});
+
+ ($master_stderr_fh = delete $opts{master_stderr_fh} or
+ $master_stderr_discard = delete $opts{master_stderr_discard});
+
+ $master_opts = delete $opts{master_opts};
+ if (defined $master_opts) {
+ if (ref $master_opts) {
+ @master_opts = @$master_opts;
+ }
+ else {
+ carp "'master_opts' argument looks like if it should be splited first"
+ if $master_opts =~ /^-\w\s+\S/;
+ @master_opts = $master_opts;
+ }
+ }
+ }
+
+ my $default_ssh_opts = delete $opts{default_ssh_opts};
+ carp "'default_ssh_opts' argument looks like if it should be splited first"
+ if defined $default_ssh_opts and not ref $default_ssh_opts and $default_ssh_opts =~ /^-\w\s+\S/;
+
+ my ($default_stdout_fh, $default_stderr_fh, $default_stdin_fh,
+ $default_stdout_file, $default_stderr_file, $default_stdin_file,
+ $default_stdout_discard, $default_stderr_discard, $default_stdin_discard);
+
+ $default_stdout_file = (delete $opts{default_stdout_discard}
+ ? '/dev/null'
+ : delete $opts{default_stdout_file});
+ $default_stdout_fh = delete $opts{default_stdout_fh}
+ unless defined $default_stdout_file;
+
+ $default_stderr_file = (delete $opts{default_stderr_discard}
+ ? '/dev/null'
+ : delete $opts{default_stderr_file});
+ $default_stderr_fh = delete $opts{default_stderr_fh}
+ unless defined $default_stderr_file;
+
+ $default_stdin_file = (delete $opts{default_stdin_discard}
+ ? '/dev/null'
+ : delete $opts{default_stdin_file});
+ $default_stdin_fh = delete $opts{default_stdin_fh}
+ unless defined $default_stdin_file;
+
+ _croak_bad_options %opts;
+
+ my @ssh_opts;
+ # TODO: are those options really requiered or just do they eat on
+ # the command line limited length?
+ push @ssh_opts, -l => $user if defined $user;
+ push @ssh_opts, -p => $port if defined $port;
+
+ my $home = do {
+ local $SIG{__DIE__};
+ local $@;
+ eval { Cwd::realpath((getpwuid $>)[7]) }
+ };
+
+ if (${^TAINT}) {
+ ($home) = $home =~ /^(.*)$/;
+ Scalar::Util::tainted($ENV{PATH}) and
+ _tcroak('Insecure $ENV{PATH}');
+ }
+
+ my $self = { _error => 0,
+ _error_prefix => [],
+ _perl_pid => $$,
+ _thread_generation => $thread_generation,
+ _ssh_cmd => $ssh_cmd,
+ _scp_cmd => $scp_cmd,
+ _rsync_cmd => $rsync_cmd,
+ _pid => undef,
+ _host => $host,
+ _host_squared => $host_squared,
+ _user => $user,
+ _port => $port,
+ _passwd => $obfuscate->($passwd),
+ _passphrase => $passphrase,
+ _key_path => $key_path,
+ _login_handler => $login_handler,
+ _timeout => $timeout,
+ _proxy_command => $proxy_command,
+ _gateway_args => $gateway,
+ _kill_ssh_on_timeout => $kill_ssh_on_timeout,
+ _batch_mode => $batch_mode,
+ _home => $home,
+ _external_master => $external_master,
+ _default_ssh_opts => $default_ssh_opts,
+ _default_stdin_fh => $default_stdin_fh,
+ _default_stdout_fh => $default_stdout_fh,
+ _default_stderr_fh => $default_stderr_fh,
+ _master_stdout_fh => $master_stdout_fh,
+ _master_stderr_fh => $master_stderr_fh,
+ _master_stdout_discard => $master_stdout_discard,
+ _master_stderr_discard => $master_stderr_discard,
+ _target_os => $target_os,
+ _default_stream_encoding => $default_stream_encoding,
+ _default_argument_encoding => $default_argument_encoding,
+ _expand_vars => $expand_vars,
+ _vars => $vars };
+ bless $self, $class;
+
+ # default file handles are opened so late in order to have the
+ # $self object to report errors
+ $self->{_default_stdout_fh} = $self->_open_file('>', $default_stdout_file)
+ if defined $default_stdout_file;
+ $self->{_default_stderr_fh} = $self->_open_file('>', $default_stderr_file)
+ if defined $default_stderr_file;
+ $self->{_default_stdin_fh} = $self->_open_file('<', $default_stdin_file)
+ if defined $default_stdin_file;
+
+ if ($self->error == OSSH_SLAVE_PIPE_FAILED) {
+ $self->_set_error(OSSH_MASTER_FAILED,
+ "Unable to create default slave stream: " . $self->error);
+ return $self;
+ }
+
+ $self->{_ssh_opts} = [$self->_expand_vars(@ssh_opts)];
+ $self->{_master_opts} = [$self->_expand_vars(@master_opts)];
+
+ $ctl_path = $self->_expand_vars($ctl_path);
+ $ctl_dir = $self->_expand_vars($ctl_dir);
+
+ unless (defined $ctl_path) {
+ $external_master and croak "external_master is set but ctl_path is not defined";
+
+ unless (defined $ctl_dir) {
+ unless (defined $self->{_home}) {
+ $self->_set_error(OSSH_MASTER_FAILED, "unable to determine home directory for uid $>");
+ return $self;
+ }
+
+ $ctl_dir = File::Spec->catdir($self->{_home}, ".libnet-openssh-perl");
+ }
+
+ my $old_umask = umask 077;
+ mkdir $ctl_dir;
+ umask $old_umask;
+ unless (-d $ctl_dir) {
+ $self->_set_error(OSSH_MASTER_FAILED, "unable to create ctl_dir $ctl_dir");
+ return $self;
+ }
+
+ my $target = join('-', grep defined, $user, $host, $port);
+
+ for (1..10) {
+ $ctl_path = File::Spec->join($ctl_dir, sprintf("%s-%d-%d", substr($target, 0, 20), $$, rand(1e6)));
+ last unless -e $ctl_path
+ }
+ if (-e $ctl_path) {
+ $self->_set_error(OSSH_MASTER_FAILED,
+ "unable to find unused name for ctl_path inside ctl_dir $ctl_dir");
+ return $self;
+ }
+ }
+ $ctl_dir = File::Spec->catpath((File::Spec->splitpath($ctl_path))[0,1], "");
+ $debug and $debug & 2 and _debug "ctl_path: $ctl_path, ctl_dir: $ctl_dir";
+
+ if ($strict_mode and !$self->_is_secure_path($ctl_dir)) {
+ $self->_set_error(OSSH_MASTER_FAILED, "ctl_dir $ctl_dir is not secure");
+ return $self;
+ }
+
+ $self->{_ctl_path} = $ctl_path;
+
+ if ($external_master) {
+ $self->_wait_for_master($async, 1);
+ }
+ else {
+ $self->_connect($async);
+ }
+ $self;
+}
+
+sub get_user { shift->{_user} }
+sub get_host { shift->{_host} }
+sub get_port { shift->{_port} }
+sub get_master_pid { shift->{_pid} }
+sub get_ctl_path { shift->{_ctl_path} }
+sub get_expand_vars { shift->{_expand_vars} }
+
+sub set_expand_vars {
+ my $self = shift;
+ $self->{_expand_vars} = (shift(@_) ? 1 : 0);
+}
+
+sub set_var {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my $k = shift;
+ $k =~ /^(?:USER|HOST|PORT)$/
+ and croak "internal variable %$k% can not be set";
+ $self->{_vars}{$k} = shift;
+}
+
+sub get_var {
+ my ($self, $k) = @_;
+ my $v = ( $k =~ /^(?:USER|HOST|PORT)$/
+ ? $self->{lc "_$k"}
+ : $self->{_vars}{$k} );
+ (defined $v ? $v : '');
+}
+
+sub _expand_vars {
+ my ($self, @str) = @_;
+ if (ref $self and $self->{_expand_vars}) {
+ for (@str) {
+ s{%(\w*)%}{length ($1) ? $self->get_var($1) : '%'}ge
+ if defined $_;
+ }
+ }
+ wantarray ? @str : $str[0]
+}
+
+sub error { shift->{_error} }
+
+sub die_on_error {
+ my $ssh = shift;
+ $ssh->{_error} and croak(@_ ? "@_: $ssh->{_error}" : $ssh->{_error});
+}
+
+
+sub _is_secure_path {
+ my ($self, $path) = @_;
+ my @parts = File::Spec->splitdir(Cwd::realpath($path));
+ my $home = $self->{_home};
+ for my $last (reverse 0..$#parts) {
+ my $dir = File::Spec->catdir(@parts[0..$last]);
+ unless (-d $dir) {
+ $debug and $debug & 2 and _debug "$dir is not a directory";
+ return undef;
+ }
+ my ($mode, $uid) = (stat $dir)[2, 4];
+ $debug and $debug & 2 and _debug "_is_secure_path(dir: $dir, file mode: $mode, file uid: $uid, euid: $>";
+ return undef unless(($uid == $> or $uid == 0 ) and (($mode & 022) == 0));
+ return 1 if (defined $home and $home eq $dir);
+ }
+ return 1;
+}
+
+sub _make_ssh_call {
+ my $self = shift;
+ my @before = @{shift || []};
+ my @args = ($self->{_ssh_cmd}, @before,
+ -S => $self->{_ctl_path},
+ @{$self->{_ssh_opts}}, $self->{_host_squared},
+ '--',
+ (@_ ? "@_" : ()));
+ $debug and $debug & 8 and _debug_dump 'call args' => \@args;
+ @args;
+}
+
+sub _scp_cmd {
+ my $self = shift;
+ $self->{_scp_cmd} ||= do {
+ my $scp = $self->{_ssh_cmd};
+ $scp =~ s/ssh$/scp/i or croak "scp command name not set";
+ $scp;
+ }
+}
+
+sub _make_scp_call {
+ my $self = shift;
+ my @before = @{shift || []};
+ my @args = ($self->_scp_cmd, @before,
+ -o => "ControlPath=$self->{_ctl_path}",
+ -S => $self->{_ssh_cmd},
+ (defined $self->{_port} ? (-P => $self->{_port}) : ()),
+ '--', @_);
+
+ $debug and $debug & 8 and _debug_dump 'scp call args' => \@args;
+ @args;
+}
+
+sub _rsync_quote {
+ my ($self, @args) = @_;
+ for (@args) {
+ if (/['"\s]/) {
+ s/"/""/g;
+ $_ = qq|"$_"|;
+ }
+ s/%/%%/;
+ }
+ wantarray ? @args : join(' ', @args);
+}
+
+sub _make_rsync_call {
+ my $self = shift;
+ my $before = shift;
+ my @transport = ($self->{_ssh_cmd}, @$before,
+ -S => $self->{_ctl_path});
+ my $transport = $self->_rsync_quote(@transport);
+ my @args = ( $self->{_rsync_cmd},
+ -e => $transport,
+ @_);
+
+ $debug and $debug & 8 and _debug_dump 'rsync call args' => \@args;
+ @args;
+}
+
+sub _make_tunnel_call {
+ @_ == 4 or croak "bad number of arguments for creating a tunnel";
+ my $self = shift;
+ my @before = @{shift||[]};
+ my $dest = join(':', @_);
+ push @before, "-W$dest";
+ my @args = $self->_make_ssh_call(\@before);
+ $debug and $debug & 8 and _debug_dump 'tunnel call args' => \@args;
+ @args;
+}
+
+sub master_exited {
+ my $self = shift;
+ my $pid = delete $self->{_pid};
+ delete $self->{_wfm_state};
+ $self->_set_error(OSSH_MASTER_FAILED, "master ssh connection broken");
+ undef;
+}
+
+sub _kill_master {
+ my $self = shift;
+ my $pid = delete $self->{_pid};
+ $debug and $debug & 32 and _debug '_kill_master: ', $pid;
+ if ($pid and $self->{_perl_pid} == $$ and $self->{_thread_generation} == $thread_generation) {
+ local $SIG{CHLD} = sub {};
+ for my $sig (0, 0, 'TERM', 'TERM', 'TERM', 'KILL', 'KILL') {
+ if ($sig) {
+ $debug and $debug & 32 and _debug "killing master with signal $sig";
+ kill $sig, $pid
+ or return;
+ }
+ for (0..5) {
+ my $r = waitpid($pid, WNOHANG);
+ $debug and $debug & 32 and _debug "waitpid(master: $pid) => pid: $r, rc: $!";
+ return if ($r == $pid or $! == Errno::ECHILD);
+ select(undef, undef, undef, 0.2);
+ }
+ }
+ warn "unable to kill SSH master connection (pid: $pid)";
+ }
+}
+
+sub _check_is_system_fh {
+ my ($name, $fh) = @_;
+ my $fn = fileno(defined $fh ? $fh : $name);
+ defined $fn and $fn >= 0 and return;
+ croak "child process $name is not a real system file handle";
+}
+
+sub _master_redirect {
+ my $self = shift;
+ my $uname = uc shift;
+ my $name = lc $uname;
+
+ no strict 'refs';
+ if ($self->{"_master_${name}_discard"}) {
+ open *$uname, '>>', '/dev/null';
+ }
+ else {
+ my $fh = $self->{"_master_${name}_fh"};
+ $fh = $self->{"_default_${name}_fh"} unless defined $fh;
+ if (defined $fh) {
+ _check_is_system_fh $uname => $fh;
+ if (fileno $fh != fileno *$uname) {
+ open *$uname, '>>&', $fh or POSIX::_exit(255);
+ }
+ }
+ }
+}
+
+sub _connect {
+ my ($self, $async) = @_;
+ $self->_set_error;
+
+ my $timeout = int((($self->{_timeout} || 90) + 2)/3);
+ my @master_opts = (@{$self->{_master_opts}},
+ -o => "ServerAliveInterval=$timeout",
+ '-x2MN');
+
+ my ($mpty, $use_pty, $pref_auths);
+ $use_pty = 1 if defined $self->{_login_handler};
+ if (defined $self->{_passwd}) {
+ $use_pty = 1;
+ $pref_auths = ($self->{_passphrase}
+ ? 'publickey'
+ : 'keyboard-interactive,password');
+ push @master_opts, -o => 'NumberOfPasswordPrompts=1';
+ }
+ elsif ($self->{_batch_mode}) {
+ push @master_opts, -o => 'BatchMode=yes';
+ }
+
+ if (defined $self->{_key_path}) {
+ $pref_auths = 'publickey';
+ push @master_opts, -i => $self->{_key_path};
+ }
+
+ my $proxy_command = $self->{_proxy_command};
+
+ my $gateway;
+ if (my $gateway_args = $self->{_gateway_args}) {
+ if (ref $gateway_args eq 'HASH') {
+ _load_module('Net::OpenSSH::Gateway');
+ my $errors;
+ unless ($gateway = Net::OpenSSH::Gateway->find_gateway(errors => $errors,
+ host => $self->{_host}, port => $self->{_port},
+ %$gateway_args)) {
+ $self->_set_error(OSSH_MASTER_FAILED, 'Unable to build gateway object', join(', ', @$errors));
+ return undef;
+ }
+ }
+ else {
+ $gateway = $gateway_args
+ }
+ $self->{_gateway} = $gateway;
+ unless ($gateway->before_ssh_connect) {
+ $self->_set_error(OSSH_MASTER_FAILED, 'Gateway setup failed', join(', ', $gateway->errors));
+ return;
+ }
+ $proxy_command = $gateway->proxy_command;
+ }
+
+ if (defined $proxy_command) {
+ push @master_opts, -o => "ProxyCommand=$proxy_command";
+ }
+
+ if ($use_pty) {
+ _load_module('IO::Pty');
+ $self->{_mpty} = $mpty = IO::Pty->new;
+ }
+
+ push @master_opts, -o => "PreferredAuthentications=$pref_auths"
+ if defined $pref_auths;
+
+ my @call = $self->_make_ssh_call(\@master_opts);
+
+ local $SIG{CHLD};
+ my $pid = fork;
+ unless (defined $pid) {
+ $self->_set_error(OSSH_MASTER_FAILED, "unable to fork ssh master: $!");
+ return undef;
+ }
+ unless ($pid) {
+ $mpty->make_slave_controlling_terminal if $mpty;
+
+ $self->_master_redirect('STDOUT');
+ $self->_master_redirect('STDERR');
+
+ if (defined $self->{_passwd}) {
+ delete $ENV{SSH_ASKPASS};
+ delete $ENV{SSH_AUTH_SOCK};
+ }
+
+ local $SIG{__DIE__};
+ eval { exec @call };
+ POSIX::_exit(255);
+ }
+ $self->{_pid} = $pid;
+ my $r = $self->_wait_for_master($async, 1);
+ $mpty->close_slave if $mpty;
+ $r;
+}
+
+sub _waitpid {
+ my ($self, $pid, $timeout) = @_;
+ $? = 0;
+ if ($pid) {
+ $timeout = $self->{_timeout} unless defined $timeout;
+
+ my $time_limit;
+ if (defined $timeout and $self->{_kill_ssh_on_timeout}) {
+ $timeout = 0 if $self->error == OSSH_SLAVE_TIMEOUT;
+ $time_limit = time + $timeout;
+ }
+ local $SIG{CHLD} = sub {};
+ while (1) {
+ my $r;
+ if (defined $time_limit) {
+ while (1) {
+ # TODO: we assume that all OSs return 0 when the
+ # process is still running, that may not be true!
+ $r = waitpid($pid, WNOHANG) and last;
+ my $remaining = $time_limit - time;
+ if ($remaining <= 0) {
+ $debug and $debug & 16 and _debug "killing SSH slave, pid: $pid";
+ kill TERM => $pid;
+ $self->_or_set_error(OSSH_SLAVE_TIMEOUT, "ssh slave failed", "timed out");
+ }
+ # There is a race condition here. We try to
+ # minimize it keeping the waitpid and the select
+ # together and limiting the sleep time to 1s:
+ my $sleep = ($remaining < 0.1 ? 0.1 : 1);
+ $debug and $debug & 16 and
+ _debug "waiting for slave, timeout: $timeout, remaining: $remaining, sleep: $sleep";
+ $r = waitpid($pid, WNOHANG) and last;
+ select(undef, undef, undef, $sleep);
+ }
+ }
+ else {
+ $r = waitpid($pid, 0);
+ }
+ $debug and $debug & 16 and _debug "_waitpid($pid) => pid: $r, rc: $!";
+ if ($r == $pid) {
+ if ($?) {
+ my $signal = ($? & 255);
+ my $errstr = "child exited with code " . ($? >> 8);
+ $errstr .= ", signal $signal" if $signal;
+ $self->_or_set_error(OSSH_SLAVE_CMD_FAILED, $errstr);
+ return undef;
+ }
+ return 1;
+ }
+ if ($r > 0) {
+ warn "internal error: spurious process $r exited";
+ next;
+ }
+ next if $! == Errno::EINTR();
+ if ($! == Errno::ECHILD) {
+ $self->_or_set_error(OSSH_SLAVE_FAILED, "child process $pid does not exist", $!);
+ return undef
+ }
+ warn "Internal error: unexpected error (".($!+0).": $!) from waitpid($pid) = $r. Report it, please!";
+
+ # wait a bit before trying again
+ select(undef, undef, undef, 0.1);
+ }
+ }
+ else {
+ $self->_or_set_error(OSSH_SLAVE_FAILED, "spawning of new process failed");
+ return undef;
+ }
+}
+
+sub wait_for_master {
+ my $self = shift;
+ @_ <= 1 or croak 'Usage: $ssh->wait_for_master([$async])';
+ return undef if $self->{_error} == OSSH_MASTER_FAILED;
+ $self->{_error} = 0;
+ return $self->_wait_for_master($_[0]) if $self->{_wfm_state};
+
+ unless (-S $self->{_ctl_path}) {
+ $self->_set_error(OSSH_MASTER_FAILED, "master ssh connection broken");
+ return undef;
+ }
+ 1;
+}
+
+sub check_master {
+ my $self = shift;
+ @_ and croak 'Usage: $ssh->check_master()';
+ $self->{_error} = 0;
+ $self->_wait_for_master;
+}
+
+sub _wait_for_master {
+ my ($self, $async, $reset) = @_;
+
+ my $state = delete $self->{_wfm_state} || 'waiting_for_mux_socket';
+ my $bout = \ ($self->{_wfm_bout});
+
+ my $mpty = $self->{_mpty};
+ my $passwd = $deobfuscate->($self->{_passwd});
+ my $login_handler = $self->{_login_handler};
+ my $pid = $self->{_pid};
+ # an undefined pid indicates we are reusing a master connection
+
+ if ($reset) {
+ $$bout = '';
+ $state = ( (defined $passwd and $pid) ? 'waiting_for_password_prompt' :
+ (defined $login_handler) ? 'waiting_for_login_handler' :
+ 'waiting_for_mux_socket' );
+ }
+
+ my $ctl_path = $self->{_ctl_path};
+ my $dt = ($async ? 0 : 0.1);
+ my $timeout = $self->{_timeout};
+ my $start_time = time;
+
+ my $fnopty;
+ my $rv = '';
+ if ($state eq 'waiting_for_password_prompt') {
+ $fnopty = fileno $mpty;
+ vec($rv, $fnopty, 1) = 1
+ }
+
+ local $self->{_error_prefix} = [@{$self->{_error_prefix}},
+ "unable to establish master SSH connection"];
+ while (1) {
+ last if (defined $timeout and (time - $start_time) > $timeout);
+
+ if (-e $ctl_path) {
+ $debug and $debug & 4 and _debug "file object found at $ctl_path";
+ unless (-S $ctl_path) {
+ $self->_set_error(OSSH_MASTER_FAILED,
+ "bad ssh master at $ctl_path, object is not a socket");
+ $self->_kill_master;
+ return undef;
+ }
+ my $check = $self->_master_ctl('check');
+ if (defined $check) {
+ my $error;
+ if ($check =~ /pid=(\d+)/) {
+ return 1 if (!$pid or $1 == $pid);
+ $error = "bad ssh master at $ctl_path, socket owned by pid $1 (pid $pid expected)";
+ }
+ elsif ($check =~ /illegal option/i) {
+ $error = "OpenSSH 4.1 or later required";
+ }
+ else {
+ $error = "Unknown error";
+ }
+ $self->_or_set_error(OSSH_MASTER_FAILED, $error);
+ }
+ $self->_kill_master;
+ return undef;
+ }
+ $debug and $debug & 4 and _debug "file object not yet found at $ctl_path";
+
+ if ($self->{_perl_pid} != $$ or $self->{_thread_generation} != $thread_generation) {
+ $self->_set_error(OSSH_MASTER_FAILED,
+ "process was forked or threaded before SSH connection had been established");
+ return undef;
+ }
+ if (!$pid) {
+ $self->_set_error(OSSH_MASTER_FAILED,
+ "socket does not exist");
+ return undef;
+ }
+ elsif (waitpid($pid, WNOHANG) == $pid or $! == Errno::ECHILD) {
+ my $error = "master process exited unexpectedly";
+ $error = "bad pass" . ($self->{_passphrase} ? 'phrase' : 'word') . " or $error"
+ if defined $self->{_passwd};
+ $self->_set_error(OSSH_MASTER_FAILED, $error);
+ return undef;
+ }
+ if ($state eq 'waiting_for_login_handler') {
+ local $SIG{__DIE__};
+ local $@;
+ if (eval { $login_handler->($self, $mpty, $bout) }) {
+ $state = 'waiting_for_mux_socket';
+ next;
+ }
+ if ($@) {
+ $self->_set_error(OSSH_MASTER_FAILED,
+ "custom login handler failed: $@");
+ return undef;
+ }
+ }
+ else {
+ my $rv1 = $rv;
+ my $n = select($rv1, undef, undef, $dt);
+ if ($n > 0) {
+ vec($rv1, $fnopty, 1)
+ or die "internal error";
+ my $read = sysread($mpty, $$bout, 4096, length $$bout);
+ if ($read) {
+ if ($state eq 'waiting_for_password_prompt') {
+ if ($$bout =~ /The authenticity of host.*can't be established/si) {
+ $self->_set_error(OSSH_MASTER_FAILED,
+ "the authenticity of the target host can't be established, the remote host "
+ . "public key is probably not present on the '~/.ssh/known_hosts' file");
+ $self->_kill_master;
+ return undef;
+ }
+ if ($$bout =~ s/^(.*:)//s) {
+ $debug and $debug & 4 and _debug "passwd/passphrase requested ($1)";
+ print $mpty "$passwd\n";
+ $state = 'waiting_for_mux_socket';
+ }
+ }
+ else { $$bout = '' }
+ next;
+ }
+ }
+ }
+ if ($async) {
+ $self->{_wfm_state} = $state;
+ return 0;
+ }
+ else {
+ select(undef, undef, undef, $dt);
+ }
+ }
+ $self->_set_error(OSSH_MASTER_FAILED, "login timeout");
+ $self->_kill_master;
+ undef;
+}
+
+sub _master_ctl {
+ my ($self, $cmd) = @_;
+ local $self->{_error_prefix} = [@{$self->{_error_prefix}},
+ "control command failed"];
+ $self->capture({ encoding => 'bytes', # don't let the encoding
+ # stuff go in the way
+ stdin_discard => 1, tty => 0,
+ stderr_to_stdout => 1, ssh_opts => [-O => $cmd]});
+}
+
+sub _make_pipe {
+ my $self = shift;
+ my ($r, $w);
+ if (pipe $r, $w) {
+ my $old = select;
+ select $r; $ |= 1;
+ select $w; $ |= 1;
+ select $old;
+ return ($r, $w);
+ }
+ $self->_set_error(OSSH_SLAVE_PIPE_FAILED, "unable to create pipe: $!");
+ return;
+}
+
+my %loaded_module;
+sub _load_module {
+ my ($module, $version) = @_;
+ $loaded_module{$module} ||= do {
+ do {
+ local $SIG{__DIE__};
+ local $@;
+ eval "require $module; 1"
+ } or croak "unable to load Perl module $module";
+ 1
+ };
+ if (defined $version) {
+ local $SIG{__DIE__};
+ local $@;
+ my $mv = eval "\$${module}::VERSION" || 0;
+ (my $mv1 = $mv) =~ s/_\d*$//;
+ croak "$module version $version required, $mv is available"
+ if $mv1 < $version;
+ }
+ 1
+}
+
+my $noquote_class = '.\\w/\\-@,:';
+my $glob_class = '*?\\[\\],{}:!^~';
+
+sub _arg_quoter {
+ sub {
+ my $quoted = join '',
+ map { ( m|^'$| ? "\\'" :
+ m|^[$noquote_class]*$|o ? $_ :
+ "'$_'" ) } split /(')/, $_[0];
+ length $quoted ? $quoted : "''";
+ }
+}
+
+sub _arg_quoter_glob {
+ sub {
+ my $arg = shift;
+ my @parts;
+ while ((pos $arg ||0) < length $arg) {
+ if ($arg =~ m|\G'|gc) {
+ push @parts, "\\'";
+ }
+ elsif ($arg =~ m|\G([$noquote_class$glob_class]+)|gco) {
+ push @parts, $1;
+ }
+ elsif ($arg =~ m|\G(\\[$glob_class\\])|gco) {
+ push @parts, $1;
+ }
+ elsif ($arg =~ m|\G\\|gc) {
+ push @parts, '\\\\'
+ }
+ elsif ($arg =~ m|\G([^$glob_class\\']+)|gco) {
+ push @parts, "'$1'";
+ }
+ else {
+ require Data::Dumper;
+ $arg =~ m|\G(.+)|gc;
+ die "Internal error: unquotable string:\n". Data::Dumper::Dumper($1) ."\n";
+ }
+ }
+ my $quoted = join('', @parts);
+ length $quoted ? $quoted : "''";
+
+ # my $arg = shift;
+ # return $arg if $arg =~ m|^[\w/\-+=?\[\],{}\@!.^~]+$|;
+ # return "''" if $arg eq '';
+ # $arg =~ s|(?<!\\)([^\w/\-+=*?\[\],{}:\@!.^\\~])|ord($1) > 127 ? $1 : $1 eq "\n" ? "'\n'" : "\\$1"|ge;
+ # $arg;
+ }
+}
+
+sub _quote_args {
+ my $self = shift;
+ my $opts = shift;
+ ref $opts eq 'HASH' or die "internal error";
+ my $quote = delete $opts->{quote_args};
+ my $quote_extended = delete $opts->{quote_args_extended};
+ my $glob_quoting = delete $opts->{glob_quoting};
+ $quote = (@_ > 1) unless defined $quote;
+
+ if ($quote) {
+ my $quoter_glob = $self->_arg_quoter_glob;
+ my $quoter = ($glob_quoting
+ ? $quoter_glob
+ : $self->_arg_quoter);
+
+ # foo => $quoter
+ # \foo => $quoter_glob
+ # \\foo => no quoting at all and disable extended quoting as it is not safe
+ my @quoted;
+ for (@_) {
+ if (ref $_) {
+ if (ref $_ eq 'SCALAR') {
+ push @quoted, $quoter_glob->($self->_expand_vars($$_));
+ }
+ elsif (ref $_ eq 'REF' and ref $$_ eq 'SCALAR') {
+ push @quoted, $self->_expand_vars($$$_);
+ undef $quote_extended;
+ }
+ else {
+ croak "invalid reference in remote command argument list"
+ }
+ }
+ else {
+ push @quoted, $quoter->($self->_expand_vars($_));
+ }
+ }
+
+ if ($quote_extended) {
+ push @quoted, '</dev/null' if $opts->{stdin_discard};
+ if ($opts->{stdout_discard}) {
+ push @quoted, '>/dev/null';
+ push @quoted, '2>&1' if ($opts->{stderr_to_stdout} || $opts->{stderr_discard})
+ }
+ else {
+ push @quoted, '2>/dev/null' if $opts->{stderr_discard};
+ }
+ }
+ wantarray ? @quoted : join(" ", @quoted);
+ }
+ else {
+ croak "reference found in argument list when argument quoting is disabled"
+ if (grep ref, @_);
+
+ my @args = $self->_expand_vars(@_);
+ wantarray ? @args : join(" ", @args);
+ }
+}
+
+sub shell_quote {
+ shift->_quote_args({quote_args => 1}, @_);
+}
+
+sub shell_quote_glob {
+ shift->_quote_args({quote_args => 1, glob_quoting => 1}, @_);
+}
+
+sub _array_or_scalar_to_list { map { defined($_) ? (ref $_ eq 'ARRAY' ? @$_ : $_ ) : () } @_ }
+
+sub make_remote_command {
+ my $self = shift;
+ $self->wait_for_master or return;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my @ssh_opts = _array_or_scalar_to_list delete $opts{ssh_opts};
+ my $tty = delete $opts{tty};
+ push @ssh_opts, ($tty ? '-qtt' : '-T') if defined $tty;
+ my $tunnel = delete $opts{tunnel};
+ my (@args);
+ if ($tunnel) {
+ @_ == 2 or croak "two arguments are required for tunnel command";
+ push @ssh_opts, "-W" . join(":", @_);
+ }
+ else {
+ @args = $self->_quote_args(\%opts, @_);
+ }
+ _croak_bad_options %opts;
+
+ my @call = $self->_make_ssh_call(\@ssh_opts, @args);
+ if (wantarray) {
+ $debug and $debug & 16 and _debug_dump make_remote_command => \@call;
+ return @call;
+ }
+ else {
+ my $call = join ' ', $self->shell_quote(@call);
+ $debug and $debug & 16 and _debug_dump 'make_remote_command (quoted)' => $call;
+ return $call
+ }
+}
+
+sub _open_file {
+ my ($self, $default_mode, $name_or_args) = @_;
+ my ($mode, @args) = (ref $name_or_args
+ ? @$name_or_args
+ : ($default_mode, $name_or_args));
+ @args = $self->_expand_vars(@args);
+ if (open my $fh, $mode, @args) {
+ return $fh;
+ }
+ else {
+ $self->_set_error(OSSH_SLAVE_PIPE_FAILED,
+ "Unable to open file '$args[0]': $!");
+ return undef;
+ }
+}
+
+sub _fileno_dup_over {
+ my ($good_fn, $fh) = @_;
+ if (defined $fh) {
+ my @keep_open;
+ my $fn = fileno $fh;
+ for (1..5) {
+ $fn >= $good_fn and return $fn;
+ $fn = POSIX::dup($fn);
+ push @keep_open, $fn;
+ }
+ POSIX::_exit(255);
+ }
+ undef;
+}
+
+sub _exec_dpipe {
+ my ($self, $cmd, $io, $err) = @_;
+ my $io_fd = _fileno_dup_over(3 => $io);
+ my $err_fd = _fileno_dup_over(3 => $err);
+ POSIX::dup2($io_fd, 0);
+ POSIX::dup2($io_fd, 1);
+ POSIX::dup2($err_fd, 2) if defined $err_fd;
+ if (ref $cmd) {
+ exec @$cmd;
+ }
+ else {
+ exec $cmd;
+ }
+}
+
+sub _delete_stream_encoding {
+ my ($self, $opts) = @_;
+ _first_defined(delete $opts->{stream_encoding},
+ $opts->{encoding},
+ $self->{_default_stream_encoding});
+}
+
+sub _delete_argument_encoding {
+ my ($self, $opts) = @_;
+ _first_defined(delete $opts->{argument_encoding},
+ delete $opts->{encoding},
+ $self->{_default_argument_encoding});
+}
+
+sub open_ex {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ $self->wait_for_master or return;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $tunnel = delete $opts{tunnel};
+ my ($stdinout_socket, $stdinout_dpipe_is_parent);
+ my $stdinout_dpipe = delete $opts{stdinout_dpipe};
+ if ($stdinout_dpipe) {
+ $stdinout_dpipe_is_parent = delete $opts{stdinout_dpipe_is_parent};
+ $stdinout_socket = 1;
+ }
+ else {
+ $stdinout_socket = delete $opts{stdinout_socket};
+ }
+
+ my ($stdin_discard, $stdin_pipe, $stdin_fh, $stdin_file, $stdin_pty,
+ $stdout_discard, $stdout_pipe, $stdout_fh, $stdout_file, $stdout_pty,
+ $stderr_discard, $stderr_pipe, $stderr_fh, $stderr_file, $stderr_to_stdout);
+ unless ($stdinout_socket) {
+ ( $stdin_discard = delete $opts{stdin_discard} or
+ $stdin_pipe = delete $opts{stdin_pipe} or
+ $stdin_fh = delete $opts{stdin_fh} or
+ $stdin_file = delete $opts{stdin_file} or
+ (not $tunnel and $stdin_pty = delete $opts{stdin_pty}) );
+
+ ( $stdout_discard = delete $opts{stdout_discard} or
+ $stdout_pipe = delete $opts{stdout_pipe} or
+ $stdout_fh = delete $opts{stdout_fh} or
+ $stdout_file = delete $opts{stdout_file} or
+ (not $tunnel and $stdout_pty = delete $opts{stdout_pty}) );
+
+ $stdout_pty and !$stdin_pty
+ and croak "option stdout_pty requires stdin_pty set";
+ }
+
+ ( $stderr_discard = delete $opts{stderr_discard} or
+ $stderr_pipe = delete $opts{stderr_pipe} or
+ $stderr_fh = delete $opts{stderr_fh} or
+ $stderr_to_stdout = delete $opts{stderr_to_stdout} or
+ $stderr_file = delete $opts{stderr_file} );
+
+ my $argument_encoding = $self->_delete_argument_encoding(\%opts);
+
+ my $ssh_opts = delete $opts{ssh_opts};
+ $ssh_opts = $self->{_default_ssh_opts} unless defined $ssh_opts;
+ my @ssh_opts = $self->_expand_vars(_array_or_scalar_to_list $ssh_opts);
+
+ my ($cmd, $close_slave_pty, @args);
+ if ($tunnel) {
+ @_ == 2 or croak 'bad number of arguments for tunnel, use $ssh->method(\\%opts, $host, $port)';
+ @args = @_;
+ }
+ else {
+ if ($stdin_pty) {
+ $close_slave_pty = delete $opts{close_slave_pty};
+ $close_slave_pty = 1 unless defined $close_slave_pty;
+ }
+
+ my $tty = delete $opts{tty};
+ push @ssh_opts, ($tty ? '-qtt' : '-T') if defined $tty;
+
+ $cmd = delete $opts{_cmd} || 'ssh';
+ $opts{quote_args_extended} = 1
+ if (not defined $opts{quote_args_extended} and $cmd eq 'ssh');
+ @args = $self->_quote_args(\%opts, @_);
+ $self->_encode_args($argument_encoding, @args) or return;
+ }
+
+ _croak_bad_options %opts;
+
+ if (defined $stdin_file) {
+ $stdin_fh = $self->_open_file('<', $stdin_file) or return
+ }
+ if (defined $stdout_file) {
+ $stdout_fh = $self->_open_file('>', $stdout_file) or return
+ }
+ if (defined $stderr_file) {
+ $stderr_fh = $self->_open_file('>', $stderr_file) or return
+ }
+
+ my ($rin, $win, $rout, $wout, $rerr, $werr);
+
+ if ($stdinout_socket) {
+ unless(socketpair $rin, $win, AF_UNIX, SOCK_STREAM, PF_UNSPEC) {
+ $self->_set_error(OSSH_SLAVE_PIPE_FAILED, "socketpair failed: $!");
+ return;
+ }
+ $wout = $rin;
+ }
+ else {
+ if ($stdin_pipe) {
+ ($rin, $win) = $self->_make_pipe or return;
+ }
+ elsif ($stdin_pty) {
+ _load_module('IO::Pty');
+ $win = IO::Pty->new;
+ unless ($win) {
+ $self->_set_error(OSSH_SLAVE_PIPE_FAILED, "unable to allocate pseudo-tty: $!");
+ return;
+ }
+ $rin = $win->slave;
+ }
+ elsif (defined $stdin_fh) {
+ $rin = $stdin_fh;
+ }
+ else {
+ $rin = $self->{_default_stdin_fh}
+ }
+ _check_is_system_fh STDIN => $rin;
+
+ if ($stdout_pipe) {
+ ($rout, $wout) = $self->_make_pipe or return;
+ }
+ elsif ($stdout_pty) {
+ $wout = $rin;
+ }
+ elsif (defined $stdout_fh) {
+ $wout = $stdout_fh;
+ }
+ else {
+ $wout = $self->{_default_stdout_fh};
+ }
+ _check_is_system_fh STDOUT => $wout;
+ }
+
+ unless ($stderr_to_stdout) {
+ if ($stderr_pipe) {
+ ($rerr, $werr) = $self->_make_pipe or return;
+ }
+ elsif (defined $stderr_fh) {
+ $werr = $stderr_fh;
+ }
+ else {
+ $werr = $self->{_default_stderr_fh};
+ }
+ _check_is_system_fh STDERR => $werr;
+ }
+
+ my @call = ( $tunnel ? $self->_make_tunnel_call(\@ssh_opts, @args) :
+ $cmd eq 'ssh' ? $self->_make_ssh_call(\@ssh_opts, @args) :
+ $cmd eq 'scp' ? $self->_make_scp_call(\@ssh_opts, @args) :
+ $cmd eq 'rsync' ? $self->_make_rsync_call(\@ssh_opts, @args) :
+ die "internal error: bad _cmd protocol" );
+
+ $debug and $debug & 16 and _debug_dump open_ex => \@call;
+
+ my $pid = fork;
+ unless ($pid) {
+ unless (defined $pid) {
+ $self->_set_error(OSSH_SLAVE_FAILED,
+ "unable to fork new ssh slave: $!");
+ return;
+ }
+
+ $stdin_discard and (open $rin, '<', '/dev/null' or POSIX::_exit(255));
+ $stdout_discard and (open $wout, '>', '/dev/null' or POSIX::_exit(255));
+ $stderr_discard and (open $werr, '>', '/dev/null' or POSIX::_exit(255));
+
+ if ($stdinout_dpipe) {
+ my $pid1 = fork;
+ defined $pid1 or POSIX::_exit(255);
+
+ unless ($pid1 xor $stdinout_dpipe_is_parent) {
+ eval { $self->_exec_dpipe($stdinout_dpipe, $win, $werr) };
+ POSIX::_exit(255);
+ }
+ }
+
+ my $rin_fd = _fileno_dup_over(0 => $rin);
+ my $wout_fd = _fileno_dup_over(1 => $wout);
+ my $werr_fd = _fileno_dup_over(2 => $werr);
+
+ if (defined $rin_fd) {
+ $win->make_slave_controlling_terminal if $stdin_pty;
+ $rin_fd == 0 or POSIX::dup2($rin_fd, 0) or POSIX::_exit(255);
+ }
+ if (defined $wout_fd) {
+ $wout_fd == 1 or POSIX::dup2($wout_fd, 1) or POSIX::_exit(255);
+ }
+ if (defined $werr_fd) {
+ $werr_fd == 2 or POSIX::dup2($werr_fd, 2) or POSIX::_exit(255);
+ }
+ elsif ($stderr_to_stdout) {
+ POSIX::dup2(1, 2) or POSIX::_exit(255);
+ }
+ do { exec @call };
+ POSIX::_exit(255);
+ }
+ $win->close_slave() if $close_slave_pty;
+ undef $win if defined $stdinout_dpipe;
+ wantarray ? ($win, $rout, $rerr, $pid) : $pid;
+}
+
+sub pipe_in {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ $self->wait_for_master or return;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $argument_encoding = $self->_delete_argument_encoding(\%opts);
+ my @args = $self->_quote_args(\%opts, @_);
+ _croak_bad_options %opts;
+
+ $self->_encode_args($argument_encoding, @args) or return;
+ my @call = $self->_make_ssh_call([], @args);
+ $debug and $debug & 16 and _debug_dump pipe_in => @call;
+ my $pid = open my $rin, '|-', @call;
+ unless ($pid) {
+ $self->_set_error(OSSH_SLAVE_FAILED,
+ "unable to fork new ssh slave: $!");
+ return;
+ }
+ return wantarray ? ($rin, $pid) : $rin;
+}
+
+sub pipe_out {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ $self->wait_for_master or return;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $argument_encoding = $self->_delete_argument_encoding(\%opts);
+ my @args = $self->_quote_args(\%opts, @_);
+ _croak_bad_options %opts;
+
+ $self->_encode_args($argument_encoding, @args) or return;
+ my @call = $self->_make_ssh_call([], @args);
+ $debug and $debug & 16 and _debug_dump pipe_out => @call;
+ my $pid = open my $rout, '-|', @call;
+ unless ($pid) {
+ $self->_set_error(OSSH_SLAVE_FAILED,
+ "unable to fork new ssh slave: $!");
+ return;
+ }
+ return wantarray ? ($rout, $pid) : $rout;
+}
+
+sub _find_encoding {
+ my ($self, $encoding, $data) = @_;
+ if (defined $encoding and $encoding ne 'bytes') {
+ _load_module('Encode');
+ my $enc = Encode::find_encoding($encoding);
+ unless (defined $enc) {
+ $self->_set_error(OSSH_ENCODING_ERROR, "bad encoding '$encoding'");
+ return
+ }
+ return $enc
+ }
+ return undef
+}
+
+sub _encode {
+ my $self = shift;
+ my $enc = shift;
+ if (defined $enc and @_) {
+ local $@;
+ eval {
+ for (@_) {
+ defined or next;
+ $_ = $enc->encode($_, Encode::FB_CROAK());
+ }
+ };
+ $self->_check_eval_ok(OSSH_ENCODING_ERROR) or return undef;
+ }
+ 1;
+}
+
+sub _encode_args {
+ if (@_ > 2) {
+ my $self = shift;
+ my $encoding = shift;
+
+ my $enc = $self->_find_encoding($encoding);
+ if ($enc) {
+ local $self->{_error_prefix} = [@{$self->{_error_prefix}}, "argument encoding failed"];
+ $self->_encode($enc, @_);
+ }
+ return !$self->error;
+ }
+ 1;
+}
+
+sub _decode {
+ my $self = shift;
+ my $enc = shift;
+ local $@;
+ eval {
+ for (@_) {
+ defined or next;
+ $_ = $enc->decode($_, Encode::FB_CROAK());
+ }
+ };
+ $self->_check_eval_ok(OSSH_ENCODING_ERROR);
+}
+
+sub _io3 {
+ my ($self, $out, $err, $in, $stdin_data, $timeout, $encoding) = @_;
+ $self->wait_for_master or return;
+ my @data = _array_or_scalar_to_list $stdin_data;
+ my ($cout, $cerr, $cin) = (defined($out), defined($err), defined($in));
+ $timeout = $self->{_timeout} unless defined $timeout;
+
+ my $has_input = grep { defined and length } @data;
+ if ($cin and !$has_input) {
+ close $in;
+ undef $cin;
+ }
+ elsif (!$cin and $has_input) {
+ croak "remote input channel is not defined but data is available for sending"
+ }
+
+ my $enc = $self->_find_encoding($encoding);
+ if ($enc and @data) {
+ local $self->{_error_prefix} = [@{$self->{_error_prefix}}, "stdin data encoding failed"];
+ $self->_encode($enc, @data) if $has_input;
+ return if $self->error;
+ }
+
+ my $bout = '';
+ my $berr = '';
+ my ($fnoout, $fnoerr, $fnoin);
+ local $SIG{PIPE} = 'IGNORE';
+
+ MLOOP: while ($cout or $cerr or $cin) {
+ $debug and $debug & 64 and _debug "io3 mloop, cin: " . ($cin || 0) .
+ ", cout: " . ($cout || 0) . ", cerr: " . ($cerr || 0);
+ my ($rv, $wv);
+
+ if ($cout or $cerr) {
+ $rv = '';
+ if ($cout) {
+ $fnoout = fileno $out;
+ vec($rv, $fnoout, 1) = 1;
+ }
+ if ($cerr) {
+ $fnoerr = fileno $err;
+ vec($rv, $fnoerr, 1) = 1
+ }
+ }
+
+ if ($cin) {
+ $fnoin = fileno $in;
+ $wv = '';
+ vec($wv, $fnoin, 1) = 1;
+ }
+
+ my $recalc_vecs;
+ FAST: until ($recalc_vecs) {
+ $debug and $debug & 64 and
+ _debug "io3 fast, cin: " . ($cin || 0) .
+ ", cout: " . ($cout || 0) . ", cerr: " . ($cerr || 0);
+ my ($rv1, $wv1) = ($rv, $wv);
+ my $n = select ($rv1, $wv1, undef, $timeout);
+ if ($n > 0) {
+ if ($cout and vec($rv1, $fnoout, 1)) {
+ my $offset = length $bout;
+ my $read = sysread($out, $bout, 20480, $offset);
+ if ($debug and $debug & 64) {
+ _debug "stdout, bytes read: ", $read, " at offset $offset";
+ $read and $debug & 128 and _hexdump substr $bout, $offset;
+ }
+ unless ($read) {
+ close $out;
+ undef $cout;
+ $recalc_vecs = 1;
+ last unless $rv =~ /[^\x00]/;
+ }
+ }
+ if ($cerr and vec($rv1, $fnoerr, 1)) {
+ my $read = sysread($err, $berr, 20480, length($berr));
+ $debug and $debug & 64 and _debug "stderr, bytes read: ", $read;
+ unless ($read) {
+ close $err;
+ undef $cerr;
+ $recalc_vecs = 1;
+ }
+ }
+ if ($cin and vec($wv1, $fnoin, 1)) {
+ my $written = syswrite($in, $data[0], 20480);
+ if ($debug and $debug & 64) {
+ _debug "stdin, bytes written: ", $written;
+ $written and $debug & 128 and _hexdump substr $data[0], 0, $written;
+ }
+ if ($written) {
+ substr($data[0], 0, $written, '');
+ while (@data) {
+ next FAST
+ if (defined $data[0] and length $data[0]);
+ shift @data;
+ }
+ }
+ close $in;
+ undef $cin;
+ $recalc_vecs = 1;
+ }
+ }
+ else {
+ next if ($n < 0 and $! == Errno::EINTR());
+ $self->_set_error(OSSH_SLAVE_TIMEOUT, 'ssh slave failed', 'timed out');
+ last MLOOP;
+ }
+ }
+ }
+ close $out if $cout;
+ close $err if $cerr;
+ close $in if $cin;
+
+ if ($enc) {
+ local $self->{_error_prefix} = [@{$self->{_error_prefix}}, 'output decoding failed'];
+ unless ($self->_decode($enc, $bout, $berr)) {
+ undef $bout;
+ undef $berr;
+ }
+ }
+ $debug and $debug & 64 and _debug "leaving _io3()";
+ return ($bout, $berr);
+}
+
+
+
+_sub_options spawn => qw(stderr_to_stdout stdin_discard stdin_fh stdin_file stdout_discard
+ stdout_fh stdout_file stderr_discard stderr_fh stderr_file
+ stdinout_dpipe stdintout_dpipe_is_parent quote_args tty ssh_opts tunnel
+ encoding argument_encoding);
+sub spawn {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ _croak_bad_options %opts;
+
+ return scalar $self->open_ex(\%opts, @_);
+}
+
+_sub_options open2 => qw(stderr_to_stdout stderr_discard stderr_fh stderr_file quote_args
+ tty ssh_opts tunnel encoding argument_encoding);
+sub open2 {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ _croak_bad_options %opts;
+
+ my ($in, $out, undef, $pid) =
+ $self->open_ex({ stdout_pipe => 1,
+ stdin_pipe => 1,
+ %opts }, @_) or return ();
+ return ($in, $out, $pid);
+}
+
+_sub_options open2pty => qw(stderr_to_stdout stderr_discard stderr_fh stderr_file quote_args tty
+ close_slave_pty ssh_opts encoding argument_encoding);
+sub open2pty {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ _croak_bad_options %opts;
+
+ my ($pty, undef, undef, $pid) =
+ $self->open_ex({ stdout_pty => 1,
+ stdin_pty => 1,
+ tty => 1,
+ %opts }, @_) or return ();
+ return ($pty, $pid);
+}
+
+_sub_options open2socket => qw(stderr_to_stdout stderr_discard stderr_fh stderr_file quote_args tty
+ ssh_opts tunnel encoding argument_encoding);
+sub open2socket {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ _croak_bad_options %opts;
+
+ my ($socket, undef, undef, $pid) =
+ $self->open_ex({ stdinout_socket => 1,
+ %opts }, @_) or return ();
+ return ($socket, $pid);
+}
+
+_sub_options open3 => qw(quote_args tty ssh_opts encoding argument_encoding);
+sub open3 {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ _croak_bad_options %opts;
+
+ my ($in, $out, $err, $pid) =
+ $self->open_ex({ stdout_pipe => 1,
+ stdin_pipe => 1,
+ stderr_pipe => 1,
+ %opts },
+ @_) or return ();
+ return ($in, $out, $err, $pid);
+}
+
+_sub_options open3pty => qw(quote_args tty close_slave_pty ssh_opts
+ encoding argument_encoding);
+sub open3pty {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ _croak_bad_options %opts;
+
+ my ($pty, undef, $err, $pid) =
+ $self->open_ex({ stdout_pty => 1,
+ stdin_pty => 1,
+ tty => 1,
+ stderr_pipe => 1,
+ %opts },
+ @_) or return ();
+ return ($pty, $err, $pid);
+}
+
+_sub_options system => qw(stdout_discard stdout_fh stdin_discard stdout_file stdin_fh stdin_file
+ quote_args stderr_to_stdout stderr_discard stderr_fh stderr_file
+ stdinout_dpipe stdinout_dpipe_is_parent tty ssh_opts tunnel encoding
+ argument_encoding);
+sub system {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $stdin_data = delete $opts{stdin_data};
+ my $timeout = delete $opts{timeout};
+ my $async = delete $opts{async};
+ _croak_bad_options %opts;
+
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{CHLD};
+
+ my $stream_encoding;
+ if (defined $stdin_data) {
+ $opts{stdin_pipe} = 1;
+ $stream_encoding = $self->_delete_stream_encoding(\%opts);
+ }
+ my ($in, undef, undef, $pid) = $self->open_ex(\%opts, @_) or return undef;
+
+ $self->_io3(undef, undef, $in, $stdin_data, $timeout, $stream_encoding) if defined $stdin_data;
+ return $pid if $async;
+ $self->_waitpid($pid, $timeout);
+}
+
+_sub_options test => qw(stdout_discard stdout_fh stdin_discard stdout_file stdin_fh stdin_file
+ quote_args stderr_to_stdout stderr_discard stderr_fh stderr_file
+ stdinout_dpipe stdinout_dpipe_is_parent stdtty ssh_opts timeout stdin_data
+ encoding stream_encoding argument_encoding);
+sub test {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ $opts{stdout_discard} = 1 unless grep defined($opts{$_}), qw(stdout_discard stdout_fh
+ stdout_file stdinout_dpipe);
+ $opts{stderr_discard} = 1 unless grep defined($opts{$_}), qw(stderr_discard stderr_fh
+ stderr_file stderr_to_stdout);
+ _croak_bad_options %opts;
+
+ $self->system(\%opts, @_);
+ my $error = $self->error;
+ unless ($error) {
+ return 1;
+ }
+ if ($error == OSSH_SLAVE_CMD_FAILED) {
+ $self->_set_error(0);
+ return 0;
+ }
+ return undef;
+}
+
+_sub_options capture => qw(stderr_to_stdout stderr_discard stderr_fh stderr_file
+ stdin_discard stdin_fh stdin_file quote_args tty ssh_opts tunnel
+ encoding argument_encoding);
+sub capture {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $stdin_data = delete $opts{stdin_data};
+ my $timeout = delete $opts{timeout};
+ _croak_bad_options %opts;
+
+ my $stream_encoding = $self->_delete_stream_encoding(\%opts);
+ $opts{stdout_pipe} = 1;
+ $opts{stdin_pipe} = 1 if defined $stdin_data;
+
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{CHLD};
+
+ my ($in, $out, undef, $pid) = $self->open_ex(\%opts, @_) or return ();
+ my ($output) = $self->_io3($out, undef, $in, $stdin_data, $timeout, $stream_encoding);
+ $self->_waitpid($pid, $timeout);
+ if (wantarray) {
+ my $pattern = quotemeta $/;
+ return split /(?<=$pattern)/, $output;
+ }
+ $output
+}
+
+_sub_options capture2 => qw(stdin_discard stdin_fh stdin_file quote_args tty ssh_opts encoding argument_encoding);
+sub capture2 {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $stdin_data = delete $opts{stdin_data};
+ my $timeout = delete $opts{timeout};
+ _croak_bad_options %opts;
+
+ my $stream_encoding = $self->_delete_stream_encoding(\%opts);
+ $opts{stdout_pipe} = 1;
+ $opts{stderr_pipe} = 1;
+ $opts{stdin_pipe} = 1 if defined $stdin_data;
+
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{CHLD};
+
+ my ($in, $out, $err, $pid) = $self->open_ex( \%opts, @_) or return ();
+ my @capture = $self->_io3($out, $err, $in, $stdin_data, $timeout, $stream_encoding);
+ $self->_waitpid($pid, $timeout);
+ wantarray ? @capture : $capture[0];
+}
+
+_sub_options open_tunnel => qw(ssh_opts stderr_discard stderr_fh stderr_file encoding argument_encoding);
+sub open_tunnel {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ $opts{stderr_discard} = 1 unless grep defined $opts{$_}, qw(stderr_discard stderr_fh stderr_file);
+ _croak_bad_options %opts;
+ @_ == 2 or croak 'Usage: $ssh->open_tunnel(\%opts, $host, $port)';
+ $opts{tunnel} = 1;
+ $self->open2socket(\%opts, @_);
+}
+
+_sub_options capture_tunnel => qw(ssh_opts stderr_discard stderr_fh stderr_file stdin_discard
+ stdin_fh stdin_file stdin_data timeout encoding stream_encoding
+ argument_encoding);
+sub capture_tunnel {
+ ${^TAINT} and &_catch_tainted_args;
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ $opts{stderr_discard} = 1 unless grep defined $opts{$_}, qw(stderr_discard stderr_fh stderr_file);
+ _croak_bad_options %opts;
+ @_ == 2 or croak 'Usage: $ssh->capture_tunnel(\%opts, $host, $port)';
+ $opts{tunnel} = 1;
+ $self->capture(\%opts, @_);
+}
+
+sub _calling_method {
+ my $method = (caller 2)[3];
+ $method =~ s/.*:://;
+ $method;
+}
+
+sub _scp_get_args {
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+
+ @_ > 0 or croak
+ 'Usage: $ssh->' . _calling_method . '(\%opts, $remote_fn1, $remote_fn2, ..., $local_fn_or_dir)';
+
+ my $glob = delete $opts{glob};
+
+ my $target = (@_ > 1 ? pop @_ : '.');
+ $target =~ m|^[^/]*:| and $target = "./$target";
+
+ my $prefix = $self->{_host_squared};
+ $prefix = "$self->{_user}\@$prefix" if defined $self->{_user};
+ my @src = map "$prefix:$_", $self->_quote_args({quote_args => 1,
+ glob_quoting => $glob},
+ @_);
+ ($self, \%opts, $target, @src);
+}
+
+sub scp_get {
+ ${^TAINT} and &_catch_tainted_args;
+ my ($self, $opts, $target, @src) = _scp_get_args @_;
+ $self->_scp($opts, @src, $target);
+}
+
+sub rsync_get {
+ ${^TAINT} and &_catch_tainted_args;
+ my ($self, $opts, $target, @src) = _scp_get_args @_;
+ $self->_rsync($opts, @src, $target);
+}
+
+sub _scp_put_args {
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+
+ @_ > 0 or croak
+ 'Usage: $ssh->' . _calling_method . '(\%opts, $local_fn1, $local_fn2, ..., $remote_dir_or_fn)';
+
+ my $glob = delete $opts{glob};
+ my $glob_flags = ($glob ? delete $opts{glob_flags} || 0 : undef);
+
+ my $prefix = $self->{_host_squared};
+ $prefix = "$self->{_user}\@$prefix" if defined $self->{_user};
+
+ my $target = $prefix . ':' . ( @_ > 1
+ ? $self->_quote_args({quote_args => 1}, pop(@_))
+ : '');
+
+ my @src = @_;
+ if ($glob) {
+ require File::Glob;
+ @src = map File::Glob::bsd_glob($_, $glob_flags), @src;
+ unless (@src) {
+ $self->_set_error(OSSH_SLAVE_FAILED,
+ "given file name patterns did not match any file");
+ return undef;
+ }
+ }
+ $_ = "./$_" for grep m|^[^/]*:|, @src;
+
+ ($self, \%opts, $target, @src);
+}
+
+sub scp_put {
+ ${^TAINT} and &_catch_tainted_args;
+ my ($self, $opts, $target, @src) = _scp_put_args @_;
+ $self->_scp($opts, @src, $target);
+}
+
+sub rsync_put {
+ ${^TAINT} and &_catch_tainted_args;
+ my ($self, $opts, $target, @src) = _scp_put_args @_;
+ $self->_rsync($opts, @src, $target);
+}
+
+_sub_options _scp => qw(stderr_to_stdout stderr_discard stderr_fh
+ stderr_file stdout_discard stdout_fh
+ stdout_file encoding argument_encoding);
+sub _scp {
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $quiet = delete $opts{quiet};
+ $quiet = 1 unless defined $quiet;
+ my $recursive = delete $opts{recursive};
+ my $copy_attrs = delete $opts{copy_attrs};
+ my $bwlimit = delete $opts{bwlimit};
+ my $async = delete $opts{async};
+ my $ssh_opts = delete $opts{ssh_opts};
+ my $timeout = delete $opts{timeout};
+ my $verbose = delete $opts{verbose};
+ _croak_bad_options %opts;
+
+ my @opts;
+ @opts = @$ssh_opts if $ssh_opts;
+ push @opts, '-q' if $quiet;
+ push @opts, '-v' if $verbose;
+ push @opts, '-r' if $recursive;
+ push @opts, '-p' if $copy_attrs;
+ push @opts, '-l', $bwlimit if defined $bwlimit;
+
+ local $self->{_error_prefix} = [@{$self->{_error_prefix}}, 'scp failed'];
+
+ my $pid = $self->open_ex({ %opts,
+ _cmd => 'scp',
+ ssh_opts => \@opts,
+ quote_args => 0 },
+ @_);
+
+ return $pid if $async;
+ $self->_waitpid($pid, $timeout);
+}
+
+my %rsync_opt_with_arg = map { $_ => 1 } qw(chmod suffix backup-dir rsync-path max-delete max-size min-size partial-dir
+ timeout modify-window temp-dir compare-dest copy-dest link-dest compress-level
+ skip-compress filter exclude exclude-from include include-from
+ out-format log-file log-file-format bwlimit protocol iconv checksum-seed);
+
+my %rsync_opt_forbiden = map { $_ => 1 } qw(rsh address port sockopts blocking-io password-file write-batch
+ only-write-batch read-batch ipv4 ipv6 version help daemon config detach
+ files-from from0 blocking-io protect-args list-only);
+
+$rsync_opt_forbiden{"no-$_"} = 1 for (keys %rsync_opt_with_arg, keys %rsync_opt_forbiden);
+
+my %rsync_error = (1, 'syntax or usage error',
+ 2, 'protocol incompatibility',
+ 3, 'errors selecting input/output files, dirs',
+ 4, 'requested action not supported: an attempt was made to manipulate 64-bit files on a platform '.
+ 'that cannot support them; or an option was specified that is supported by the client and not '.
+ 'by the server.',
+ 5, 'error starting client-server protocol',
+ 6, 'daemon unable to append to log-file',
+ 10, 'error in socket I/O',
+ 11, 'error in file I/O',
+ 12, 'error in rsync protocol data stream',
+ 13, 'errors with program diagnostics',
+ 14, 'error in IPC code',
+ 20, 'received SIGUSR1 or SIGINT',
+ 21, 'some error returned by waitpid()',
+ 22, 'error allocating core memory buffers',
+ 23, 'partial transfer due to error',
+ 24, 'partial transfer due to vanished source files',
+ 25, 'the --max-delete limit stopped deletions',
+ 30, 'timeout in data send/receive',
+ 35, 'timeout waiting for daemon connection');
+
+my %rsync_opt_open_ex = map { $_ => 1 } qw(stderr_to_stdout
+ stderr_discard stderr_fh
+ stderr_file stdout_discard
+ stdout_fh stdout_file encoding
+ argument_encoding);
+sub _rsync {
+ my $self = shift;
+ my %opts = (ref $_[0] eq 'HASH' ? %{shift()} : ());
+ my $async = delete $opts{async};
+ my $verbose = delete $opts{verbose};
+ my $quiet = delete $opts{quiet};
+ my $copy_attrs = delete $opts{copy_attrs};
+ my $timeout = delete $opts{timeout};
+ $quiet = 1 unless (defined $quiet or $verbose);
+
+ my @opts = qw(--blocking-io) ;
+ push @opts, '-q' if $quiet;
+ push @opts, '-p' if $copy_attrs;
+ push @opts, '-' . ($verbose =~ /^\d+$/ ? 'v' x $verbose : 'v') if $verbose;
+
+ my %opts_open_ex = ( _cmd => 'rsync',
+ quote_args => 0 );
+
+ for my $opt (keys %opts) {
+ my $value = $opts{$opt};
+ if (defined $value) {
+ if ($rsync_opt_open_ex{$opt}) {
+ $opts_open_ex{$opt} = $value;
+ }
+ else {
+ my $opt1 = $opt;
+ $opt1 =~ tr/_/-/;
+ $rsync_opt_forbiden{$opt1} and croak "forbiden rsync option '$opt' used";
+ if ($rsync_opt_with_arg{$opt1}) {
+ push @opts, "--$opt1=$_" for _array_or_scalar_to_list($value)
+ }
+ else {
+ $value = !$value if $opt1 =~ s/^no-//;
+ push @opts, ($value ? "--$opt1" : "--no-$opt1");
+ }
+ }
+ }
+ }
+
+ local $self->{_error_prefix} = [@{$self->{_error_prefix}}, 'rsync failed'];
+
+ my $pid = $self->open_ex(\%opts_open_ex, @opts, '--', @_);
+ return $pid if $async;
+ $self->_waitpid($pid, $timeout) and return 1;
+
+ if ($self->error == OSSH_SLAVE_CMD_FAILED and $?) {
+ my $err = ($? >> 8);
+ my $errstr = $rsync_error{$err};
+ $errstr = 'Unknown rsync error' unless defined $errstr;
+ my $signal = $? & 255;
+ my $signalstr = ($signal ? " (signal $signal)" : '');
+ $self->_set_error(OSSH_SLAVE_CMD_FAILED,
+ "command exited with code $err$signalstr: $errstr");
+ }
+ return undef
+}
+
+_sub_options sftp => qw(autoflush timeout argument_encoding encoding block_size
+ queue_size late_set_perm);
+
+sub sftp {
+ ${^TAINT} and &_catch_tainted_args;
+ @_ & 1 or croak 'Usage: $ssh->sftp(%sftp_opts)';
+ _load_module('Net::SFTP::Foreign', '1.47');
+ my ($self, %opts) = @_;
+ my $stderr_fh = delete $opts{stderr_fh};
+ my $stderr_discard = delete $opts{stderr_discard};
+ my $fs_encoding = _first_defined(delete $opts{fs_encoding},
+ $opts{argument_encoding},
+ $opts{encoding},
+ $self->{_default_argument_encoding});
+ undef $fs_encoding if (defined $fs_encoding and $fs_encoding eq 'bytes');
+ _croak_bad_options %opts;
+ $opts{timeout} = $self->{_timeout} unless defined $opts{timeout};
+ $self->wait_for_master or return undef;
+ my ($in, $out, $pid) = $self->open2( { ssh_opts => '-s',
+ stderr_fh => $stderr_fh,
+ stderr_discard => $stderr_discard },
+ 'sftp' )
+ or return undef;
+
+ my $sftp = Net::SFTP::Foreign->new(transport => [$out, $in, $pid],
+ dirty_cleanup => 0,
+ fs_encoding => $fs_encoding,
+ %opts);
+ if ($sftp->error) {
+ $self->_or_set_error(OSSH_SLAVE_SFTP_FAILED, "unable to create SFTP client", $sftp->error);
+ return undef;
+ }
+ $sftp
+}
+
+sub DESTROY {
+ my $self = shift;
+ my $pid = $self->{_pid};
+ local $@;
+ $debug and $debug & 2 and _debug("DESTROY($self, pid: ", $pid, ")");
+ if ($pid and $self->{_perl_pid} == $$ and $self->{_thread_generation} == $thread_generation) {
+ $debug and $debug & 32 and _debug("killing master");
+ local $?;
+ local $!;
+
+ unless ($self->{_wfm_state}) {
+ # we have successfully created the master connection so we
+ # can send control commands:
+ $debug and $debug & 32 and _debug("sending exit control to master");
+ $self->_master_ctl('exit');
+ }
+ $self->_kill_master;
+ }
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Net::OpenSSH - Perl SSH client package implemented on top of OpenSSH
+
+=head1 SYNOPSIS
+
+ use Net::OpenSSH;
+
+ my $ssh = Net::OpenSSH->new($host);
+ $ssh->error and
+ die "Couldn't establish SSH connection: ". $ssh->error;
+
+ $ssh->system("ls /tmp") or
+ die "remote command failed: " . $ssh->error;
+
+ my @ls = $ssh->capture("ls");
+ $ssh->error and
+ die "remote ls command failed: " . $ssh->error;
+
+ my ($out, $err) = $ssh->capture2("find /root");
+ $ssh->error and
+ die "remote find command failed: " . $ssh->error;
+
+ my ($rin, $pid) = $ssh->pipe_in("cat >/tmp/foo") or
+ die "pipe_in method failed: " . $ssh->error;
+
+ print $rin, "hello\n";
+ close $rin;
+
+ my ($rout, $pid) = $ssh->pipe_out("cat /tmp/foo") or
+ die "pipe_out method failed: " . $ssh->error;
+
+ while (<$rout>) { print }
+ close $rout;
+
+ my ($in, $out ,$pid) = $ssh->open2("foo");
+ my ($pty, $pid) = $ssh->open2pty("foo");
+ my ($in, $out, $err, $pid) = $ssh->open3("foo");
+ my ($pty, $err, $pid) = $ssh->open3pty("login");
+
+ my $sftp = $ssh->sftp();
+ $sftp->error and die "SFTP failed: " . $sftp->error;
+
+
+=head1 DESCRIPTION
+
+Net::OpenSSH is a secure shell client package implemented on top of
+OpenSSH binary client (C<ssh>).
+
+=head2 Under the hood
+
+This package is implemented around the multiplexing feature found in
+later versions of OpenSSH. That feature allows reuse of a previous SSH
+connection for running new commands (I believe that OpenSSH 4.1 is the
+first one to provide all the required functionality).
+
+When a new Net::OpenSSH object is created, the OpenSSH C<ssh> client
+is run in master mode, establishing a permanent (actually, for the
+lifetime of the object) connection to the server.
+
+Then, every time a new operation is requested a new C<ssh> process is
+started in slave mode, effectively reusing the master SSH connection
+to send the request to the remote side.
+
+=head2 Net::OpenSSH Vs Net::SSH::.* modules
+
+Why should you use Net::OpenSSH instead of any of the other Perl SSH
+clients available?
+
+Well, this is my (biased) opinion:
+
+L<Net::SSH::Perl|Net::SSH::Perl> is not well maintained nowadays
+(update: a new maintainer has stepped in so this situation could
+change!!!), requires a bunch of modules (some of them very difficult
+to install) to be acceptably efficient and has an API that is limited
+in some ways.
+
+L<Net::SSH2|Net::SSH2> is much better than Net::SSH::Perl, but not
+completely stable yet. It can be very difficult to install on some
+specific operative systems and its API is also limited, in the same
+way as L<Net::SSH::Perl|Net::SSH::Perl>.
+
+Using L<Net::SSH::Expect|Net::SSH::Expect>, in general, is a bad
+idea. Handling interaction with a shell via Expect in a generic way
+just can not be reliably done.
+
+Net::SSH is just a wrapper around any SSH binary commands available on
+the machine. It can be very slow as they establish a new SSH
+connection for every operation performed.
+
+In comparison, Net::OpenSSH is a pure perl module that doesn't have
+any mandatory dependencies (obviously, besides requiring OpenSSH
+binaries).
+
+Net::OpenSSH has a very perlish interface. Most operations are
+performed in a fashion very similar to that of the Perl builtins and
+common modules (i.e. L<IPC::Open2|IPC::Open2>).
+
+It is also very fast. The overhead introduced by launching a new ssh
+process for every operation is not appreciable (at least on my Linux
+box). The bottleneck is the latency intrinsic to the protocol, so
+Net::OpenSSH is probably as fast as an SSH client can be.
+
+Being based on OpenSSH is also an advantage: a proved, stable, secure
+(to paranoic levels), interoperable and well maintained implementation
+of the SSH protocol is used.
+
+On the other hand, Net::OpenSSH does not work on Windows, not even
+under Cygwin.
+
+Net::OpenSSH specifically requires the OpenSSH SSH client (AFAIK, the
+multiplexing feature is not available from any other SSH
+client). However, note that it will interact with any server software,
+not just servers running OpenSSH C<sshd>.
+
+For password authentication, L<IO::Pty|IO::Pty> has to be
+installed. Other modules and binaries are also required to implement
+specific functionality (for instance
+L<Net::SFTP::Foreign|Net::SFTP::Foreign>, L<Expect|Expect> or
+L<rsync(1)|rsync(1)|>).
+
+Net::OpenSSH and Net::SSH2 do not support version 1 of the SSH
+protocol.
+
+=head1 API
+
+=head2 Optional arguments
+
+Almost all methods in this package accept as first argument an
+optional reference to a hash containing parameters (C<\%opts>). For
+instance, these two method calls are equivalent:
+
+ my $out1 = $ssh->capture(@cmd);
+ my $out2 = $ssh->capture({}, @cmd);
+
+=head2 Error handling
+
+Most methods return undef (or an empty list) to indicate failure.
+
+The L</error> method can always be used to explicitly check for
+errors. For instance:
+
+ my ($output, $errput) = $ssh->capture2({timeout => 1}, "find /");
+ $ssh->error and die "ssh failed: " . $ssh->error;
+
+=head2 Net::OpenSSH methods
+
+These are the methods provided by the package:
+
+=over 4
+
+=item Net::OpenSSH->new($host, %opts)
+
+Creates a new SSH master connection
+
+C<$host> can be a hostname or an IP address. It may also
+contain the name of the user, her password and the TCP port
+number where the server is listening:
+
+ my $ssh1 = Net::OpenSSH->new('jack@foo.bar.com');
+ my $ssh2 = Net::OpenSSH->new('jack:secret@foo.bar.com:10022');
+ my $ssh3 = Net::OpenSSH->new('jsmith@2001:db8::1428:57ab'); # IPv6
+
+IPv6 addresses may optionally be enclosed in brackets:
+
+ my $ssh4 = Net::OpenSSH->new('jsmith@[::1]:1022');
+
+This method always succeeds in returning a new object. Error checking
+has to be performed explicitly afterwards:
+
+ my $ssh = Net::OpenSSH->new($host, %opts);
+ $ssh->error and die "Can't ssh to $host: " . $ssh->error;
+
+If you have problems getting Net::OpenSSH to connect to the remote
+host read the troubleshooting chapter near the end of this document.
+
+Accepted options:
+
+=over 4
+
+=item user => $user_name
+
+Login name
+
+=item port => $port
+
+TCP port number where the server is running
+
+=item passwd => $passwd
+
+=item password => $passwd
+
+User given password for authentication.
+
+Note that using password authentication in automated scripts is a very
+bad idea. When possible, you should use public key authentication
+instead.
+
+=item passphrase => $passphrase
+
+Uses given passphrase to open private key.
+
+=item key_path => $private_key_path
+
+Uses the key stored on the given file path for authentication.
+
+=item gateway => $gateway
+
+If the given argument is a gateway object as returned by
+L<Net::OpenSSH::Gateway/find_gateway> method, use it to connect to
+the remote host.
+
+If it is a hash reference, call the C<find_gateway> method first.
+
+For instance, the following code fragments are equivalent:
+
+ my $gateway = Net::OpenSSH::Gateway->find_gateway(
+ proxy => 'http://proxy.corporate.com');
+ $ssh = Net::OpenSSH->new($host, gateway => $gateway);
+
+and
+
+ $ssh = Net::OpenSSH->new($host,
+ gateway => { proxy => 'http://proxy.corporate.com'});
+
+=item proxy_command => $proxy_command
+
+Use the given command to establish the connection to the remote host
+(see C<ProxyCommand> on L<ssh_config(5)>).
+
+=item batch_mode => 1
+
+Disables querying the user for password and passphrases.
+
+=item ctl_dir => $path
+
+Directory where the SSH master control socket will be created.
+
+This directory and its parents must be writable only by the current
+effective user or root, otherwise the connection will be aborted to
+avoid insecure operation.
+
+By default C<~/.libnet-openssh-perl> is used.
+
+=item ssh_cmd => $cmd
+
+Name or full path to OpenSSH C<ssh> binary. For instance:
+
+ my $ssh = Net::OpenSSH->new($host, ssh_cmd => '/opt/OpenSSH/bin/ssh');
+
+=item scp_cmd => $cmd
+
+Name or full path to OpenSSH C<scp> binary.
+
+By default it is inferred from the C<ssh> one.
+
+=item rsync_cmd => $cmd
+
+Name or full path to C<rsync> binary. Defaults to C<rsync>.
+
+=item timeout => $timeout
+
+Maximum acceptable time that can elapse without network traffic or any
+other event happening on methods that are not immediate (for instance,
+when establishing the master SSH connection or inside methods
+C<capture>, C<system>, C<scp_get>, etc.).
+
+See also L</Timeouts>.
+
+=item kill_ssh_on_timeout => 1
+
+This option tells Net::OpenSSH to kill the local slave SSH process
+when some operation times out.
+
+See also L</Timeouts>.
+
+=item strict_mode => 0
+
+By default, the connection will be aborted if the path to the socket
+used for multiplexing is found to be non-secure (for instance, when
+any of the parent directories is writable by other users).
+
+This option can be used to disable that feature. Use with care!!!
+
+=item async => 1
+
+By default, the constructor waits until the multiplexing socket is
+available. That option can be used to defer the waiting until the
+socket is actually used.
+
+For instance, the following code connects to several remote machines
+in parallel:
+
+ my (%ssh, %ls);
+ # multiple connections are stablished in parallel:
+ for my $host (@hosts) {
+ $ssh{$host} = Net::OpenSSH->new($host, async => 1);
+ }
+ # then to run some command in all the hosts (sequentially):
+ for my $host (@hosts) {
+ $ssh{$host}->system('ls /');
+ }
+
+=item master_opts => [...]
+
+Additional options to pass to the C<ssh> command when establishing the
+master connection. For instance:
+
+ my $ssh = Net::OpenSSH->new($host,
+ master_opts => [-o => "ProxyCommand corkscrew httpproxy 8080 $host"]);
+
+=item default_ssh_opts => [...]
+
+Default slave SSH command line options for L</open_ex> and derived
+methods.
+
+For instance:
+
+ my $ssh = Net::OpenSSH->new($host,
+ default_ssh_options => [-o => "ConnectionAttempts=0"]);
+
+=item default_stdin_fh => $fh
+
+=item default_stdout_fh => $fh
+
+=item default_stderr_fh => $fh
+
+Default I/O streams for L</open_ex> and derived methods (currently, that
+means any method but L</pipe_in> and L</pipe_out> and I plan to remove
+those exceptions soon!).
+
+For instance:
+
+ open my $stderr_fh, '>>', '/tmp/$host.err' or die ...;
+ open my $stdout_fh, '>>', '/tmp/$host.log' or die ...;
+
+ my $ssh = Net::OpenSSH->new($host, default_stderr_fh => $stderr_fh,
+ default_stdout_fh => $stdout_fh);
+ $ssh->error and die "SSH connection failed: " . $ssh->error;
+
+ $ssh->scp_put("/foo/bar*", "/tmp")
+ or die "scp failed: " . $ssh->error;
+
+=item default_stdin_file = $fn
+
+=item default_stdout_file = $fn
+
+=item default_stderr_file = $fn
+
+Opens the given filenames and use it as the defaults.
+
+=item master_stdout_fh => $fh
+
+=item master_stderr_fh => $fh
+
+Redirect corresponding stdio streams of the master SSH process to
+given filehandles.
+
+=item master_stdout_discard => $bool
+
+=item master_stderr_discard => $bool
+
+Discard corresponding stdio streams.
+
+=item expand_vars => $bool
+
+Activates variable expansion inside command arguments and file paths.
+
+See L</"Variable expansion"> below.
+
+=item vars => \%vars
+
+Initial set of variables.
+
+=item external_master => 1
+
+Instead of launching a new OpenSSH client in master mode, the module
+tries to reuse an already existent one. C<ctl_path> must also be
+passed when this option is set. See also L</get_ctl_path>.
+
+Example:
+
+ $ssh = Net::OpenSSH->new('foo', external_master => 1, ctl_path = $path);
+
+=item default_encoding => $encoding
+
+=item default_stream_encoding => $encoding
+
+=item default_argument_encoding => $encoding
+
+Set default encodings. See L</Data encoding>.
+
+=item login_handler => \&custom_login_handler
+
+Some remote SSH server may require a custom login/authentication
+interaction not natively supported by Net::OpenSSH. In that cases, you
+can use this option to replace the default login logic.
+
+The callback will be invoked repeatly as C<custom_login_handler($ssh,
+$pty, $data)> where C<$ssh> is the current Net::OpenSSH object, C<pty>
+a L<IO::Pty> object attached to the slave C<ssh> process tty and
+C<$data> a reference to an scalar you can use at will.
+
+The login handler must return 1 after the login process has completed
+successfully or 0 in case it still needs to do something else. If some
+error happens, it must die.
+
+Note, that blocking operations should not be performed inside the
+login handler (at least if you want the C<async> and C<timeout>
+features to work).
+
+See also the sample script C<login_handler.pl> in the C<samples>
+directory.
+
+Usage of this option is incompatible with the C<password> and
+C<passphrase> options, you will have to handle password or passphrases
+from the custom handler yourself.
+
+=back
+
+=item $ssh->error
+
+Returns the error condition for the last performed operation.
+
+The returned value is a dualvar as $! (see L<perlvar/"$!">) that
+renders an informative message when used in string context or an error
+number in numeric context (error codes appear in
+L<Net::OpenSSH::Constants|Net::OpenSSH::Constants>).
+
+=item $ssh->get_user
+
+=item $ssh->get_host
+
+=item $ssh->get_port
+
+Return the corresponding SSH login parameters.
+
+=item $ssh->get_ctl_path
+
+X<get_ctl_path>Returns the path to the socket where the OpenSSH master
+process listens for new multiplexed connections.
+
+=item ($in, $out, $err, $pid) = $ssh->open_ex(\%opts, @cmd)
+
+X<open_ex>I<Note: this is a low level method that, probably, you don't need to use!>
+
+That method starts the command C<@cmd> on the remote machine creating
+new pipes for the IO channels as specified on the C<%opts> hash.
+
+If C<@cmd> is omitted, the remote user shell is run.
+
+Returns four values, the first three (C<$in>, C<$out> and C<$err>)
+correspond to the local side of the pipes created (they can be undef)
+and the fourth (C<$pid>) to the PID of the new SSH slave process. An
+empty list is returned on failure.
+
+Note that C<waitpid> has to be used afterwards to reap the
+slave SSH process.
+
+Accepted options:
+
+=over 4
+
+=item stdin_pipe => 1
+
+Creates a new pipe and connects the reading side to the stdin stream
+of the remote process. The writing side is returned as the first
+value (C<$in>).
+
+=item stdin_pty => 1
+
+Similar to C<stdin_pipe>, but instead of a regular pipe it uses a
+pseudo-tty (pty).
+
+Note that on some OSs (i.e. HP-UX, AIX), ttys are not reliable. They
+can overflow when large chunks are written or when data is
+written faster than it is read.
+
+=item stdin_fh => $fh
+
+Duplicates C<$fh> and uses it as the stdin stream of the remote process.
+
+=item stdin_file => $filename
+
+=item stdin_file => \@open_args
+
+Opens the file of the given name for reading and uses it as the remote
+process stdin stream.
+
+If an array reference is passed its contents are used as the arguments
+for the underlying open call. For instance:
+
+ $ssh->system({stdin_file => ['-|', 'gzip -c -d file.gz']}, $rcmd);
+
+=item stdin_discard => 1
+
+Uses /dev/null as the remote process stdin stream.
+
+=item stdout_pipe => 1
+
+Creates a new pipe and connects the writting side to the stdout stream
+of the remote process. The reading side is returned as the second
+value (C<$out>).
+
+=item stdout_pty => 1
+
+Connects the stdout stream of the remote process to the
+pseudo-pty. This option requires C<stdin_pty> to be also set.
+
+=item stdout_fh => $fh
+
+Duplicates C<$fh> and uses it as the stdout stream of the remote
+process.
+
+=item stdout_file => $filename
+
+=item stdout_file => \@open_args
+
+Opens the file of the given filename and redirect stdout there.
+
+=item stdout_discard => 1
+
+Uses /dev/null as the remote process stdout stream.
+
+=item stdinout_socket => 1
+
+Creates a new socketpair, attachs the stdin an stdout streams of the
+slave SSH process to one end and returns the other as the first value
+(C<$in>) and undef for the second (C<$out>).
+
+Example:
+
+ my ($socket, undef, undef, $pid) = $ssh->open_ex({stdinout_socket => 1},
+ '/bin/netcat $dest');
+
+See also L</open2socket>.
+
+=item stdinout_dpipe => $cmd
+
+=item stdinout_dpipe => \@cmd
+
+Runs the given command locally attaching its stdio streams to those of
+the remote SSH command. Conceptually it is equivalent to the
+L<dpipe(1)> shell command.
+
+=item stderr_pipe => 1
+
+Creates a new pipe and connects the writting side to the stderr stream
+of the remote process. The reading side is returned as the third
+value (C<$err>).
+
+Example:
+
+ my $pid = $ssh->open_ex({stdinout_dpipe => 'vncviewer -stdio'},
+ x11vnc => '-inetd');
+
+=item stderr_fh => $fh
+
+Duplicates C<$fh> and uses it as the stderr stream of the remote process.
+
+=item stderr_file => $filename
+
+Opens the file of the given name and redirects stderr there.
+
+=item stderr_to_stdout => 1
+
+Makes stderr point to stdout.
+
+=item tty => $bool
+
+Tells ssh to allocate a pseudo-tty for the remote process. By default,
+a tty is allocated if remote command stdin stream is attached to a
+tty.
+
+When this flag is set and stdin is not attached to a tty, the ssh
+master and slave processes may generate spurious warnings about failed
+tty operations. This is caused by a bug present in older versions of
+OpenSSH.
+
+=item close_slave_pty => 0
+
+When a pseudo pty is used for the stdin stream, the slave side is
+automatically closed on the parent process after forking the ssh
+command.
+
+This option dissables that feature, so that the slave pty can be
+accessed on the parent process as C<$pty-E<gt>slave>. It will have to
+be explicitly closed (see L<IO::Pty|IO::Pty>)
+
+=item quote_args => $bool
+
+See L</"Shell quoting"> below.
+
+=item ssh_opts => \@opts
+
+List of extra options for the C<ssh> command.
+
+This feature should be used with care, as the given options are not
+checked in any way by the module, and they could interfere with it.
+
+=item tunnel => $bool
+
+Instead of executing a command in the remote host, this option
+instruct Net::OpenSSH to create a TCP tunnel. The arguments become the
+target IP and port.
+
+Example:
+
+ my ($in, $out, undef, $pid) = $ssh->open_ex({tunnel => 1}, $IP, $port);
+
+See also L</Tunnels>.
+
+=item encoding => $encoding
+
+=item argument_encoding => $encoding
+
+Set encodings. See L</Data encoding>.
+
+=back
+
+Usage example:
+
+ # similar to IPC::Open2 open2 function:
+ my ($in_pipe, $out_pipe, undef, $pid) =
+ $ssh->open_ex( { stdin_pipe => 1,
+ stdout_pipe => 1 },
+ @cmd )
+ or die "open_ex failed: " . $ssh->error;
+ # do some IO through $in/$out
+ # ...
+ waitpid($pid);
+
+=item $ssh->system(\%opts, @cmd)
+
+Runs the command C<@cmd> on the remote machine.
+
+Returns true on sucess, undef otherwise.
+
+The error status is set to C<OSSH_SLAVE_CMD_FAILED> when the remote
+command exits with a non zero code (the code is available from C<$?>,
+see L<perlvar/"$?">).
+
+Example:
+
+ $ssh->system('ls -R /')
+ or die "ls failed: " . $ssh->error";
+
+As for C<system> builtin, C<SIGINT> and C<SIGQUIT> signals are
+blocked. (see L<perlfunc/system>). Also, setting C<$SIG{CHLD}> to
+C<IGNORE> or to a custom signal handler will interfere with this
+method.
+
+Accepted options:
+
+=over 4
+
+=item stdin_data => $input
+
+=item stdin_data => \@input
+
+Sends the given data through the stdin stream to the remote
+process.
+
+For example, the following code creates a file on the remote side:
+
+ $ssh->system({stdin_data => \@data}, "cat >/tmp/foo")
+ or die "unable to write file: " . $ssh->error;
+
+=item timeout => $timeout
+
+The operation is aborted after C<$timeout> seconds elapsed without
+network activity.
+
+See also L</Timeouts>.
+
+=item async => 1
+
+Does not wait for the child process to exit. The PID of the new
+process is returned.
+
+Note that when this option is combined with C<stdin_data>, the given
+data will be transferred to the remote side before returning control
+to the caller.
+
+See also the L</spawn> method documentation below.
+
+=item stdin_fh => $fh
+
+=item stdin_discard => $bool
+
+=item stdout_fh => $fh
+
+=item stdout_discard => $bool
+
+=item stderr_fh => $fh
+
+=item stderr_discard => $bool
+
+=item stderr_to_stdout => $bool
+
+=item stdinout_dpipe => $cmd
+
+=item tty => $bool
+
+See the L</open_ex> method documentation for an explanation of these
+options.
+
+=back
+
+=item $ok = $ssh->test(\%opts, @cmd);
+
+Runs the given command and returns its success/failure exit status as
+1 or 0 respectively. Returns undef when something goes wrong in the
+SSH layer.
+
+Error status is not set to OSSH_SLAVE_CMD_FAILED when the remote
+command exits with a non-zero code.
+
+By default this method discards the remote command C<stdout> and
+C<sterr> streams.
+
+Usage example:
+
+ if ($ssh->test(ps => -C => $executable)) {
+ say "$executable is running on remote machine"
+ }
+ else {
+ die "something got wrong: ". $ssh->error if $ssh->error;
+
+ say "$executable is not running on remote machine"
+ }
+
+This method support the same set of options as C<system>, except
+C<async> and C<tunnel>.
+
+=item $output = $ssh->capture(\%opts, @cmd);
+
+=item @output = $ssh->capture(\%opts, @cmd);
+
+This method is conceptually equivalent to the perl backquote operator
+(i.e. C<`ls`>): it runs the command on the remote machine and captures
+its output.
+
+In scalar context returns the output as a scalar. In list context
+returns the output broken into lines (it honors C<$/>, see
+L<perlvar/"$/">).
+
+When an error happens while capturing (for instance, the operation
+times out), the partial captured output will be returned. Error
+conditions have to be explicitly checked using the L</error>
+method. For instance:
+
+ my $output = $ssh->capture({ timeout => 10 },
+ "echo hello; sleep 20; echo bye");
+ $ssh->error and
+ warn "operation didn't complete successfully: ". $ssh->error;
+ print $output;
+
+Setting C<$SIG{CHLD}> to a custom signal handler or to C<IGNORE> will
+interfere with this method.
+
+Accepted options:
+
+=over 4
+
+=item stdin_data => $input
+
+=item stdin_data => \@input
+
+=item timeout => $timeout
+
+See L</Timeouts>.
+
+=item stdin_fh => $fh
+
+=item stdin_discard => $bool
+
+=item stderr_fh => $fh
+
+=item stderr_discard => $bool
+
+=item stderr_to_stdout => $bool
+
+=item tty => $bool
+
+See the L</open_ex> method documentation for an explanation of these
+options.
+
+=back
+
+=item ($output, $errput) = $ssh->capture2(\%opts, @cmd)
+
+captures the output sent to both stdout and stderr by C<@cmd> on the
+remote machine.
+
+Setting C<$SIG{CHLD}> to a custom signal handler or to C<IGNORE> will
+also interfere with this method.
+
+The accepted options are:
+
+=over 4
+
+=item stdin_data => $input
+
+=item stdin_data => \@input
+
+See the L</system> method documentation for an explanation of these
+options.
+
+=item timeout => $timeout
+
+See L</Timeouts>.
+
+=item stdin_fh => $fh
+
+=item stdin_discard => $bool
+
+=item tty => $bool
+
+See the L</open_ex> method documentation for an explanation of these
+options.
+
+=back
+
+=item ($in, $pid) = $ssh->pipe_in(\%opts, @cmd)
+
+X<pipe_in>This method is similar to the following Perl C<open> call
+
+ $pid = open $in, '|-', @cmd
+
+but running @cmd on the remote machine (see L<perlfunc/open>).
+
+No options are currently accepted.
+
+There is no need to perform a waitpid on the returned PID as it will
+be done automatically by perl when C<$in> is closed.
+
+Example:
+
+ my ($in, $pid) = $ssh->pipe_in('cat >/tmp/fpp')
+ or die "pipe_in failed: " . $ssh->error;
+ print $in $_ for @data;
+ close $in or die "close failed";
+
+=item ($out, $pid) = $ssh->pipe_out(\%opts, @cmd)
+
+X<pipe_out>Reciprocal to previous method, it is equivalent to
+
+ $pid = open $out, '-|', @cmd
+
+running @cmd on the remote machine.
+
+No options are currently accepted.
+
+=item ($in, $out, $pid) = $ssh->open2(\%opts, @cmd)
+
+=item ($pty, $pid) = $ssh->open2pty(\%opts, @cmd)
+
+=item ($socket, $pid) = $ssh->open2socket(\%opts, @cmd)
+
+=item ($in, $out, $err, $pid) = $ssh->open3(\%opts, @cmd)
+
+=item ($pty, $err, $pid) = $ssh->open3pty(\%opts, @cmd)
+
+Shortcuts around L</open_ex> method.
+
+=item $pid = $ssh->spawn(\%opts, @_)
+
+X<spawn>Another L</open_ex> shortcut, it launches a new remote process
+in the background and returns the PID of the local slave SSH process.
+
+At some later point in your script, C<waitpid> should be called on the
+returned PID in order to reap the slave SSH process.
+
+For instance, you can run some command on several hosts in parallel
+with the following code:
+
+ my %conn = map { $_ => Net::OpenSSH->new($_, async => 1) } @hosts;
+ my @pid;
+ for my $host (@hosts) {
+ open my($fh), '>', "/tmp/out-$host.txt"
+ or die "unable to create file: $!";
+ push @pid, $conn{$host}->spawn({stdout_fh => $fh}, $cmd);
+ }
+
+ waitpid($_, 0) for @pid;
+
+Note that C<spawn> shouldn't be used to start detached remote
+processes that may survive the local program (see also the L</FAQ>
+about running remote processes detached).
+
+=item ($socket, $pid) = $ssh->open_tunnel(\%opts, $dest_host, $port)
+
+X<open_tunnel>Similar to L</open2socket>, but instead of running a
+command, it opens a TCP tunnel to the given address. See also
+L</Tunnels>.
+
+=item $out = $ssh->capture_tunnel(\%opts, $dest_host, $port)
+
+=item @out = $ssh->capture_tunnel(\%opts, $dest_host, $port)
+
+X<capture_tunnel>Similar to L</capture>, but instead of running a command, it opens a
+TCP tunnel.
+
+Example:
+
+ $out = $ssh->capture_tunnel({stdin_data => join("\r\n",
+ "GET / HTTP/1.0",
+ "Host: www.perl.org",
+ "", "") },
+ 'www.perl.org', 80)
+
+See also L</Tunnels>.
+
+=item $ssh->scp_get(\%opts, $remote1, $remote2,..., $local_dir_or_file)
+
+=item $ssh->scp_put(\%opts, $local, $local2,..., $remote_dir_or_file)
+
+These two methods are wrappers around the C<scp> command that allow
+transfers of files to/from the remote host using the existing SSH
+master connection.
+
+When transferring several files, the target argument must point to an
+existing directory. If only one file is to be transferred, the target
+argument can be a directory or a file name or can be ommited. For
+instance:
+
+ $ssh->scp_get({glob => 1}, '/var/tmp/foo*', '/var/tmp/bar*', '/tmp');
+ $ssh->scp_put('/etc/passwd');
+
+Both L</scp_get> and L</scp_put> methods return a true value when all
+the files are transferred correctly, otherwise they return undef.
+
+Accepted options:
+
+=over 4
+
+=item quiet => 0
+
+By default, C<scp> is called with the quiet flag C<-q> enabled in
+order to suppress progress information. This option allows reenabling
+the progress indication bar.
+
+=item verbose => 1
+
+Calls C<scp> with the C<-v> flag.
+
+=item recursive => 1
+
+Copy files and directories recursively.
+
+=item glob => 1
+
+Allow expansion of shell metacharacters in the sources list so that
+wildcards can be used to select files.
+
+=item glob_flags => $flags
+
+Second argument passed to L<File::Glob::bsd_glob|File::Glob/bsd_glob> function. Only
+available for L</scp_put> method.
+
+=item copy_attrs => 1
+
+Copies modification and access times and modes from the original
+files.
+
+=item bwlimit => $Kbits
+
+Limits the used bandwith, specified in Kbit/s.
+
+=item timeout => $secs
+
+The transfer is aborted if the connection does not finish before the
+given timeout elapses. See also L</Timeouts>.
+
+=item async => 1
+
+Doesn't wait for the C<scp> command to finish. When this option is
+used, the method returns the PID of the child C<scp> process.
+
+For instance, it is possible to transfer files to several hosts in
+parallel as follows:
+
+ use Errno;
+ my (%pid, %ssh);
+ for my $host (@hosts) {
+ $ssh{$host} = Net::OpenSSH->new($host, async => 1);
+ }
+ for my $host (@hosts) {
+ $pid{$host} = $ssh{$host}->scp_put({async => 1}, $local_fn, $remote_fn)
+ or warn "scp_put to $host failed: " . $ssh{$host}->error . "\n";
+ }
+ for my $host (@hosts) {
+ if (my $pid = $pid{$host}) {
+ if (waitpid($pid, 0) > 0) {
+ my $exit = ($? >> 8);
+ $exit and warn "transfer of file to $host failed ($exit)\n";
+ }
+ else {
+ redo if ($! == EINTR);
+ warn "waitpid($pid) failed: $!\n";
+ }
+ }
+ }
+
+=item stdout_fh => $fh
+
+=item stderr_fh => $fh
+
+=item stderr_to_stdout => 1
+
+These options are passed unchanged to method L</open_ex>, allowing
+capture of the output of the C<scp> program.
+
+Note that C<scp> will not generate progress reports unless its stdout
+stream is attached to a tty.
+
+=back
+
+=item $ssh->rsync_get(\%opts, $remote1, $remote2,..., $local_dir_or_file)
+
+=item $ssh->rsync_put(\%opts, $local1, $local2,..., $remote_dir_or_file)
+
+These methods use C<rsync> over SSH to transfer files from/to the remote
+machine.
+
+They accept the same set of options as the SCP ones.
+
+Any unrecognized option will be passed as an argument to the C<rsync>
+command (see L<rsync(1)>). Underscores can be used instead of dashes
+in C<rsync> option names.
+
+For instance:
+
+ $ssh->rsync_get({exclude => '*~',
+ verbose => 1,
+ safe_links => 1},
+ '/remote/dir', '/local/dir');
+
+=item $sftp = $ssh->sftp(%sftp_opts)
+
+Creates a new L<Net::SFTP::Foreign|Net::SFTP::Foreign> object for SFTP interaction that
+runs through the ssh master connection.
+
+=item @call = $ssh->make_remote_command(%opts, @cmd)
+
+=item $call = $ssh->make_remote_command(\%opts, @cmd)
+
+This method returns the arguments required to execute a command on the
+remote machine via SSH. For instance:
+
+ my @call = $ssh->make_remote_command(ls => "/var/log");
+ system @call;
+
+In scalar context, returns the arguments quoted and joined into one
+string:
+
+ my $remote = $ssh->make_remote_comand("cd /tmp/ && tar xf -");
+ system "tar cf - . | $remote";
+
+=item $ssh->wait_for_master($async)
+
+When the connection has been established by calling the constructor
+with the C<async> option, this call allows to advance the process.
+
+If C<$async> is true, it will perform any work that can be done
+inmediately without waiting (for instance, entering the password or
+checking for the existence of the multiplexing socket) and then
+return. If a false value is given, it will finalize the connection
+process and wait until the multiplexing socket is available.
+
+It returns a true value after the connection has been succesfully
+established. False is returned if the connection process fails or if
+it has not yet completed (then, the L</error> method can be used to
+distinguish between both cases).
+
+=item $ssh->check_master
+
+This method runs several checks to ensure that the master connection
+is still alive.
+
+=item $ssh->shell_quote(@args)
+
+Returns the list of arguments quoted so that they will be restored to
+their original form when parsed by the remote shell.
+
+In scalar context returns the list of arguments quoted and joined.
+
+Usually this task is done automatically by the module. See L</"Shell
+quoting"> below.
+
+This method can also be used as a class method.
+
+Example:
+
+ my $quoted_args = Net::OpenSSH->shell_quote(@args);
+ system('ssh', '--', $host, $quoted_args);
+
+=item $ssh->shell_quote_glob(@args)
+
+This method is like the previous C<shell_quote> but leaves wildcard
+characters unquoted.
+
+It can be used as a class method also.
+
+=item $ssh->set_expand_vars($bool)
+
+Enables/disables variable expansion feature (see L</"Variable
+expansion">).
+
+=item $ssh->get_expand_vars
+
+Returns current state of variable expansion feature.
+
+=item $ssh->set_var($name, $value)
+
+=item $ssh->get_var($name, $value)
+
+These methods allow to change and to retrieve the value of the logical
+value of the given name.
+
+=item $ssh->get_master_pid
+
+Returns the PID of the master SSH process
+
+=item $ssh->master_exited
+
+This methods allows to tell the module that the master process has
+exited when we get its PID from some external wait or waitpid
+call. For instance:
+
+ my $ssh = Net::OpenSSH->new('foo', async => 1);
+
+ # create new processes
+ # ...
+
+ # rip them...
+ my $master_pid = $ssh->master_pid;
+ while ((my $pid = wait) > 0) {
+ if ($pid == $master_pid) {
+ $ssh->master_exited;
+ }
+ }
+
+If your program rips the master process and this method is not called,
+the OS could reassign the PID to a new unrelated process and the
+module would try to kill it at object destruction time.
+
+=back
+
+=head2 Shell quoting
+
+By default, when invoking remote commands, this module tries to mimic
+perl C<system> builtin in regard to argument processing. Quoting
+L<perlfunc/system>:
+
+ Argument processing varies depending on the number of arguments. If
+ there is more than one argument in LIST, or if LIST is an array with
+ more than one value, starts the program given by the first element
+ of the list with arguments given by the rest of the list. If there
+ is only one scalar argument, the argument is checked for shell
+ metacharacters, and if there are any, the entire argument is passed
+ to the system's command shell for parsing (this is "/bin/sh -c" on
+ Unix platforms, but varies on other platforms).
+
+Take for example Net::OpenSSH L</system> method:
+
+ $ssh->system("ls -l *");
+ $ssh->system('ls', '-l', '/');
+
+The first call passes the argument unchanged to ssh and it is executed
+in the remote side through the shell which interprets metacharacters.
+
+The second call escapes any shell metacharacters so that, effectively,
+it is equivalent to calling the command directly and not through the
+shell.
+
+Under the hood, as the Secure Shell protocol does not provide for this
+mode of operation and always spawns a new shell where it runs the
+given command, Net::OpenSSH quotes any shell metacharacters in the
+comand list.
+
+All the methods that invoke a remote command (system, open_ex, etc.)
+accept the option C<quote_args> that allows to force/disable shell
+quoting.
+
+For instance:
+
+ $ssh->system({quote_args => 1}, "/path with spaces/bin/foo");
+
+will correctly handle the spaces in the program path.
+
+The shell quoting mechanism implements some extensions (for instance,
+performing redirections to /dev/null on the remote side) that can be
+dissabled with the option C<quote_args_extended>:
+
+ $ssh->system({ stderr_discard => 1,
+ quote_args => 1, quote_args_extended => 0 },
+ @cmd);
+
+The option C<quote_args> can also be used to disable quoting when more
+than one argument is passed. For instance, to get some pattern
+expanded by the remote shell:
+
+ $ssh->system({quote_args => 0}, 'ls', '-l', "/tmp/files_*.dat");
+
+The method C<shell_quote> can be used to selectively quote some
+arguments and leave others untouched:
+
+ $ssh->system({quote_args => 0},
+ $ssh->shell_quote('ls', '-l'),
+ "/tmp/files_*.dat");
+
+When the glob option is set in scp and rsync file transfer methods, an
+alternative quoting method that knows about file wildcards and passes
+them unquoted is used. The set of wildcards recognized currently is
+the one supported by L<bash(1)>.
+
+Another way to selectively use quote globing or fully disable quoting
+for some specific arguments is to pass them as scalar references or
+double scalar references respectively. In practice, that means
+prepending them with one or two backslashes. For instance:
+
+ # quote the last argument for globing:
+ $ssh->system('ls', '-l', \'/tmp/my files/filed_*dat');
+
+ # append a redirection to the remote command
+ $ssh->system('ls', '-lR', \\'>/tmp/ls-lR.txt');
+
+ # expand remote shell variables and glob in the same command:
+ $ssh->system('tar', 'czf', \\'$HOME/out.tgz', \'/var/log/server.*.log');
+
+As shell quoting is a tricky matter, I expect bugs to appear in this
+area. You can see how C<ssh> is called, and the quoting used setting
+the following debug flag:
+
+ $Net::OpenSSH::debug |= 16;
+
+Also, the current shell quoting implementation expects a shell
+compatible with Unix C<sh> in the remote side. It will not work as
+expected if for instance, the remote machine runs Windows, VMS or it
+is a router.
+
+As a workaround, do any required quoting yourself and pass the quoted
+command as a string so that no further quoting is performed. For
+instance:
+
+ # for VMS
+ $ssh->system('DIR/SIZE NFOO::USERS:[JSMITH.DOCS]*.TXT;0');
+
+I plan to add support for different quoting mechanisms in the
+future... if you need it now, just ask for it!!!
+
+The current quoting mechanism does not handle possible aliases defined
+by the remote shell. In that case, to force execution of the command
+instead of the alias, the full path to the command must be used.
+
+=head2 Timeouts
+
+In order to stop remote processes when they timeout, the ideal aproach
+would be to send them signals through the SSH connection as specified
+by the protocol standard.
+
+Unfortunatelly OpenSSH does not implement that feature so Net::OpenSSH
+has to use other imperfect approaches:
+
+=over 4
+
+=item * close slave I/O streams
+
+Closing the STDIN and STDOUT streams of the unresponsive remote
+process will effectively deliver a SIGPIPE when it tries to access any
+of them.
+
+Remote processes may not access STDIN or STDOUT and even them,
+Net::OpenSSH can only close these channels when it is capturing them,
+so this approach does not always work.
+
+=item * killing the local SSH slave process
+
+This action may leave the remote process running, creating a remote
+orphan so Net::OpenSSH does not use it unless the construction option
+C<kill_ssh_on_timeout> is set.
+
+=back
+
+Luckily, future versions of OpenSSH will support signaling remote
+processes via the mux channel.
+
+=head2 Variable expansion
+
+The variable expansion feature allows to define variables that are
+expanded automatically inside command arguments and file paths.
+
+This feature is disabled by default. It is intended to be used with
+L<Net::OpenSSH::Parallel|Net::OpenSSH::Parallel> and other similar
+modules.
+
+Variables are delimited by a pair of percent signs (C<%>), for
+instance C<%HOST%>. Also, two consecutive percent signs are replaced
+by a single one.
+
+The special variables C<HOST>, C<USER> and C<PORT> are maintained
+internally by the module and take the obvious values.
+
+Variable expansion is performed before shell quoting (see L</"Shell
+quoting">).
+
+Some usage example:
+
+ my $ssh = Net::OpenSSH->new('server.foo.com', expand_vars => 1);
+ $ssh->set_var(ID => 42);
+ $ssh->system("ls >/tmp/ls.out-%HOST%-%ID%");
+
+will redirect the output of the C<ls> command to
+C</tmp/ls.out-server.foo.com-42> on the remote host.
+
+=head2 Tunnels
+
+Besides running commands on the remote host, Net::OpenSSH also allows
+to tunnel TCP connections to remote machines reachable from the SSH
+server.
+
+That feature is made available through the C<tunnel> option of the
+L</open_ex> method, and also through wrapper methods L</open_tunnel>
+and L</capture_tunnel> and most others where it makes sense.
+
+Example:
+
+ $ssh->system({tunnel => 1,
+ stdin_data => "GET / HTTP/1.0\r\n\r\n",
+ stdout_file => "/tmp/$server.res"},
+ $server, 80)
+ or die "unable to retrieve page: " . $ssh->error;
+
+or capturing the output of several requests in parallel:
+
+ my @pids;
+ for (@servers) {
+ my $pid = $ssh->spawn({tunnel => 1,
+ stdin_file => "/tmp/request.req",
+ stdout_file => "/tmp/$_.res"},
+ $_, 80);
+ if ($pid) {
+ push @pids, $pid;
+ }
+ else {
+ warn "unable to spawn tunnel process to $_: " . $ssh->error;
+ }
+ }
+ waitpid ($_, 0) for (@pids);
+
+Under the hood, in order to create a tunnel, a new C<ssh> process is
+spawned with the option C<-W${address}:${port}> (available from
+OpenSSH 5.4 and upwards) making it redirect its stdio streams to the
+remote given address. Unlike when C<ssh> C<-L> options is used to
+create tunnels, no TCP port is opened on the local machine at any time
+so this is a perfectly secure operation.
+
+The PID of the new process is returned by the named methods. It must
+be reaped once the pipe or socket handlers for the local side of the
+tunnel have been closed.
+
+OpenSSH 5.4 or later is required for the tunnels functionality to
+work. Also, note that tunnel forwarding may be administratively
+forbidden at the server side (see L<sshd(8)> and L<sshd_config(5)> or
+the documentation provided by your SSH server vendor).
+
+=head2 Data encoding
+
+Net::OpenSSH has some support for transparently converting the data send
+or received from the remote server to Perl internal unicode
+representation.
+
+The methods supporting that feature are those that move data from/to
+Perl data structures (i.e. C<capture>, C<capture2>, C<capture_tunnel>
+and methods supporting the C<stdin_data> option). Data accessed through
+pipes, sockets or redirections is not affected by the encoding options.
+
+It is also possible to set the encoding of the command and arguments
+passed to the remote server on the command line.
+
+By default, if no encoding option is given on the constructor or on the
+method calls, Net::OpenSSH will not perform any encoding transformation,
+effectively processing the data as latin1.
+
+When data can not be converted between the Perl internal
+representation and the selected encoding inside some Net::OpenSSH
+method, it will fail with an C<OSSH_ENCODING_ERROR> error.
+
+The supported encoding options are as follows:
+
+=over 4
+
+=item stream_encoding => $encoding
+
+sets the encoding of the data send and received on capture methods.
+
+=item argument_encoding => $encoding
+
+sets the encoding of the command line arguments
+
+=item encoding => $encoding
+
+sets both C<argument_encoding> and C<stream_encoding>.
+
+=back
+
+The constructor also accepts C<default_encoding>,
+C<default_stream_encoding> and C<default_argument_encoding> that set the
+defaults.
+
+=head2 Diverting C<new>
+
+When a code ref is installed at C<$Net::OpenSSH::FACTORY>, calls to new
+will be diverted through it.
+
+That feature can be used to transparently implement connection
+caching, for instance:
+
+ my $old_factory = $Net::OpenSSH::FACTORY;
+ my %cache;
+
+ sub factory {
+ my ($class, %opts) = @_;
+ my $signature = join("\0", $class, map { $_ => $opts{$_} }, sort keys %opts);
+ my $old = $cache{signature};
+ return $old if ($old and $old->error != OSSH_MASTER_FAILED);
+ local $Net::OpenSSH::FACTORY = $old_factory;
+ $cache{$signature} = $class->new(%opts);
+ }
+
+ $Net::OpenSSH::FACTORY = \&factory;
+
+... and I am sure it can be abused in several other ways!
+
+
+=head1 3rd PARTY MODULE INTEGRATION
+
+=head2 Expect
+
+Sometimes you would like to use L<Expect> to control some program
+running in the remote host. You can do it as follows:
+
+ my ($pty, $pid) = $ssh->open2pty(@cmd)
+ or die "unable to run remote command @cmd";
+ my $expect = Expect->init($pty);
+
+Then, you will be able to use the new Expect object in C<$expect> as
+usual.
+
+=head2 Net::Telnet
+
+This example is adapted from L<Net::Telnet> documentation:
+
+ my ($pty, $pid) = $ssh->open2pty({stderr_to_stdout => 1})
+ or die "unable to start remote shell: " . $ssh->error;
+ my $telnet = Net::Telnet->new(-fhopen => $pty,
+ -prompt => '/.*\$ $/',
+ -telnetmode => 0,
+ -cmd_remove_mode => 1,
+ -output_record_separator => "\r");
+
+ $telnet->waitfor(-match => $telnet->prompt,
+ -errmode => "return")
+ or die "login failed: " . $telnet->lastline;
+
+ my @lines = $telnet->cmd("who");
+
+ ...
+
+ $telnet->close;
+ waitpid($pid, 0);
+
+=head2 mod_perl and mod_perl2
+
+L<mod_perl> and L<mod_perl2> tie STDIN and STDOUT to objects that are
+not backed up by real file descriptors at the operative system
+level. Net::OpenSSH will fail if any of these handles is used
+explicetly or implicitly when calling some remote command.
+
+The workaround is to redirect them to C</dev/null> or to some file:
+
+ open my $def_in, '<', '/dev/null' or die "unable to open /dev/null";
+ my $ssh = Net::OpenSSH->new($host,
+ default_stdin_fh => $def_in);
+
+ my $out = $ssh->capture($cmd1);
+ $ssh->system({stdout_discard => 1}, $cmd2);
+ $ssh->system({stdout_to_file => '/tmp/output'}, $cmd3);
+
+Also, note that from a security stand point, running ssh from inside
+the webserver process is not a great idea. An attacker exploiting some
+Apache bug would be able to access the ssh keys and passwords and gain
+unlimited access to the remote systems.
+
+If you can, use a queue (as L<TheSchwartz|TheSchwartz>) or any other
+mechanism to execute the ssh commands from another process running
+under a different user account.
+
+At a minimum, ensure that C<~www-data/.ssh> (or similar) is not
+accessible through the web server!
+
+=head2 Other modules
+
+CPAN contains several modules that rely on SSH to perform their duties
+as for example L<IPC::PerlSSH|IPC::PerlSSH> or
+L<GRID::Machine|GRID::Machine>.
+
+Often, it is possible to instruct them to go through a Net::OpenSSH
+multiplexed connection employing some available constructor
+option. For instance:
+
+ use Net::OpenSSH;
+ use IPC::PerlIPC;
+ my $ssh = Net::OpenSSH->new(...);
+ $ssh->error and die "unable to connect to remote host: " . $ssh->error;
+ my @cmd = $ssh->make_remote_command('/usr/bin/perl');
+ my $ipc = IPC::PerlSSH->new(Command => \@cmd);
+ my @r = $ipc->eval('...');
+
+or...
+
+ use GRID::Machine;
+ ...
+ my @cmd = $ssh->make_remote_command('/usr/bin/perl');
+ my $grid = GRID::Machine->new(command => \@cmd);
+ my $r = $grid->eval('print "hello world!\n"');
+
+In other cases, some kind of plugin mechanism is provided by the 3rd
+party modules to allow for different transports. The method C<open2>
+may be used to create a pair of pipes for transport in these cases.
+
+=head1 TROUBLESHOOTING
+
+Usually, Net::OpenSSH works out of the box, but when it fails, some
+users have a hard time finding the cause of the problem. This mini
+troubleshooting guide should help you to find and solve it.
+
+=over 4
+
+=item 1 - check the error message
+
+Add in your script, after the Net::OpenSSH constructor call, an error
+check:
+
+ $ssh = Net::OpenSSH->new(...);
+ $ssh->error and die "SSH connection failed: " . $ssh->error;
+
+The error message will tell what has gone wrong.
+
+=item 2 - OpenSSH version
+
+Ensure that you have a version of C<ssh> recent enough:
+
+ $ ssh -V
+ OpenSSH_5.1p1 Debian-5, OpenSSL 0.9.8g 19 Oct 2007
+
+OpenSSH version 4.1 was the first to support the multiplexing feature
+and is the minimal required by the module to work. I advise you to use
+the latest OpenSSH (currently 5.8) or at least a more recent
+version.
+
+The C<ssh_cmd> constructor option lets you select the C<ssh> binary to
+use. For instance:
+
+ $ssh = Net::OpenSSH->new($host,
+ ssh_cmd => "/opt/OpenSSH/5.8/bin/ssh")
+
+Some hardware vendors (i.e. Sun) include custom versions of OpenSSH
+bundled with the operative system. In priciple, Net::OpenSSH should
+work with these SSH clients as long as they are derived from some
+version of OpenSSH recent enough. Anyway, I advise you to use the real
+OpenSSH software if you can!
+
+=item 3 - run ssh from the command line
+
+Check you can connect to the remote host using the same parameters you
+are passing to Net::OpenSSH. In particular, ensure that you are
+running C<ssh> as the same local user.
+
+If you are running your script from a webserver, the user
+would probably be C<www>, C<apache> or something alike.
+
+Common problems are:
+
+=over 4
+
+=item *
+
+Remote host public key not present in known_hosts file.
+
+The SSH protocol uses public keys to identify the remote hosts so that
+they can not be supplanted by some malicious third parties.
+
+For OpenSSH, usually the server public key is stored in
+C</etc/ssh/ssh_host_dsa_key.pub> or in
+C</etc/ssh/ssh_host_rsa_key.pub> and that key should be copied into the
+C<~/.ssh/known_hosts> file in the local machine (other SSH
+implementations may use other file locations).
+
+Maintaining the server keys when several hosts and clients are
+involved may be somewhat inconvenient, so most SSH clients, by
+default, when a new connection is stablished to a host whose key is
+not in the C<known_hosts> file, show the key and ask the user if he
+wants the key copied there.
+
+=item *
+
+Wrong remote host public key in known_hosts file.
+
+This is another common problem that happens when some server is
+replaced or reinstalled from scratch and its public key changes
+becomming different to that installed on the C<known_hosts> file.
+
+The easiest way to solve that problem is to remove the old key from
+the C<known_hosts> file by hand using any editor and then to connect
+to the server replying C<yes> when asked to save the new key.
+
+=item *
+
+Wrong permissions for the C<~/.ssh> directory or its contents.
+
+OpenSSH client performs several checks on the access permissions of
+the C<~/.ssh> directory and its contents and refuses to use them when
+misconfigured. See the FILES section from the L<ssh(1)> man page.
+
+=item *
+
+Incorrect settings for password or public key authentication.
+
+Check that you are using the right password or that the user public
+key is correctly installed on the server.
+
+=back
+
+=item 4 - security checks on the multiplexing socket
+
+Net::OpenSSH performs some security checks on the directory where the
+multiplexing socket is going to be placed to ensure that it can not be
+accessed by other users.
+
+The default location for the multiplexing socket is under
+C<~/.libnet-openssh-perl>. It can be changed using the C<ctl_dir> and
+C<ctl_path> constructor arguments.
+
+The requirements for that directory and all its parents are:
+
+=over 4
+
+=item *
+
+They have to be owned by the user executing the script or by root
+
+=item *
+
+Their permission masks must be 0755 or more restrictive, so nobody
+else has permissions to perform write operations on them.
+
+=back
+
+The constructor option C<strict_mode> disables these security checks,
+but you should not use it unless you understand its implications.
+
+=item 5 - file system must support sockets
+
+Some file systems (as for instance FAT or AFS) do not support placing
+sockets inside them.
+
+Ensure that the C<ctl_dir> path does not lay into one of those file
+systems.
+
+=back
+
+=head1 FAQ
+
+Frequent questions about the module:
+
+=over
+
+=item Connecting to switches, routers, etc.
+
+B<Q>: I can't get the method C<system>, C<capture>, etc., to work when
+connecting to some router, switch, etc. What I am doing wrong?
+
+B<A>: Roughly, the SSH protocol allows for two modes of operation:
+command mode and interactive mode.
+
+Command mode is designed to run single commands on the remote host. It
+opens an SSH channel between both hosts, ask the remote computer to
+run some given command and when it finnish the channel is closed. It
+is what you get, for instance, when you run something as...
+
+ $ ssh my.unix.box cat foo.txt
+
+... and it is also the way Net::OpenSSH runs commands on the remote
+host.
+
+Interactive mode launches a shell on the remote hosts with its stdio
+streams redirected to the local ones so that the user can
+transparently interact with it.
+
+Some devices (as probably the one you are using) do not run an
+standard, general purpose shell (i.e. C<bash>, C<csh> or C<ksh>) but
+some custom program specially targeted and limited to the task of
+configuring the device.
+
+Usually, the SSH server running on these devices does not support
+command mode. It unconditionally attachs the restricted shell to any
+incomming SSH connection and waits for the user to enter commands
+through the redirected stdin stream.
+
+The only way to workaround this limitation is to make your script talk
+to the restricted shell (1-open a new SSH session, 2-wait for the
+shell prompt, 3-send a command, 4-read the output until you get to the
+shell prompt again, repeat from 3). The best tool for this task is
+probably L<Expect>, used alone, as wrapped by L<Net::SSH::Expect> or
+combined with Net::OpenSSH (see L</Expect>).
+
+There are some devices that support command mode but that only accept
+one command per connection. In that cases, using L<Expect> is also
+probably the best option.
+
+=item Connection fails
+
+B<Q>: I am unable to make the module connect to the remote host...
+
+B<A>: Have you read the trubleshooting section? (see
+L</TROUBLESHOOTING>).
+
+=item Disable StrictHostKeyChecking
+
+B<Q>: Why don't you run C<ssh> with C<StrictHostKeyChecking=no>?
+
+B<A>: Using C<StrictHostKeyChecking=no> relaxes the default security
+level of SSH and it will be relatively easy to end with a
+misconfigured SSH (for instance, when C<known_hosts> is unwriteable)
+that could be forged to connect to a bad host in order to perform
+man-in-the-middle attacks, etc.
+
+I advice you to do not use that option unless you fully understand its
+implications from a security point of view.
+
+If you want to use it anyway, past it to the constructor:
+
+ $ssh = Net::OpenSSH->new($host,
+ master_opts => [-o => "StrictHostKeyChecking=no"],
+ ...);
+
+
+=item child process 14947 does not exist: No child processes
+
+B<Q>: Calls to C<system>, C<capture> or C<capture2> fail with the
+previous error, what I am doing wrong?
+
+B<A>: That usually happens when C<$SIG{CHLD}> is set to C<IGNORE> or
+to some custom handler reaping child processes by itself. In order to
+solve the problem just disable the handler during the method call:
+
+ local $SIG{CHLD};
+ $ssh->system($cmd);
+
+=item child process STDIN/STDOUT/STDERR is not a real system file
+handle
+
+B<Q>: Calls to C<system>, C<capture>, etc. fail with the previous
+error, what's happening?
+
+B<A>: The reported stdio stream is closed or is not attached to a real
+file handle (i.e. it is a tied handle). Redirect it to C</dev/null> or
+to a real file:
+
+ my $out = $ssh->capture({stdin_discard => 1, stderr_to_stdout => 1},
+ $cmd);
+
+See also the L<mod_perl> entry above.
+
+=item Solaris (and AIX and probably others)
+
+B<Q>: I was trying Net::OpenSSH on Solaris and seem to be running into
+an issue...
+
+B<A>: The SSH client bundled with Solaris is an early fork of OpenSSH
+that does not provide the multiplexing functionality required by
+Net::OpenSSH. You will have to install the OpenSSH client.
+
+Precompiled packages are available from Sun Freeware
+(L<http://www.sunfreeware.com>). There, select your OS version an CPU
+architecture, download the OpenSSH package and its dependencies and
+install them. Note that you do B<not> need to configure Solaris to use
+the OpenSSH server C<sshd>.
+
+Ensure that OpenSSH client is in your path before the system C<ssh> or
+alternatively, you can hardcode the full path into your scripts
+as follows:
+
+ $ssh = Net::OpenSSH->new($host,
+ ssh_cmd => '/usr/local/bin/ssh');
+
+AIX and probably some other unixen, also bundle SSH clients lacking
+the multiplexing functionality and require installation of the real
+OpenSSH.
+
+=item Can't change working directory
+
+B<Q>: I want to run some command inside a given remote directory but I
+am unable to change the working directory. For instance:
+
+ $ssh->system('cd /home/foo/bin');
+ $ssh->systen('ls');
+
+does not list the contents of C</home/foo/bin>.
+
+What am I doing wrong?
+
+B<A>: Net::OpenSSH (and, for that matter, all the SSH modules
+available from CPAN but L<Net::SSH::Expect>) runs every command in a
+new session so most shell builtins that are run for its side effects
+become useless (i.e. C<cd>, C<export>, C<ulimit>, C<umask>, etc.,
+usually, you can list them running help from the shell).
+
+A work around is to combine several commands in one, for instance:
+
+ $ssh->system('cd /home/foo/bin && ls');
+
+Note the use of the shell C<&&> operator instead of C<;> in order to
+abort the command as soon as any of the subcommands fail.
+
+Also, several commands can be combined into one while still using the
+multi-argument quoting feature as follows:
+
+ $ssh->system(@cmd1, \\'&&', @cmd2, \\'&&', @cmd3, ...);
+
+=item Running detached remote processes
+
+B<Q>: I need to be able to ssh into several machines from my script,
+launch a process to run in the background there, and then return
+immediately while the remote programs keep running...
+
+B<A>: If the remote systems run some Unix/Linux variant, the right
+approach is to use L<nohup(1)> that will disconnect the remote process
+from the stdio streams and to ask the shell to run the command on the
+background. For instance:
+
+ $ssh->system("nohup $long_running_command &");
+
+Also, it may be possible to demonize the remote program. If it is
+written in Perl you can use L<App::Daemon> for that (actually, there
+are several CPAN modules that provided that kind of functionality).
+
+In any case, note that you shouldn't use L</spawn> for that.
+
+=item MaxSessions server limit reached
+
+B<Q>: I created an C<$ssh> object and then fork a lot children
+processes which use this object. When the children number is bigger
+than C<MaxSessions> as defined in sshd configuration (defaults to 10),
+trying to fork new remote commands will prompt the user for the
+password.
+
+B<A>: When the slave SSH client gets a response from the remote
+servers saying that the maximum number of sessions for the current
+connection has been reached, it fallbacks to open a new direct
+connection without going through the multiplexing socket.
+
+To stop that for happening, the following hack can be used:
+
+ $ssh = Net::OpenSSH->new(host,
+ default_ssh_opts => ['-oConnectionAttempts=0'],
+ ...);
+
+=item Running remote commands with sudo
+
+B<Q>: How can I run remote commands using C<sudo> to become root first?
+
+B<A>: The simplest way is to tell C<sudo> to read the password from
+stdin with the C<-S> flag and to do not use cached credentials
+with the C<-k> flag. You may also like to use the C<-p> flag to tell
+C<sudo> to print an empty prompt. For instance:
+
+ my @out = $ssh->capture({stdin_data => $sudo_passwd},
+ 'sudo', '-Sk',
+ '-p', '',
+ '--',
+ @cmd);
+
+=back
+
+=head1 SEE ALSO
+
+OpenSSH client documentation L<ssh(1)>, L<ssh_config(5)>, the project
+web L<http://www.openssh.org> and its FAQ
+L<http://www.openbsd.org/openssh/faq.html>. L<scp(1)> and
+L<rsync(1)>. The OpenSSH Wikibook
+L<http://en.wikibooks.org/wiki/OpenSSH>.
+
+L<Net::OpenSSH::Gateway> for detailed instruction about how to get
+this module to connect to hosts through proxies and other SSH gateway
+servers.
+
+Core perl documentation L<perlipc>, L<perlfunc/open>,
+L<perlfunc/waitpid>.
+
+L<IO::Pty|IO::Pty> to known how to use the pseudo tty objects returned
+by several methods on this package.
+
+L<Net::SFTP::Foreign|Net::SFTP::Foreign> provides a compatible SFTP
+implementation.
+
+L<Expect|Expect> can be used to interact with commands run through
+this module on the remote machine (see also the C<expect.pl> and
+<autosudo.pl> scripts in the sample directory).
+
+L<SSH::OpenSSH::Parallel> is an advanced scheduler that allows to run
+commands in remote hosts in parallel. It is obviously based on
+Net::OpenSSH.
+
+L<SSH::Batch|SSH::Batch> allows to run remote commands in parallel in
+a cluster. It is build on top on C<Net::OpenSSH> also.
+
+Other Perl SSH clients: L<Net::SSH::Perl|Net::SSH::Perl>,
+L<Net::SSH2|Net::SSH2>, L<Net::SSH|Net::SSH>,
+L<Net::SSH::Expect|Net::SSH::Expect>, L<Net::SCP|Net::SCP>.
+
+L<Net::OpenSSH::Compat> is a package offering a set of compatibility
+layers for other SSH modules on top of Net::OpenSSH.
+
+L<IPC::PerlSSH|IPC::PerlSSH>, L<GRID::Machine|GRID::Machine> allow
+execution of Perl code in remote machines through SSH.
+
+L<SSH::RPC|SSH::RPC> implements an RPC mechanism on top of SSH using
+Net::OpenSSH to handle the connections.
+
+=head1 BUGS AND SUPPORT
+
+Support for the gateway feature is highly experimental.
+
+Support for data encoding is experimental.
+
+Support for taint mode is experimental.
+
+Tested on Linux, OpenBSD, NetBSD and Solaris with OpenSSH 5.1 to 5.9.
+
+Net::OpenSSH does not work on Windows. OpenSSH multiplexing feature
+requires passing file handles through sockets, something that is not
+supported by any version of Windows.
+
+It doesn't work on VMS either... well, probably, it doesn't work on
+anything not resembling a modern Linux/Unix OS.
+
+To report bugs send an email to the address that appear below or use
+the CPAN bug tracking system at L<http://rt.cpan.org>.
+
+B<Post questions related to how to use the module in Perlmonks>
+L<http://perlmoks.org/>, you will probably get faster responses than
+if you address me directly and I visit Perlmonks quite often, so I
+will see your question anyway.
+
+The source code of this module is hosted at GitHub:
+L<http://github.com/salva/p5-Net-OpenSSH>.
+
+=head2 Commercial support
+
+Commercial support, professional services and custom software
+development around this module are available through my current
+company. Drop me an email with a rough description of your
+requirements and we will get back to you ASAP.
+
+=head2 My wishlist
+
+If you like this module and you're feeling generous, take a look at my
+Amazon Wish List: L<http://amzn.com/w/1WU1P6IR5QZ42>.
+
+Also consider contributing to the OpenSSH project this module builds
+upon: L<http://www.openssh.org/donations.html>.
+
+=head1 TODO
+
+- *** add tests for scp, rsync and sftp methods
+
+- *** add support for more target OSs (quoting, OpenVMS, Windows & others)
+
+- better timeout handling in system and capture methods
+
+- make L</pipe_in> and L</pipe_out> methods L</open_ex> based
+
+- add C<scp_cat> and similar methods
+
+- async disconnect
+
+- currently wait_for_master does not honor timeout
+
+- auto_discard_streams feature for mod_perl2 and similar environments
+
+- add proper shell quoting for Windows (see
+ L<http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx>).
+
+Send your feature requests, ideas or any feedback, please!
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2008-2011 by Salvador FandiE<ntilde>o
+(sfandino@yahoo.com)
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.10.0 or,
+at your option, any later version of Perl 5 you may have available.
+
+=cut
diff --git a/lib/Net/OpenSSH/Constants.pm b/lib/Net/OpenSSH/Constants.pm
new file mode 100644
index 0000000..42fa6de
--- /dev/null
+++ b/lib/Net/OpenSSH/Constants.pm
@@ -0,0 +1,70 @@
+package Net::OpenSSH::Constants;
+
+our $VERSION = '0.51_07';
+
+use strict;
+use warnings;
+use Carp;
+
+require Exporter;
+our @ISA = qw(Exporter);
+our %EXPORT_TAGS = (error => []);
+
+my %error = ( OSSH_MASTER_FAILED => 1,
+ OSSH_SLAVE_FAILED => 2,
+ OSSH_SLAVE_PIPE_FAILED => 3,
+ OSSH_SLAVE_TIMEOUT => 4,
+ OSSH_SLAVE_CMD_FAILED => 5,
+ OSSH_SLAVE_SFTP_FAILED => 6,
+ OSSH_ENCODING_ERROR => 7
+ );
+
+for my $key (keys %error) {
+ no strict 'refs';
+ my $value = $error{$key};
+ *{$key} = sub () { $value };
+ push @{$EXPORT_TAGS{error}}, $key
+}
+
+our @EXPORT_OK = map { @{$EXPORT_TAGS{$_}} } keys %EXPORT_TAGS;
+$EXPORT_TAGS{all} = [@EXPORT_OK];
+
+1;
+
+__END__
+
+=head1 NAME
+
+Net::OpenSSH::Constants - Constant definitions for Net::OpenSSH
+
+=head1 SYNOPSIS
+
+ use Net::OpenSSH::Constants qw(:error);
+
+=head1 DESCRIPTION
+
+This module exports the following constants:
+
+=over 4
+
+=item :error
+
+ OSSH_MASTER_FAILED - some error related to the master SSH connection happened
+ OSSH_SLAVE_FAILED - some error related to a slave SSH connection happened
+ OSSH_SLAVE_PIPE_FAILED - unable to create pipe to communicate with slave process
+ OSSH_SLAVE_TIMEOUT - slave process timeout
+ OSSH_SLAVE_CMD_FAILED - child process exited with a non zero status
+ OSSH_SLAVE_SFTP_FAILED - creation of SFTP client failed
+ OSS_ENCODING_ERROR - some error related to the encoding/decoding of strings happened
+
+=back
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2008, 2009 by Salvador FandiE<ntilde>o (sfandino@yahoo.com)
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.10.0 or,
+at your option, any later version of Perl 5 you may have available.
+
+=cut