今回はマルチユーザーのWebターミナル操作を実現するためのnode.jsを紹介します。node.jsを使用してマルチユーザーのWebターミナル操作を実現するための注意事項は何ですか?以下は実際的なケースです。 。
ターミナル (コマンドライン) は、ローカル IDE の共通機能として、git 操作とプロジェクトのファイル操作を非常に強力にサポートします。 WebIDE では、Web 疑似端末が存在しない場合、カプセル化されたコマンド ライン インターフェイスを提供するだけでは開発者を完全に満足させることができません。そのため、より良いユーザー エクスペリエンスを提供するために、Web 疑似端末の開発が急務となっています。議題。リサーチ
私たちの理解では、ターミナルはコマンド ライン ツールに似ており、シェルを実行できるプロセスです。コマンド ラインに一連のコマンドを入力して Enter キーを押すたびに、端末プロセスは子プロセスをフォークして、入力されたコマンドを実行します。端末プロセスは、システム コール wait4() を通じて子プロセスの終了を監視し、出力します。公開された子プロセスの実行情報を通じて。 Web 側でローカリゼーションと同様のターミナル機能を実装する場合、ネットワークの遅延と信頼性の保証、ローカリゼーションに可能な限り近いシェル ユーザー エクスペリエンス、Web ターミナル UI の幅と高さ、出力情報の適応、セキュリティ アクセスなど、さらに多くの作業が必要になる場合があります。制御や権限管理などWeb ターミナルを具体的に実装する前に、シェルの機能実装、ユーザー エクスペリエンス、およびセキュリティ (Web ターミナルはオンライン サーバーで提供される機能) のどれが最もコアであるかを評価する必要があります。 、そのため、セキュリティが保証されている必要があります。この2つの機能を確保することを前提として初めて、Web疑似端末は正式にスタートすることができます。 まず、これら 2 つの関数の技術的な実装について考えてみましょう (サーバー側のテクノロジは、nodejs を使用します): ノード ネイティブ モジュールは、対話型入力の実装と出力の実行に使用できる repl モジュールを提供し、タブ補完、カスタマイズされた出力スタイル、その他の機能も提供します。ただし、ノード関連のコマンドのみを実行できるため、何を実現することはできません。システムシェルの目的を実行します。 ノードのネイティブ モジュール child_porcess は、基盤となる libuv をカプセル化する uv_spawn 関数である spawn を提供します。基盤となる実行システムは、fork および execvp を呼び出してシェル コマンドを実行します。ただし、タブのオートコンプリート、履歴コマンドを表示する矢印キーなど、疑似ターミナルの他の機能は提供されません。 したがって、ノードのネイティブモジュールを使用して擬似端末をサーバ側で実装することは不可能であり、擬似端末の原理とノード側の実装方向を引き続き検討する必要がある。 疑似端末擬似端末は実際の端末ではなく、カーネルが提供する「サービス」です。ターミナル サービスは通常、次の 3 つの層で構成されます:
キャラクターデバイス、中間レベルのライン規律、および最下位レベルのハードウェアドライバーに提供されるトップレベルの入出力インターフェイス その中で、トップレベルのインターフェイスは (読み取り、書き込み) などのシステム コール関数を通じて実装されることが多く、基礎となるハードウェア ドライバーはカーネルによって提供される擬似端末のマスター/スレーブ デバイス通信を担当します。行規律は比較的抽象的に見えますが、実際、機能的に言えば、入力プロセス中の割り込み文字 (ctrl) の処理など、入出力情報の「処理」を担当します。 + c) およびいくつかのバックスペース文字 (バックスペースと削除) などを使用し、出力改行文字 n を rn などに変換します。 擬似端末はマスター デバイスとスレーブ デバイスの 2 つの部分に分かれており、これらはデフォルトの回線規則を実装する双方向パイプ (ハードウェア ドライバー) を介して接続されます。擬似端末マスターからの入力はすべてスレーブに反映され、その逆も同様です。スレーブデバイスの出力情報もパイプを介してマスターデバイスに送信され、擬似端末のスレーブデバイスでシェルが実行され、端末機能が完成する。 擬似端末のスレーブデバイスは、端末のタブ補完やその他のシェル特殊コマンドを実際にシミュレートできます。したがって、ノードネイティブモジュールがニーズを満たすことができないという前提の下で、最下層に焦点を当て、OSがどのような機能を提供するかを確認する必要があります。 。現在、glibc ライブラリは posix_openpt インターフェイスを提供していますが、そのプロセスは少し面倒です: posix_openpt を使用して、擬似端末マスター デバイスを開きます。 スレーブ デバイスの権限を設定するには、grantpt を使用します。 対応するスレーブ デバイスのロックを解除し、スレーブ デバイス名 (/dev/pts/123 と同様) を取得するには、unlockpt を使用します。 マスター (スレーブ) デバイスは、書き込みと操作の実行 したがって、より優れたカプセル化を備えた pty ライブラリが登場しました。これは、forkpty 関数だけで上記のすべての機能を実現できます。ノード C++ 拡張モジュールを作成し、pty ライブラリを使用して、疑似ターミナル内のデバイスからコマンド ラインを実行するターミナルを実装します。 疑似端末セキュリティの問題については、記事の最後で説明します。 疑似端末の実装アイデア
擬似端末のマスター/スレーブデバイスの特性に応じて、擬似端末のライフサイクルとそのリソースをマスターデバイスが配置されている親プロセスで管理し、子プロセスでシェルを実行します。スレーブデバイスが配置されている場所 実行プロセス中の情報と結果がメインデバイスに送信され、メインデバイスが配置されているプロセスが stdout を外部に提供します。
ここで pty.js の実装アイデアを学びましょう:
まず、pty_forkpty (sunOS と互換性のある forkpty の posix 実装) を通じて、
Unix など) マスター/スレーブデバイスを作成し、子プロセスに権限を設定 (setuid、setgid) した後、システムコール pty_execvpe (execvpe のカプセル化) を実行すると、マスターデバイスの入力情報が取得されます。ここで実行されます (子プロセスによって実行されるファイル。sh の場合、stdin がリッスンされます);
親プロセスは、メインデバイスの fd (双方向データ送信用の net.Socket オブジェクトを作成できる) などの関連オブジェクトをノード層に公開し、同時に libuv のメッセージ キューを登録します。子プロセスの終了時にトリガーされる async -> async メッセージ、pty_after_waitpid 関数を実行します。
最後に、親プロセスは、uv_thread_create を呼び出して前の子プロセスの終了メッセージをリッスンすることによって子プロセスを作成します (システム コール wait4 を実行して、特定の pid をリッスンするプロセスをブロックします。終了情報は 3 番目のパラメーターに格納されます) )、pty_waitpid 関数 wait4 関数はカプセル化されており、関数の最後で uv_async_send(&baton->async) が実行されてメッセージがトリガーされます。
最下層で pty モデルを実装した後、いくつかの stdio 操作をノード層で実行する必要があります。親プロセス内でシステムコールを実行することで擬似端末本体デバイスが作成され、本体デバイスのファイルディスクリプタがfdを通じてノード層に公開されるため、擬似端末の入出力も読み込まれ、 fd に従って書き込み、PIPE、FILE などの対応するファイル タイプを作成して完了します。実際、OS レベルでは、擬似端末マスター デバイスは双方向通信を行う PIPE とみなされます。 net.Socket(fd) を介してノード層にソケットを作成し、データ フローの双方向 IO を実装します。これにより、擬似端末のスレーブ デバイスもマスター デバイスと同じ入力を持ち、対応するコマンドがサブデバイスで実行されます。サブプロセスの出力もPIPEを介してメインデバイスに反映され、ノード層のSocketオブジェクトのデータイベントをトリガーします。
ここでの親プロセス、マスターデバイス、子プロセス、スレーブデバイスの入出力の説明が少しわかりにくいので、ここで説明します。親プロセスとメインデバイスの関係は、親プロセスがシステムコール(PIPEとみなすことができる)によりメインデバイスを作成し、メインデバイスのfdを取得する。親プロセスは fd の接続を作成します
ソケットは子プロセス(スレーブデバイス)への入出力を実装します。 子プロセスが forkpty を渡す
作成後、login_tty 操作が実行されて子プロセスの stdin、stderr、stderr がリセットされ、すべてがスレーブ デバイス (PIPE のもう一方の端) の fd にコピーされます。したがって、子プロセスの入力と出力はすべてスレーブデバイスの fd に関連付けられ、子プロセスの出力データは PIPE を経由し、親プロセスのコマンドは PIPE から読み込まれます。詳細については、リファレンス forkpty 実装を参照してください
また、ptyライブラリは疑似端末のサイズ設定を提供しており、疑似端末の出力情報のレイアウト情報をパラメータで調整できるため、Web上でコマンドラインの幅と高さを調整する機能も提供します側では、pty レイヤーに擬似端末を設定するだけです。文字単位で測定されるウィンドウ サイズを設定するだけです。
Web端末のセキュリティを保証
glibc が提供する pty ライブラリに基づいて擬似端末バックエンドを実装する場合、セキュリティの保証はありません。 Web ターミナルを介してサーバー上のディレクトリを直接操作したいと考えていますが、擬似ターミナルのバックグラウンドを介して root 権限を直接取得できますが、これはサーバーのセキュリティに直接影響するため、実装する必要があります。ユーザーが同時にオンラインであり、各ユーザーのアクセス権を設定でき、特定のディレクトリにアクセスでき、オプションで bash コマンドを設定でき、ユーザーは互いに隔離され、ユーザーは現在の状態を認識しない「システム」です。この環境はシンプルで簡単に導入できます。
最も適切なテクノロジーの選択は docker です。カーネル レベルの分離として、ハードウェア リソースを最大限に活用でき、ホストの をマッピングするのに非常に便利です。しかし、docker は万能ではありません。プログラムが docker コンテナーで実行される場合、各ユーザーへのコンテナーの割り当てはさらに複雑になり、運用保守担当者の制御下になくなります。これがいわゆる DooD (港湾労働者
Docker の外) -- ボリューム経由
「/usr/local/bin/docker」などのバイナリ ファイルの場合は、ホストの docker コマンドを使用して兄弟イメージを開いてビルド サービスを実行します。 docker-in-docker モードの使用には、業界で、特に レベルでよく議論される多くの欠点があり、それについては参考資料を参照してください。したがって、Docker テクノロジーは、コンテナー内で既に実行されているサービスのユーザー アクセスのセキュリティ問題を解決するのには適していません。
次に、単一マシン上でのソリューションを検討する必要があります。現時点では、作者は 2 つの選択肢しか考えていません:
コマンド ACL は、ホワイトリスト制限付き bash chroot コマンドによって実装され、ユーザーごとにシステム ユーザーを作成し、ユーザーのアクセス範囲を制限します
まず第一に、コマンド ホワイトリスト方式は廃止されるべきです。第一に、Linux の異なるリリースの bash が同じであるという保証はありません。第二に、タブ コマンド補完機能のせいで、すべてのコマンドを効果的に使い果たすことができません。および 擬似端末によって提供される特殊文字 delete が存在する場合、現在入力されているコマンドと効果的に一致することはできません。したがって、ホワイトリスト方式には抜け穴が多すぎるため、放棄する必要があります。
/bin/bash -r によってトリガーされる制限付き bash は、ユーザーが明示的に「cd ディレクトリ」にアクセスできないように制限できますが、多くの欠点があります:
完全に信頼できないソフトウェアの実行を許可するには不十分です。シェル スクリプトであることが判明したコマンドが実行されると、rbash はスクリプトを実行するためにシェル内に作成された制限をオフにします。ユーザーが bash を実行するか、rbash からダッシュを実行すると、無制限のシェルが取得されます。制限された bash から抜け出す方法はたくさんあります
シェルは予測が容易ではありません。
結局、解決策は chroot しかないようです。 chroot はユーザーのルート ディレクトリを変更し、指定されたルート ディレクトリでコマンドを実行します。指定されたルート ディレクトリから飛び出すことはできないため、元のシステムのすべてのディレクトリに同時にアクセスすることはできません。chroot は元のシステムから分離されたシステム ディレクトリ構造を作成するため、元のシステムのさまざまなコマンドは使用できません。 「新しいシステム」は新しくて空であるため、最後に、複数のユーザーが使用する場合に分離され、透過的になるため、私たちのニーズを完全に満たします。
そのため、最終的に Web 端末のセキュリティ ソリューションとして chroot を選択しました。ただし、chroot を使用するには、新しいユーザーの作成だけでなくコマンドの初期化など、多くの追加の処理が必要になります。 「新しいシステム」は空であり、「ls、pmd」などの実行可能なバイナリ ファイルがないことも上で述べました。そのため、「新しいシステム」を初期化する必要があります。ただし、多くのバイナリ ファイルは多くのライブラリに静的にリンクされているだけでなく、実行時にダイナミック リンク ライブラリ (dll) に依存するため、各コマンドが依存する多くの dll を見つける必要もあり、非常に面倒です。ユーザーがこの退屈なプロセスから解放されるようにするために、jailkit が登場しました。
ジェイルキット、とても便利です
Jailkit は、その名前が示すように、ユーザーを刑務所に入れるために使用されます。 jailkit は、内部で chroot を使用してユーザーのルート ディレクトリを作成し、バイナリ ファイルとそのすべての DLL を初期化してコピーするための一連の手順を提供します。これらの機能は、設定ファイル を通じて操作できます。したがって、実際の開発では、jailkit を初期化シェル スクリプトとともに使用して、ファイル システムの分離を実現します。
ここでの初期化シェルとは、前処理スクリプトを指します。chroot は各ユーザーのルート ディレクトリを設定する必要があるため、コマンド ライン権限を持つ各ユーザーに対して対応するユーザーがシェルに作成され、基本ユーザーはjailkit 設定ファイルを通じてコピーされます。バイナリ ファイルとその DLL (基本的なシェル命令、git、vim、ruby など)。最後に、特定のコマンドに対して追加の処理が実行され、アクセス許可がリセットされます。
「新しいシステム」と元のシステムの間のファイル マッピングを処理するプロセスでは、依然としていくつかのスキルが必要です。以前、作者は chroot で設定したユーザーのルート ディレクトリ以外のディレクトリをソフト リンクの形式でマッピングしていましたが、jail 内のソフト リンクにアクセスすると、やはりエラーが報告され、ファイルが見つかりませんでした。 chroot の特性として、ルート ディレクトリの外部にあるファイル システムにアクセスする権限はありません。ハード リンクを通じてマッピングが確立されている場合、chroot によって設定されたユーザー ルート ディレクトリ内のハード リンク ファイルを変更することは可能ですが、削除を伴う操作は行われません。作成などが正しく実行できず、元のシステムのディレクトリにマッピングされ、ハード リンクがそのディレクトリに接続できないため、ハード リンクは最終的にマウントを通じて要件を満たしません。
--bind 実装 (mount --bind /home/ttt/abc など)
/usr/local/abc マウントされたディレクトリ(/usr/local/abc)のディレクトリ情報(ブロック)をシールドし、マウントされたディレクトリとマウントされたディレクトリとのマッピング関係をメモリ上に維持します/usr/ local/へのアクセス。 abc は、メモリ マッピング テーブルを通じて /home/ttt/abc のブロックをクエリし、ディレクトリ マッピングを実現するための操作を実行します。
最後に、「新しいシステム」を初期化した後、擬似ターミナルを介してjail関連のコマンドを実行する必要があります:
sudo jk_chrootlaunch -j /usr/local/jailuser/${creator} -u ${creator} -x /bin/bashr
bash プログラムを開いた後、PIPE 経由でメイン デバイスが受信した Web ターミナル入力 (WebSocket 経由) と通信できます。 この記事の事例を読んだ後は、この方法を習得したと思います。さらに興味深い情報については、php 中国語 Web サイトの他の関連記事に注目してください。 推奨読書: 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);
}
以上がnode.jsは複数ユーザー向けのWeb端末操作を実装しますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。