首頁 web前端 js教程 使用node.js如何實現多用戶web終端顯示

使用node.js如何實現多用戶web終端顯示

Jun 23, 2018 pm 04:11 PM
node.js 多用戶

這篇文章主要介紹了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實現

另外,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中如何實作側滑選單元件#

以上是使用node.js如何實現多用戶web終端顯示的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

圖文詳解Node V8引擎的記憶體和GC 圖文詳解Node V8引擎的記憶體和GC Mar 29, 2023 pm 06:02 PM

這篇文章帶大家深入了解NodeJS V8引擎的記憶體和垃圾回收器(GC),希望對大家有幫助!

一文聊聊Node中的記憶體控制 一文聊聊Node中的記憶體控制 Apr 26, 2023 pm 05:37 PM

基於無阻塞、事件驅動建立的Node服務,具有記憶體消耗低的優點,非常適合處理海量的網路請求。在海量請求的前提下,就需要考慮「記憶體控制」的相關問題了。 1. V8的垃圾回收機制與記憶體限制 Js由垃圾回收機

聊聊如何選擇一個最好的Node.js Docker映像? 聊聊如何選擇一個最好的Node.js Docker映像? Dec 13, 2022 pm 08:00 PM

選擇一個Node的Docker映像看起來像是小事,但是映像的大小和潛在漏洞可能會對你的CI/CD流程和安全造成重大的影響。那我們要如何選擇一個最好Node.js Docker映像呢?

深入聊聊Node中的File模組 深入聊聊Node中的File模組 Apr 24, 2023 pm 05:49 PM

文件模組是對底層文件操作的封裝,例如文件讀寫/打開關閉/刪除添加等等文件模組最大的特點就是所有的方法都提供的**同步**和**異步**兩個版本,具有sync 字尾的方法都是同步方法,沒有的都是異

聊聊Node.js中的 GC (垃圾回收)機制 聊聊Node.js中的 GC (垃圾回收)機制 Nov 29, 2022 pm 08:44 PM

Node.js 是如何做 GC (垃圾回收)的?下面這篇文章就來帶大家了解一下。

一起聊聊Node中的事件循環 一起聊聊Node中的事件循環 Apr 11, 2023 pm 07:08 PM

事件循環是 Node.js 的基本組成部分,透過確保主執行緒不被阻塞來實現非同步編程,了解事件循環對建立高效應用程式至關重要。以下這篇文章就來帶大家深入了解Node中的事件循環 ,希望對大家有幫助!

聊聊用pkg將Node.js專案打包為執行檔的方法 聊聊用pkg將Node.js專案打包為執行檔的方法 Dec 02, 2022 pm 09:06 PM

如何用pkg打包nodejs可執行檔?以下這篇文章跟大家介紹一下使用pkg將Node專案打包為執行檔的方法,希望對大家有幫助!

深入了解Node中的Buffer 深入了解Node中的Buffer Apr 25, 2023 pm 07:49 PM

一開始的時候 JS 只在瀏覽器端運行,對於 Unicode 編碼的字串容易處理,但對於二進位和非 Unicode 編碼的字串處理困難。並且二進制是電腦最底層的資料格式,視訊/音訊/程式/網路包

See all articles