Note: This is an excerpt from my blog post originally published on PerlTricks.com.

While SSH is a staple of remote system administration, sometimes only a GUI will do. Perhaps the remote system doesn’t have a terminal environment to connect to; perhaps the target application doesn’t present an adequate command line interface; perhaps there is an existing GUI session you need to interact with. There can be all kinds of reasons.

For this purpose, a generic type of remote desktop service called VNC is commonly used. The servers are easy to install, start on seemingly all platforms, and lots of hardware has a VNC server embedded for remote administration. Clients are similarly easy to use, but when building a management console in the web, wouldn’t it be nice to have the console view right in your browser?

Luckily, there is a pure JavaScript VNC client called noVNC.

noVNC listens for VNC traffic over WebSockets, which is convenient for browsers but isn’t supported by most VNC servers. To overcome this problem, they provide a command-line application called Websockify.

Websockify is a relay that connects to a TCP connection (the VNC server) and exposes the traffic as a WebSocket stream that a browser client can listen on. While this does fix the problem, it isn’t an elegant solution. Each VNC Server needs its own instance of Websockify requiring a separate port. Further, you either need to leave these connected at all times in case of a web client or else spawn them on demand and clean them up later.

Mojolicious to the Rescue

Mojolicious has a built-in event-based TCP Client and native WebSocket handling. If you are already serving your site with Mojolicious, why not let it do the TCP/WebSocket relay work, too? Even if you aren’t, the on-demand nature of the solution I’m going to show would be useful as a stand-alone app for this single purpose versus the websockify application.

Here is a Mojolicious::Lite application which serves the noVNC client when you request a url like /192.168.0.1. When the page loads, the client requests the WebSocket route at /proxy?target=192.168.0.1 which establishes the bridge. This example is bundled with my forthcoming wrapper module with a working name of Mojo::Websockify. The code is remarkably simple:

use Mojolicious::Lite;

use Mojo::IOLoop;

websocket '/proxy' => sub {
  my $c = shift;
  $c->render_later->on(finish => sub { warn 'websocket closing' });

  my $tx = $c->tx;
  $tx->with_protocols('binary');

  my $host = $c->param('target') || '127.0.0.1';
  my $port = $host =~ s{:(\d+)$}{} ? $1 : 5901;

  Mojo::IOLoop->client(address => $host, port => $port, sub {
    my ($loop, $err, $tcp) = @_;

    $tx->finish(4500, "TCP connection error: $err") if $err;
    $tcp->on(error => sub { $tx->finish(4500, "TCP error: $_[1]") });

    $tcp->on(read => sub {
      my ($tcp, $bytes) = @_;
      $tx->send({binary => $bytes});
    });

    $tx->on(binary => sub {
      my ($tx, $bytes) = @_;
      $tcp->write($bytes);
    });

    $tx->on(finish => sub {
      $tcp->close;
      undef $tcp;
      undef $tx;
    });
  });
};

get '/*target' => sub {
  my $c = shift;
  my $target = $c->stash('target');
  my $url = $c->url_for('proxy')->query(target => $target);
  $url->path->leading_slash(0); # novnc assumes no leading slash :(
  $c->render(
    vnc  =>
    base => $c->tx->req->url->to_abs,
    path => $url,
  );
};

app->start;

 

Read the rest on PerlTricks.com.