File Coverage

blib/lib/Net/SSH/Perl.pm
Criterion Covered Total %
statement 102 281 36.3
branch 15 132 11.3
condition 8 75 10.6
subroutine 24 51 47.0
pod 12 22 54.5
total 161 561 28.7


line stmt bran cond sub pod time code
1             # $Id: Perl.pm,v 1.126 2009/02/02 01:18:27 turnstep Exp $
2              
3             package Net::SSH::Perl;
4 4     4   198669 use strict;
  4         13  
  4         137  
5 4     4   27 use warnings;
  4         9  
  4         137  
6              
7 4     4   1561 use Net::SSH::Perl::Packet;
  4         13  
  4         155  
8 4     4   32 use Net::SSH::Perl::Buffer;
  4         11  
  4         95  
9 4     4   4110 use Net::SSH::Perl::Config;
  4         13  
  4         172  
10 4     4   35 use Net::SSH::Perl::Constants qw( :protocol :compat :hosts );
  4         9  
  4         18  
11 4     4   1827 use Net::SSH::Perl::Cipher;
  4         15  
  4         160  
12 4     4   1730 use Net::SSH::Perl::Util qw( :hosts _read_yes_or_no _current_user_win32 );
  4         11  
  4         27  
13              
14 4     4   1399 use Errno;
  4         5560  
  4         225  
15              
16 4     4   32 use vars qw( $VERSION $CONFIG $HOSTNAME );
  4         8  
  4         387  
17             $CONFIG = {};
18              
19 4     4   28 use Socket qw(IPPROTO_TCP TCP_NODELAY);
  4         9  
  4         939  
20 4     4   1724 use IO::Socket;
  4         58580  
  4         22  
21 4     4   2061 use Fcntl;
  4         13  
  4         1126  
22 4     4   33 use Symbol;
  4         12  
  4         306  
23 4     4   32 use Carp qw( croak );
  4         12  
  4         226  
24 4     4   1386 use File::Spec::Functions qw( catfile );
  4         3387  
  4         273  
25 4     4   1208 use Sys::Hostname;
  4         4066  
  4         1330  
26             eval {
27             $HOSTNAME = hostname();
28             };
29              
30             $VERSION = '2.14';
31              
32 0     0 0 0 sub VERSION { $VERSION }
33              
34             sub new {
35 4     4 1 548 my $class = shift;
36 4         15 my $host = shift;
37 4 50       15 croak "usage: ", __PACKAGE__, "->new(\$host)"
38             unless defined $host;
39 4         18 my $ssh = bless { host => $host }, $class;
40 4         24 my %p = @_;
41 4         22 $ssh->{_test} = delete $p{_test};
42 4         29 $ssh->_init(%p);
43 4 50       17 $ssh->_connect unless $ssh->{_test};
44 4         85 $ssh;
45             }
46              
47 0     0 1 0 sub protocol { $_[0]->{use_protocol} }
48              
49             sub set_protocol {
50 0     0 0 0 my $ssh = shift;
51 0         0 my $proto = shift;
52 0         0 $ssh->{use_protocol} = $proto;
53 0 0       0 my $proto_class = join '::', __PACKAGE__,
54             ($proto == PROTOCOL_SSH2 ? "SSH2" : "SSH1");
55 0         0 (my $lib = $proto_class . ".pm") =~ s!::!/!g;
56 0         0 require $lib;
57 0         0 bless $ssh, $proto_class;
58 0         0 $ssh->debug($proto_class->version_string);
59 0         0 $ssh->_proto_init;
60             }
61              
62 4     4   38 use vars qw( @COMPAT );
  4         10  
  4         13250  
