【Django】DjangoによるWeb開発プロジェクトPart8 - xterm.jsとWebSocketでWebブラウザのターミナルからコンテナ操作しているように見せかけるまで -

## はじめに

こんにちは。久々にプライベートの開発を行ったので、今回はDjangoアプリ上でxterm.jsとWebSocketを利用してWebブラウザでターミナル操作を`擬似的`に行える方法を共有したいと思います。

因みに、WebSocketについては、以前RailsアプリでActionCableの開発を行った時にまとめてあるのでWebSocketについて詳しく知りたい方はこちらの記事を参考にして下さい→[【Rails】websocket_railsで開発していたチャットをActionCableにリプレース](https://qiita.com/himrock922/items/cbc6142af12b84bb8f44)

## なぜターミナル操作が必要なのか

現在、jaistingと言うコンテナホスティング用のWebアプリケーションを開発中です。このアプリケーションでは、

1. コンテナをWebブラウザ上で作成できる
2. コンテナとホストとの疎通性・及びホストの外への疎通性が取れるようにIPFW + NATの設定がWebブラウザ上で行える
3. 準仮想化機構であるjailの起動・停止ができる

の３つがあります。この後は、コンテナへPort Forwardingを有効にしてリモートログインをすれば自由にコンテナを取り扱うことができます。
しかし、それを行うために必要な操作・設定をどこかで行う必要があります。現在は、ホストにSSH接続してchrootやchdirで直接jail上の操作を行うことができますが、それでは一々ホストに公開鍵を登録しなければならないので面倒です。
そこで、ブラウザ上で擬似的なターミナルを表示させて、WebSocket経由でコンテナで実行したいコマンドを渡しその結果を返せるようにします。これにより、エンドユーザの操作からは、`ターミナル上でコンテナの操作をしているように見せかける`仕組みになると推察しています。

今回作る機能を組み合わせたJaistingのポンチ絵はこちら→
![IMG_1572.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144991/7a1beb80-fc90-b669-3216-541641854761.jpeg)

今回はこのコネクションの中で操作する部分を作っていきたいと思います。

## Channelsとは

Djangoで非同期的な通信、またHTTPだけでなくWebSocketは勿論の事、MQTT、chatbotのような通信も扱えるツールです。

## Channelsインストール

以下のコマンド・設定は[https://channels.readthedocs.io/en/latest/installation.html](https://channels.readthedocs.io/en/latest/installation.html)より引用

```zsh
% pip install channels
```

```python:jaisting/settings.py
INSTALLED_APPS = (
    'channels',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
)

ASGI_APPLICATION = "jaisting.routing.application"
```

```python:jaisting/routing.py
from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({
    # Empty for now (http->django views is added by default)
})
```

## コンテナの操作を行えるConsumerを作る

サーバサイド側でWebSocketによる接続、データ送信等のリクエストが送信された場合、応答を返すためのコードをConsumerとして作ります。

```python:jails/consumer.py
from channels.generic.websocket import WebsocketConsumer
import json
import libioc

class VNCConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        data_json = json.loads(text_data)
        try:
            pdb.set_trace()
            jail = libioc.Jail(data_json["jail_name"])
            response = jail.exec(data_json["message"].split(" "))
        except (libioc.errors.JailNotFound):
           self.send(text_data='%s not found' % data_json["jail_name"])
        self.send(text_data=response[0])
```

本当はVNCではないですが、やりたい事は似たような事なので、このような名前にしています。
`connect`や`receive`メソッドでコネクション確立要求やデータ送信が行われたときにacceptでコネクションを受けつけ、受信したデータからコマンド実行を行い、レスポンスをクライアントにsendして返すようにしています。

WebSocketへのパスの指定は以下のようにします。

```python:jaisting/routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import jails.routing

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            jails.routing.websocket_urlpatterns
        )
    ),
})
```

```python:jails/routing.py
from channels.routing import ProtocolTypeRouter
from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'/websocket', consumers.VNCConsumer)
]
```

## クライアントサイドで疑似ターミナルを立ち上げコンテナ操作をしているように見せかける

クライアント側は`xterm.js`と呼ばれるフロントエンドでターミナルを立ち上げることができるOSSが便利です。また、こちらのxterm.jsはアドオンツールとして`xterm-addon-attach`と呼ばれるxterm.jsにアタッチすることでWebSocketによる通信をサポートしてくれます。
後は、コネクションの確立、データ送信のクライアントサイドのコードを書くこと及びxterm.jsで利用できるメソッドを利用してターミナルっぽい動きをさせます。以下はクライアントサイドの全コードとなります。

```vue:jail_connect.vue
<template>
  <div id="terminal"></div>
</template>

<script>
    import axios from 'axios'
    import { Terminal } from 'xterm';
    import { AttachAddon } from 'xterm-addon-attach';

    axios.defaults.xsrfCookieName = 'csrftoken';
    axios.defaults.xsrfHeaderName = 'X-CSRFToken';

    export default {
        name: 'Jails-Connect',
        mounted: function () {
          const term = new Terminal();
          const socket = new WebSocket("ws://localhost/jails/websocket");
          const attachAddon = new AttachAddon(socket);
          socket.addEventListener( "message", (response) => {
            term.write("$ " + response.data + "\r\n");
            term.write("$ ");
          });
          let current_line = "";
          let cursor = 0;
          const jail_name = document.getElementById("jail_name").value;
          term.open(document.getElementById('terminal'));
          term.write("$ ");
          term.onKey((data) => {
            switch(data.domEvent.key) {
              case "Enter":
                term.write("\r\n");
                if(current_line) {
                  socket.send(JSON.stringify({
                    'message': current_line,
                    "jail_name": jail_name
                  }));
                } else {
                  term.write("\r\n");
                  term.write("$ ");
                }
                cursor = 0;
                current_line = "";
                break;
              case "ArrowUp":
              case "ArrowDown":
                break;
              case "ArrowLeft":
                if(cursor > 0) {
                  cursor -= 1;
                  term.write(data.key);
                }
                break;
              case "Backspace":
                if(cursor > 0) {
                  cursor -= 1;
                  current_line = current_line.slice(0, -1);
                  term.write('\b \b');
                }
                break;
              default:
                if (cursor < 120) {
                  cursor += 1;
                  current_line += data.key;
                  term.write(data.key);
                }
                break;
            }
          });
        }
    }
</script>

<style scoped>
  .vue-loading {
    color: #0DC5C1;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-left: -75x;
    margin-top: 150px;
    overflow: auto;
  }
</style>
```

## まとめ

これだけだと、ifconfigのような単発な結果は得られるのですが、vimやら、pingと言ったコンテナとのコネクションをずっと確立させなければならないものはまだできていません。
とりあえず、本日はここまで。
そろそろ、リファクタリングしたいので、誰か協力者募集中です汗

## 参考文献

* [https://channels.readthedocs.io/en/latest/introduction.html](https://channels.readthedocs.io/en/latest/introduction.html)
* [https://medium.com/swlh/local-echo-xterm-js-5210f062377e](https://medium.com/swlh/local-echo-xterm-js-5210f062377e)
* [https://xtermjs.org/docs/](https://xtermjs.org/docs/)
* [https://stackoverflow.com/questions/56828930/how-to-remove-the-last-line-in-xterm-js](https://stackoverflow.com/questions/56828930/how-to-remove-the-last-line-in-xterm-js)
* [http://www.denzow.me/entry/2018/03/27/002350](http://www.denzow.me/entry/2018/03/27/002350)
* [https://channels.readthedocs.io/en/latest/tutorial/part_1.html](https://channels.readthedocs.io/en/latest/tutorial/part_1.html)
* [https://channels.readthedocs.io/en/latest/tutorial/part_2.html](https://channels.readthedocs.io/en/latest/tutorial/part_2.html)
* [https://stackoverflow.com/questions/54189424/xterm-js-getting-current-line-text](https://stackoverflow.com/questions/54189424/xterm-js-getting-current-line-text)

## シリーズ

[Part 7](https://qiita.com/himrock922/items/28dc4d0178fc0bba81a9) ←