File Coverage

blib/lib/Net/SSH/Perl.pm
Criterion Covered Total %
statement 122 281 43.4
branch 19 132 14.3
condition 8 75 10.6
subroutine 31 51 60.7
pod 12 22 54.5
total 192 561 34.2


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