File Coverage

blib/lib/Net/SSH/Mechanize/Session.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Net::SSH::Mechanize::Session;
2 1     1   2927 use Moose;
  0            
  0            
3             use MooseX::Params::Validate;
4             use AnyEvent;
5             use Carp qw(croak);
6             our @CARP_NOT = qw(Net::SSH::Mechanize AnyEvent);
7              
8             our $VERSION = '0.1.3'; # 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             my ($msg, $cv) = @_;
65             sub {
66             my $h = shift;
67             return unless my $text = $h->rbuf;
68             $h->{rbuf} = '';
69             $cv->croak("$msg: $text");
70             }
71             }
72              
73             sub _warn_with {
74             my ($msg) = @_;
75             sub {
76             my $h = shift;
77             return unless my $text = $h->rbuf;
78             $h->{rbuf} = '';
79             warn "$msg: $text";
80             }
81             }
82              
83             sub _push_write {
84             my $handle = shift;
85              
86             # print qq(writing: "@_"\n); # DB
87             $handle->push_write(@_);
88             }
89              
90              
91             sub _match {
92             my $handle = shift;
93             my $re = shift;
94             return unless $handle->{rbuf};
95             my @captures = $handle->{rbuf} =~ /$re/;
96             if (!@captures) {
97             # print qq(not matching $re: "$handle->{rbuf}"\n); # DB
98             return;
99             }
100              
101             # printf qq(matching $re with: "%s"\n), substr $handle->{rbuf}, 0, $+[0]; # DB
102              
103             substr $handle->{rbuf}, 0, $+[0], "";
104             return @captures;
105             }
106              
107             sub _define_automation {
108             my $self = shift;
109             my $states = {@_};
110             my $function = (caller 1)[3];
111            
112             my ($stdin, $stderr) = map { $self->delegate($_)->handle } qw(pty stderr);
113              
114             my $state = 'start';
115             my $cb;
116             $cb = sub {
117             # printf "before: state is %s %s\n", $function, $state; # DB
118             $state = $states->{$state}->(@_);
119             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             if (!$states->{$state}) { # terminal state, stop reading
124             # $stderr->on_read(undef); # cancel errors on stderr
125             $stdin->{rbuf} = '';
126             return 1;
127             }
128              
129             # $stdin->push_read($cb);
130             return;
131             };
132             $stdin->push_read($cb);
133              
134             # printf "$Coro::current exiting _define_automation\n"; # DB
135             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             my $self = shift;
143             my $done = AnyEvent->condvar;
144              
145             my $stdin = $self->delegate('pty')->handle;
146             my $stderr = $self->delegate('stderr')->handle;
147              
148             # Make this a no-op if we've already logged in
149             if ($self->is_logged_in) {
150             $done->send($stdin, $self);
151             return $done;
152             }
153              
154             $self->_error_event->cb(sub {
155             # print "_error_event sent\n"; # DB
156             $done->croak(shift->recv);
157             });
158              
159             my $timeout;
160             my $delay = $self->login_timeout;
161             $timeout = AnyEvent->timer(
162             after => $delay,
163             cb => sub {
164             undef $timeout;
165             # print "timing out login\n"; # DB
166             $done->croak("login timed out after $delay seconds");
167             },
168             );
169              
170             # capture stderr output, interpret as an error
171             $stderr->on_read(_croak_with "error" => $done);
172              
173             $self->_define_automation(
174             start => sub {
175             if (_match($stdin => $passwd_prompt_re)) {
176             if (!$self->connection_params->has_password) {
177             $done->croak('password requested but none provided');
178             return 'auth_failure';
179             }
180             my $passwd = $self->connection_params->password;
181             _push_write($stdin => "$passwd\n");
182             return 'sent_passwd';
183             }
184            
185             if (_match($stdin => $initial_prompt_re)) {
186             _push_write($stdin => qq(PS1=$prompt; export PS1\n));
187             return 'expect_prompt';
188             }
189             # FIXME limit buffer size and time
190             return 'start';
191             },
192            
193             sent_passwd => sub {
194             if (_match($stdin => $passwd_prompt_re)) {
195             my $msg = $stderr->{rbuf} || '';
196             $done->croak("auth failure: $msg");
197             return 'auth_failure';
198             }
199            
200             if (_match($stdin => $initial_prompt_re)) {
201             _push_write($stdin => qq(PS1=$prompt; export PS1\n));
202             return 'expect_prompt';
203             }
204            
205             return 'sent_passwd';
206             },
207            
208             expect_prompt => sub {
209             if (_match($stdin => $prompt_re)) {
210             # Cancel stderr monitor
211             $stderr->on_read(undef);
212              
213             $self->_set_logged_in(1);
214             $done->send($stdin, $self); # done
215             return 'finished';
216             }
217            
218             return 'expect_prompt';
219             },
220            
221             auth_failure => 0,
222             finished => 0,
223             );
224              
225             return $done;
226             }
227              
228            
229             sub login {
230             # return (shift->login_async(@_)->recv)[1];
231             my ($cv) = shift->login_async(@_);
232             # printf "$Coro::current about to call recv\n"; # DB
233             my $v = ($cv->recv)[1];
234             # printf "$Coro::current about to called recv\n"; # DB
235             return $v;
236             }
237              
238             sub logout {
239             my $self = shift;
240             croak "cannot use session yet, as it is not logged in"
241             if !$self->is_logged_in;
242              
243             _push_write($self->delegate('pty')->handle => "exit\n");
244             return $self;
245             }
246              
247             sub capture_async {
248             my $self = shift;
249             my ($cmd) = pos_validated_list(
250             \@_,
251             { isa => 'Str' },
252             );
253              
254             croak "cannot use session yet, as it is not logged in"
255             if !$self->is_logged_in;
256              
257             my $stdin = $self->delegate('pty')->handle;
258             my $stderr = $self->delegate('stderr')->handle;
259              
260             $cmd =~ s/\s*\z/\n/ms;
261              
262             # send command
263             _push_write($stdin => $cmd);
264              
265             # read result
266             my $cumdata = '';
267              
268             # we want the _error_event condvar to trigger a croak sent to $done.
269             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             $done->croak(shift->recv);
274             });
275              
276             # capture stderr output, interpret as a warning
277             $stderr->on_read(_warn_with "unexpected stderr from command");
278              
279             my $read_output_cb = sub {
280             my ($handle) = @_;
281             return unless defined $handle->{rbuf};
282            
283             # print "got: $handle->{rbuf}\n"; # DB
284            
285             $cumdata .= $handle->{rbuf};
286             $handle->{rbuf} = '';
287            
288             $cumdata =~ /(.*?)$prompt_re/ms
289             or return;
290              
291             # cancel stderr monitor
292             $stderr->on_read(undef);
293              
294             $done->send($handle, $1);
295             return 1;
296             };
297            
298             $stdin->push_read($read_output_cb);
299            
300             return $done;
301             }
302              
303              
304             sub capture {
305             return (shift->capture_async(@_)->recv)[1];
306             }
307              
308              
309             sub sudo_capture_async {
310             my $self = shift;
311             my ($cmd) = pos_validated_list(
312             \@_,
313             { isa => 'Str' },
314             );
315              
316             croak "cannot use session yet, as it is not logged in"
317             if !$self->is_logged_in;
318              
319             my $done = AnyEvent->condvar;
320             $self->_error_event->cb(sub {
321             # print "_error_event sent\n"; DB
322             $done->croak(shift->recv);
323             });
324              
325             # we know we'll need the password, so check this up-front
326             if (!$self->connection_params->has_password) {
327             croak 'password requested but none provided';
328             }
329              
330             my $stdin = $self->delegate('pty')->handle;
331             my $stderr = $self->delegate('stderr')->handle;
332              
333             my $timeout;
334             my $delay = $self->login_timeout;
335             $timeout = AnyEvent->timer(
336             after => $delay,
337             cb => sub {
338             undef $timeout;
339             # print "timing out login\n"; # DB
340             $done->croak("sudo_capture timed out after $delay seconds");
341             },
342             );
343              
344             # capture stderr output, interpret as an error
345             $stderr->on_read(_croak_with "error" => $done);
346              
347             # ensure command has a trailing newline
348             $cmd =~ s/\s*\z/\n/ms;
349              
350             # get captured result here
351             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             _push_write($stdin => "sudo -K; sudo -p '$sudo_passwd_prompt' sh\n");
360              
361             $self->_define_automation(
362             start => sub {
363             if (_match($stdin => $sudo_passwd_prompt_re)) {
364             my $passwd = $self->connection_params->password;
365             # print "sending password\n"; # DB
366             _push_write($stdin => "$passwd\n");
367             return 'sent_passwd';
368             }
369            
370             # FIXME limit buffer size and time
371             return 'start';
372             },
373            
374             sent_passwd => sub {
375             if (_match($stdin => $sudo_passwd_prompt_re)) {
376             my $msg = $stderr->{rbuf} || '';
377             $done->croak("auth failure: $msg");
378             return 'auth_failure';
379             }
380            
381             if (_match($stdin => $prompt_re)) {
382             # Cancel stderr monitor
383             $stderr->on_read(undef);
384              
385             _push_write($stdin => $cmd);
386             return 'sent_cmd';
387             }
388            
389             return 'sent_passwd';
390             },
391            
392             sent_cmd => sub {
393             if (my ($data) = _match($stdin => qr/(.*?)$prompt_re/sm)) {
394             $cumdata .= $data;
395             # print "got data: $data\n<$stdin->{rbuf}>\n"; # DB
396              
397             $stdin->{rbuf} = '';
398              
399             # capture stderr output, interpret as a warning
400             $stderr->on_read(_warn_with "unexpected stderr from sudo command");
401              
402             # exit sudo shell
403             _push_write($stdin => "exit\n");
404            
405             return 'exited_shell';
406             }
407            
408             $cumdata .= $stdin->{rbuf};
409             $stdin->{rbuf} = '';
410             return 'sent_cmd';
411             },
412              
413             exited_shell => sub {
414             if (_match($stdin => $prompt_re)) {
415             # Cancel stderr monitor
416             $stderr->on_read(undef);
417              
418             # remove any output from the exit
419             # FIXME should this check that everything has been consumed?
420             $stdin->{rbuf} = '';
421              
422             $done->send($stdin, $cumdata); # done, send data collected
423             return 'finished';
424             }
425            
426             return 'exited_shell';
427             },
428              
429             auth_failure => 0,
430             finished => 0,
431             );
432              
433             return $done;
434             }
435              
436             sub sudo_capture {
437             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.3
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>.