File Coverage

lib/App/Tel.pm
Criterion Covered Total %
statement 222 517 42.9
branch 58 192 30.2
condition 23 79 29.1
subroutine 31 53 58.4
pod 20 20 100.0
total 354 861 41.1


line stmt bran cond sub pod time code
1             package App::Tel;
2 6     6   170526 use strict;
  6         15  
  6         170  
3 6     6   32 use warnings;
  6         14  
  6         201  
4 6     6   7415 use Expect qw( exp_continue );
  6         293675  
  6         436  
5 6     6   59 use POSIX qw(:sys_wait_h :unistd_h); # For WNOHANG
  6         11  
  6         46  
6 6     6   8141 use Hash::Merge::Simple qw (merge);
  6         3016  
  6         347  
7 6     6   6196 use Module::Load;
  6         7697  
  6         44  
8 6     6   3690 use App::Tel::HostRange qw (check_hostrange);
  6         21  
  6         456  
9 6     6   3011 use App::Tel::Passwd;
  6         17  
  6         241  
10 6     6   2979 use App::Tel::Color;
  6         13  
  6         241  
11 6     6   2813 use App::Tel::Macro;
  6         15  
  6         318  
12 6     6   4355 use Time::HiRes qw ( sleep );
  6         8016  
  6         30  
13 6     6   1046 use v5.10;
  6         18  
