Beefy Boxes and Bandwidth Generously Provided by pair Networks
more useful options

bcvi - run vi over a 'back-channel'

by grantm (Parson)
on Apr 23, 2007 at 10:19 UTC ( #611462=CUFP: print w/replies, xml ) Need Help??

Note the code shown here eventually developed into App-BCVI on CPAN and there's an introductory article about bcvi here

Here's an idea that I've developed to proof-of-concept stage. Now that I've got it working, I need to decide if it's useful - your input would be appreciated.

To recap from a previous node, I run a Linux desktop (GNOME) and my editor of choice is gvim. That node describes a way of opening files in gvim by pointing and clicking - even when the files live on another computer.

The problem is, I'm not really a point and click kind of guy. It's a very useful facility for infrequently repeated tasks, but for things I do all the time (like editing files) pointing and clicking is just slow and inefficient.

So what I want is to be able to type "vi filename" and have the file open in a gvim window back on my workstation - regardless of which server I happen to be ssh'd into when I type the command.

The most obvious solution is to use SSH X11-Forwarding but that requires installing gvim and X11 libraries on servers that don't really need such things and it's also rather painful to use over slow WAN links.

The solution I've come up with is to use SSH port forwarding to provide a 'back-channel' from a server to my workstation. To illustrate: if I happen to be logged into the server "neptune" and I type "vi somefile", then a message is sent over the back-channel and this command gets run on my workstation: "gvim scp://neptune//path/to/somefile"

There are a few pieces to the puzzle. For easy deployment, I've implemented most of them in a single script called 'bcvi' (for back-channel vi) which uses different command line options to access the different functions.

Here's how it works step-by-step:

  1. I log into GNOME on my workstation and a session startup command starts a listener process with this command:

    bcvi -l &

  2. I type "ssh neptune" and a shell alias translates that to: bcvi --wrap-ssh -- neptune. This in turn results in bcvi invoking ssh with the appropriate options to enable port forwarding for the back-channel and with some environment settings for the remote server.

  3. On the remote server, my .profile unpacks the environment settings (to identify the port that bcvi is listening on) and sets up a shell alias:

    alias vi="bcvi"
  4. Now when I type "vi somefile", bcvi works out the absolute pathname of the file, opens a TCP connection to the listener process on my workstation (via the SSH port-forward) and sends it a message indicating the host name and filename to be editted. The listener process translates that into an appropriate gvim command-line with an SCP URI for the filename and hey presto up pops the file in a gvim window.

That does all sound kind of complex but it's implemented using core Perl modules so I just have to copy one file to a server and add two lines to my .profile and it's deployed. Then I just ssh to a host, vi a file and the magic happens transparently.

At this stage, the back-channel uses a very simple protocol with no authentication. This means any other user on the remote server can send filenames to my listener process and spam my desktop with gvim windows. If this turns out to be a tool that other people find useful then I'd welcome input on making the whole thing a bit more secure.

Anyway, here's the code:

#!/usr/bin/perl ###################################################################### +######## # # Script: bcvi # # Author: Grant McLean <> # # Description: # # The 'Back-Channel vim' tool works with SSH to allow commands run on +an # SSH server to invoke processes back on the originating SSH client ma +chine. # use strict; use warnings; use Pod::Usage; use Getopt::Long qw(GetOptions); use File::Spec; use Sys::Hostname; my %opt = ( port => ($< * 10 + 9), command => 'vi', ); if(!GetOptions(\%opt, 'help|?', 'listener|l', 'port|p=s', 'command|c=s', 'unpack-term', 'wrap-ssh|s', )) { pod2usage(-exitval => 1, -verbose => 0); } pod2usage(-exitstatus => 0, -verbose => 2) if($opt{'help'}); if($opt{listener}) { my $listener = BCVI::Listener->new(%opt); $listener->loop(); } elsif($opt{'unpack-term'}) { unpack_term(); } elsif($opt{'wrap-ssh'}) { wrap_ssh(); } else { run_command(@ARGV); } exit 0; sub run_command { my @files = map { File::Spec->rel2abs($_) } @_; my $command = $opt{command}; my($alias, $gateway, $port) = connection_details(); my $sock = IO::Socket::INET->new(PeerAddr => "$gateway:$port") or die "Can't connect to '$gateway:$port': $!\n"; binmode($sock); my $welcome = <$sock>; if(!$welcome or $welcome !~ /^200 READY/) { die "No listener?\n"; } $sock->write(join qq{\x0A}, $alias, $command, @files, qq{\x0A}) or die "Error sending command through backchannel: $!"; } sub connection_details { my $env = $ENV{BCVI_CONF} || ''; my($alias, $gateway, $port) = $env =~ m{^([^:]+):(?:([^:]+):)?(\d+ +)$}; $alias ||= hostname(); $gateway ||= 'localhost'; $port ||= $opt{port}; return($alias, $gateway, $port); } sub unpack_term { my @parts = split /\x0D?\x0A/, $ENV{TERM} || ''; return unless @parts > 1; print "TERM=$parts[0]\n"; shift @parts; foreach (@parts) { print if s{^(\w+)=(.*)$}{export $1="$2"\n}; } } sub wrap_ssh { my(@args, @hosts, %need_arg); $need_arg{$_} = 1 foreach qw(b c D e F i L l m O o p R S); my @orig = @ARGV; while(@ARGV) { $_ = shift @ARGV; if(/^-(.)(.*)$/) { push @args, $_; push @args, shift @ARGV if $need_arg{$1} && !length($2) && + @ARGV; } else { push @args, $_; push @hosts, $_; } } if(@hosts == 1) { my($target) = @hosts; $target =~ s{^.*\@}{}; $ENV{TERM} = "$ENV{TERM}\nBCVI_CONF=${target}:localhost:$opt{p +ort}"; unshift @args, "-R $opt{port}:localhost:$opt{port}"; } else { @args = @orig; } system 'ssh', @args; } package BCVI::Listener; use IO::Socket::INET; use IO::Select; use Scalar::Util qw(refaddr); use File::Spec; use POSIX qw(:errno_h); sub new { my($class, %opt) = @_; _save_pid(); my $listener = IO::Socket::INET->new( LocalAddr => "localhost:$opt{port}", ReuseAddr => 1, Proto => 'tcp', Listen => 5, ) or die "Error creating listener for port 'localhost:$opt{port}': + $!"; return bless { _listener => $listener, _selector => IO::Select->new($listener), _options => \%opt, }, $class; } sub _save_pid { my $home = (getpwuid($<))[7]; my $pid_file = File::Spec->catfile($home, ''); if(-e $pid_file) { my $old_pid = do { local($/); open my $fh, $pid_file or die "open($pid_file): $ +!"; <$fh>; }; _kill_pid($old_pid) if $old_pid; } open my $fh, '>', $pid_file or die "open(>$pid_file): $!"; print $fh "$$\n"; } sub _kill_pid { my($pid) = @_; chomp($pid); foreach my $i (1..5) { if(kill 0, $pid) { kill($i > 2 ? 9 : 1, $pid); } elsif($! == ESRCH) { # no such process return; } elsif($! == EPERM) { # process belongs to another user return; } sleep 1; } } sub loop { my $self = shift; while(my @ready = $self->{_selector}->can_read) { foreach my $fh (@ready) { if($fh == $self->{_listener}) { $self->accept_connection(); } else { $self->handle_data($fh); } } } } sub accept_connection { my $self = shift; my $sock = $self->{_listener}->accept; binmode($sock); my $key = refaddr($sock); $self->{_selector}->add($sock); $self->{$key} = { fh => $sock, buf => '', }; $sock->write(qq{200 READY\x0A}); } sub handle_data { my($self, $sock) = @_; my $key = refaddr($sock); my $buf; my $bytes = sysread $sock, $buf, 4096; if($bytes) { $self->{$key}->{buf} .= $buf; if($self->{$key}->{buf} =~ m{^(.*?)\x0A\x0A\z}s) { $self->run_command($1); $self->{$key}->{buf} = ''; } } else { $self->{_selector}->remove($sock); $sock->close; delete $self->{$key}; } } sub run_command { my($self, $string) = @_; my($alias, $command, @files) = split /\x0A/, $string; my $method = "execute_${command}"; if(!$self->can($method)) { warn "Unsupported command '$command' - ignored\n"; return; } $self->$method($alias, @files); } sub execute_vi { my($self, $alias, @files) = @_; s{^}{scp://$alias/} foreach @files; system('gvim', '--', @files); } __END__ =head1 NAME bcvi - Back-channel vi, proxy commands back over ssh =head1 SYNOPSIS bcvi [options] [<files>] Options: -l|--listener start in listener mode -p|--port <port> listener port number -c|--command <cmnd> command to send over back-channel -s|--wrap-ssh pass all args after -- to ssh --unpack-term unpack the overloaded TERM variable -?|--help detailed help message =head1 DESCRIPTION This utility works with SSH to allow commands run on the SSH server to + be 'proxied' back to the SSH client machine. For example: =over 4 =item * user F<sue> establishes an SSH connection from her workstation to a se +rver named F<pluto> and runs the command C<bcvi .bashrc> =item * bcvi tunnels the details back to sue's workstation which then invokes +the command C<gvim scp://pluto//home/sue/.bashrc> =item * the result is that sue gets a responsive GUI editor running on her loc +al machine, but editing a file on the remote machine =back =head1 OPTIONS =over 4 =item B<--listener> (alias: -l) Start a (background) listener process. =item B<< --port <port> >> (alias: -p) Port number to listen on (default is user_id * 10 + 9). =item B<< --command <cmnd> >> (alias: -c) Use C<cmnd> as the command to send over the back-channel (default: vi) =item B<< --unpack-term >> This option is intended for use from a F<.profile> script. It outputs + a snippet to shell script to be passed to C<eval> in the calling shell. The C<bcvi> script overloads the TERM environment variable (which is poropagated by ssh) to 'smuggle' config data to the remote shell. =item B<-?> Display this documentation. =back =head1 USING BCVI You'll need to start a listener process on your workstation (perhaps f +rom your window manager session startup? bcvi -l & To ssh to a server with tunnelling enabled: bcvi --wrap-ssh -- hostname To enable bcvi on all ssh connections: alias ssh="bcvi --wrap-ssh --" On a target server, you'll need to unpack the overloaded TERM variable +: test -n "$(which bcvi)" && eval "$(bcvi --unpack-term)" To use vi over the back-channel: bcvi filename Perhaps via an alias: alias vi="bcvi" =head1 COPYRIGHT Copyright 2007 Grant McLean E<lt>grantm@cpan.orgE<gt> This library is free software; you can redistribute it and/or modify i +t under the same terms as Perl itself. =cut

Replies are listed 'Best First'.
Re: bcvi - run vi over a 'back-channel'
by jmason (Novice) on Jul 02, 2007 at 13:08 UTC
    very nice! here's a patch that adds two things:

    1. shared-secret authentication, using a setting in a config file in ~/.bcvi.rc

    2. reading the default port from that file. (I prefer to specify my own port on both ends and not "smuggle" it via $TERM).

    hope it helps ;)

