ホームページ  >  記事  >  ウェブフロントエンド  >  Node.jsを使用してマルチユーザーWebターミナル表示を実装する方法

Node.jsを使用してマルチユーザーWebターミナル表示を実装する方法

亚连
亚连オリジナル
2018-06-23 16:11:471260ブラウズ

この記事では、マルチユーザーWeb端末をサポートするための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 関数を提供します。下部の fork と execvp でシェル コマンドを実行します。ただし、タブのオートコンプリート、履歴コマンドを表示するための矢印キーなど、疑似端末の他の機能は提供されません。

したがって、ノード上のネイティブ モジュールを使用して疑似端末を実装することは不可能です。サーバー側での実装の方向性と擬似端末の原理を引き続き検討する必要があります。

擬似端末

擬似端末とは、実際の端末ではなく、カーネルが提供する「サービス」です。ターミナル サービスは通常、キャラクタ デバイスに提供される最上位の入出力インターフェイス、中間レベルの回線制御、および最下位のハードウェア ドライバーの 3 つの層で構成されます。このうち、最上位のインターフェイスはシステム コール関数を通じて実装されることがよくあります。 (read 、 write) など; 基礎となるハードウェア ドライバーは、カーネルによって提供される疑似端末のマスター/スレーブ デバイス通信を担当しますが、実際には、機能的には次のことを担当します。入出力情報の「処理」。たとえば、入力プロセス中に割り込み文字 (ctrl + c) と一部のロールバック文字 (バックスペースと削除) を処理し、同時に出力改行文字 n を rn に変換します。等

擬似端末は、マスター デバイスとスレーブ デバイスの 2 つの部分に分かれており、デフォルトの回線規律を実装する双方向パイプ (ハードウェア ドライバー) を介して下部で接続されます。擬似端末マスターからの入力はすべてスレーブに反映され、その逆も同様です。スレーブデバイスの出力情報もパイプを介してマスターデバイスに送信され、擬似端末のスレーブデバイスでシェルが実行され、端末機能が完成する。

疑似ターミナルのスレーブデバイスは、ターミナルのタブ補完やその他のシェル特殊コマンドを実際にシミュレートできるため、ノードネイティブモジュールがニーズを満たすことができないという前提の下で、最下層に焦点を当て、OSが何であるかを確認する必要があります。どのような機能を提供しますか。現在、glibc ライブラリは posix_openpt インターフェイスを提供していますが、そのプロセスは少し面倒です:

posix_openpt を使用して疑似端末マスター デバイスを開きます。 Granpt スレーブ デバイスの権限を設定します。 lockopt 対応するスレーブ デバイスのロックを解除します。 スレーブ デバイス名を取得します (同様の/dev/pts/123) マスター (スレーブ) デバイスの読み取り、書き込み、および操作の実行

したがって、より優れたカプセル化を備えた pty ライブラリが登場し、上記の機能はすべて forkpty 関数だけで実現できます。ノード C++ 拡張モジュールを作成し、pty ライブラリを使用して、疑似ターミナル内のデバイスからコマンド ラインを実行するターミナルを実装します。

疑似端末セキュリティの問題については、記事の最後で説明します。

擬似端末の実装アイデア

擬似端末のマスターデバイスとスレーブデバイスの特性に応じて、マスターデバイスが配置されている親プロセスで擬似端末のライフサイクルとリソースを管理し、スレーブデバイスが配置されている子プロセスシェルで実行され、実行処理中の情報と結果は双方向パイプラインを通じてメインデバイスに送信され、メインデバイスが配置されているプロセスは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 (sunOS や unix などのシステムと互換性のある forkpty の posix 実装) を通じてマスター/スレーブ デバイスを作成し、次に子プロセスで権限 (setuid、setgid) を設定し、システム コール pty_execvpe (execvpe のカプセル化) を実行します。 )、次にマスターデバイス すべての入力情報がここで実行されます (子プロセスによって実行されるファイルは sh であり、stdin をリッスンします)

