File Coverage

bin/bcvi
Criterion Covered Total %
statement 38 662 5.7
branch 0 230 0.0
condition 0 75 0.0
subroutine 14 132 10.6
pod 0 45 0.0
total 52 1144 4.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             ##############################################################################
3             #
4             # Script: bcvi
5             #
6             # The 'Back-Channel vim' tool works with SSH to allow commands which are run
7             # on an SSH server to invoke processes back on the originating SSH client
8             # machine.
9             #
10             # Use 'bcvi --help' for the documentation
11             #
12             # Copyright (c) 2007-2012 Grant McLean
13             #
14              
15 1     1   864 use strict;
  1         2  
  1         91  
16 1     1   4 use warnings;
  1         2  
  1         66  
17              
18             require 5.008;
19              
20             ##############################################################################
21             # This package implements some common functionality required by both the
22             # client and the server.
23             #
24             # It also serves as the entry-point for the command-line script.
25             ##############################################################################
26              
27             package App::BCVI;
28              
29             our $VERSION = '3.09';
30              
31 1     1   14 use File::Spec;
  1         2  
  1         18  
32 1     1   4 use File::Path;
  1         1  
  1         80  
33 1     1   1283 use Getopt::Long qw();
  1         12307  
  1         42  
34 1     1   1207 use Encode qw(encode decode);
  1         11441  
  1         92  
35 1     1   868 use IO::Socket::INET;
  1         24425  
  1         8  
