0

0

使用node.js如何实现多用户web终端显示

亚连

亚连

发布时间:2018-06-23 16:11:47

|

1516人浏览过

|

来源于php中文网

原创

这篇文章主要介绍了node.js支持多用户web终端实现方案以及web终端安全性保证的解决方法,一起学习参考下。

terminal(命令行)作为本地IDE普遍拥有的功能,对项目的git操作以及文件操作有着非常强大的支持。对于WebIDE,在没有web伪终端的情况下,仅仅提供封装的命令行接口是完全不能满足开发者使用,因此为了更好的用户体验,web伪终端的开发也就提上日程。

调研

终端,在我们认知范围内略同于命令行工具,通俗点说就是可以执行shell的进程。每次在命令行中输入一串命令,敲入回车,终端进程都会fork一个子进程,用来执行输入的命令,终端进程通过系统调用wait4()监听子进程退出,同时通过暴露的stdout输出子进程执行信息。

如果在web端实现一个类似于本地化的终端功能,需要做的可能会更多:网络时延及可靠性保证、shell用户体验尽量接近本地化、web终端UI宽高与输出信息适配、安全准入控制与权限管理等。在具体实现web终端之前,需要评估这些功能那些是最核心的,很明确:shell的功能实现及用户体验、安全性(web终端是在线上服务器中提供的一个功能,因此安全性是必须要保证的)。只有在保证这两个功能的前提下,web伪终端才可以正式上线。

下面首先针对这两个功能考虑下技术实现(服务端技术采用nodejs):

node原生模块提供了repl模块,它可以用来实现交互式输入并执行输出,同时提供tab补全功能,自定义输出样式等功能,可是它只能执行node相关命令,因此无法达到我们想要执行系统shell的目的 node原生模块child_porcess,它提供了spawn这种封装了底层libuv的uv_spawn函数,底层执行系统调用fork和execvp,执行shell命令。但是它未提供伪终端的其它特点,如tab自动补全、方向键显示历史命令等操作

因此,服务端采用node的原生模块是无法实现一个伪终端的,需要继续探索伪终端的原理和node端的实现方向。

伪终端

伪终端不是真正的终端,而是内核提供的一个“服务”。终端服务通常包括三层:

最顶层提供给字符设备的输入输出接口中间层的线路规程(line discipline)底层的硬件驱动

其中,最顶层的接口往往通过系统调用函数实现,如(read,write);而底层的硬件驱动程序则负责伪终端的主从设备通信,它由内核提供;线路规程看起来则比较抽象,但是实际上从功能上说它负责输入输出信息的“加工”,如处理输入过程中的中断字符(ctrl + c)以及一些回退字符(backspace 和 delete)等,同时转换输出的换行符n为rn等。

一个伪终端分为两部分:主设备和从设备,他们底层通过实现默认线路规程的双向管道连接(硬件驱动)。伪终端主设备的任何输入都会反映到从设备上,反之亦然。从设备的输出信息也通过管道发送给主设备,这样可以在伪终端的从设备中执行shell,完成终端的功能。

伪终端的从设备中,可以真实的模拟终端的tab补全和其他的shell特殊命令,因此在node原生模块不能满足需求的前提下,我们需要把目光放到底层,看看OS提供了什么功能。目前,glibc库提供了posix_openpt接口,不过流程有些繁琐:

使用posix_openpt打开一个伪终端主设备 grantpt设置从设备的权限 unlockpt解锁对应的从设备获取从设备名称(类似 /dev/pts/123)主(从)设备读写,执行操作

因此出现了封装更好的pty库,仅仅通过一个forkpty函数便可以实现上述所有功能。通过编写一个node的c++扩展模块,搭配pty库实现一个在伪终端从设备执行命令行的terminal。

关于伪终端安全性的问题,我们在文章的最后在进行讨论。

伪终端实现思路

根据伪终端的主从设备的特性,我们在主设备所在的父进程中管理伪终端的生命周期及其资源,在从设备所在的子进程中执行shell,执行过程中的信息及结果通过双向管道传输给主设备,由主设备所在的进程向外提供stdout。

在此处借鉴pty.js的实现思路:

pid_t pid = pty_forkpty(&master, name, NULL, &winp);

 switch (pid) {
 case -1:
  return Nan::ThrowError("forkpty(3) failed.");
 case 0:
  if (strlen(cwd)) chdir(cwd);

  if (uid != -1 && gid != -1) {
  if (setgid(gid) == -1) {
   perror("setgid(2) failed.");
   _exit(1);
  }
  if (setuid(uid) == -1) {
   perror("setuid(2) failed.");
   _exit(1);
  }
  }

  pty_execvpe(argv[0], argv, env);

  perror("execvp(3) failed.");
  _exit(1);
 default:
  if (pty_nonblock(master) == -1) {
  return Nan::ThrowError("Could not set master fd to nonblocking.");
  }

  Local<Object> obj = Nan::New<Object>();
  Nan::Set(obj,
  Nan::New<String>("fd").ToLocalChecked(),
  Nan::New<Number>(master));
  Nan::Set(obj,
  Nan::New<String>("pid").ToLocalChecked(),
  Nan::New<Number>(pid));
  Nan::Set(obj,
  Nan::New<String>("pty").ToLocalChecked(),
  Nan::New<String>(name).ToLocalChecked());

  pty_baton *baton = new pty_baton();
  baton->exit_code = 0;
  baton->signal_code = 0;
  baton->cb.Reset(Local<Function>::Cast(info[8]));
  baton->pid = pid;
  baton->async.data = baton;

  uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid);

  uv_thread_create(&baton->tid, pty_waitpid, static_cast<void*>(baton));

  return info.GetReturnValue().Set(obj);
 }

