使用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。
标签
反馈
这篇文章有什么问题吗?请通过在GitHub上打开一个问题或拉取请求来帮助我们。