36              
37             my %class_map = (
38             base => 'App::BCVI',
39             client => 'App::BCVI::Client',
40             server => 'App::BCVI::Server',
41             pod => 'App::BCVI::POD',
42             );
43              
44             my %response_message = (
45             100 => "Ready ($App::BCVI::VERSION)",
46             200 => "Success",
47             300 => "Response follows",
48             900 => "Permission denied",
49             910 => "Unrecognised command",
50             );
51              
52             my $LF = "\x0A";
53              
54             my(
55             %options, %option_name, %commands, @aliases, @installables,
56             %plugin_loaded, @plugins,
57             );
58              
59             run(@ARGV) unless caller(); # Don't run anything if loaded via 'require'
60              
61             sub run {
62 0     0 0   App::BCVI->base_init();
63              
64 0           App::BCVI->load_plugins();
65              
66 0           App::BCVI->base_class()->process_command_line(@_);
67              
68 0           exit;
69             }
70              
71 0     0 0   sub version { return $App::BCVI::VERSION; }
72 0     0 0   sub base_class { return $class_map{base}; }
73 0     0 0   sub client_class { return $class_map{client}; }
74 0     0 0   sub server_class { return $class_map{server}; }
75 0     0 0   sub pod_class { return $class_map{pod}; }
76 0     0 0   sub map_class { $class_map{$_[1]} = $_[2]; }
77 0     0 0   sub sock { shift->{sock}; }
78              
79 0     0 0   sub installable_files { return @installables; }
80 0     0 0   sub message_from_code { $response_message{$_[1]}; }
81              
82             sub base_init {
83 0     0 0   my($class) = @_;
84              
85 0           $class->register_option(
86             name => 'help',
87             alias => '?',
88             dispatch_to => 'show_help',
89             summary => 'detailed help message',
90             description => <<'END_POD'
91             Display this documentation.
92             END_POD
93             );
94              
95 0           $class->register_option(
96             name => 'debug',
97             alias => 'd',
98             summary => 'turn on debugging',
99             description => <<'END_POD'
100             Turn on debugging messages.
101             END_POD
102             );
103              
104 0           $class->register_option(
105             name => 'add-aliases',
106             dispatch_to => 'add_aliases',
107             summary => 'install bcvi into shell startup files',
108             description => <<'END_POD'
109             Edit the bash startup script to add (or update) the required command aliases
110             for bcvi.
111             END_POD
112             );
113              
114 0           $class->register_option(
115             name => 'listener',
116             alias => 'l',
117             dispatch_to => 'start_listener',
118             summary => 'start in listener mode',
119             description => <<'END_POD'
120             Start a background listener process. Also generates a new authentication key.
121             END_POD
122             );
123              
124 0           $class->register_option(
125             name => 'install',
126             arg_name => '',
127             dispatch_to => 'install_to_hosts',
128             summary => 'copy bcvi to named hosts and install aliases',
129             description => <<'END_POD'
130             Copy the C script to the C<$HOME/bin> directory on the specified host
131             and then remotely invoke it with the C<--add-aliases> option.
132             END_POD
133             );
134              
135 0           $class->register_option(
136             name => 'unpack-term',
137             dispatch_to => 'unpack_term',
138             summary => 'unpack the overloaded TERM variable',
139             description => <<'END_POD'
140             This option is intended for use from a F<.profile> script. It outputs a
141             snippet of shell script to be passed to C in the calling shell.
142             END_POD
143             );
144              
145 0           $class->register_option(
146             name => 'wrap-ssh',
147             alias => 's',
148             dispatch_to => 'wrap_ssh',
149             summary => 'pass all args after -- to ssh',
150             description => <<'END_POD'
151             A wrapper around invoking ssh to connect to a specified host. Ensures the
152             environment is set up to pass the authentication key and other data to the
153             C client on the remote server.
154             END_POD
155             );
156              
157 0           $class->register_option(
158             name => 'version',
159             alias => 'v',
160             dispatch_to => 'show_versions',
161             summary => 'display bcvi version number',
162             description => <<'END_POD'
163             When invoking a command use this option to indicate that the arguments are not
164             filenames and the translation of relative pathnames to absolute should be
165             skipped.
166             END_POD
167             );
168              
169 0           $class->register_option(
170             name => 'no-path-xlate',
171             alias => 'n',
172             summary => 'skip translation of args from relative to absolute',
173             description => <<'END_POD'
174             Displays the version number of the bcvi client and if applicable, of the
175             listener process.
176             END_POD
177             );
178              
179 0           $class->register_option(
180             name => 'port',
181             alias => 'p',
182             arg_spec => '=i',
183             arg_name => '',
184             summary => 'port number for listener/port-forward',
185             description => <<'END_POD'
186             When used with C<--listener>, this is the port the server process will listen
187             on. When used with C<--wrap-ssh> this is the port number on the remote machine
188             that will be forwarded back to the listener process. The default in both cases
189             is calculated using the user's numeric UID multiplied by 10 and added to 9.
190             The intent is to reduce the chance of collisions with other bcvi users.
191             END_POD
192             );
193              
194 0           $class->register_option(
195             name => 'command',
196             alias => 'c',
197             arg_spec => '=s',
198             arg_name => '',
199             summary => 'command to send over back-channel',
200             description => <<'END_POD'
201             Use C as the command to send over the back-channel (default: vi).
202             Recognised commands are described in L below.
203             END_POD
204             );
205              
206 0           $class->register_option(
207             name => 'reuse-auth',
208             summary => "don't generate a new auth key on listener startup",
209             description => <<'END_POD'
210             A new (random) authorisation key is generated when the listener process is
211             started - this will invalidate the keys in use by existing SSH sessions.
212             This option is for use when it is necessary to restart the listener process
213             without invalidating client keys.
214             END_POD
215             );
216              
217 0           $class->register_option(
218             name => 'plugin-help',
219             arg_spec => '=s',
220             arg_name => '',
221             dispatch_to => 'plugin_help',
222             summary => "display documentation for ",
223             description => <<'END_POD'
224             The --help output includes a list of installed plugins. Use this option to
225             read the documentation for a named plugin.
226             END_POD
227             );
228              
229              
230 0           $class->register_command(
231             name => 'vi',
232             description => <<'END_POD'
233             Invokes C on the remote file - after translating the host+path to
234             an scp URI. This is the default command if no C<--command> option is
235             specified. If multiple filenames are supplied, the first will be opened
236             in gvim and you should use C<:n> to load the 'next' file.
237             END_POD
238             );
239              
240 0           $class->register_command(
241             name => 'viwait',
242             description => <<'END_POD'
243             This command works exactly the same as C above, except it waits for the
244             editor process to exit before bcvi exits on the remote machine. This is
245             primarily for use with C. Note: when used with C, the file
246             will not be updated on the remote machine until you exit the editor on your
247             workstation.
248             END_POD
249             );
250              
251 0           $class->register_command(
252             name => 'scpd',
253             description => <<'END_POD'
254             Uses C to copy the specified files or directories to the calling user's
255             F<~/Desktop>.`
256             END_POD
257             );
258              
259              
260 0           $class->add_home_bin();
261 0           $class->register_aliases(
262             'test -n "$(which bcvi)" && eval "$(bcvi --unpack-term)"',
263             'test -n "${BCVI_CONF}" && alias vi="bcvi"',
264             'test -n "${BCVI_CONF}" && alias suvi="EDITOR=\'bcvi -c viwait\' sudoedit"',
265             'test -n "${BCVI_CONF}" && alias bcp="bcvi -c scpd"',
266             );
267              
268 0           $class->pod_class->init();
269              
270             }
271              
272              
273             sub register_option {
274 0     0 0   my $class = shift;
275 0           my $opt = { @_ };
276 0           my $key = $opt->{name};
277              
278 0 0 0       if(!defined $key or !length $key) {
279 0           die "Can't register option without 'name'";
280             }
281              
282 0           my($package, $filename, $line) = caller();
283 0           $opt->{provider} = "$package at $filename line $line";
284 0           my $taken = $options{$key};
285 0 0 0       if($taken && !$opt->{force_override}) {
286 0           warn "option '--$key' already registered by $taken->{provider}\n";
287             }
288 0 0         if($opt->{alias}) {
289 0           foreach my $a (map { s/^-+//; $_ } split /\|/, $opt->{alias}) {
  0            
  0            
290 0 0 0       if($option_name{$a} && !$opt->{force_override}) {
291 0 0         if($taken = $options{$option_name{$a}}) {
292 0           warn "alias '$a' already registered for option "
293             . "'--$taken->{name}' by $taken->{provider}\n";
294             }
295             }
296 0           $option_name{$a} = $key;
297             }
298             }
299 0           $options{$key} = $opt;
300             }
301              
302              
303             sub register_command {
304 0     0 0   my $class = shift;
305 0           my $cmd = { @_ };
306 0           my $key = $cmd->{name};
307              
308 0 0 0       if(!defined $key or !length $key) {
309 0           die "Can't register command without 'name'";
310             }
311              
312 0   0       $cmd->{dispatch_to} ||= "execute_$key";
313              
314 0           my($package, $filename, $line) = caller();
315 0           $cmd->{provider} = "$package at $filename line $line";
316 0 0 0       warn "option '$key' already registered by $commands{$key}->{provider}\n"
317             if $commands{$key} && !$cmd->{force_override};
318 0           $commands{$key} = $cmd;
319             }
320              
321              
322             sub each_option {
323 0     0 0   my($class, $sub) = @_;
324              
325 0           $sub->($options{$_}) foreach sort keys %options;
326             }
327              
328              
329             sub each_command {
330 0     0 0   my($class, $sub) = @_;
331              
332 0           $sub->($commands{$_}) foreach sort keys %commands;
333             }
334              
335              
336             sub command_handler {
337 0     0 0   my($class, $name) = @_;
338              
339 0 0         return unless defined $name;
340 0 0         return "execute_commands_pod" if $name eq 'commands_pod';
341 0 0         my $spec = $commands{$name} or return;
342 0           return $spec->{dispatch_to};
343             }
344              
345              
346             sub add_home_bin {
347 0     0 0   my $class = shift;
348 0           my $home_bin = $class->home_directory . '/bin';
349 0           $class->register_aliases(
350             qq{echo \$PATH | grep -q $home_bin || PATH="$home_bin:\$PATH"},
351             );
352             }
353              
354              
355             sub register_aliases {
356 0     0 0   my $class = shift;
357 0           push @aliases, @_;
358             }
359              
360              
361             sub register_installable {
362 0     0 0   my $class = shift;
363 0           my($package, $filename, $line) = caller();
364 0           push @installables, $filename;
365             }
366              
367              
368             sub shell_aliases {
369 0     0 0   my($self) = @_;
370              
371             return
372 0           "## START-BCVI\n"
373 0           . join("\n", map { " $_" } @aliases)
374             . "\n## END-BCVI\n";
375             }
376              
377              
378             sub load_plugins {
379 0     0 0   my($class) = @_;
380              
381 0           my $dir = $class->conf_directory();
382 0           foreach my $file (sort glob("$dir/*.pm")) {
383 0           $class->load_plugin_file($file);
384             }
385             }
386              
387              
388             sub load_plugin_file {
389 0     0 0   my($class, $file) = @_;
390              
391 0           my @parts = File::Spec->splitdir($file);
392 0           my $key = pop @parts;
393 0 0         return if $plugin_loaded{$key};
394              
395 0           eval { require $file; };
  0            
396 0 0         if($@) {
397 0           die qq{Error loading plugin "$file"\n$@\n}
398             }
399              
400 0           $plugin_loaded{$key} = $file;
401             }
402              
403              
404             sub hook_client_class {
405 0     0 0   my($class) = @_;
406              
407 0           my($calling_class, $calling_file) = caller();
408 0           my $client_class = $class->client_class();
409 0           $class->map_class(client => $calling_class);
410              
411 1     1   2499 no strict 'refs';
  1         3  
  1         147  
412 0           unshift @{"${calling_class}::ISA"}, $client_class;
  0            
413 0           push @plugins, { class => $calling_class, file => $calling_file };
414 0           return 1;
415             }
416              
417              
418             sub hook_server_class {
419 0     0 0   my($class) = @_;
420              
421 0           my($calling_class, $calling_file) = caller();
422 0           my $server_class = $class->server_class();
423 0           $class->map_class(server => $calling_class);
424              
425 1     1   13 no strict 'refs';
  1         2  
  1         1740  
426 0           unshift @{"${calling_class}::ISA"}, $server_class;
  0            
427 0           push @plugins, { class => $calling_class, file => $calling_file };
428 0           return 1;
429             }
430              
431              
432             sub process_command_line {
433 0     0 0   my($class, @args) = @_;
434              
435 0           my $opt = $class->option_defaults();
436 0           my @cfg = $class->getopt_config();
437              
438 0           local(@ARGV) = @args;
439 0 0         Getopt::Long::GetOptions($opt, @cfg) or $class->die_synopsis();
440              
441 0 0         my $handler = $opt->{listener}
442             ? $class->server_class
443             : $class->client_class;
444              
445 0           $handler->new(_options => $opt)->dispatch(@ARGV);
446             }
447              
448              
449             sub option_defaults {
450 0     0 0   return { };
451             }
452              
453              
454             sub getopt_config {
455 0     0 0   my($class) = @_;
456              
457 0           my @spec;
458             $class->each_option(sub {
459 0     0     my($o) = @_;
460 0           my $def = $o->{name};
461 0 0         $def .= "|$o->{alias}" if defined $o->{alias};
462 0 0         $def .= $o->{arg_spec} if defined $o->{arg_spec};
463 0           push @spec, $def;
464 0           });
465             return @spec
466 0           }
467              
468              
469             sub die_synopsis {
470 0     0 0   my($class, $message) = @_;
471              
472 0 0         warn "$message\n" if $message;
473 0           $class->pod_class->synopsis();
474 0           exit(1);
475             }
476              
477              
478             sub new {
479 0     0 0   my $class = shift;
480              
481 0           return bless { @_ }, $class;
482             }
483              
484              
485             sub DEBUG {
486 0     0 0   my $self = shift;
487              
488 0 0         return unless $self->opt('debug');
489 0           warn "$_\n" foreach @_;
490             }
491              
492              
493             sub dispatch {
494 0     0 0   my($self, @args) = @_;
495              
496 0 0 0       if(my $method = $self->dispatch_option) {
    0          
497 0           $self->DEBUG("Dispatching to: $method");
498 0           $self->$method(@args);
499 0           exit;
500             }
501             elsif(!$self->opt('command') and !@args) {
502 0           $self->die_synopsis();
503             }
504              
505 0           $self->send_command(@args);
506             }
507              
508              
509             sub opt {
510 0     0 0   my($self, $key) = @_;
511              
512 0           return $self->{_options}->{$key};
513             }
514              
515              
516             sub set_opt {
517 0     0 0   my($self, $key, $value) = @_;
518              
519 0           return $self->{_options}->{$key} = $value;
520             }
521              
522              
523             sub dispatch_option {
524 0     0 0   my($self) = @_;
525              
526 0           my @set;
527             $self->each_option(sub {
528 0     0     my($o) = @_;
529 0 0 0       push @set, $o if $o->{dispatch_to} && defined $self->opt($o->{name});
530 0           });
531 0 0         return unless @set;
532 0 0         if(@set > 1) {
533 0           @set = map { "--$_->{name}" } @set;
  0            
534 0           my $last = pop @set;
535 0           $self->die_synopsis(
536             "Which did you want: " . join(', ', @set) . " or $last?"
537             );
538             }
539 0           return $set[0]->{dispatch_to};
540             }
541              
542              
543             sub default_port {
544 0     0 0   return( ($< * 10 + 9) % 65536 );
545             }
546              
547              
548             sub listen_address {
549 0     0 0   return 'localhost';
550             };
551              
552              
553             sub default_command {
554 0     0 0   return 'vi';
555             }
556              
557              
558             sub read_file {
559 0     0 0   my($self, $path) = @_;
560              
561 0 0         return unless -e $path;
562 0 0         return if -d $path;
563 0           my $data = do {
564 0 0         open my $fh, '<', $path or die "open($path): $!\n";
565 0           local($/) = undef;
566 0           <$fh>;
567             };
568 0           return $data;
569             }
570              
571              
572             sub home_directory {
573 0     0 0   return (getpwuid($>))[7];
574             }
575              
576              
577             sub conf_directory {
578 0     0 0   my($self) = @_;
579              
580 0           my $conf_dir = File::Spec->catdir($self->home_directory(), '.config', 'bcvi');
581 0 0         File::Path::mkpath($conf_dir) unless -d $conf_dir;
582 0           return $conf_dir;
583             }
584              
585              
586             sub auth_key_filename {
587 0     0 0   return File::Spec->catfile(shift->conf_directory(), 'listener_key');
588             }
589              
590              
591             sub listener_port_filename {
592 0     0 0   return File::Spec->catfile(shift->conf_directory(), 'listener_port');
593             }
594              
595              
596             sub get_listener_auth_key {
597 0     0 0   my($self) = @_;
598              
599 0           my $auth_file = $self->auth_key_filename();
600 0 0         my $auth_key = $self->read_file($auth_file)
601             or die "Auth key file does not exist: $auth_file";
602 0           chomp($auth_key);
603 0           return $auth_key;
604             }
605              
606              
607             sub list_plugins {
608 0     0 0   my($self) = @_;
609              
610 0           my @plugins;
611 0           foreach my $name (sort keys %plugin_loaded) {
612 0           my $path = $plugin_loaded{$name};
613 0           $name =~ s{[.]pm$}{};
614 0 0         if(my $title = $self->pod_class->extract_title($path)) {
615 0           push @plugins, "$name - $title";
616             }
617             else {
618 0           push @plugins, "$name - no documentation";
619             }
620             }
621 0           return @plugins;
622             }
623              
624              
625             sub plugin_help {
626 0     0 0   my($self) = @_;
627              
628 0           my $plugin = $self->opt('plugin-help');
629 0 0         if(my $path = $plugin_loaded{"${plugin}.pm"}) {
630 0           $self->pod_class->show_plugin_help($path);
631             }
632             else {
633 0           die "Can't find plugin: '$plugin'\n";
634             }
635             }
636              
637              
638             ##############################################################################
639             # The App::BCVI::Client class implements the command-line UI and the client
640             # side of the BCVI protocol. You can use inheritance to customise the
641             # behaviour of this class.
642             ##############################################################################
643              
644             package App::BCVI::Client;
645              
646 1     1   3471 BEGIN { @App::BCVI::Client::ISA = qw( App::BCVI ); }
647              
648              
649             sub get_connection_details {
650 0     0     my($self) = @_;
651              
652 0 0         if(not $ENV{BCVI_CONF}) {
653 0           die "The \$BCVI_CONF environment variable has not been set\n";
654             }
655 0           my($alias, $gateway, $port, $auth_key) = split(/:/, $ENV{BCVI_CONF});
656 0           $self->{host_alias} = $alias;
657 0           $self->{gateway_address} = $gateway;
658 0           $self->{port} = $port;
659 0           $self->{auth_key} = $auth_key;
660             }
661              
662              
663 0     0     sub host_alias { return shift->{host_alias}; }
664 0     0     sub gateway_address { return shift->{gateway_address}; }
665 0     0     sub port { return shift->{port}; }
666 0     0     sub auth_key { return shift->{auth_key}; }
667 0     0     sub server_version { return shift->{server_version}; }
668 0     0     sub response_code { return shift->{response_code}; }
669 0     0     sub response_message { return shift->{response_message}; }
670 0     0     sub response { return shift->{response}; }
671              
672              
673             sub send_command {
674 0     0     my($self, @files) = @_;
675              
676 0   0       my $command = $self->opt('command') || $self->default_command();
677              
678 0           my $sock = $self->connect_to_listener();
679              
680 0           my $request =
681             "Auth-Key: " . $self->auth_key . $LF .
682             "Host-Alias: " . $self->host_alias . $LF .
683             "Command: " . $command . $LF;
684 0           $self->DEBUG($request);
685 0 0         $sock->write($request)
686             or die "Error sending command through backchannel: $!";
687              
688 0           $self->send_body(\@files);
689 0 0         return if $self->check_response() =~ /^(?:200|300)$/;
690 0           die $self->response_message . "\n";
691             }
692              
693              
694             sub send_body {
695 0     0     my($self, $files) = @_;
696              
697 0           my $translate_paths = ! $self->opt('no-path-xlate');
698              
699 0 0         my $body = join '', map {
700 0           $_ = File::Spec->rel2abs($_) if $translate_paths;
701 0           "$_$LF";
702             } @$files;
703              
704 0 0         $self->sock->write(
705             'Content-Length: ' . length($body) . $LF .
706             $LF .
707             $body
708             ) or die "Error sending command through backchannel: $!";
709             }
710              
711              
712             sub check_response {
713 0     0     my($self) = @_;
714              
715 0 0         my $response = $self->sock->getline() or die "Server hung up\n";
716 0           chomp($response);
717 0           $self->DEBUG("Received response: $response");
718 0 0         if(my($code, $message) = $response =~ m{^(\d\d\d) (.*)$}) {
719 0           ($self->{response_code}, $self->{response_message}) = ($code, $message);
720 0 0         $self->read_response() if $code eq '300';
721 0           return $code;
722             }
723 0           die "Unexpected response: '$response'\n";
724             }
725              
726              
727             sub read_response {
728 0     0     my($self) = @_;
729              
730 0           my $sock = $self->sock();
731 0           my $resp = {};
732 0   0       while(my($line) = $sock->getline() || '') {
733 0           chomp($line);
734 0 0         last if $line eq '';
735 0           $line = Encode::decode('utf8', $line);
736 0 0         if(my($name, $value) = $line =~ m{^(\S+)\s*:\s*(.*)$}) {
737 0           $name =~ s/-/_/;
738 0           $resp->{lc($name)} = $value;
739             }
740             }
741 0           $self->{response} = $resp;
742              
743 0   0       my $bytes = $resp->{content_length} || return;;
744              
745 0           my $buf = '';
746 0           while(my $count = $sock->read($buf, $bytes, length($buf))) {
747 0           $bytes -= $count;
748 0 0         last if $bytes < 1;
749             }
750 0           $resp->{body} = $buf;
751             }
752              
753              
754             sub connect_to_listener {
755 0     0     my($self) = @_;
756              
757 0           $self->get_connection_details();
758 0           $self->DEBUG("Connecting to: " . $self->gateway_address . ':' . $self->port);
759              
760 0           my $peer = $self->gateway_address . ':' . $self->port;
761 0 0         my $sock = IO::Socket::INET->new(
762             PeerAddr => $peer,
763             ) or die "Can't connect to '$peer': $!\n";
764              
765 0           binmode($sock);
766 0 0         my $welcome = $sock->getline() or die "No listener?\n";
767 0           chomp($welcome);
768 0           $self->DEBUG("Server banner: $welcome");
769 0 0         if($welcome =~ /^100 Ready \(([^)]+)\)/) {
770 0           $self->{server_version} = $1;
771             }
772 0           $self->{sock} = $sock;
773             }
774              
775              
776             sub show_versions {
777 0     0     my($self) = @_;
778              
779 0           print "bcvi client: $App::BCVI::VERSION\n";
780              
781 0 0         if($ENV{BCVI_CONF}) {
782 0           $self->connect_to_listener();
783 0           print "bcvi server: " . $self->server_version . "\n";
784             }
785             }
786              
787              
788             sub show_help {
789 0     0     my($self) = @_;
790              
791 0           $self->pod_class->full_pod($self);
792             }
793              
794              
795             sub commands_pod {
796 0     0     my($self) = @_;
797              
798 0           eval {
799 0           $self->set_opt(command => 'commands_pod');
800 0           $self->send_command();
801             };
802 0 0         if($@) {
803 0           $@ = '';
804 0           return;
805             }
806 0           return $self->response->{body};
807             }
808              
809              
810             sub wrap_ssh {
811 0     0     my($self, @args_in) = @_;
812              
813 0 0         if(my($target, @args_out) = $self->parse_ssh_args(@args_in)) {
814 0           $self->execute_wrapped_ssh($target, @args_out);
815             }
816             else {
817 0           warn "bcvi: unable to extract hostname from ssh command line\n";
818 0           $self->execute_ssh(@args_in);
819             }
820             }
821              
822              
823             sub parse_ssh_args {
824 0     0     my($self, @args_in) = @_;
825              
826 0           my %need_arg = map { $_ => 1} split //, 'bcDeFiLlmOopRS';
  0            
827              
828 0           my(@args_out, @hosts, $user);
829 0           while(@args_in) {
830 0           $_ = shift @args_in;
831 0 0         if(/^-l(.*)$/) { # extract username if specified with -l
832 0 0         $user = $1 ? $1 : $args_in[0];
833             }
834 0 0         if(/^-(.)(.*)$/) {
835 0           push @args_out, $_;
836 0 0 0       push @args_out, shift @args_in
      0        
837             if $need_arg{$1} && !length($2) && @args_in;
838             }
839             else {
840 0           push @args_out, $_;
841 0           push @hosts, $_;
842             }
843             }
844 0 0         return unless @hosts == 1;
845 0           my($target) = @hosts;
846 0 0 0       if($user and $target !~ /@/) {
847 0           $target = $user . '@' . $target;
848             }
849 0           return($target, @args_out);
850             }
851              
852              
853             sub execute_wrapped_ssh {
854 0     0     my($self, $target, @args) = @_;
855              
856 0   0       my $remote_port = $self->opt('port') || $self->default_port();
857 0           my $local_port = $self->listener_port();
858 0           $ENV{TERM} = "$ENV{TERM}\n"
859             . "BCVI_CONF=${target}:localhost:$remote_port:"
860             . $self->get_listener_auth_key();
861 0           unshift @args, "-R $remote_port:localhost:$local_port";
862 0           $self->execute_ssh(@args);
863             }
864              
865              
866             sub execute_ssh {
867 0     0     my($self, @args) = @_;
868              
869 0           system 'ssh', @args;
870             }
871              
872              
873             sub listener_port {
874 0     0     my($self) = @_;
875              
876 0 0         my $port = $self->read_file($self->listener_port_filename())
877             or return $self->default_port();
878 0           chomp($port);
879 0           return $port;
880             }
881              
882              
883             sub unpack_term {
884 0     0     my($self) = @_;
885              
886 0   0       my @parts = split /\x0D?\x0A/, $ENV{TERM} || '';
887 0 0         return unless @parts > 1;
888 0           print "TERM=$parts[0]\n";
889 0           shift @parts;
890 0           foreach (@parts) {
891 0 0         print if s{^(\w+)=(.*)$}{export $1="$2"\n};
892             }
893             }
894              
895              
896             sub install_to_hosts {
897 0     0     my($self, @args) = @_;
898              
899 0 0         die "You must list one or more target hostnames\n" unless @args;
900              
901 0           my $fail_count = 0;
902 0           foreach my $host ( @args ) {
903 0 0         $self->install_to_host($host) or $fail_count++;
904             }
905 0 0         exit 1 if $fail_count;
906             }
907              
908              
909             sub install_to_host {
910 0     0     my($self, $host) = @_;
911              
912 0 0 0       $self->install_bin_directory($host)
      0        
      0        
913             && $self->install_bcvi_script($host)
914             && $self->install_plugins($host)
915             && $self->install_remote_aliases($host)
916             && return 1;
917 0           $self->report_install_failure($host);
918 0           return;
919             }
920              
921              
922             sub install_bin_directory {
923 0     0     my($self, $host) = @_;
924              
925 0 0         if(system("ssh $host test -d ./bin") != 0) {
926 0           print "Creating ~/bin directory on $host\n";
927 0 0         if( system("ssh $host mkdir ./bin") != 0 ) {
928 0           warn "** mkdir failed on $host";
929 0           return;
930             }
931             }
932 0           return 1;
933             }
934              
935              
936             sub install_bcvi_script {
937 0     0     my($self, $host) = @_;
938              
939 0           print "Copying bcvi to remote bin directory on $host\n";
940 0           my $output = `scp $0 $host:bin/bcvi 2>&1`;
941 0 0         if($? != 0) {
942 0           warn "** failed to copy bcvi to remote bin directory on $host\n"
943             . $output;
944 0           return;
945             }
946 0           return 1;
947             }
948              
949              
950             sub install_plugins {
951 0     0     my($self, $host) = @_;
952              
953 0 0         return 1 unless @installables;
954 0 0         if(system("ssh $host test -d ./.config/bcvi") != 0) {
955 0           print "Creating plugins directory on $host\n";
956 0 0         if( system("ssh $host mkdir -p ./.config/bcvi") != 0 ) {
957 0           warn "** mkdir of .config/bcvi failed on $host\n";
958 0           return;
959             }
960             }
961 0           print "Copying plugin files to $host\n";
962 0           my $output = `scp @installables $host:.config/bcvi 2>&1`;
963 0 0         if($? != 0) {
964 0           warn "** failed to copy bcvi to remote plugins directory on $host\n"
965             . $output;
966 0           return;
967             }
968 0           return 1;
969             }
970              
971              
972             sub install_remote_aliases {
973 0     0     my($self, $host) = @_;
974              
975 0 0         if( system("ssh $host bin/bcvi --add-aliases") != 0 ) {
976 0           warn "** failed to install aliases on $host\n";
977 0           return;
978             }
979 0           return 1;
980             }
981              
982              
983             sub report_install_failure {
984 0     0     my($self, $host) = @_;
985 0           warn "\n*** Installation of bcvi on host '$host' failed ***\n\n";
986             }
987              
988              
989             sub add_aliases {
990 0     0     my($self) = @_;
991              
992 0           my $bcvi_commands = $self->shell_aliases();
993              
994 0 0         $self->update_existing_aliases($bcvi_commands)
995             or $self->aliases_initial_install($bcvi_commands);
996             }
997              
998              
999             sub update_existing_aliases {
1000 0     0     my($self, $bcvi_commands) = @_;
1001              
1002 0           foreach my $file ( $self->candidate_rc_files() ) {
1003 0 0         my($script) = $self->read_file($file) or next;
1004 0 0         if(index($script, $bcvi_commands) > -1) {
1005 0           print "Found bcvi commands in $file\n";
1006 0           return 1;
1007             }
1008 0 0         if($script =~ s{^## START-BCVI.*^## END-BCVI\r?\n}{$bcvi_commands}sm) {
1009 0 0         open my $fh, '>', $file or die "open($file): $!";
1010 0           print $fh $script;
1011 0           close($fh);
1012 0           print "Updated bcvi commands in $file\n";
1013 0           return 1;
1014             }
1015 0 0         if($script =~ m{^[^#]*\bbcvi\b}m) {
1016 0           print "Adhoc bcvi commands found in $file\n"
1017             . "*** Manual update may be required. ***\n"
1018             . "*** Consider deleting commands and re-adding. ***\n";
1019 0           return 1;
1020             }
1021             }
1022 0           return; # No existing aliases found
1023             }
1024              
1025              
1026             sub aliases_initial_install {
1027 0     0     my($self, $bcvi_commands) = @_;
1028              
1029 0           my $target = $self->preferred_rc_file();
1030              
1031 0 0         open my $fh, '>>', $target or die "open(>>$target): $!";
1032 0           print $fh "\n$bcvi_commands\n";
1033 0           close($fh);
1034 0           print "Added bcvi commands to $target\n";
1035             }
1036              
1037              
1038             sub candidate_rc_files {
1039 0     0     my($self) = @_;
1040 0           my $home = $self->home_directory();
1041             return(
1042 0           "$home/.bashrc_local",
1043             "$home/.bashrc",
1044             "$home/.bash_profile_local",
1045             "$home/.bash_profile",
1046             "$home/.profile",
1047             "$home/.common-configs/bashrc",
1048             );
1049             }
1050              
1051              
1052             sub preferred_rc_file {
1053 0     0     my($self) = @_;
1054              
1055             # Add to .bashrc_local if it is referenced from .bashrc
1056              
1057 0           my $home = $self->home_directory();
1058 0           my $bashrc = "$home/.bashrc";
1059 0           my $bashrc_local = "$home/.bashrc_local";
1060              
1061 0   0       my $script = $self->read_file($bashrc) || '';
1062              
1063 0 0         return $script =~ m{/[.]bashrc_local\b}
1064             ? $bashrc_local
1065             : $bashrc;
1066             }
1067              
1068              
1069             ##############################################################################
1070             # The App::BCVI::Server class implements the server ('listener') side of the
1071             # BCVI protocol. You can use inheritance to customise the behaviour of this
1072             # class.
1073             ##############################################################################
1074              
1075             package App::BCVI::Server;
1076              
1077 1     1   49 BEGIN { @App::BCVI::Server::ISA = qw( App::BCVI ); }
1078              
1079 1     1   11 use Digest::MD5 qw(md5_hex);
  1         4  
  1         105  
1080 1     1   7 use Errno qw();
  1         1  
  1         2320  
1081              
1082              
1083             sub start_listener {
1084 0     0     my($self) = @_;
1085              
1086 0           $self->kill_current_listener();
1087 0           $self->save_pid();
1088 0           $self->generate_auth_key();
1089 0           $self->create_listener_socket();
1090 0           $self->accept_loop();
1091 0           die "bcvi listener accept loop terminated unexpectedly\n";
1092             }
1093              
1094              
1095 0     0     sub auth_key { shift->{auth_key}; }
1096 0     0     sub client_sock { shift->{client}; }
1097 0     0     sub request { shift->{request}; }
1098 0     0     sub calling_host { shift->{request}->{host_alias}; }
1099              
1100              
1101             sub kill_current_listener {
1102 0     0     my($self) = @_;
1103              
1104 0 0         my($pid) = $self->read_file($self->pid_file) or return;
1105 0           foreach my $i (1..5) {
1106 0 0         if(kill 0, $pid) {
    0          
    0          
1107 0 0         kill($i > 2 ? 9 : 1, $pid);
1108             }
1109             elsif($!{ESRCH}) { # no such process
1110 0           return;
1111             }
1112             elsif($!{EPERM}) { # pid file was probably stale
1113 0           return;
1114             }
1115 0           sleep 1;
1116             }
1117             }
1118              
1119              
1120             sub save_pid {
1121 0     0     my($self) = @_;
1122              
1123 0           my $pid_file = $self->pid_file;
1124 0 0         open my $fh, '>', $pid_file or die "open(>$pid_file): $!";
1125 0           print $fh "$$\n";
1126             }
1127              
1128              
1129             sub save_port {
1130 0     0     my($self, $port) = @_;
1131              
1132 0           my $port_file = $self->listener_port_filename;
1133 0 0         open my $fh, '>', $port_file or die "open(>$port_file): $!";
1134 0           print $fh "$port\n";
1135             }
1136              
1137              
1138             sub pid_file {
1139 0     0     return File::Spec->catfile(shift->conf_directory(), 'listener_pid');
1140             }
1141              
1142              
1143             sub generate_auth_key {
1144 0     0     my($self) = @_;
1145              
1146 0 0         if($self->opt('reuse-auth')) {
1147 0           $self->{auth_key} = $self->get_listener_auth_key();
1148 0           $self->DEBUG("Reusing auth key: $self->{auth_key}");
1149 0           return;
1150             }
1151              
1152 0           my $data = "$self" . $$ . time() . rand();
1153 0           $self->{auth_key} = md5_hex($data);
1154 0           $self->DEBUG("Generated new auth key: $self->{auth_key}");
1155              
1156 0           my $auth_file = $self->auth_key_filename();
1157 0 0         open my $fh, '>', $auth_file or die "open(>$auth_file): $!";
1158 0           print $fh $self->{auth_key}, "\n";
1159             }
1160              
1161              
1162             sub create_listener_socket {
1163 0     0     my($self) = @_;
1164              
1165 0   0       my $port = $self->opt('port') || $self->default_port();
1166 0           $self->save_port($port);
1167 0           my $local_addr = $self->listen_address . ':' . $port;
1168 0           $self->DEBUG("Starting listener on: $local_addr");
1169 0 0         $self->{sock} = IO::Socket::INET->new(
1170             LocalAddr => $local_addr,
1171             ReuseAddr => 1,
1172             Proto => 'tcp',
1173             Listen => 5,
1174             Blocking => 1,
1175             ) or die "Error creating listener for port '$local_addr': $!";
1176             }
1177              
1178              
1179             sub accept_loop {
1180 0     0     my($self) = @_;
1181              
1182 0           $SIG{CHLD} = 'IGNORE'; # let Perl reap the zombies
1183              
1184 0           my $sock = $self->sock();
1185 0           while(1) {
1186 0           my $new = $sock->accept();
1187 0 0         next if $!{EINTR};
1188 0           $self->DEBUG("Accepted connection");
1189 0 0         if(fork()) { # In parent
1190 0           close $new;
1191             }
1192             else { # In child
1193 0           close $sock;
1194 0           $self->{sock} = $new;
1195 0           $self->dispatch_request();
1196 0           exit(0);
1197             }
1198             }
1199             }
1200              
1201              
1202             sub dispatch_request {
1203 0     0     my($self) = @_;
1204              
1205 0           $self->send_response(100);
1206 0           my $req = $self->collect_headers();
1207 0 0         $self->DEBUG("Calling host: " . $self->calling_host) if $self->calling_host;
1208 0 0         $self->validate_auth_key($req->{auth_key})
1209             or $self->exit_response(900);
1210 0           $self->DEBUG("Received command: $req->{command}");
1211 0 0         my $method = $self->command_handler($req->{command})
1212             or $self->exit_response(910);
1213 0           $self->DEBUG("Dispatching to: $method");
1214 0           $self->$method();
1215 0           $self->send_response(200);
1216             }
1217              
1218              
1219             sub validate_auth_key {
1220 0     0     my($self, $key) = @_;
1221              
1222 0 0 0       return 1 if $key && $key eq $self->auth_key;
1223 0           my $alias = $self->calling_host();
1224 0 0         $self->DEBUG("Invalid Auth-Key in request from $alias") if $key;
1225 0           return;
1226             }
1227              
1228              
1229             sub send_response {
1230 0     0     my($self, $code) = @_;
1231              
1232 0   0       my $message = $self->message_from_code($code) || 'Invalid response code';
1233 0           $message = Encode::encode('utf8', $message);
1234 0           $self->DEBUG("Sending response: $code $message");
1235 0           $self->sock->write(qq{$code $message\x0A});
1236             }
1237              
1238              
1239             sub exit_response {
1240 0     0     my($self, $code) = @_;
1241              
1242 0           $self->send_response($code);
1243 0           exit(0);
1244             }
1245              
1246              
1247             sub collect_headers {
1248 0     0     my($self) = @_;
1249              
1250 0           my $sock = $self->sock();
1251 0           my $req = {};
1252 0   0       while(my($line) = $sock->getline() || '') {
1253 0           chomp($line);
1254 0 0         last if $line eq '';
1255 0           $line = Encode::decode('utf8', $line);
1256 0 0         if(my($name, $value) = $line =~ m{^(\S+)\s*:\s*(.*)$}) {
1257 0           $name =~ s/-/_/;
1258 0           $req->{lc($name)} = $value;
1259             }
1260             }
1261 0           $self->{request} = $req;
1262             }
1263              
1264              
1265             sub read_request_body {
1266 0     0     my($self) = @_;
1267              
1268 0           my $bytes = $self->request->{content_length};
1269 0           my $sock = $self->sock();
1270 0           my $buf = '';
1271 0           while(my $count = $sock->read($buf, $bytes, length($buf))) {
1272 0           $bytes -= $count;
1273 0 0         last if $bytes < 1;
1274             }
1275 0           return $buf;
1276             }
1277              
1278              
1279             sub get_filenames {
1280 0     0     my($self) = @_;
1281              
1282 0           my @files = split /\x0a/, Encode::decode('utf8', $self->read_request_body());
1283 0           $self->DEBUG("Filename: $_") foreach @files;
1284 0           return @files;
1285             }
1286              
1287              
1288             sub execute_commands_pod {
1289 0     0     my($self) = @_;
1290              
1291 0           $self->send_response(300);
1292 0           my $pod = $self->pod_class->command_detail();
1293 0           $pod = Encode::encode('utf8', $pod);
1294 0 0         $self->sock->write(
1295             'Content-Type: text/pod' . $LF .
1296             'Content-Length: ' . length($pod) . $LF .
1297             $LF .
1298             $pod
1299             ) or die "Error sending response body: $!";
1300             }
1301              
1302              
1303             sub execute_vi {
1304 0     0     my($self) = @_;
1305              
1306 0           my $alias = $self->calling_host();
1307 0           my @files = map { "scp://$alias/$_" } $self->get_filenames();
  0            
1308 0           system('gvim', '--', @files);
1309             }
1310              
1311              
1312             sub execute_viwait {
1313 0     0     my($self) = @_;
1314              
1315 0           my $alias = $self->calling_host();
1316 0           my @files = map { "scp://$alias/$_" } $self->get_filenames();
  0            
1317 0           system('gvim', '-f', '--', @files);
1318             }
1319              
1320              
1321             sub execute_scpd {
1322 0     0     my($self) = @_;
1323              
1324 0           my $dest = File::Spec->catdir($self->home_directory(), 'Desktop');
1325              
1326 0           my $alias = $self->calling_host();
1327 0           my @files = map { "$alias:$_" } $self->get_filenames();
  0            
1328              
1329 0           system('scp', '-q', '-r', '--', @files, $dest);
1330             }
1331              
1332              
1333             ##############################################################################
1334             # The App::BCVI::POD class implements POD extraction and formatting on
1335             # platforms where POD::Text is available.
1336             ##############################################################################
1337              
1338             package App::BCVI::POD;
1339              
1340 1     1   1368 use Pod::Text;
  1         41764  
  1         1741  
1341              
1342             my $pod_skeleton;
1343              
1344              
1345             sub init {
1346 0     0     my($class) = @_;
1347              
1348 0           $pod_skeleton = do {
1349 0           local($/) = undef;
1350 0           ;
1351             };
1352 0           close(DATA);
1353             }
1354              
1355              
1356             sub full_pod {
1357 0     0     my($class, $app) = @_;
1358              
1359 0           my $commands_pod;
1360 0 0 0       if($app && $app->can('commands_pod')) {
1361 0           $commands_pod = $app->commands_pod();
1362             }
1363 0   0       $commands_pod ||= $class->command_detail();
1364 0           my $plugins_pod = $class->plugins_pod($app);
1365              
1366 0           my $pager = $class->pager();
1367 0           my $pod = $pod_skeleton;
1368 0           $pod =~ s{^=for BCVI_OPTIONS_SUMMARY\s*\n}{ $class->option_summary()}me;
  0            
1369 0           $pod =~ s{^=for BCVI_OPTIONS\s*\n}{ $class->option_detail() }me;
  0            
1370 0           $pod =~ s{^=for BCVI_COMMANDS\s*\n}{ $commands_pod }me;
  0            
1371 0           $pod =~ s{^=for BCVI_PLUGINS\s*\n}{ $plugins_pod }me;
  0            
1372 0 0         if(Pod::Text->isa('Pod::Simple')) {
1373 0           my $parser = Pod::Text->new (sentence => 1, width => 78);
1374 0           $parser->output_fh($pager);
1375 0           $parser->parse_string_document($pod);
1376             }
1377             else {
1378 0 0         open my $pod_fh, '<', \$pod or die "open(\$pod): $!";
1379 0           my $parser = Pod::Text->new (sentence => 1, width => 78);
1380 0           $parser->parse_from_filehandle($pod_fh, $pager);
1381             }
1382             }
1383              
1384              
1385             sub synopsis {
1386 0     0     my($class) = @_;
1387              
1388 0           my $pod = $pod_skeleton;
1389 0           $pod =~ s{\A.*?^=head1 SYNOPSIS\s*?\n}{Usage:}ms;
1390 0           $pod =~ s{^=.*\z}{}ms;
1391 0           $pod .= $class->option_summary();
1392              
1393 0           print $pod;
1394             }
1395              
1396              
1397             sub option_summary {
1398 0     0     my($class) = @_;
1399              
1400 0           my $w = 12;
1401 0           my @lines;
1402             App::BCVI->base_class->each_option(sub {
1403 0     0     my($o) = @_;
1404 0           my $short = "--$o->{name}";
1405 0 0         $short .= "|-$o->{alias}" if $o->{alias};
1406 0 0         $short .= " $o->{arg_name}" if $o->{arg_name};
1407 0           push @lines, [ $short, $o->{summary} ];
1408 0 0         $w = length($short) if length($short) > $w;
1409 0           });
1410              
1411 0           return join('', map { sprintf(" %-*s %s\n", $w, @$_) } @lines) . "\n";
  0            
1412             }
1413              
1414              
1415             sub option_detail {
1416 0     0     my($class) = @_;
1417              
1418 0           my @lines = "=over 4\n";
1419             App::BCVI->base_class->each_option(sub {
1420 0     0     my($o) = @_;
1421 0           my $pod = "\n=item B<--$o->{name}>";
1422 0 0         $pod .= " (alias: -$o->{alias})" if $o->{alias};
1423 0 0         $pod .= " $o->{arg_name}" if $o->{arg_name};
1424 0           $pod .= "\n\n$o->{description}\n";
1425 0           push @lines, $pod;
1426 0           });
1427 0           push @lines, "\n=back\n\n";
1428              
1429 0           return join '', @lines;
1430             }
1431              
1432              
1433             sub command_detail {
1434 0     0     my($class) = @_;
1435              
1436 0           my @lines = "=over 4\n";
1437             App::BCVI->base_class->each_command(sub {
1438 0     0     my($c) = @_;
1439 0           my $pod = "\n=item B<$c->{name}>";
1440 0           $pod .= "\n\n$c->{description}\n";
1441 0           push @lines, $pod;
1442 0           });
1443 0           push @lines, "\n=back\n\n";
1444              
1445 0           return join '', @lines;
1446             }
1447              
1448              
1449             sub plugins_pod {
1450 0     0     my($class, $app) = @_;
1451              
1452 0 0         if(my @plugin_list = $app->list_plugins()) {
1453 0 0         my $s = @plugin_list == 1 ? '' : 's';
1454 0           return join("\n\n",
1455             "You have the following plugin$s installed:",
1456             @plugin_list,
1457             "Use C<< bcvi --plugin-help plugin-name >> for detailed info.",
1458             ) . "\n\n";
1459             }
1460              
1461 0           return "You have no plugins installed.\n\n";
1462             }
1463              
1464              
1465             sub extract_title {
1466 0     0     my($class, $path) = @_;
1467              
1468 0 0         open my $fh, '<', $path or return;
1469 0           my $name_section = 0;
1470 0           while(<$fh>) {
1471 0           chomp;
1472 0 0 0       if(/^=head1\s+NAME$/) {
    0          
1473 0           $name_section++;
1474             }
1475             elsif($name_section and /\S/) {
1476 0           s{^.+?\s-\s}{};
1477 0           return $_;
1478             }
1479             }
1480 0           return;
1481             }
1482              
1483              
1484             sub show_plugin_help {
1485 0     0     my($class, $path) = @_;
1486              
1487 0           my $pager = $class->pager();
1488 0 0         if(Pod::Text->isa('Pod::Simple')) {
1489 0           my $parser = Pod::Text->new (sentence => 1, width => 78);
1490 0           $parser->output_fh($pager);
1491 0           $parser->parse_file($path);
1492             }
1493             else {
1494 0 0         open my $pod_fh, '<', $path or die "open($path): $!";
1495 0           my $parser = Pod::Text->new (sentence => 1, width => 78);
1496 0           $parser->parse_from_filehandle($pod_fh, $pager);
1497             }
1498             }
1499              
1500              
1501             sub pager {
1502 0 0   0     my @commands = $ENV{PAGER} ? ( $ENV{PAGER} ) : qw(pager less more);
1503 0           foreach my $file (@commands) {
1504 0           foreach my $dir ( File::Spec->path() ) {
1505 0           my $exe_path = File::Spec->catfile($dir, $file);
1506 0 0         if(-x $exe_path) {
1507 0 0         open my $fh, '|-', $exe_path or next;
1508 0           return $fh;
1509             }
1510             }
1511             }
1512 0           return \*STDOUT;
1513             }
1514              
1515              
1516             1;
1517              
1518             __DATA__