Re: bcvi - run vi over a 'back-channel'
by Anonymous Monk on Jul 05, 2007 at 22:55 UTC
    The right way to get back-channels through ssh is through the agent forwarding. "Agent forwarding" is really just a magic TCP connection that follows you around and always goes back to your home host. This requires a bit more infrastructure (need to make a fake "agent" that proxies to a real agent or to your bespoke app, as appropriate), and ideally coming up with some way to let multiple "back channels" coordinate, but once you have that it's pretty slick.

    There's only one implementation of this idea I've seen in practice, though there are plenty of other potential uses: ssh-xfer.

    -- Nathaniel

      I'm not quite sure what you mean by "the right way" to get back channels. Are you suggesting that SSH has some sort of generic support for agent forwarding? I'm familiar with SSH's authentication agent forwarding (and in fact couldn't get by without it) but it looks to me to be 'baked in' to SSH and in particular the ssh daemon on the target host. I don't think I'd get very far trying to deploy bcvi by requiring a customised version of the SSH daemon on target hosts. Are you aware of some way to configure support for agents other than SSH's own authentication agent?

      Is there some functionality you think bcvi lacks that would be made possible by using an 'agent' implementation?

Re: bcvi - run vi over a 'back-channel'
by Anonymous Monk on Dec 31, 2007 at 04:09 UTC
    already done, TRAMP!

      At the risk of feeding a troll ...

      There's nothing in the linked page that in any way mimics the functionality of bcvi - namely allowing you to type 'vi filename' in a remote shell window and have a gvim session start on your workstation.

      The page seems to describe a plugin for Emacs (called TRAMP) that offers equivalent functionality to Vim's 'netrw' feature.

      It would certainly be possible to modify bcvi to work with Emacs+TRAMP rather than gvim+netrw.

        Hi, I was wondering : wheter if the netrw plugin for vi was mandatory. Because I can make it work for me. After installation I get a result connection refused. penkoad@sd-10516:~$ bcvi docs.txt Can't connect to 'localhost:10019': Connexion refusée Even though i issued (unsafely) an xhost + command to give client access to anybody. I'm puzzled. Is there a manipulation on the server more than installing bcvi script and adding the alias in the session ? regards

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: CUFP [id://611462]
Approved by andye
Front-paged by Arunbear
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others about the Monastery: (5)
As of 2021-06-24 15:52 GMT
Find Nodes?
    Voting Booth?
    What does the "s" stand for in "perls"? (Whence perls)

    Results (129 votes). Check out past polls.