File Coverage

blib/lib/XAS/Lib/SSH/Client.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package XAS::Lib::SSH::Client;
2              
3             our $VERSION = '0.01';
4              
5 1     1   860 use IO::Select;
  1         1  
  1         38  
6 1     1   4 use Errno 'EAGAIN';
  1         3  
  1         36  
7 1     1   211 use Net::SSH2 ':all';
  0            
  0            
8              
9             use XAS::Class
10             debug => 0,
11             version => $VERSION,
12             base => 'XAS::Base',
13             mixin => 'XAS::Lib::Mixins::Bufops',
14             accessors => 'ssh chan sock select exit_code exit_signal',
15             mutators => 'attempts',
16             utils => ':validation dotid trim',
17             import => 'class',
18             vars => {
19             PARAMS => {
20             -port => { optional => 1, default => 22 },
21             -timeout => { optional => 1, default => 0.2 },
22             -username => { optional => 1, default => undef},
23             -host => { optional => 1, default => 'localhost' },
24             -eol => { optional => 1, default => "\015\012" },
25             -password => { optional => 1, default => undef, depends => [ 'username' ] },
26             -priv_key => { optional => 1, default => undef, depends => [ 'pub_key', 'username' ] },
27             -pub_key => { optional => 1, default => undef, depends => [ 'priv_key', 'username' ] },
28             },
29             ERRNO => 0,
30             ERRSTR => '',
31             }
32             ;
33              
34             #use Data::Dumper;
35              
36             # ----------------------------------------------------------------------
37             # Public Methods
38             # ----------------------------------------------------------------------
39              
40             sub connect {
41             my $self = shift;
42              
43             my ($errno, $name, $errstr);
44              
45             $self->class->var('ERRNO', 0);
46             $self->class->var('ERRSTR', '');
47              
48             if ($self->ssh->connect($self->host, $self->port)) {
49              
50             if ($self->pub_key) {
51              
52             unless ($self->ssh->auth_publickey($self->username,
53             $self->pub_key, $self->priv_key)) {
54              
55             ($errno, $name, $errstr) = $self->ssh->error();
56              
57             $self->class->var('ERRNO', $errno);
58             $self->class->var('ERRSTR', $errstr);
59              
60             $self->throw_msg(
61             dotid($self->class) . '.autherr',
62             'ssh_client_autherr',
63             $name, $errstr
64             );
65              
66             }
67              
68             } else {
69              
70             unless ($self->ssh->auth_password($self->username, $self->password)) {
71              
72             ($errno, $name, $errstr) = $self->ssh->error();
73              
74             $self->class->var('ERRNO', $errno);
75             $self->class->var('ERRSTR', $errstr);
76              
77             $self->throw_msg(
78             dotid($self->class) . '.autherr',
79             'ssh_client_autherr',
80             $name, $errstr
81             );
82              
83             }
84              
85             }
86              
87             $self->{'sock'} = $self->ssh->sock();
88             $self->{'chan'} = $self->ssh->channel();
89             $self->{'select'} = IO::Select->new($self->sock);
90              
91             $self->setup();
92              
93             } else {
94              
95             ($errno, $name, $errstr) = $self->ssh->error();
96              
97             $self->class->var('ERRNO', $errno);
98             $self->class->var('ERRSTR', $errstr);
99              
100             $self->throw_msg(
101             dotid($self->class) . '.conerr',
102             'ssh_client_conerr',
103             $name, $errstr
104             );
105              
106             }
107              
108             }
109              
110             sub setup {
111             my $self = shift;
112              
113             }
114              
115             sub pending {
116             my $self = shift;
117              
118             return length($self->{'buffer'});
119              
120             }
121              
122             sub disconnect {
123             my $self = shift;
124              
125             if (my $chan = $self->chan) {
126              
127             $chan->send_eof();
128             $chan->close();
129              
130             }
131              
132             if (my $ssh = $self->ssh) {
133              
134             $ssh->disconnect();
135              
136             }
137              
138             }
139              
140             sub get {
141             my $self = shift;
142             my ($length) = validate_params(\@_, [
143             { optional => 1, default => 512 }
144             ]);
145              
146             my $output = '';
147              
148             # extract $length from buffer. if the buffer size is > $length then
149             # try to refill buffer. If there is nothing to read, then return
150             # the remainder of the buffer.
151             #
152             # Patterned after some libssh2 examples and C network programming
153             # "best practices".
154              
155             if ($self->pending > $length) {
156              
157             $output = $self->buf_slurp(\$self->{'buffer'}, $length);
158              
159             } else {
160              
161             $self->_fill_buffer();
162              
163             my $l = ($self->pending > $length) ? $length : $self->pending;
164             $output = $self->buf_slurp(\$self->{'buffer'}, $l);
165              
166             }
167              
168             return $output;
169              
170             }
171              
172             sub gets {
173             my $self = shift;
174              
175             my $buffer;
176             my $output = '';
177              
178             while (my $buf = $self->get()) {
179              
180             $buffer .= $buf;
181              
182             if ($output = $self->buf_get_line(\$buffer, $self->eol)) {
183              
184             $self->{'buffer'} = $buffer . $self->{'buffer'};
185             last;
186              
187             }
188              
189             }
190              
191             return trim($output);
192              
193             }
194              
195             sub put {
196             my $self = shift;
197             my ($buffer) = validate_params(\@_, [1]);
198              
199             my $counter = 0;
200             my $working = 1;
201             my $written = 0;
202             my $bufsize = length($buffer);
203              
204             $self->class->var('ERRNO', 0);
205             $self->class->var('ERRSTR', '');
206              
207             # Setup non-blocking writes. Keep writting until nothing is left.
208             # Returns the number of bytes written, if any.
209             #
210             # Patterned after some libssh2 examples and C network programming
211             # "best practices".
212              
213             $self->chan->blocking(0);
214              
215             do {
216              
217             if (my $bytes = $self->chan->write($buffer)) {
218              
219             $written += $bytes;
220             $buffer = substr($buffer, $bytes);
221             $working = 0 if ($written >= $bufsize);
222              
223             } else {
224              
225             my ($errno, $name, $errstr) = $self->ssh->error();
226             if ($errno == LIBSSH2_ERROR_EAGAIN) {
227              
228             $counter++;
229              
230             $working = 0 if ($counter > $self->attempts);
231             $self->_waitsocket() if ($counter <= $self->attempts);
232              
233             } else {
234              
235             $self->chan->blocking(1);
236              
237             $self->class->var('ERRNO', $errno);
238             $self->class->var('ERRSTR', $errstr);
239              
240             $self->throw_msg(
241             dotid($self->class) . '.protoerr',
242             'ssh_client_protoerr',
243             $name, $errstr
244             );
245              
246             }
247              
248             }
249              
250             } while ($working);
251              
252             $self->chan->blocking(1);
253              
254             return $written;
255              
256             }
257              
258             sub puts {
259             my $self = shift;
260             my ($buffer) = validate_params(\@_, [1]);
261              
262             my $output = sprintf("%s%s", trim($buffer), $self->eol);
263             my $written = $self->put($output);
264              
265             return $written;
266              
267             }
268              
269             sub errno {
270             my $class = shift;
271             my ($value) = validate_params(\@_, [
272             { optional => 1, default => undef }
273             ]);
274              
275             class->var('ERRNO', $value) if (defined($value));
276              
277             return class->var('ERRNO');
278              
279             }
280              
281             sub errstr {
282             my $class = shift;
283             my ($value) = validate_params(\@_, [
284             { optional => 1, default => undef }
285             ]);
286              
287             class->var('ERRSTR', $value) if (defined($value));
288              
289             return class->var('ERRSTR');
290              
291             }
292              
293             sub DESTROY {
294             my $self = shift;
295              
296             $self->disconnect();
297              
298             }
299              
300             # ----------------------------------------------------------------------
301             # Private Methods
302             # ----------------------------------------------------------------------
303              
304             sub init {
305             my $class = shift;
306              
307             my $self = $class->SUPER::init(@_);
308              
309             $self->{'ssh'} = Net::SSH2->new();
310             $self->{'buffer'} = '';
311              
312             $self->attempts(5); # number of EAGAIN attempts
313              
314             return $self;
315              
316             }
317              
318             sub _fill_buffer {
319             my $self = shift;
320              
321             my $read = 0;
322             my $counter = 0;
323             my $working = 1;
324              
325             # Setup non-blocking read. Keep reading until nothing is left.
326             # i.e. the reads timeout.
327              
328             $self->chan->blocking(0);
329              
330             while ($working) {
331              
332             my $buf;
333              
334             if (my $bytes = $self->chan->read($buf, 512)) {
335              
336             $self->{'buffer'} .= $buf;
337             $read += $bytes;
338              
339             } else {
340              
341             my $syserr = $! + 0;
342             my ($errno, $name, $errstr) = $self->ssh->error();
343              
344             if (($errno == LIBSSH2_ERROR_EAGAIN) || ($syserr == EAGAIN)) {
345              
346             $counter++;
347            
348             $working = 0 if ($counter > $self->attempts);
349             $self->_waitsocket() if ($counter <= $self->attempts);
350              
351             } else {
352              
353             $self->chan->blocking(1);
354              
355             $self->class->var('ERRNO', $errno);
356             $self->class->var('ERRSTR', $errstr);
357              
358             $self->throw_msg(
359             dotid($self->class) . '.protoerr',
360             'ssh_client_protoerr',
361             $name, $errstr
362             );
363              
364             }
365              
366             }
367              
368             }
369              
370             $self->chan->blocking(1);
371              
372             return $read;
373              
374             }
375            
376             sub _waitsocket {
377             my $self = shift;
378              
379             my $to = $self->timeout;
380             my $dir = $self->ssh->block_directions();
381              
382             # If $dir is 1, then input is blocking.
383             # If $dir is 2, then output is blocking.
384             #
385             # Patterned after some libssh2 examples.
386              
387             if ($dir == 1) {
388              
389             $self->select->can_read($to);
390              
391             } else {
392              
393             $self->select->can_write($to);
394              
395             }
396              
397             return $! + 0;
398              
399             }
400              
401             1;
402              
403             __END__
404              
405             =head1 NAME
406              
407             XAS::Lib::SSH::Client - A SSH based client
408              
409             =head1 SYNOPSIS
410              
411             use XAS::Lib::SSH::Client;
412              
413             my $client = XAS::Lib::SSH::Client->new(
414             -host => 'auburn-xen-01',
415             -username => 'root',
416             -password => 'secret',
417             );
418              
419             $client->connect();
420            
421             $client->put($data);
422             $data = $client->get();
423              
424             $client->disconnect();
425              
426             =head1 DESCRIPTION
427              
428             The module provides basic network connectivity along with input/output methods
429             using the SSH protocol. It can authenticate using username/password or
430             username/public key/private key/password.
431              
432             =head1 METHODS
433              
434             =head2 new
435              
436             This initializes the object. It takes the following parameters:
437              
438             =over 4
439              
440             =item B<-username>
441              
442             An optional username to use when connecting to the server.
443              
444             =item B<-password>
445              
446             An optional password to use for authentication.
447              
448             =item B<-pub_key>
449              
450             An optional public ssh key file to use.
451              
452             =item B<-priv_key>
453              
454             An optional private ssh key to use.
455              
456             =item B<-host>
457              
458             The server to connect too. Defaults to 'localhost'.
459              
460             =item B<-port>
461              
462             The port to use on the server. It defaults to 22.
463              
464             =item B<-timeout>
465              
466             The number of seconds to timeout writes. It must be compatible with IO::Select.
467             Defaults to 0.2.
468              
469             =item B<-eol>
470              
471             The EOL to use, defaults to "\015\012".
472              
473             =back
474              
475             =head2 connect
476              
477             This method makes a connection to the server.
478              
479             =head2 setup
480              
481             This method sets up the channel to be used. It needs to be overridden
482             to be useful.
483              
484             =head2 get($length)
485              
486             This block reads data from the channel. A buffer is returned when it reaches
487             $length or timeout, whichever is first.
488              
489             =over 4
490              
491             =item B<$length>
492              
493             An optional length for the buffer. Defaults to 512 bytes.
494              
495             =back
496              
497             =head2 gets
498              
499             This reads a buffer delimited by the eol from the channel.
500              
501             =head2 errno
502              
503             A class method to return the SSH error number.
504              
505             =head2 errstr
506              
507             A class method to return the SSH error string.
508              
509             =head2 put($buffer)
510              
511             This method will write a buffer to the channel. Returns the number of
512             bytes written.
513              
514             =over 4
515              
516             =item B<$buffer>
517              
518             The buffer to be written.
519              
520             =back
521              
522             =head2 puts($buffer)
523              
524             This writes a buffer that is terminated with eol to the channel. Returns the
525             number of bytes written.
526              
527             =over 4
528              
529             =item B<$buffer>
530              
531             The buffer to send over the socket.
532              
533             =back
534              
535             =head2 disconnect
536              
537             This method closes the connection.
538              
539             =head1 MUTATORS
540              
541             =head2 attempts
542              
543             This is used when reading data from the channel. It triggers how many
544             times to attempt reading from the channel when a LIBSSH2_ERROR_EAGAIN
545             error occurs. The default is 5 times.
546              
547             =head1 SEE ALSO
548              
549             =over 4
550              
551             =item L<XAS::Lib::SSH::Server|XAS::Lib::SSH::Server>
552              
553             =item L<XAS::Lib::SSH::Client::Exec|XAS::Lib::SSH::Client::Exec>
554              
555             =item L<XAS::Lib::SSH::Client::Shell|XAS::Lib::SSH::Client::Shell>
556              
557             =item L<XAS::Lib::SSH::Client::Subsystem|XAS::Lib::SSH::Client::Subsystem>
558              
559             =item L<Net::SSH2|https://metacpan.org/pod/Net::SSH2>
560              
561             =item L<XAS|XAS>
562              
563             =back
564              
565             =head1 AUTHOR
566              
567             Kevin L. Esteb, E<lt>kevin@kesteb.usE<gt>
568              
569             =head1 COPYRIGHT AND LICENSE
570              
571             Copyright (c) 2012-2015 Kevin L. Esteb
572              
573             This is free software; you can redistribute it and/or modify it under
574             the terms of the Artistic License 2.0. For details, see the full text
575             of the license at http://www.perlfoundation.org/artistic_license_2_0.
576              
577             =cut