親プロセスは、関連オブジェクトをノード層に公開します (fd など)。メインデバイス (この fd を通じて、net.Socket オブジェクトを作成できます。双方向データ送信)、libuv のメッセージキューを登録します &baton->async 子プロセスが終了すると、&baton->async メッセージがトリガーされ、 pty_after_waitpid 関数が実行されます。

最後に、親プロセスは、前の子プロセスの終了メッセージをリッスンするために uv_thread_create を呼び出して子プロセスを作成します (システム コール wait4 を実行し、特定の pid をリッスンするプロセスをブロックし、終了します)。 pty_waitpid 関数は wait4 関数をカプセル化し、同時に uv_async_send(&baton at the function ->async) トリガー メッセージを実行します。

最下層で pty モデルを実装した後、いくつかの stdio 操作をノード層で行う必要があります。擬似端末の本体は親プロセスでシステムコールを実行することで作成され、本体のファイルディスクリプタはfdを通じてノード層に公開されるため、擬似端末の入出力も同様になります。 fd に従って読み取りおよび書き込みを行い、PIPE、FILE などの対応するファイル タイプを作成して完了します。実際、OS レベルでは、擬似端末マスター デバイスは双方向通信を行う PIPE とみなされます。 net.Socket(fd) を介してノード層にソケットを作成し、データ フローの双方向 IO を実現します。擬似端末のスレーブ デバイスもマスター デバイスと同じ入力を持ち、対応するコマンドがサブデバイスで実行されます。サブプロセスの出力もPIPEを介してメインデバイスに反映され、ノード層のSocketオブジェクトのデータイベントをトリガーします。

ここでの親プロセス、マスターデバイス、子プロセス、スレーブデバイスの入出力の説明が少しわかりにくいので、ここで説明します。親プロセスとメインデバイスの関係は、親プロセスがシステムコール(PIPEとみなすことができる)によりメインデバイスを作成し、メインデバイスのfdを取得する。親プロセスはfdのconnectソケットを作成することで子プロセス(スレーブデバイス)への入出力を実現します。 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 out of 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は、名前が示すように、ユーザーを刑務所に入れるために使用されます。 jailkit は内部的に chroot を使用してユーザーのルート ディレクトリを作成し、バイナリ ファイルとそのすべての DLL を初期化してコピーするための一連の指示を提供します。これらの機能は構成ファイルを通じて操作できます。したがって、実際の開発では、jailkit を初期化シェル スクリプトとともに使用して、ファイル システムの分離を実現します。

ここでの初期化シェルは、前処理スクリプトを指します。chroot は各ユーザーのルート ディレクトリを設定する必要があるため、コマンド ライン権限を持つ対応するユーザーがシェルで作成され、jailkit 設定ファイルを通じてコピーされます。基本的なシェル命令、git、vim、ruby などの dll、最後に、特定のコマンドに対して追加の処理が実行され、アクセス許可がリセットされます。

「新しいシステム」と元のシステムの間のファイル マッピングを処理するプロセスでは、依然としていくつかのスキルが必要です。以前、作者は chroot で設定したユーザーのルート ディレクトリ以外のディレクトリをソフト リンクの形式でマッピングしていましたが、jail 内のソフト リンクにアクセスすると、やはりエラーが報告され、ファイルが見つかりませんでした。 chroot の特性として、ルート ディレクトリの外部にあるファイル システムにアクセスする権限はありません。ハード リンクを通じてマッピングが確立されている場合、chroot によって設定されたユーザー ルート ディレクトリ内のハード リンク ファイルを変更することは可能ですが、削除を伴う操作は行われません。作成などが正しく実行できず、元のシステムのディレクトリにマッピングされ、ハード リンクがそのディレクトリに接続できないため、ハード リンクは最終的に mount --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 経由) と通信します。

上記は私があなたのためにまとめたものです。

関連記事:

React.setStateを使用する際の注意点

vueでページの共通ヘッダーをコンポーネント化する方法(詳細チュートリアル)

関数スロットルと関数アンチシェイクについて(詳細なチュートリアル)

three.jsを使用して3Dシネマを実装する方法

Vueでサイドスライドメニューコンポーネントを実装する方法

以上がNode.jsを使用してマルチユーザーWebターミナル表示を実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。