63             @COMPAT = (
64             [ '^OpenSSH[-_]2\.[012]' => SSH_COMPAT_OLD_SESSIONID, ],
65             [ 'MindTerm' => 0, ],
66             [ '^2\.1\.0 ' => SSH_COMPAT_BUG_SIGBLOB |
67             SSH_COMPAT_BUG_HMAC |
68             SSH_COMPAT_OLD_SESSIONID, ],
69             [ '^2\.0\.' => SSH_COMPAT_BUG_SIGBLOB |
70             SSH_COMPAT_BUG_HMAC |
71             SSH_COMPAT_OLD_SESSIONID |
72             SSH_COMPAT_BUG_PUBKEYAUTH |
73             SSH_COMPAT_BUG_X11FWD, ],
74             [ '^2\.[23]\.0 ' => SSH_COMPAT_BUG_HMAC, ],
75             [ '^2\.[2-9]\.' => 0, ],
76             [ '^2\.4$' => SSH_COMPAT_OLD_SESSIONID, ],
77             [ '^3\.0 SecureCRT' => SSH_COMPAT_OLD_SESSIONID, ],
78             [ '^1\.7 SecureFX' => SSH_COMPAT_OLD_SESSIONID, ],
79             [ '^2\.' => SSH_COMPAT_BUG_HMAC, ],
80             );
81              
82             sub _compat_init {
83 0     0   0 my $ssh = shift;
84 0         0 my($version) = @_;
85 0         0 $ssh->{datafellows} = 0;
86 0         0 for my $rec (@COMPAT) {
87 0         0 my($re, $mask) = @$rec[0, 1];
88 0 0       0 if ($version =~ /$re/) {
89 0         0 $ssh->debug("Compat match: '$version' matches pattern '$re'.");
90 0         0 $ssh->{datafellows} = $mask;
91 0         0 return;
92             }
93             }
94 0         0 $ssh->debug("No compat match: $version.");
95             }
96              
97       0 0   sub version_string { }
98              
99 0     0 0 0 sub client_version_string { $_[0]->{client_version_string} }
100 0     0 0 0 sub server_version_string { $_[0]->{server_version_string} }
101              
102             sub _current_user {
103 4 50   4   21 if ( $^O eq 'MSWin32' ) {
104 0         0 return _current_user_win32();
105             }
106              
107 4         7 my $user;
108 4         11 eval { $user = scalar getpwuid $> };
  4         827  
109 4         22 return $user;
110             }
111              
112             sub _init {
113 4     4   11 my $ssh = shift;
114              
115 4         13 my %arg = @_;
116             my $user_config = delete $arg{user_config}
117 4   66     33 || catfile($ENV{HOME} || $ENV{USERPROFILE}, '.ssh', 'config');
118             my $sys_config = delete $arg{sys_config}
119             || $^O eq 'MSWin32'
120 4 50 33     41 ? catfile($ENV{WINDIR}, 'ssh_config')
121             : "/etc/ssh_config";
122              
123 4   100     23 my $directives = delete $arg{options} || [];
124              
125 4 50       16 if (my $proto = delete $arg{protocol}) {
126 0         0 push @$directives, "Protocol $proto";
127             }
128              
129 4         34 my $cfg = Net::SSH::Perl::Config->new($ssh->{host}, %arg);
130 4         15 $ssh->{config} = $cfg;
131              
132             # Merge config-format directives given through "options"
133             # (just like -o option to ssh command line). Do this before
134             # reading config files so we override files.
135 4         14 for my $d (@$directives) {
136 4         15 $cfg->merge_directive($d);
137             }
138              
139 4         14 for my $f (($user_config, $sys_config)) {
140 8         51 $ssh->debug("Reading configuration data $f");
141 8         30 $cfg->read_config($f);
142             }
143              
144 4 100       23 if (my $real_host = $ssh->{config}->get('hostname')) {
145 3         8 $ssh->{host} = $real_host;
146             }
147              
148 4         15 my $user = _current_user();
149 4 50 33     43 if ($user && $user eq "root" &&
      33        
150             !defined $ssh->{config}->get('privileged')) {
151 4         23 $ssh->{config}->set('privileged', 1);
152             }
153              
154 4 50       18 unless ($ssh->{config}->get('protocol')) {
155 4         23 $ssh->{config}->set('protocol',
156             PROTOCOL_SSH1 | PROTOCOL_SSH2 | PROTOCOL_SSH1_PREFERRED);
157             }
158              
159 4 50       18 unless (defined $ssh->{config}->get('password_prompt_login')) {
160 4         32 $ssh->{config}->set('password_prompt_login', 1);
161             }
162 4 50       17 unless (defined $ssh->{config}->get('password_prompt_host')) {
163 4         16 $ssh->{config}->set('password_prompt_host', 1);
164             }
165 4 50       15 unless (defined $ssh->{config}->get('number_of_password_prompts')) {
166 4         17 $ssh->{config}->set('number_of_password_prompts', 3);
167             }
168             }
169              
170       0     sub _proto_init { }
171              
172       0 1   sub register_handler { }
173              
174 9     9 1 562 sub config { $_[0]->{config} }
175              
176             sub configure {
177 0     0 0 0 my $class = shift;
178 0         0 $CONFIG = { @_ };
179             }
180              
181             sub ssh {
182 0     0 0 0 my($host, @cmd) = @_;
183 0         0 my($user);
184 0 0       0 ($host, $user) = $host =~ m!(.+)@(.+)! ?
185             ($2, $1) : ($host, _current_user());
186 0         0 my $ssh = __PACKAGE__->new($host, %$CONFIG);
187 0         0 $ssh->login($user);
188 0         0 my($out, $err, $exit) = $ssh->cmd(join ' ', @cmd);
189 0         0 print $out;
190 0 0       0 print STDERR $err if $err;
191             }
192              
193             sub issh {
194 0     0 0 0 my($host, @cmd) = @_;
195 0         0 print join(' ', @cmd), "\n";
196 0         0 print "Proceed: [y/N]:";
197 0         0 my $x = scalar();
198 0 0       0 if ($x =~ /^y/i) {
199 0         0 $CONFIG->{interactive} = 1;
200 0         0 ssh($host, @cmd);
201             }
202             }
203              
204             sub _connect {
205 0     0   0 my $ssh = shift;
206 0         0 my $sock = $ssh->_create_socket;
207              
208 0         0 my $raddr = inet_aton($ssh->{host});
209 0 0       0 croak "Net::SSH: Bad host name: $ssh->{host}"
210             unless defined $raddr;
211 0   0     0 my $rport = $ssh->{config}->get('port') || 'ssh';
212 0 0       0 if ($rport =~ /\D/) {
213 0         0 my @serv = getservbyname(my $serv = $rport, 'tcp');
214 0   0     0 $rport = $serv[2] || 22;
215             }
216 0         0 $ssh->debug("Connecting to $ssh->{host}, port $rport.");
217 0 0       0 connect($sock, sockaddr_in($rport, $raddr))
218             or die "Can't connect to $ssh->{host}, port $rport: $!";
219              
220 0         0 select((select($sock), $|=1)[0]);
221              
222 0         0 $ssh->{session}{sock} = $sock;
223 0         0 $ssh->_exchange_identification;
224              
225 0 0       0 if ($^O eq 'MSWin32') {
226 0         0 my $nonblocking = 1;
227 0         0 ioctl $sock, 0x8004667e, \\$nonblocking;
228             }
229             else {
230 0 0       0 defined($sock->blocking(0))
231             or die "Can't set socket non-blocking: $!";
232             }
233              
234 0         0 $ssh->debug("Connection established.");
235             }
236              
237             sub _create_socket {
238 0     0   0 my $ssh = shift;
239 0         0 my $sock = gensym;
240              
241 0         0 my ($p,$end,$delta) = (0,1,1); # normally we use whatever port we can get
242 0 0       0 ($p,$end,$delta) = (1023,512,-1) if $ssh->{config}->get('privileged');
243              
244             # allow an explicit bind address
245 0         0 my $addr = $ssh->{config}->get('bind_address');
246 0 0       0 $addr = inet_aton($addr) if $addr;
247 0 0 0     0 ($p,$end,$delta) = (10000,65535,1) if $addr and not $p;
248 0   0     0 $addr ||= INADDR_ANY;
249              
250 0         0 for(; $p != $end; $p += $delta) {
251 0 0 0     0 socket($sock, AF_INET, SOCK_STREAM, getprotobyname('tcp') || 0) ||
252             croak "Net::SSH: Can't create socket: $!";
253 0         0 setsockopt($sock, IPPROTO_TCP, TCP_NODELAY, 1);
254 0 0 0     0 last if not $p or bind($sock, sockaddr_in($p,$addr));
255 0 0       0 if ($! =~ /Address already in use/i) {
256 0 0       0 close($sock) or warn qq{Could not close socket: $!\n};
257 0         0 next;
258             }
259 0         0 croak "Net::SSH: Can't bind socket to port $p: $!";
260             }
261 0 0       0 if($p) {
262 0         0 $ssh->debug("Allocated local port $p.");
263 0         0 $ssh->{config}->set('localport', $p);
264             }
265              
266 0         0 $sock;
267             }
268              
269       0     sub _disconnect { }
270              
271             sub fatal_disconnect {
272 0     0 0 0 my $ssh = shift;
273 0         0 $ssh->_disconnect(@_);
274 0         0 croak @_;
275             }
276              
277             sub _read_version_line {
278 0     0   0 my $ssh = shift;
279 0         0 my $sock = $ssh->{session}{sock};
280 0         0 my $line;
281 0         0 for(;;) {
282 0         0 my $s = IO::Select->new($sock);
283 0         0 my @ready = $s->can_read;
284 0         0 my $buf;
285 0         0 my $len = sysread($sock, $buf, 1);
286 0 0       0 unless(defined($len)) {
287 0 0 0     0 next if $!{EAGAIN} || $!{EWOULDBLOCK};
288 0         0 croak "Read from socket failed: $!";
289             }
290 0 0       0 croak "Connection closed by remote host" if $len == 0;
291 0         0 $line .= $buf;
292 0 0 0     0 croak "Version line too long: $line"
293             if substr($line, 0, 4) eq "SSH-" and length($line) > 255;
294 0 0       0 croak "Pre-version line too long: $line" if length($line) > 4*1024;
295 0 0       0 return $line if $buf eq "\n";
296             }
297             }
298              
299             sub _read_version {
300 0     0   0 my $ssh = shift;
301 0         0 my ($line, $line_out);
302 0         0 do {
303 0         0 $line = $line_out = $ssh->_read_version_line;
304             } while (substr($line, 0, 4) ne "SSH-");
305 0         0 chomp($line_out);
306 0         0 $ssh->debug("Remote version string: $line_out");
307 0         0 return $line;
308             }
309              
310 0     0 1 0 sub sock { $_[0]->{session}{sock} }
311              
312             sub _exchange_identification {
313 0     0   0 my $ssh = shift;
314 0         0 my $sock = $ssh->{session}{sock};
315 0         0 my $remote_id = $ssh->_read_version;
316 0         0 ($ssh->{server_version_string} = $remote_id) =~ s/\cM?\n$//;
317 0         0 my($remote_major, $remote_minor, $remote_version) = $remote_id =~
318             /^SSH-(\d+)\.(\d+)-([^\n\r]+)[\r]*\n$/;
319 0         0 $ssh->debug("Remote protocol version $remote_major.$remote_minor, remote software version $remote_version");
320              
321 0         0 my $proto = $ssh->config->get('protocol');
322 0         0 my($mismatch, $set_proto);
323 0 0       0 if ($remote_major == 1) {
    0          
324 0 0 0     0 if ($remote_minor == 99 && $proto & PROTOCOL_SSH2 &&
    0 0        
325             !($proto & PROTOCOL_SSH1_PREFERRED)) {
326 0         0 $set_proto = PROTOCOL_SSH2;
327             }
328             elsif (!($proto & PROTOCOL_SSH1)) {
329 0         0 $mismatch = 1;
330             }
331             else {
332 0         0 $set_proto = PROTOCOL_SSH1;
333             }
334             }
335             elsif ($remote_major == 2) {
336 0 0       0 if ($proto & PROTOCOL_SSH2) {
337 0         0 $set_proto = PROTOCOL_SSH2;
338             }
339             }
340 0 0       0 if ($mismatch) {
341 0 0       0 croak sprintf "Protocol major versions differ: %d vs. %d",
342             ($proto & PROTOCOL_SSH2) ? PROTOCOL_MAJOR_2 :
343             PROTOCOL_MAJOR_1, $remote_major;
344             }
345 0         0 my $compat20 = $set_proto == PROTOCOL_SSH2;
346 0 0       0 my $buf = sprintf "SSH-%d.%d-%s\n",
    0          
347             $compat20 ? PROTOCOL_MAJOR_2 : PROTOCOL_MAJOR_1,
348             $compat20 ? PROTOCOL_MINOR_2 : PROTOCOL_MINOR_1,
349             $VERSION;
350 0         0 $ssh->{client_version_string} = substr $buf, 0, -1;
351 0         0 syswrite $sock, $buf;
352              
353 0         0 $ssh->set_protocol($set_proto);
354 0         0 $ssh->_compat_init($remote_version);
355             }
356              
357             sub debug {
358 8     8 1 17 my $ssh = shift;
359 8 50       30 if ($ssh->{config}->get('debug')) {
360 0 0       0 printf STDERR "%s@_\n", $HOSTNAME ? "$HOSTNAME: " : '';
361             }
362             }
363              
364             sub login {
365 1     1 1 3 my $ssh = shift;
366 1         5 my($user, $pass) = @_;
367 1 50       7 if (!defined $ssh->{config}->get('user')) {
368 0 0       0 $ssh->{config}->set('user',
369             defined $user ? $user : _current_user());
370             }
371 1 50 33     13 if (!defined $pass && exists $CONFIG->{ssh_password}) {
372 0         0 $pass = $CONFIG->{ssh_password};
373             }
374 1         6 $ssh->{config}->set('pass', $pass);
375             }
376              
377       0     sub _login { }
378              
379       0 1   sub cmd { }
380       0 1   sub shell { }
381              
382             sub incoming_data {
383 0     0 1   my $ssh = shift;
384 0 0         if (!exists $ssh->{session}{incoming_data}) {
385 0 0         $ssh->{session}{incoming_data} = Net::SSH::Perl::Buffer->new( MP => $ssh->protocol == PROTOCOL_SSH2 ? 'SSH2' : 'SSH1' );
386             }
387 0           $ssh->{session}{incoming_data};
388             }
389              
390             sub session_id {
391 0     0 1   my $ssh = shift;
392 0 0 0       $ssh->{session}{id} = shift if @_ and not defined $ssh->{session}{id};
393 0           $ssh->{session}{id};
394             }
395              
396 0     0 1   sub packet_start { Net::SSH::Perl::Packet->new($_[0], type => $_[1]) }
397              
398             sub check_host_key {
399 0     0 0   my $ssh = shift;
400 0           my($key, $host, $u_hostfile, $s_hostfile) = @_;
401             my $strict_host_key_checking =
402 0   0       $ssh->{config}->get('strict_host_key_checking') || 'no';
403 0   0       $host ||= $ssh->{host};
404 0   0       $u_hostfile ||= $ssh->{config}->get('user_known_hosts');
405 0   0       $s_hostfile ||= $ssh->{config}->get('global_known_hosts');
406 0           my $port = $ssh->{config}->get('port');
407              
408 0 0 0       if (defined $port && $port =~ /\D/) {
409 0           my @serv = getservbyname(my $serv = $port, 'tcp');
410 0           $port = $serv[2];
411             }
412              
413 0           my $hash_known_hosts = $ssh->{config}->get('hash_known_hosts');
414 0           my $check_ip = $ssh->{config}->get('check_host_ip');
415 0 0         $check_ip = 1 unless defined $check_ip;
416              
417 0           my $status = _check_host_in_hostfile($host, $port, $u_hostfile, $key);
418 0 0 0       unless (defined $status && ($status == HOST_OK || $status == HOST_CHANGED)) {
      0        
419 0           $status = _check_host_in_hostfile($host, $port, $s_hostfile, $key);
420             }
421              
422 0 0         if ($status == HOST_OK) {
    0          
423 0           $ssh->debug("Host '$host' is known and matches the host key.");
424             }
425             elsif ($status == HOST_NEW) {
426 0 0         if ($strict_host_key_checking =~ /(ask|yes)/) {
427 0 0         if (!$ssh->{config}->get('interactive')) {
428 0           croak "Host key verification failed.";
429             }
430 0           my $prompt =
431             qq(The authenticity of host '$host' can't be established.
432 0           Key fingerprint is @{[ $key->fingerprint($ssh->config->get('fingerprint_hash')) ]}.
433             Are you sure you want to continue connecting (yes/no)?);
434 0 0         unless (_read_yes_or_no($prompt, "yes")) {
435 0           croak "Aborted by user!";
436             }
437             }
438 0           _add_host_to_hostfile($host, $port, $u_hostfile, $key, $hash_known_hosts);
439 0           $ssh->debug("Permanently added '$host' to the list of known hosts.");
440             }
441             else {
442 0           croak "Host key for '$host' has changed!";
443             }
444              
445 0 0 0       return unless $check_ip && $host =~ /[a-z][A-Z]+/;
446 0 0         my $ip = inet_ntoa(inet_aton($host)) or return;
447              
448 0           $status = _check_host_in_hostfile($ip, $port, $u_hostfile, $key);
449 0 0 0       unless (defined $status && ($status == HOST_OK || $status == HOST_CHANGED)) {
      0        
450 0           $status = _check_host_in_hostfile($ip, $port, $s_hostfile, $key);
451             }
452 0 0         if ($status == HOST_NEW) {
    0          
453 0           _add_host_to_hostfile($ip, $port, $u_hostfile, $key, $hash_known_hosts);
454             }
455             elsif ($status == HOST_CHANGED) {
456 0           my $prompt =
457             qq(The host key for IP address '$ip' does not match that for '$host'.
458             Are you sure you want to continue connecting (yes/no)?);
459 0 0         unless (_read_yes_or_no($prompt, "yes")) {
460 0           croak "Aborted by user!";
461             }
462             }
463             }
464              
465             1;
466             __END__