使用Mojolicious和noVNC获取浏览器内的远程桌面

虽然SSH是远程系统管理的基本工具,但有时只有图形用户界面才足够。可能远程系统没有终端环境可供连接;可能目标应用程序没有足够的命令行界面;可能需要与现有的GUI会话进行交互。原因可能有很多。

为此,通常使用一种称为VNC的通用远程桌面服务。服务器易于安装,似乎可以在所有平台上启动,很多硬件都内置了VNC服务器以便远程管理。客户端同样易于使用,但当你构建一个网络管理控制台时,是不是希望在浏览器中直接查看控制台视图呢?

幸运的是,有一个纯JavaScript VNC客户端,名为noVNC。noVNC监听通过WebSockets的VNC流量,这对浏览器来说很方便,但大多数VNC服务器不支持。为了解决这个问题,他们提供了一个名为Websockify的命令行应用程序。

Websockify是一个中继程序,它连接到TCP连接(VNC服务器),并将流量暴露为WebSocket流,这样浏览器客户端就可以监听。虽然这确实解决了问题,但这不是一种优雅的解决方案。每个VNC服务器都需要自己的Websockify实例,需要单独的端口。进一步来说,你要么需要一直保持这些连接,以防万一有Web客户端,要么按需启动它们,然后在之后清理。

Mojolicious来拯救

Mojolicious内置基于事件的TCP客户端和本地WebSocket处理。如果你已经用Mojolicious提供服务,为什么不让它也做TCP/WebSocket的中继工作呢?即使你不是,我所展示的按需解决方案作为独立应用程序,用于此单一目的,也比websockify应用程序更有用。

这里有一个Mojolicious::Lite应用程序,当你请求类似/192.168.0.1的URL时,它会提供noVNC客户端。当页面加载时,客户端请求位于/proxy?target=192.168.0.1的WebSocket路由,从而建立桥接。这个示例包含在我即将推出的包装模块中,该模块的临时名称为Mojo::Websockify。代码非常简单

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;

下面的get路由并不是非常令人兴奋。它是前端路由,用于渲染noVNC客户端,并告诉它WebSocket URL。

websocket路由更有趣,我将在下面详细解释。在切换控制器后,我们告诉服务器不要尝试渲染模板(render_later),然后订阅完成处理程序。这实际上是向服务器的一个提示,表明我们打算稍后发起WebSocket连接。通常这是通过订阅消息事件之一或连接时发送数据来完成的,但在这个案例中,我们不会这样做,直到TCP连接建立。然后从查询参数中提取目标主机和端口后,我们就可以建立TCP连接。

Mojo::IOLoop->client仅接受连接参数和连接后要执行的操作的回调。我们使用此回调来建立我们的中继。WebSocket协议将所有低于4000的关闭状态保留为内部使用,所以我习惯于使用标准的HTTP状态,并在前面加一个4。因此,在设置TCP错误处理时,无论是初始连接还是后续错误,传递给WebSocket finish方法的都是4500。

继电器本身是接下来的两个方法调用。首先,当TCP套接字触发一个read事件时,我们获取其原始字节并将它们(作为二进制消息)发送给WebSocket客户端。然后当WebSocket发送二进制帧(即接收到二进制消息时)我们将它写回到TCP连接。最后,当WebSocket关闭时,我们也关闭TCP连接并清理我们的处理程序。

简单,不是吗?!

附加说明

这里缺少一些东西。首先,我没有在这个例子中提到安全性。如果流的一部分是公开可用的,您将想要加密流量并在服务器后面设置认证。另一个风险是“反向压力”问题,其中流开始发送大量数据。

你可能注意到我跳过了一行,在Chrome的最近版本之前,这并不是必要的。当WebSocket连接首次建立时,它会调用with_protocols('binary')。noVNC的早期版本也支持将TCP流量作为base64编码的文本发送,因为WebSocket的早期实现并没有像现代版本那样区分文本和二进制帧类型。WebSocket协议允许客户端请求一个由应用程序定义的“子协议”,noVNC使用它来请求二进制或base64,后者已经被弃用并移除。客户端仍然请求二进制子协议,而Chrome的最近版本如果服务器没有表示它可以处理此请求,则会拒绝连接。

这不应该在CPAN上吗?

我希望将这个TCP/WebSocket桥接逻辑包装成一个名为Mojo::Websockify的模块,并将noVNC客户端作为示例。然而,这里简单展示的逻辑实际上很难以通用的、可扩展的方式打包。例如,您可能想要检查TCP服务是否已经被某个数据库锁定表使用,或者允许使用消息代理在客户端之间远程接管会话。我可能只是简化常见情况,并内置一些“反向压力”问题的保护。同时,我希望您喜欢看到Mojolicious的WebSocket和TCP服务是多么简单美丽。

快乐编程!


这篇文章最初发布在PerlTricks.com

标签

Joel Berger

Joel Berger是一位科学家和Perl程序员。他是Mojolicious网络框架的核心贡献者,经常撰写博客,可以在Twitter上找到。

浏览他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上打开一个问题或拉取请求来帮助我们。