首先通过pty_forkpty(forkpty的posix实现,兼容 sunOS和 unix等系统)创建主从设备,然后在子进程中设置权限之后(setuid、setgid),执行系统调用pty_execvpe(execvpe的封装),此后主设备的输入信息都会在此得到执行(子进程执行的文件为sh,会侦听stdin);

父进程则向node层暴露相关对象,如主设备的fd(通过该fd可以创建net.Socket对象进行数据双向传输),同时注册libuv的消息队列&baton->async,当子进程退出时触发&baton->async消息,执行pty_after_waitpid函数;

最后父进程通过调用uv_thread_create创建一个子进程,用于侦听上一个子进程的退出消息(通过执行系统调用wait4,阻塞侦听特定pid的进程,退出信息存放在第三个参数中),pty_waitpid函数封装了wait4函数,同时在函数末尾执行uv_async_send(&baton->async)触发消息。

在底层实现pty模型后,在node层需要做一些stdio的操作。由于伪终端主设备是在父进程中执行系统调用的创建的,而且主设备的文件描述符通过fd暴露给node层,那么伪终端的输入输出也就通过读写根据fd创建对应的文件类型如PIPE、FILE来完成。其实,在OS层面就是把伪终端主设备看为一个PIPE,双向通信。在node层通过net.Socket(fd)创建一个套接字实现数据流的双向IO,伪终端的从设备也有着主设备相同的输入,从而在子进程中执行对应的命令,子进程的输出也会通PIPE反应在主设备中,进而触发node层Socket对象的data事件。

此处关于父进程、主设备、子进程、从设备的输入输出描述有些让人迷惑,在此解释。父进程与主设备的关系是:父进程通过系统调用创建主设备(可看做是一个PIPE),并获取主设备的fd。父进程通过创建该fd的connect socket实现向子进程(从设备)的输入输出。 而子进程通过forkpty 创建后执行login_tty操作,重置了子进程的stdin、stderr和stderr,全部复制为从设备的fd(PIPE的另一端)。因此子进程输入输出都是与从设备的fd相关联的,子进程输出数据走的是PIPE,并从PIPE中读入父进程的命令。详情请看参考文献之forkpty实现

情感家园企业站5.0 多语言多风格版
情感家园企业站5.0 多语言多风格版

一套面向小企业用户的企业网站程序!功能简单,操作简单。实现了小企业网站的很多实用的功能,如文章新闻模块、图片展示、产品列表以及小型的下载功能,还同时增加了邮件订阅等相应模块。公告,友情链接等这些通用功能本程序也同样都集成了!同时本程序引入了模块功能,只要在系统默认模板上创建模块,可以在任何一个语言环境(或任意风格)的适当位置进行使用!

下载

另外,pty库提供了伪终端的大小设置,因此我们通过参数可以调整伪终端输出信息的布局信息,因此这也提供了在web端调整命令行宽高的功能,只需在pty层设置伪终端窗口大小即可,该窗口是以字符为单位。

web终端安全性保证

基于glibc提供的pty库实现伪终端后台,是没有任何安全性保证的。我们想通过web终端直接操作服务端的某个目录,但是通过伪终端后台可以直接获取root权限,这对服务而言是不可容忍的,因为它直接影响着服务器的安全,所有需要实现一个:可多用户同时在线、可配置每个用户访问权限、可访问特定目录的、可选择配置bash命令、用户间相互隔离、用户无感知当前环境且环境简单易部署的“系统”。

最为适合的技术选型是docker,作为一种内核层面的隔离,它可以充分利用硬件资源,且十分方便映射宿主机的相关文件。但是docker并不是万能的,如果程序运行在docker容器中,那么为每个用户再分配一个容器就会变得复杂得多,而且不受运维人员掌控,这就是所谓的DooD(docker out of docker)-- 通过volume “/usr/local/bin/docker”等二进制文件,使用宿主机的docker命令,开启兄弟镜像运行构建服务。而采用业界经常讨论的docker-in-docker模式会存在诸多缺点,特别是文件系统层面的,这在参考文献中可以找到。因此,docker技术并不适合已经运行在容器中的服务解决用户访问安全问题。

接下来需要考虑单机上的解决方案。目前笔者只想到两种方案:

命令ACL,通过命令白名单的方式实现 restricted bash chroot,针对每个用户创建一个系统用户,监禁用户访问范围

首先,命令白名单的方式是最应该排除的,首先无法保证不同release的linux的bash是相同的;其次无法有效穷举所有的命令;最后由于伪终端提供的tab命令补全功能以及特殊字符如delete的存在,无法有效匹配当前输入的命令。因此白名单方式漏洞太多,放弃。