14              
15              
16             =head1 NAME
17              
18             App::Tel - A script for logging into devices
19              
20             =head1 VERSION
21              
22             0.201509
23              
24             =cut
25              
26             our $VERSION = '0.201509';
27              
28              
29             =head1 SYNOPSIS
30              
31             tel gw1-my-dev
32              
33             See the README and COMMANDS files for examples of usage and ways to extend the
34             application.
35              
36             =head1 AUTHOR
37              
38             Robert Drake, C<< <rdrake at cpan.org> >>
39              
40             =head1 COPYRIGHT & LICENSE
41              
42             Copyright 2006 Robert Drake, all rights reserved.
43              
44             This program is free software; you can redistribute it and/or modify it
45             under the same terms as Perl itself.
46              
47             =cut
48              
49             #### GLOBALS
50             # For reasons related to state I needed to make $_winch_it global
51             # because it needs to be written to inside signals.
52             my $_winch_it=0;
53              
54             sub _winch_handler {
55 0     0   0 $_winch_it=1;
56             }
57              
58             sub _winch {
59 0     0   0 my $session = shift->{'session'};
60             # these need to be wrapped in eval or you get Given filehandle is not a
61             # tty in clone_winsize_from if you call winch() under a scripted
62             # environment like rancid (or just under par, or anywhere there is no pty)
63 0         0 eval {
64 0         0 $session->slave->clone_winsize_from(\*STDIN);
65 0 0       0 kill WINCH => $session->pid if $session->pid;
66             };
67 0         0 $_winch_it=0;
68 0         0 $SIG{WINCH} = \&_winch_handler;
69             }
70              
71             =head1 METHODS
72              
73             =head2 new
74              
75             my $tel = App::Tel->new();
76              
77             Creates a new App::Tel object.
78              
79             =cut
80              
81             sub new {
82 5     5 1 1401 my $infile = IO::File->new;
83 5         252 $infile->IO::File::fdopen( \*STDIN, 'r' );
84              
85 5         408 my $self = {
86             'stdin' => Expect->exp_init($infile),
87             'stdin_fileno' => $infile->fileno,
88             'connected' => 0,
89             'enabled' => 0,
90             'title_stack' => 0,
91             'log_stdout' => 1,
92             'profile' => {},
93             };
94              
95 5         790 bless($self, 'App::Tel');
96 5         17 return $self;
97             }
98              
99             =head2 disconnect
100              
101             $self->disconnect($hard_close);
102              
103             Tears down the session gracefully and resets internal variables to their
104             default values. Useful if you want to connect to another host after the first
105             one.
106              
107             If you supply a true value, it will hard_close the session.
108              
109             =cut
110              
111             sub disconnect {
112 1     1 1 3 my $self = shift;
113 1         4 my $hard = shift;
114 1         5 $self->{profile} = {};
115 1 50       37 $self->{timeout} = $self->{opts}->{t} ? $self->{opts}->{t} : 90;
116 1         4 $self->{banners} = undef;
117 1         4 $self->{methods} = ();
118 1         10 $self->connected(0);
119 1         6 $self->{colors}=();
120 1         4 $self->{enabled}=0;
121 1 50       8 if ($self->{title_stack} > 0) {
122 1         3 $self->{title_stack}--;
123 1         33 print "\e[23t";
124             }
125 1 50       6 if ($hard) {
126 0         0 $self->session->hard_close();
127             } else {
128 1         6 $self->session->soft_close();
129             }
130             }
131              
132              
133             =head2 send
134              
135             $self->send("text\r");
136              
137             Wrapper for Expect's send() method.
138              
139             =cut
140              
141             sub send {
142 5     5 1 36 return shift->{'session'}->send(@_);
143             }
144              
145             =head2 expect
146              
147             $self->expect("text");
148              
149             Wrapper for Expect's expect() method. If you don't specify a timeout this
150             will use the default script timeout.
151              
152             =cut
153              
154             sub expect {
155 3     3 1 10 my $self = shift;
156 3 100       21 if ($#_ < 2) {
157 1         9 return $self->{'session'}->expect($self->{'timeout'}, @_);
158             } else {
159 2         29 return $self->{'session'}->expect(@_);
160             }
161             }
162              
163             =head2 load_config
164              
165             Loads the config from /etc/telrc, /usr/local/etc/telrc, $ENV{HOME}/.telrc2, or
166             it can be appended to by using the environment variable TELRC, or overridden
167             by calling load_config with an argument:
168              
169             $self->load_config('/home/user/.my_custom_override');
170              
171             =cut
172              
173             sub load_config {
174 2     2 1 62 my $self = shift;
175 2         6 my @configs = @_;
176 2 50       9 @configs = ( "/etc/telrc", "/usr/local/etc/telrc", "$ENV{HOME}/.telrc2") if (!@configs);
177 2 50       9 push(@configs, $ENV{TELRC}) if (defined($ENV{TELRC}));
178 2         4 our $telrc;
179 2         3 my $config;
180              
181 2         5 foreach my $conf (@configs) {
182 2 100       216 if (-r $conf) {
183 1         887 require $conf;
184 1         5 push(@{$config->{'telrc_file'}}, $conf);
  1         4  
185 1         7 $config = merge($config, $telrc);
186             }
187             }
188              
189 2 100       59 if (!defined($config->{'telrc_file'})) {
190 1         23 warn "No configuration files loaded. You may need to run mktelrc.";
191             }
192              
193             # load global syntax highlighting things if found
194 2         64 for(@{$config->{syntax}}) {
  2         8  
195 1         12 App::Tel::Color::load_syntax($_,$self->{opts}->{d});
196             }
197 2         10 return $config;
198             }
199              
200             =head2 hostname
201              
202             $hostname = $self->hostname("hostname");
203              
204             Called to parse the hostname provided by the user when first making a
205             connection. If the hostname has special attributes like a port designation,
206             it's parsed here.
207              
208             This also looks up the profile for the hostname to see if it needs to be
209             translated because of an alias. The final hostname is stored and returned.
210              
211              
212             =cut
213              
214             sub hostname {
215 9     9 1 6581 my $self = shift;
216 9         16 my $hostname = shift;
217              
218 9 50       20 if (!defined($hostname)) {
219 0         0 return $self->{hostname};
220             }
221              
222 9         26 $hostname =~ s#/tftpboot/##;
223 9         13 $hostname =~ s/-confg//;
224              
225 9 100 100     120 if ($hostname =~ qr@(ssh|telnet)://\[([a-zA-Z0-9\-\.\:]+)\](?::(\d+))?@) {
    100          
    100          
    100          
226 2 100       9 $self->{port} = $3 if ($3);
227 2         7 $self->methods($1);
228 2         4 $hostname = $2;
229             } elsif ($hostname =~ qr@(ssh|telnet)://([a-zA-Z0-9\-\.]+)(?::(\d+))?@) {
230 2 100       8 $self->{port} = $3 if ($3);
231 2         6 $self->methods($1);
232 2         5 $hostname = $2;
233             } elsif ($hostname =~ /\[(\S+)\](?::(\d+))?/) {
234 2 100       9 $self->{port} = $2 if ($2);
235 2 100       8 $self->methods('telnet') if ($2);
236 2         4 $hostname = $1;
237             } elsif (($hostname =~ tr/://) < 2 && $hostname =~ /(\S+):(\d+)$/) {
238 1         6 $self->{port} = $2;
239 1         5 $self->methods('telnet');
240 1         2 $hostname = $1;
241             }
242              
243             # load profile based on the hostname
244 9         33 $self->rtr_find($hostname);
245              
246             # if rtr->hostname is defined as code then call it to determine the real hostname
247             # if it's not then treat it as an alias.
248 9 50       19 if (defined($self->profile->{hostname})) {
249 0 0       0 if (ref($self->profile->{hostname}) eq 'CODE') {
250 0         0 $hostname = &{$self->profile->{hostname}}($hostname);
  0         0  
251             } else {
252 0         0 $hostname = $self->profile->{hostname};
253             }
254             }
255 9         17 $self->{hostname} = $hostname;
256 9         23 return $self->{hostname};
257             }
258              
259             =head2 methods
260              
261             $self->methods();
262              
263             This is used to determine the method used to connect to the remote device.
264             Generally, the CLI argument -m has the highest priority. The uri should be
265             second, profiles third, and the defaults would be last. If called without
266             arguments it will return whatever the currently decided method array is. If
267             called with an argument that will be set as the new method array.
268              
269             If you call it multiple times it won't change right now. I may need to
270             rethink this later but right now it works with the way the program flows.
271             $self->disconnect removes all methods so connecting to another router will run
272             this again.
273              
274             $self->methods('ssh', 'telnet');
275              
276             =cut
277              
278             sub methods {
279 7     7 1 12 my $self = shift;
280              
281 7 100       26 if (@_) {
    50          
    50          
    50          
282 6         7 @{$self->{methods}} = @_;
  6         20  
283             } elsif (defined($self->{methods})) {
284 0         0 return $self->{methods};
285             } elsif ($self->{opts}->{m}) {
286 0         0 $self->{methods} = [ $self->{opts}->{m} ];
287             } elsif (defined($self->{'profile'}->{method})) {
288 1         4 @{$self->{methods}} = split(/,/, $self->{'profile'}->{method});
  1         5  
289             } else {
290 0         0 $self->{methods} = [ 'ssh', 'telnet' ];
291             }
292              
293 7         18 return $self->{methods};
294             }
295              
296             sub _banners {
297 1     1   7 my $self = shift;
298 1 50       20 return $self->{banners} if ($self->{banners});
299 1         6 my $config = $self->{'config'};
300              
301             # if there are no banners then we want to return an empty list
302 1         9 $self->{banners} = [];
303 1         10 while (my ($regex, $profile) = each %{$config->{banners}}) {
  1         17  
304 0     0   0 push @{$self->{banners}}, [ $regex, sub { $self->profile($profile); exp_continue; } ];
  0         0  
  0         0  
  0         0  
305             }
306 1         160 return $self->{banners};
307             }
308              
309             =head2 rtr_find
310              
311             my $profile = $self->rtr_find($regex);
312              
313             Find the router by hostname/regex and load the config associated with it.
314             Load a profile for it if there is one.
315              
316             =cut
317              
318             sub rtr_find {
319 10     10 1 17 my $self = shift;
320 10         13 my $host = shift;
321 10         18 my $profile = $self->{'profile'};
322 10         13 my $config = $self->{'config'};
323              
324 10         13 foreach my $h (@{$config->{rtr}}) {
  10         35  
325 1         3 my $h2 = $h->{regex};
326 1 50 33     16 if ($host =~ /$h2/i || check_hostrange($h2, $host)) {
327 1         4 $profile=merge($profile, $h);
328 1         68 last;
329             }
330             }
331 10         18 $self->{'profile'}=$profile;
332              
333             # if the host specified a profile to load then load it here
334 10 50       30 if (defined($profile->{profile})) {
335 0         0 return $self->profile($profile->{profile});
336             }
337              
338 10         24 return $profile;
339             }
340              
341             =head2 profile
342              
343             $profile = $self->profile;
344             $profile = $self->profile('profilename', $replace);
345              
346             Called without arguments this will return the current profile. If called with
347             a profilename it will load that new profile on top of whatever profile
348             currently exists. You can set the second argument to true if you want to
349             replace the current profile with the new one.
350              
351             =cut
352              
353             sub profile {
354 14     14 1 36 my $self = shift;
355 14         31 my $profile_arg = shift;
356 14         23 my $replace = shift;
357 14         27 my $stdin = $self->{'stdin'};
358 14         31 my $config = $self->{'config'};
359 14         23 my $session = $self->{'session'};
360 14         23 my $profile = $self->{'profile'};
361              
362 14 100       76 return $profile if (!defined($profile_arg));
363              
364 2 50       9 if ($replace) {
365             # wipe out the old profile if we're replacing it.
366 2         4 $profile = {};
367             }
368              
369 2         11 foreach(split(/\+/, $profile_arg)) {
370 2         20 $profile = merge($profile, $config->{profile}{$_});
371              
372             # load handlers for profile
373 2 50       70 if ($profile->{handlers}) {
374 0         0 foreach my $v (keys %{$profile->{handlers}}) {
  0         0  
375 0         0 $stdin->set_seq($v, $profile->{handlers}{$v}, [ \$self ]);
376             }
377             }
378             # load syntax highlight
379 2 50       7 if ($profile->{syntax}) {
380 0         0 App::Tel::Color::load_syntax($profile->{syntax},$self->{opts}->{d});
381             }
382 2         7 $profile->{profile_name}=$_;
383             }
384              
385             # add some sane defaults if the profile doesn't have them
386 2   66     11 $profile->{'user'} ||= $ENV{'USER'};
387 2         6 $self->{'profile'}=$profile;
388 2         8 return $profile;
389             }
390              
391             sub _stty_rows {
392 0     0   0 my $new_rows = shift;
393 0         0 eval {
394 0         0 Module::Load::load Term::ReadKey;
395 0         0 my ($columns, $rows, $xpix, $ypix) = GetTerminalSize(\*STDOUT);
396 0         0 SetTerminalSize($columns, $new_rows, $xpix, $ypix, \*STDOUT);
397             };
398              
399 0 0       0 warn $@ if ($@);
400             }
401              
402             =head2 password
403              
404             my $password = $self->password;
405              
406             This pulls the password from the config. If the password is blank it checks
407             to see if you have a password manager, then tries to load the password from
408             there. It then tries to use an OS keyring.
409              
410             By default it will pull the regular password. To pull the enable password
411             you can call it with $self->password('enable');
412              
413             =cut
414              
415             sub password {
416 2     2 1 7 my $self = shift;
417 2         6 my $type = shift;
418 2         16 my $profile = $self->profile;
419 2         8 my $router = $self->{hostname};
420              
421 2   100     34 $type ||= 'password';
422              
423 2 50 66     18 warn "Unknown password type $type" if ($type ne 'password' && $type ne 'enable');
424              
425 2 50 33     25 if (defined($profile->{$type}) && $profile->{$type} ne '') {
426 2         30 return $profile->{$type};
427             }
428              
429             # if enable is blank but password has something then use the same password
430             # for enable.
431 0 0 0     0 if ($type eq 'enable' and $profile->{$type} eq '' && $profile->{'password'} ne '') {
      0        
432 0         0 return $profile->{'password'};
433             }
434              
435             # if we get here, the password is blank so try other means
436 0         0 my $pass = App::Tel::Passwd::load_from_profile($profile);
437 0 0       0 if ($pass ne '') {
438 0         0 return $pass;
439             }
440              
441             # I was wondering how to decide what to prompt for, but I think it should
442             # be whichever profile loaded. So maybe we check for hostname and check
443             # for profile name and then save as profile name. If they want to be
444             # explicit they should specify the password format as KEYRING or
445             # something.. I dunno.
446 0         0 App::Tel::Passwd::keyring($profile->{user}, $profile->{profile_name}, $profile->{profile_name});
447              
448             # if they make it here and still don't have a password then none was
449             # defined anywhere and we probably should prompt for one. Consider
450             # turning off echo then normal read.
451 0         0 return App::Tel::Passwd::input_password($router);
452             }
453              
454             =head2 session
455              
456             my $session = $self->session;
457              
458             Expect won't let you reuse a connection that has spawned a process, so you
459             can call this with an argument to reset the session. If called without an
460             argument it will return the current session (If it exists) or create a new
461             session.
462              
463             =cut
464              
465             sub session {
466 2     2 1 5 my $self = shift;
467 2         39 my $renew = shift;
468              
469 2 100 66     33 return $self->{'session'} if (!$renew && defined($self->{'session'}));
470 1         2 my $session = $self->{'session'};
471              
472 1 50 33     6 $session->soft_close() if ($session && $session->pid());
473 1         6 $session = new Expect;
474              
475             # install sig handler for window size change
476 1         22774 $SIG{WINCH} = \&_winch_handler;
477              
478              
479 1         26 $session->log_stdout(1);
480 1         61 $self->{'session'} = $session;
481 1         6 return $session;
482             }
483              
484             =head2 connect
485              
486             my $session = $self->connect('routername');
487              
488             This sets up the session. If there already is a session open it closes and opens a new one.
489              
490             =cut
491              
492             sub connect {
493 1     1 1 1 my $self = shift;
494 1         3 my @arguments = shift;
495              
496 1         4 $self->connected(0);
497 1         7 my $session = $self->session(1);
498 1         9 $session->spawn(@arguments);
499 1         6495 return $session;
500             }
501              
502             =head2 connected
503              
504             if ($self->connected);
505             or
506             $self->connected(1);
507              
508             Returns connection status, or sets the status to whatever value is supplied to
509             the method.
510              
511             Note: This isn't the session state, but an indicator that our session has
512             gotten through the login stage and is now waiting for input. i.e., the router
513             is at a prompt of some kind.
514              
515             =cut
516              
517             sub connected {
518 6     6 1 63 my $self = shift;
519 6         13 my $status = shift;
520 6 100       32 if ($status) {
521 1         6 $self->{connected}=$status;
522             }
523              
524 6         48 return $self->{connected};
525             }
526              
527             =head2 enable
528              
529             my $enabled = $self->enable;
530              
531             if enablecmd is set then this method attempts to enable the user
532              
533             =cut
534              
535             sub enable {
536 1     1 1 5 my $self = shift;
537 1         5 my $profile = $self->profile;
538              
539 1 50       8 if ($profile->{enablecmd}) {
540 1         7 $self->send($profile->{enablecmd} . "\r");
541              
542 1   33     104 $profile->{ena_username_prompt} ||= qr/[Uu]ser[Nn]ame:|Login:/;
543 1   33     18 $profile->{ena_password_prompt} ||= qr/[Pp]ass[Ww]ord/;
544              
545             # we need to be able to handle routers that prompt for username and password
546             # need to check the results to see if enable succeeded
547             $self->expect($self->{timeout},
548 0     0   0 [ $profile->{ena_username_prompt} => sub { $self->send("$profile->{user}\r"); exp_continue; } ],
  0         0  
549 1     1   18 [ $profile->{ena_password_prompt} => sub { $self->send($self->password('enable') . "\r"); } ]
  1         671  
550             );
551             }
552              
553 1         190 $self->{enabled}=1;
554 1         8 return $self->{enabled};
555             }
556              
557             =head2 login
558              
559             my $something = $self->login("hostname");
560              
561             Cycles through the connection methods trying each in the order specified by
562             the profile until we successfully connect to the host. Returns connected
563             status (true or false).
564              
565             =cut
566              
567             sub login {
568 1     1 1 10 my $self = shift;
569 1         2 my $hostname = shift;
570              
571             # dumb stuff to alias $rtr to the contents of $self->{'profile'}
572             # needed because we reload the profile in the expect loop and can't update
573             # the alias.
574 1         2 our $rtr;
575 1         5 *rtr = \$self->{'profile'};
576              
577 1         3 my $ssho = '-o StrictHostKeyChecking=no';
578 1 50 33     12 if (defined($rtr->{sshoptions}) && scalar $rtr->{sshoptions} > 0) {
579 1         2 my @sshoptions = @{$rtr->{sshoptions}};
  1         5  
580 1         6 $ssho = '-o '. join(' -o ', @sshoptions);
581             }
582              
583 1 50       7 my $cipher = $rtr->{ciphertype} ? ('-c ' . $rtr->{ciphertype}) : '';
584              
585             # because we use last METHOD; in anonymous subs this suppresses the
586             # warning of "exiting subroutine via last;"
587 6     6   42 no warnings 'exiting';
  6         18  
  6         20720  
588             # handle MOTD profile loading, and other things parsed from the config
589 1         2 my @dynamic;
590 1 50       5 if (defined($rtr->{prompt})) {
591 0     0   0 push @dynamic, [ qr/$rtr->{prompt}/, sub { $self->connected(1); last METHOD; } ];
  0         0  
  0         0  
592             }
593              
594             # handle prompts in foreign languages or other things we didn't think of
595 1   33     20 $rtr->{username_prompt} ||= qr/[Uu]ser[Nn]ame:|[Ll]ogin:/;
596 1   33     16 $rtr->{password_prompt} ||= qr/[Pp]ass[Ww]ord/;
597              
598 1   33     9 $self->{port} ||= $self->{opts}->{p}; # get port from CLI
599 1   33     9 $self->{port} ||= $rtr->{port}; # or the profile
600             # if it's not set in the profile or CLI above, it gets set in the
601             # method below, but needs to be reset on each loop to change from
602             # telnet to ssh defaults
603              
604 1         2 METHOD: for (@{$self->methods}) {
  1         6  
605 1         3 my $allied_shit=0;
606              
607 1         3 my $p = $self->{port};
608              
609 1 50 0     7 if ($_ eq 'ssh') { $p ||= 22; $self->connect("ssh -p $p -l $rtr->{user} $ssho $cipher $hostname"); }
  0 50       0  
  0 50       0  
610 0   0     0 elsif ($_ eq 'telnet') { $p ||= 23; $self->connect("telnet $hostname $p"); }
  0         0  
611             # for testing. can pass an expect script to the other side and use it's output as our input.
612 1         4 elsif ($_ eq 'exec') { $self->connect($hostname); }
613 0         0 else { die "No program defined for method $_\n"; }
614              
615             # suppress stdout if needed
616 1         21 $self->{'session'}->log_stdout($self->{log_stdout});
617              
618             # need to make this optional
619             # also need to make it display whatever the user cares about.
620 1         125 print "\e[22t\033]0;$_ $hostname\007";
621 1         8 $self->{title_stack}++;
622 1     0   52 $SIG{INT} = sub { for (1..$self->{title_stack}) { print "\e[23t"; } $self->{title_stack}=0; };
  0         0  
  0         0  
  0         0  
623             $self->expect($self->{timeout},
624 1         16 @{$self->_banners},
625             @dynamic,
626             # fucking shitty allied telesyn
627 0     0   0 [ qr/User Access Verification - RADIUS/ => sub { $allied_shit=1;
628 0         0 $self->send("$rtr->{user}\r".$self->password()."\r"); exp_continue } ],
  0         0  
629             [ qr/User Access Verification - Local/ => sub {
630 0     0   0 $self->send("$rtr->{user}\r".$self->password()."\r"); $self->connected(1); last METHOD; } ],
  0         0  
  0         0  
631             [ $rtr->{username_prompt} => sub {
632 0 0   0   0 $self->send("$rtr->{user}\r") unless($allied_shit); exp_continue; } ],
  0         0  
633             [ $rtr->{password_prompt} => sub {
634 1 50   1   36345 if ($allied_shit) {
635 0         0 exp_continue;
636             } else {
637 1         13 $self->send($self->password() ."\r");
638 1         186 $self->connected(1);
639 1         23 last METHOD;
640             }
641             } ],
642             [ qr/Name or service not known|hostname nor servname provided, or not known|could not resolve / => sub
643             {
644             # if host lookup fails then check to see if there is an alternate method defined
645 0 0 0 0   0 if ($rtr->{hostsearch} && !$rtr->{hostsearched}) {
646 0         0 $hostname = &{$rtr->{hostsearch}}($hostname);
  0         0  
647 0         0 $rtr->{hostsearched}=1;
648 0         0 redo METHOD;
649             } else {
650 0         0 warn "unknown host: $hostname\n";
651             # skip to next host if this one doesn't exist
652 0         0 last METHOD;
653             }
654             }
655             ],
656 0     0   0 [ qr/Corrupted/ => sub { next METHOD; } ],
657             # almost never needed anymore. Some people might not want a
658             # fallback to des. If anyone does we need to make it optional
659             #[ qr/cipher type \S+ not supported/ => sub { $rtr->{ciphertype}="des"; redo METHOD; } ],
660 0     0   0 [ qr/ssh_exchange_identification/ => sub { next METHOD; } ],
661 0     0   0 [ qr/[Cc]onnection (refused|closed)/ => sub { next METHOD; } ],
662 0     0   0 [ qr/key_verify failed/ => sub { next METHOD; } ],
663 0     0   0 [ 'eof' => sub { next METHOD; } ],
664 1     0   12 [ 'timeout' => sub { next METHOD; } ],
  0         0  
665             );
666             }
667              
668 1   50     85 $rtr->{logoutcmd} ||= "logout";
669 1   50     16 $rtr->{prompt} ||= '#';
670              
671 1 50       15 warn "Connection to $hostname failed.\n" if !$self->connected;
672 1         21 return $self->connected;
673             }
674              
675             =head2 logging
676              
677             $self->logging('filename');
678              
679             Turns on logging for this session. If you specify a filename it will log to
680             /tmp/<filename>.log, otherwise it will use /tmp/<hostname>.log.
681              
682             =cut
683              
684             sub logging {
685 0     0 1   my $self = shift;
686 0           my $file = shift;
687 0   0       $file ||= $self->{hostname};
688 0 0         unlink ("/tmp/$file.log") if (-f "/tmp/$file.log");
689 0           $self->session->log_file("/tmp/$file.log");
690             }
691              
692             =head2 interact
693              
694             $self->interact($input, $escape);
695              
696             This is a copy of Expect's interact() command. It's been rewritten in parts
697             to customize it for our needs, but might be very similar otherwise. It's
698             mainly a setup script for the call to interconnect().
699              
700             =cut
701              
702             sub interact {
703 0     0 1   my $self = shift;
704 0           my $session = $self->{'session'};
705 0           my $in_object = shift;
706 0           my $escape_sequence = shift;
707              
708 0           my @old_group = $session->set_group();
709             # we know the input is STDIN and that it's an object.
710 0           my $out_object = Expect->exp_init(\*STDOUT);
711 0           $out_object->manual_stty(1);
712 0           $session->set_group($out_object);
713              
714 0           $in_object->set_group($session);
715 0 0         $in_object->set_seq($escape_sequence,undef) if defined($escape_sequence);
716             # interconnect normally sets stty -echo raw. Interact really sort
717             # of implies we don't do that by default. If anyone wanted to they could
718             # set it before calling interact, of use interconnect directly.
719 0           my $old_manual_stty_val = $session->manual_stty();
720 0           $session->manual_stty(1);
721             # I think this is right. Don't send stuff from in_obj to stdout by default.
722             # in theory whatever 'session' is should echo what's going on.
723 0           my $old_log_stdout_val = $session->log_stdout();
724 0           $session->log_stdout(0);
725 0           $in_object->log_stdout(0);
726             # Allow for the setting of an optional EOF escape function.
727             # $in_object->set_seq('EOF',undef);
728             # $session->set_seq('EOF',undef);
729 0           $self->interconnect($in_object);
730 0           $session->log_stdout($old_log_stdout_val);
731 0           $session->set_group(@old_group);
732             # If old_group was undef, make sure that occurs. This is a slight hack since
733             # it modifies the value directly.
734             # Normally an undef passed to set_group will return the current groups.
735             # It is possible that it may be of worth to make it possible to undef
736             # The current group without doing this.
737 0 0         unless (@old_group) {
738 0           @{${*$session}{exp_Listen_Group}} = ();
  0            
  0            
739             }
740 0           $session->manual_stty($old_manual_stty_val);
741             }
742              
743             =head2 interconnect
744              
745             $self->interconnect(@handles);
746              
747             This is a copy of Expect's interconnect() method that has been modified to
748             support the new things we needed. The main differences are colorize and our
749             winch handler.
750              
751             Future versions might support AnyEvent::IO or AnyEvent::Socket, but I might
752             contribute that back to Expect's core stuff. I might also try to figure out
753             how to make the colorize stuff more hookable so we could use Expect's methods
754             without rewriting them.
755              
756             =cut
757              
758             sub interconnect {
759 0     0 1   my $self = shift;
760 0           my @handles = ($self->{'session'}, $_[0]);
761              
762 0           my ( $nread );
763 0           my ( $rout, $emask, $eout );
764 0           my ( $escape_character_buffer );
765 0           my ( $read_mask, $temp_mask ) = ( '', '' );
766              
767             # Get read/write handles
768 0           foreach my $handle (@handles) {
769 0           $temp_mask = '';
770 0           vec( $temp_mask, $handle->fileno(), 1 ) = 1;
771 0           $read_mask = $read_mask | $temp_mask;
772             }
773 0 0         if ($Expect::Debug) {
774 0           print STDERR "Read handles:\r\n";
775 0           foreach my $handle (@handles) {
776 0           print STDERR "\tRead handle: ";
777 0           print STDERR "'${*$handle}{exp_Pty_Handle}'\r\n";
  0            
778 0           print STDERR "\t\tListen Handles:";
779 0           foreach my $write_handle ( @{ ${*$handle}{exp_Listen_Group} } ) {
  0            
  0            
780 0           print STDERR " '${*$write_handle}{exp_Pty_Handle}'";
  0            
781             }
782 0           print STDERR ".\r\n";
783             }
784             }
785              
786             # I think if we don't set raw/-echo here we may have trouble. We don't
787             # want a bunch of echoing crap making all the handles jabber at each other.
788 0           foreach my $handle (@handles) {
789 0 0         unless ( ${*$handle}{"exp_Manual_Stty"} ) {
  0            
790              
791             # This is probably O/S specific.
792 0           ${*$handle}{exp_Stored_Stty} = $handle->exp_stty('-g');
  0            
793 0           print STDERR "Setting tty for ${*$handle}{exp_Pty_Handle} to 'raw -echo'.\r\n"
794 0 0         if ${*$handle}{"exp_Debug"};
  0            
795 0           $handle->exp_stty("raw -echo");
796             }
797 0           foreach my $write_handle ( @{ ${*$handle}{exp_Listen_Group} } ) {
  0            
  0            
798 0 0         unless ( ${*$write_handle}{"exp_Manual_Stty"} ) {
  0            
799 0           ${*$write_handle}{exp_Stored_Stty} =
800 0           $write_handle->exp_stty('-g');
801 0           print STDERR "Setting ${*$write_handle}{exp_Pty_Handle} to 'raw -echo'.\r\n"
802 0 0         if ${*$handle}{"exp_Debug"};
  0            
803 0           $write_handle->exp_stty("raw -echo");
804             }
805             }
806             }
807              
808 0 0         print STDERR "Attempting interconnection\r\n" if $Expect::Debug;
809              
810             # Wait until the process dies or we get EOF
811             # In the case of !${*$handle}{exp_Pid} it means
812             # the handle was exp_inited instead of spawned.
813             CONNECT_LOOP:
814 0           while (1) {
815              
816             # test each handle to see if it's still alive.
817 0           foreach my $read_handle (@handles) {
818 0           waitpid( ${*$read_handle}{exp_Pid}, WNOHANG )
819 0           if ( exists( ${*$read_handle}{exp_Pid} )
820 0 0 0       and ${*$read_handle}{exp_Pid} );
  0            
821 0 0 0       if ( exists( ${*$read_handle}{exp_Pid} )
  0   0        
822 0           and ( ${*$read_handle}{exp_Pid} )
823 0           and ( !kill( 0, ${*$read_handle}{exp_Pid} ) ) )
824             {
825             print STDERR
826 0           "Got EOF (${*$read_handle}{exp_Pty_Handle} died) reading ${*$read_handle}{exp_Pty_Handle}\r\n"
  0            
827 0 0         if ${*$read_handle}{"exp_Debug"};
  0            
828             last CONNECT_LOOP
829 0 0         unless defined( ${ ${*$read_handle}{exp_Function} }{"EOF"} );
  0            
  0            
830             last CONNECT_LOOP
831 0           unless &{ ${ ${*$read_handle}{exp_Function} }{"EOF"} }
  0            
  0            
832 0 0         ( @{ ${ ${*$read_handle}{exp_Parameters} }{"EOF"} } );
  0            
  0            
  0            
833             }
834             }
835              
836 0           my $nfound = select( $rout = $read_mask, undef, $eout = $emask, undef );
837              
838             # Is there anything to share? May be -1 if interrupted by a signal...
839 0 0         $self->_winch() if $_winch_it;
840 0 0 0       next CONNECT_LOOP if not defined $nfound or $nfound < 1;
841              
842             # Which handles have stuff?
843 0           my @bits = split( //, unpack( 'b*', $rout ) );
844             #$eout = 0 unless defined($eout);
845             #my @ebits = split( //, unpack( 'b*', $eout ) );
846             # print "Ebits: $eout\r\n";
847 0           foreach my $read_handle (@handles) {
848 0 0         if ( $bits[ $read_handle->fileno() ] ) {
849             # it would be nice if we could say read until TELNET_GA or the
850             # equivilant, but that's not something we can be sure would be
851             # there. Cisco doesn't always fill the buffers just because
852             # they could, so there is a chance even though we tell them we
853             # can accept 10k they'll only send 1k and we split the middle
854             # of a regex. With escape sequences that isn't a big deal.
855             # With colorizing it causes problems because it's data already
856             # written to the screen so you can't take it back (without big
857             # work)
858             $nread = sysread(
859 0           $read_handle, ${*$read_handle}{exp_Pty_Buffer},
860 0           10240
861             );
862              
863             # don't bother trying to colorize input from the user
864 0 0         if ($read_handle->fileno() != $self->{stdin_fileno}) {
865 0           foreach my $color (@{$self->{colors}}) {
  0            
866 0           ${*$read_handle}{exp_Pty_Buffer} = $color->colorize(${*$read_handle}{exp_Pty_Buffer});
  0            
  0            
867             }
868             }
869             # Appease perl -w
870 0 0         $nread = 0 unless defined($nread);
871 0           print STDERR "interconnect: read $nread byte(s) from ${*$read_handle}{exp_Pty_Handle}.\r\n"
872 0 0         if ${*$read_handle}{"exp_Debug"} > 1;
  0            
873              
874             # Test for escape seq. before printing.
875             # Appease perl -w
876 0 0         $escape_character_buffer = ''
877             unless defined($escape_character_buffer);
878 0           $escape_character_buffer .= ${*$read_handle}{exp_Pty_Buffer};
  0            
879 0           foreach my $escape_sequence ( keys( %{ ${*$read_handle}{exp_Function} } ) ) {
  0            
  0            
880 0           print STDERR "Tested escape sequence $escape_sequence from ${*$read_handle}{exp_Pty_Handle}"
881 0 0         if ${*$read_handle}{"exp_Debug"} > 1;
  0            
882              
883             # Make sure it doesn't grow out of bounds.
884             $escape_character_buffer = $read_handle->_trim_length(
885             $escape_character_buffer,
886 0           ${*$read_handle}{"exp_Max_Accum"}
887 0 0         ) if ( ${*$read_handle}{"exp_Max_Accum"} );
  0            
888 0 0         if ( $escape_character_buffer =~ /($escape_sequence)/ ) {
889 0           my $match = $1;
890 0 0         if ( ${*$read_handle}{"exp_Debug"} ) {
  0            
891 0           print STDERR
892 0           "\r\ninterconnect got escape sequence from ${*$read_handle}{exp_Pty_Handle}.\r\n";
893              
894             # I'm going to make the esc. seq. pretty because it will
895             # probably contain unprintable characters.
896 0           print STDERR "\tEscape Sequence: '"
897             . _trim_length(
898             undef,
899             _make_readable($escape_sequence)
900             ) . "'\r\n";
901 0           print STDERR "\tMatched by string: '" . _trim_length( undef, _make_readable($match) ) . "'\r\n";
902             }
903              
904             # Print out stuff before the escape.
905             # Keep in mind that the sequence may have been split up
906             # over several reads.
907             # Let's get rid of it from this read. If part of it was
908             # in the last read there's not a lot we can do about it now.
909 0 0         if ( ${*$read_handle}{exp_Pty_Buffer} =~ /([\w\W]*)($escape_sequence)/ ) {
  0            
910 0           $read_handle->_print_handles($1);
911             } else {
912 0           $read_handle->_print_handles( ${*$read_handle}{exp_Pty_Buffer} );
  0            
913             }
914              
915             # Clear the buffer so no more matches can be made and it will
916             # only be printed one time.
917 0           ${*$read_handle}{exp_Pty_Buffer} = '';
  0            
918 0           $escape_character_buffer = '';
919              
920             # Do the function here. Must return non-zero to continue.
921             # More cool syntax. Maybe I should turn these in to objects.
922             last CONNECT_LOOP
923 0           unless &{ ${ ${*$read_handle}{exp_Function} }{$escape_sequence} }
  0            
  0            
924 0 0         ( @{ ${ ${*$read_handle}{exp_Parameters} }{$escape_sequence} } );
  0            
  0            
  0            
925             }
926             }
927 0 0         $nread = 0 unless defined($nread); # Appease perl -w?
928 0           waitpid( ${*$read_handle}{exp_Pid}, WNOHANG )
929 0           if ( defined( ${*$read_handle}{exp_Pid} )
930 0 0 0       && ${*$read_handle}{exp_Pid} );
  0            
931 0 0         if ( $nread == 0 ) {
932 0           print STDERR "Got EOF reading ${*$read_handle}{exp_Pty_Handle}\r\n"
933 0 0         if ${*$read_handle}{"exp_Debug"};
  0            
934             last CONNECT_LOOP
935 0 0         unless defined( ${ ${*$read_handle}{exp_Function} }{"EOF"} );
  0            
  0            
936             last CONNECT_LOOP
937 0           unless &{ ${ ${*$read_handle}{exp_Function} }{"EOF"} }
  0            
  0            
938 0 0         ( @{ ${ ${*$read_handle}{exp_Parameters} }{"EOF"} } );
  0            
  0            
  0            
939             }
940 0 0         last CONNECT_LOOP if ( $nread < 0 ); # This would be an error
941 0           $read_handle->_print_handles( ${*$read_handle}{exp_Pty_Buffer} );
  0            
942             }
943             }
944             }
945 0           foreach my $handle (@handles) {
946 0 0         unless ( ${*$handle}{"exp_Manual_Stty"} ) {
  0            
947 0           $handle->exp_stty( ${*$handle}{exp_Stored_Stty} );
  0            
948             }
949 0           foreach my $write_handle ( @{ ${*$handle}{exp_Listen_Group} } ) {
  0            
  0            
950 0 0         unless ( ${*$write_handle}{"exp_Manual_Stty"} ) {
  0            
951 0           $write_handle->exp_stty( ${*$write_handle}{exp_Stored_Stty} );
  0            
952             }
953             }
954             }
955              
956 0           return;
957             }
958              
959             =head2 run_commands
960              
961             $self->run_commands(@commands);
962              
963             TODO: Document this
964              
965              
966             =cut
967              
968             sub run_commands {
969 0     0 1   my $self = shift;
970 0           my $opts = $self->{opts};
971              
972 0           foreach my $arg (@_) {
973 0           $arg =~ s/\\r/\r/g; # fix for reload\ry. I believe 'perldoc quotemeta' explains why this happens
974 0           chomp($arg);
975 0           $self->send("$arg\r");
976 0           $self->expect($self->{timeout},'-re', $self->profile->{prompt});
977 0 0         sleep($opts->{s}) if ($opts->{s});
978             }
979             }
980              
981             =head2 control_loop
982              
983             $self->control_loop();
984              
985             This is where control should be passed once the session is logged in. This
986             handles CLI commands passed via the -c option, or scripts executed with the -x
987             option. It also handles autocommands passed via either option -a on the
988             command line, or via autocmds in the profile.
989              
990             Calling this without any commands will just run interact()
991              
992             =cut
993              
994             sub control_loop {
995 0     0 1   my $self = shift;
996 0           my $profile = $self->profile;
997 0           my $opts = $self->{opts};
998 0           my $prompt = $profile->{prompt};
999 0           my $pagercmd = $profile->{pagercmd};
1000 0           my $autocmds;
1001             my @args;
1002              
1003 0 0         if ($opts->{a}) {
1004 0           $autocmds = [ split(/;/, $opts->{a}) ];
1005             } else {
1006 0           $autocmds = $profile->{autocmds};
1007             }
1008              
1009 0           $self->_winch();
1010              
1011             # should -c override -x or be additive? or error if both are specified?
1012              
1013 0 0         @args = split(/;/, $opts->{c}) if ($opts->{c});
1014              
1015 0 0         if ($opts->{x}) {
1016 0           for (@{$opts->{x}}) {
  0            
1017 0 0         open(my $X, '<', $_) || die "Can't open file $_\n";
1018 0           push(@args,<$X>);
1019 0           close $X;
1020             }
1021             }
1022              
1023 0 0         if (@args) {
1024 0           $self->expect($self->{timeout},'-re',$prompt);
1025 0 0         if (ref($pagercmd) eq 'CODE') {
    0          
1026 0           $pagercmd->();
1027             } elsif ($pagercmd) {
1028 0           $self->run_commands("$pagercmd\r");
1029             }
1030 0           $self->run_commands(@args);
1031 0           $self->send($profile->{logoutcmd} ."\r");
1032             } else {
1033 0 0         die 'STDIN Not a tty' if (!POSIX::isatty($self->{stdin}));
1034 0 0         if ($autocmds) {
1035 0           $self->expect($self->{timeout},'-re',$prompt);
1036 0           $self->run_commands(@$autocmds);
1037             }
1038 0           $self->interact($self->{stdin}, '\cD');
1039             # q\b is to end anything that's at a More prompt or other dialog and
1040             # get you back to the command prompt
1041             # would be nice to detect if the session is closing down and not send
1042             # this. I've tried checking for session and session->pid but they
1043             # both still exist at this point so unless we wait for soft_close
1044             # we're kinda stuck doing this.
1045 0           $self->send("q\b" . $profile->{logoutcmd}. "\r");
1046             }
1047             }
1048              
1049             1;