File Coverage

blib/lib/Net/SSH/Mechanize/Session.pm
Criterion Covered Total %
statement 143 174 82.1
branch 29 48 60.4
condition 0 4 0.0
subroutine 26 32 81.2
pod 7 7 100.0
total 205 265 77.3


line stmt bran cond sub pod time code
1             package Net::SSH::Mechanize::Session;
2 5     5   30 use Moose;
  5         10  
  5         41  
3 5     5   34378 use MooseX::Params::Validate;
  5         268030  
  5         51  
4 5     5   2463 use AnyEvent;
  5         11  
  5         102  
5 5     5   25 use Carp qw(croak);
  5         11  
  5         8361  
6             our @CARP_NOT = qw(Net::SSH::Mechanize AnyEvent);
7              
8             our $VERSION = '0.1.2'; # VERSION
9              
10             extends 'AnyEvent::Subprocess::Running';
11              
12             my $passwd_prompt_re = qr/assword:\s*/;
13              
14             my $initial_prompt_re = qr/^.*?\Q$ \E$/m;
15             my $sudo_initial_prompt_re = qr/^.*?\Q$ \E$/m;
16              
17             # Create a random text delimiter
18             # We want chars A-Z, a-z, 0-9, _- => 26+26+10 = 64 different characters.
19             # First we generate a random string of ASCII chars 1..65,
20             my $delim = pack "C*", map { int(rand 64)+1 } 1..20;
21              
22             # Then we map it to the characters we want.
23             $delim =~ tr/\x01-\x40/A-Za-z0-9_-/;
24              
25             my $prompt = "$delim";
26              
27             my $sudo_passwd_prompt = "$delim-passwd";
28              
29             my $prompt_re = qr/\Q$prompt\E$/sm;
30              
31             my $sudo_passwd_prompt_re = qr/^$sudo_passwd_prompt$/;
32              
33              
34             has 'connection_params' => (
35             isa => 'Net::SSH::Mechanize::ConnectParams',
36             is => 'rw',
37             # Note: this made rw and unrequired so that it can be supplied
38             # after AnyEvent::Subprocess::Job constructs the instance
39             );
40              
41             has 'is_logged_in' => (
42             isa => 'Bool',
43             is => 'ro',
44             writer => '_set_logged_in',
45             );
46              
47             has '_error_event' => (
48             is => 'rw',
49             isa => 'AnyEvent::CondVar',
50             default => sub { return AnyEvent->condvar },
51             );
52              
53              
54             # The log-in timeout limit in seconds
55             has 'login_timeout' => (
56             is => 'rw',
57             isa => 'Int',
58             default => 30,
59             );
60              
61             # helper function
62              
63             sub _croak_with {
64 5     5   21 my ($msg, $cv) = @_;
65             sub {
66 0     0   0 my $h = shift;
67 0 0       0 return unless my $text = $h->rbuf;
68 0         0 $h->{rbuf} = '';
69 0         0 $cv->croak("$msg: $text");
70             }
71 5         63 }
72              
73             sub _warn_with {
74 6     6   22 my ($msg) = @_;
75             sub {
76 2     2   109 my $h = shift;
77 2 50       23 return unless my $text = $h->rbuf;
78 2         18 $h->{rbuf} = '';
79 2         177 warn "$msg: $text";
80             }
81 6         69 }
82              
83             sub _push_write {
84 21     21   407 my $handle = shift;
85              
86             # print qq(writing: "@_"\n); # DB
87 21         109 $handle->push_write(@_);
88             }
89              
90              
91             sub _match {
92 30     30   57 my $handle = shift;
93 30         66 my $re = shift;
94 30 100       131 return unless $handle->{rbuf};
95 23         247 my @captures = $handle->{rbuf} =~ /$re/;
96 23 100       82 if (!@captures) {
97             # print qq(not matching $re: "$handle->{rbuf}"\n); # DB
98 5         24 return;
99             }
100              
101             # printf qq(matching $re with: "%s"\n), substr $handle->{rbuf}, 0, $+[0]; # DB
102              
103 18         110 substr $handle->{rbuf}, 0, $+[0], "";
104 18         92 return @captures;
105             }
106              
107             sub _define_automation {
108 5     5   15 my $self = shift;
109 5         47 my $states = {@_};
110 5         50 my $function = (caller 1)[3];
111            
112 5         18 my ($stdin, $stderr) = map { $self->delegate($_)->handle } qw(pty stderr);
  10         754  
113              
114 5         644 my $state = 'start';
115 5         14 my $cb;
116             $cb = sub {
117             # printf "before: state is %s %s\n", $function, $state; # DB
118 23     23   1349318 $state = $states->{$state}->(@_);
119 23 50       87 exists $states->{$state}
120             or die "something is wrong, next state returned is an unknown name: '$state'";
121              
122             # printf "after: state is %s %s\n", $function, $state; # DB
123 23 100       65 if (!$states->{$state}) { # terminal state, stop reading
124             # $stderr->on_read(undef); # cancel errors on stderr
125 5         13 $stdin->{rbuf} = '';
126 5         13 return 1;
127             }
128              
129             # $stdin->push_read($cb);
130 18         46 return;
131 5         22 };
132 5         49 $stdin->push_read($cb);
133              
134             # printf "$Coro::current exiting _define_automation\n"; # DB
135 5         73 return $state;
136             };
137              
138             # FIXME check code for possible self-ref closures which may cause mem leaks
139              
140              
141             sub login_async {
142 2     2 1 6 my $self = shift;
143 2         55 my $done = AnyEvent->condvar;
144              
145 2         29 my $stdin = $self->delegate('pty')->handle;
146 2         328 my $stderr = $self->delegate('stderr')->handle;
147              
148             # Make this a no-op if we've already logged in
149 2 50       604 if ($self->is_logged_in) {
150 0         0 $done->send($stdin, $self);
151 0         0 return $done;
152             }
153              
154             $self->_error_event->cb(sub {
155             # print "_error_event sent\n"; # DB
156 0     0   0 $done->croak(shift->recv);
157 2         64 });
158              
159 2         87 my $timeout;
160 2         65 my $delay = $self->login_timeout;
161             $timeout = AnyEvent->timer(
162             after => $delay,
163             cb => sub {
164 0     0   0 undef $timeout;
165             # print "timing out login\n"; # DB
166 0         0 $done->croak("login timed out after $delay seconds");
167             },
168 2         24 );
169              
170             # capture stderr output, interpret as an error
171 2         69 $stderr->on_read(_croak_with "error" => $done);
172              
173             $self->_define_automation(
174             start => sub {
175 4 100   4   22 if (_match($stdin => $passwd_prompt_re)) {
176 2 50       155 if (!$self->connection_params->has_password) {
177 0         0 $done->croak('password requested but none provided');
178 0         0 return 'auth_failure';
179             }
180 2         75 my $passwd = $self->connection_params->password;
181 2         18 _push_write($stdin => "$passwd\n");
182 2         245 return 'sent_passwd';
183             }
184            
185 2 50       6 if (_match($stdin => $initial_prompt_re)) {
186 0         0 _push_write($stdin => qq(PS1=$prompt; export PS1\n));
187 0         0 return 'expect_prompt';
188             }
189             # FIXME limit buffer size and time
190 2         7 return 'start';
191             },
192            
193             sent_passwd => sub {
194 2 50   2   9 if (_match($stdin => $passwd_prompt_re)) {
195 0   0     0 my $msg = $stderr->{rbuf} || '';
196 0         0 $done->croak("auth failure: $msg");
197 0         0 return 'auth_failure';
198             }
199            
200 2 50       9 if (_match($stdin => $initial_prompt_re)) {
201 2         19 _push_write($stdin => qq(PS1=$prompt; export PS1\n));
202 2         605 return 'expect_prompt';
203             }
204            
205 0         0 return 'sent_passwd';
206             },
207            
208             expect_prompt => sub {
209 2 50   2   18 if (_match($stdin => $prompt_re)) {
210             # Cancel stderr monitor
211 2         19 $stderr->on_read(undef);
212              
213 2         141 $self->_set_logged_in(1);
214 2         37 $done->send($stdin, $self); # done
215 2         44 return 'finished';
216             }
217            
218 0         0 return 'expect_prompt';
219             },
220            
221 2         149 auth_failure => 0,
222             finished => 0,
223             );
224              
225 2         16 return $done;
226             }
227              
228            
229             sub login {
230             # return (shift->login_async(@_)->recv)[1];
231 2     2 1 60 my ($cv) = shift->login_async(@_);
232             # printf "$Coro::current about to call recv\n"; # DB
233 2         37 my $v = ($cv->recv)[1];
234             # printf "$Coro::current about to called recv\n"; # DB
235 2         124 return $v;
236             }
237              
238             sub logout {
239 2     2 1 1647 my $self = shift;
240 2 50       88 croak "cannot use session yet, as it is not logged in"
241             if !$self->is_logged_in;
242              
243 2         17 _push_write($self->delegate('pty')->handle => "exit\n");
244 2         1836 return $self;
245             }
246              
247             sub capture_async {
248 3     3 1 9 my $self = shift;
249 3         36 my ($cmd) = pos_validated_list(
250             \@_,
251             { isa => 'Str' },
252             );
253              
254 3 50       1705 croak "cannot use session yet, as it is not logged in"
255             if !$self->is_logged_in;
256              
257 3         21 my $stdin = $self->delegate('pty')->handle;
258 3         763 my $stderr = $self->delegate('stderr')->handle;
259              
260 3         665 $cmd =~ s/\s*\z/\n/ms;
261              
262             # send command
263 3         19 _push_write($stdin => $cmd);
264              
265             # read result
266 3         1064 my $cumdata = '';
267              
268             # we want the _error_event condvar to trigger a croak sent to $done.
269 3         116 my $done = AnyEvent->condvar;
270             # FIXME check _error_event for expiry?
271             $self->_error_event->cb(sub {
272             # print "xxxx _error_event\n"; # DB
273 0     0   0 $done->croak(shift->recv);
274 3         179 });
275              
276             # capture stderr output, interpret as a warning
277 3         78 $stderr->on_read(_warn_with "unexpected stderr from command");
278              
279             my $read_output_cb = sub {
280 6     6   333 my ($handle) = @_;
281 6 50       26 return unless defined $handle->{rbuf};
282            
283             # print "got: $handle->{rbuf}\n"; # DB
284            
285 6         20 $cumdata .= $handle->{rbuf};
286 6         23 $handle->{rbuf} = '';
287            
288 6 100       89 $cumdata =~ /(.*?)$prompt_re/ms
289             or return;
290              
291             # cancel stderr monitor
292 3         18 $stderr->on_read(undef);
293              
294 3         51 $done->send($handle, $1);
295 3         58 return 1;
296 3         150 };
297            
298 3         20 $stdin->push_read($read_output_cb);
299            
300 3         75 return $done;
301             }
302              
303              
304             sub capture {
305 3     3 1 2182 return (shift->capture_async(@_)->recv)[1];
306             }
307              
308              
309             sub sudo_capture_async {
310 3     3 1 7 my $self = shift;
311 3         21 my ($cmd) = pos_validated_list(
312             \@_,
313             { isa => 'Str' },
314             );
315              
316 3 50       912 croak "cannot use session yet, as it is not logged in"
317             if !$self->is_logged_in;
318              
319 3         57 my $done = AnyEvent->condvar;
320             $self->_error_event->cb(sub {
321             # print "_error_event sent\n"; DB
322 0     0   0 $done->croak(shift->recv);
323 3         90 });
324              
325             # we know we'll need the password, so check this up-front
326 3 50       132 if (!$self->connection_params->has_password) {
327 0         0 croak 'password requested but none provided';
328             }
329              
330 3         16 my $stdin = $self->delegate('pty')->handle;
331 3         439 my $stderr = $self->delegate('stderr')->handle;
332              
333 3         385 my $timeout;
334 3         92 my $delay = $self->login_timeout;
335             $timeout = AnyEvent->timer(
336             after => $delay,
337             cb => sub {
338 0     0   0 undef $timeout;
339             # print "timing out login\n"; # DB
340 0         0 $done->croak("sudo_capture timed out after $delay seconds");
341             },
342 3         23 );
343              
344             # capture stderr output, interpret as an error
345 3         45 $stderr->on_read(_croak_with "error" => $done);
346              
347             # ensure command has a trailing newline
348 3         95 $cmd =~ s/\s*\z/\n/ms;
349              
350             # get captured result here
351 3         7 my $cumdata = '';
352              
353             # FIXME escape/untaint $passwd_prompt_re
354             # use full path names
355              
356             # Authenticate. Erase any cached sudo authentication first - we
357             # want to guarantee that we will get a password prompt. Then
358             # start a new shell with sudo.
359 3         12 _push_write($stdin => "sudo -K; sudo -p '$sudo_passwd_prompt' sh\n");
360              
361             $self->_define_automation(
362             start => sub {
363 6 100   6   14 if (_match($stdin => $sudo_passwd_prompt_re)) {
364 3         91 my $passwd = $self->connection_params->password;
365             # print "sending password\n"; # DB
366 3         12 _push_write($stdin => "$passwd\n");
367 3         285 return 'sent_passwd';
368             }
369            
370             # FIXME limit buffer size and time
371 3         6 return 'start';
372             },
373            
374             sent_passwd => sub {
375 3 50   3   8 if (_match($stdin => $sudo_passwd_prompt_re)) {
376 0   0     0 my $msg = $stderr->{rbuf} || '';
377 0         0 $done->croak("auth failure: $msg");
378 0         0 return 'auth_failure';
379             }
380            
381 3 50       10 if (_match($stdin => $prompt_re)) {
382             # Cancel stderr monitor
383 3         17 $stderr->on_read(undef);
384              
385 3         30 _push_write($stdin => $cmd);
386 3         595 return 'sent_cmd';
387             }
388            
389 0         0 return 'sent_passwd';
390             },
391            
392             sent_cmd => sub {
393 3 50   3   51 if (my ($data) = _match($stdin => qr/(.*?)$prompt_re/sm)) {
394 3         8 $cumdata .= $data;
395             # print "got data: $data\n<$stdin->{rbuf}>\n"; # DB
396              
397 3         8 $stdin->{rbuf} = '';
398              
399             # capture stderr output, interpret as a warning
400 3         9 $stderr->on_read(_warn_with "unexpected stderr from sudo command");
401              
402             # exit sudo shell
403 3         106 _push_write($stdin => "exit\n");
404            
405 3         347 return 'exited_shell';
406             }
407            
408 0         0 $cumdata .= $stdin->{rbuf};
409 0         0 $stdin->{rbuf} = '';
410 0         0 return 'sent_cmd';
411             },
412              
413             exited_shell => sub {
414 3 50   3   10 if (_match($stdin => $prompt_re)) {
415             # Cancel stderr monitor
416 3         14 $stderr->on_read(undef);
417              
418             # remove any output from the exit
419             # FIXME should this check that everything has been consumed?
420 3         28 $stdin->{rbuf} = '';
421              
422 3         12 $done->send($stdin, $cumdata); # done, send data collected
423 3         30 return 'finished';
424             }
425            
426 0         0 return 'exited_shell';
427             },
428              
429 3         1196 auth_failure => 0,
430             finished => 0,
431             );
432              
433 3         13 return $done;
434             }
435              
436             sub sudo_capture {
437 3     3 1 1347 return (shift->sudo_capture_async(@_)->recv)[1];
438             }
439              
440              
441             __PACKAGE__->meta->make_immutable;
442             1;
443              
444             __END__
445              
446             =head1 NAME
447              
448             Net::SSH::Mechanize::Session - manage a running ssh process.
449              
450             =head1 VERSION
451              
452             version 0.1.2
453              
454             =head1 SYNOPSIS
455              
456             This class represents a sunning C<ssh> process. It is a subclass of
457             C<AnyEvent::Subprocess::Running>, with methods to manage the
458             authentication and other interaction with the sub-process.
459              
460             Typically you will not create one directly, but obtain one via
461             C<< Net::SSH::Mechanize::Session->login >>,
462             or C<< Net::SSH::Mechanize->session >>
463              
464             You might invoke methods directly, or via C<Net::SSH::Mechanize>
465             instance's methods which delegate to the instance's C<session>
466             attribute (which is an instance of this class).
467              
468             use Net::SSH::Mechanize;
469              
470             my $mech = Net::SSH::Mechanize->new(hostname => 'somewhere');
471              
472             my $session = $mech->session;
473             # ...
474              
475             =head1 CLASS METHODS
476              
477             =head2 C<< $obj = $class->new(%params) >>
478              
479             Creates a new instance. Not intended for public use. Use
480             C<< Net::SSH::Mechanize->session >> instead.
481              
482             =head1 INSTANCE ATTRIBUTES
483              
484             =head2 C<< $params = $obj->connection_params >>
485              
486             This is a read-only accessor for the C<connection_params> instance
487             passed to the constructor by C<Net::SSH::Mechanize>.
488              
489             =head2 C<< $obj->login_timeout($integer) >>
490             =head2 C<< $integer = $obj->login_timeout >>
491              
492             This is a read-write accessor to the log-in timeout parameter passed
493             to the constructor.
494              
495             If you plan to modify it, do so before C<< ->login >> or
496             C<< ->login_async >> has been invoked or it will not have any effect
497             on anything.
498              
499             =head1 INSTANCE METHODS
500              
501             Note, all of these will throw an exception if used before C<< ->login >>
502             or before C<< ->login_async >> has successfully completed, except
503             of course C<< ->login >> and C<< ->login_async >> themselves.
504             These latter methods do nothing after the first invocation.
505              
506             =head2 C<< $session = $obj->login >>
507              
508             This method logs into the remote host using the defined connection
509             parameters, and returns a C<Net::SSH::Mechanize::Session> instance on
510             success, or throws an exception on failure.
511              
512             It is safe to use in C<AnyEvent> applications or C<Coro> co-routines,
513             because the implementation is asynchronous and will not block the
514             whole process.
515              
516             =head2 C<< $condvar = $obj->login_async >>
517              
518             This is an asynchronous method used to implement the synchronous
519             C<< ->login >> method. It returns an AnyEvent::CondVar instance
520             immediately, which can be used to wait for completion, or register a
521             callback to be notified when the log-in has completed.
522              
523             =head2 C<< $obj->logout >>
524              
525             Logs out of the remote host by issuing an "exit" command.
526              
527             =head2 C<< $condvar = $obj->capture_async($command) >>
528              
529             The returns a condvar immediately, which can be used to wait for
530             successful completion (or otherwise) of the command(s) defined by
531             C<$command>.
532              
533             =head2 C<< $result = $obj->capture($command) >>
534              
535             This invokes the command(s) defined by C<$command> on the remote host,
536             and returns the result.
537              
538             =head2 C<< $condvar = $obj->sudo_capture_async($command) >>
539              
540             The returns a condvar immediately, which can be used to wait for
541             successful completion (or otherwise) in a sudo'ed sub-shell of the
542             command(s) defined by C<$command>.
543              
544             A password is required in C<connection_params> for this to
545             authenticate with sudo.
546              
547             =head2 C<< $result = $obj->sudo_capture($command) >>
548              
549             This invokes the command(s) defined by C<$command> in a sudo'ed sub-shell
550             on the remote host, and returns the result.
551              
552              
553             =head1 AUTHOR
554              
555             Nick Stokoe C<< <wulee@cpan.org> >>
556              
557              
558             =head1 LICENCE AND COPYRIGHT
559              
560             Copyright (c) 2011, Nick Stokoe C<< <wulee@cpan.org> >>. All rights reserved.
561              
562             This module is free software; you can redistribute it and/or
563             modify it under the same terms as Perl itself. See L<perlartistic>.