restricted bash,通过/bin/bash -r触发,可以限制使用者显式“cd directory”,但有这诸多缺点:

不足以允许执行完全不受信任的软件。当一个被发现是shell脚本的命令被执行时,rbash会关闭在shell中生成的任何限制来执行脚本。当用户从rbash运行bash或dash,那么他们获得了无限制的shell。有很多方法来打破受限制的bash shell,这是不容易预测的。

最后,貌似只有一个解决方案了,即chroot。chroot修改了用户的根目录,在制定的根目录下运行指令。在指定根目录下无法跳出该目录,因此无法访问原系统的所有目录;同时chroot会创建一个与原系统隔离的系统目录结构,因此原系统的各种命令无法在“新系统”中使用,因为它是全新的、空的;最后,多个用户使用时他们是隔离的、透明的,完全满足我们的需求。

因此,我们最终选择chroot作为web终端的安全性解决方案。但是,使用chroot需要做非常多的额外处理,不仅包括新用户的创建,还包括命令的初始化。上文也提到“新系统”是空的,所有可执行二进制文件都没有,如“ls,pmd”等,因此初始化“新系统”是必须的。可是许多二进制文件不仅仅静态链接了许多库,还在运行时依赖动态链接库(dll),为此还需要找到每个命令依赖的诸多dll,异常繁琐。为了帮助使用者从这种无趣的过程中解脱出来,jailkit应运而生。

jailkit,真好用

jailkit,顾名思义用来监禁用户。jailkit内部使用chroot实现创建用户根目录,同时提供了一系列指令来初始化、拷贝二进制文件及其所有的dll,而这些功能都可以通过配置文件进行操作。因此,在实际开发中采用jailkit搭配初始化shell脚本来实现文件系统隔离。

此处的初始化shell指的是预处理脚本,由于chroot需要针对每个用户设置根目录,因此在shell中为每个开通命令行权限的使用者创建对应的user,并通过jailkit配置文件拷贝基本的二进制文件及其dll,如基本的shell指令、git、vim、ruby等;最后再针对某些命令做额外的处理,以及权限重置。

在处理“新系统”与原系统的文件映射过程中,还是需要一些技巧。笔者曾经将chroot设定的用户根目录之外的其他目录通过软链接的形式建立映射,可是在jail监狱中访问软链接时仍会报错,找不到该文件,这还是由于chroot的特性导致的,没有权限访问根目录之外的文件系统;如果通过硬链接建立映射,则针对chroot设定的用户根目录中的硬链接文件做修改是可以的,但是涉及到删除、创建等操作是无法正确映射到原系统的目录的,而且硬链接无法连接目录,因此硬链接不满足需求;最后通过mount --bind实现,如 mount --bind /home/ttt/abc /usr/local/abc它通过屏蔽被挂载的目录(/usr/local/abc)的目录信息(block),并在内存中维护被挂载目录与挂载目录的映射关系,对/usr/local/abc的访问都会通过传内存的映射表查询/home/ttt/abc的block,然后进行操作,实现目录的映射。

最后,初始化“新系统”完毕后,就需要通过伪终端执行jail相关命令:

sudo jk_chrootlaunch -j /usr/local/jailuser/${creater} -u ${creater} -x /bin/bashr

开启bash程序之后便通过PIPE与主设备接收到的web终端输入(通过websocket)进行通信即可。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

使用React.setState有哪些需要注意的地方

在vue中如何将页面公用的头部组件化(详细教程)

在JS中有关函数节流和函数防抖(详细教程)

使用three.js如何实现3D影院

在Vue中如何实现侧滑菜单组件

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

16

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

23

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

75

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

95

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

218

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

420

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

168

2026.03.04

Swift iOS架构设计与MVVM模式实战
Swift iOS架构设计与MVVM模式实战

本专题聚焦 Swift 在 iOS 应用架构设计中的实践,系统讲解 MVVM 模式的核心思想、数据绑定机制、模块拆分策略以及组件化开发方法。内容涵盖网络层封装、状态管理、依赖注入与性能优化技巧。通过完整项目案例,帮助开发者构建结构清晰、可维护性强的 iOS 应用架构体系。

222

2026.03.03

C++高性能网络编程与Reactor模型实践
C++高性能网络编程与Reactor模型实践

本专题围绕 C++ 在高性能网络服务开发中的应用展开,深入讲解 Socket 编程、多路复用机制、Reactor 模型设计原理以及线程池协作策略。内容涵盖 epoll 实现机制、内存管理优化、连接管理策略与高并发场景下的性能调优方法。通过构建高并发网络服务器实战案例,帮助开发者掌握 C++ 在底层系统与网络通信领域的核心技术。

33

2026.03.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Node.js 教程
Node.js 教程

共57课时 | 13.1万人学习

【web前端】Node.js快速入门
【web前端】Node.js快速入门

共16课时 | 2.1万人学习

Node.js-前端工程化必学
Node.js-前端工程化必学

共19课时 | 3.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号