這次帶給大家node.js實作web終端操作多用戶,node.js實作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); }</void></function></string></string></number></string></number></string></object></object>
首先透過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)進行通訊即可。
相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!
推薦閱讀:
以上是node.js實作web終端操作多用戶的詳細內容。更多資訊請關注PHP中文網其他相關文章!

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。


熱AI工具

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

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

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SublimeText3 Linux新版
SublimeText3 Linux最新版

Dreamweaver Mac版
視覺化網頁開發工具

禪工作室 13.0.1
強大的PHP整合開發環境

mPDF
mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

VSCode Windows 64位元 下載
微軟推出的免費、功能強大的一款IDE編輯器