# Vue+websocket+django实现WebSSH demo

​

## 效果：

​ 如下图，实现了webssh，可以通过web方式，进行实时命令处理

{% file src="/files/Pwt3Jq0xDCHlMG0HU92t" %}

***

**注意：Django 3.0不支持dwebsocket模块，启动时，会报错：**

```
TypeError: WebSocketMiddleware() takes no arguments
```

因此，如果使用Django 3.0，必须使用channels

## 前端：

​ 包依赖："xterm": "^4.14.1", "xterm-addon-fit": "^0.5.0"

​ 封装SSH组件：/components/SSH/index.vue

```
<template>
  <div ref="xterm" class="terminal" :style="styleVar" />
</template>

<script>
import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'

export default {
  name: 'xterm',
  props: {
    ip: { type: String },   // 通过父组件传递登录ip
    height: {
      type: Number,  // xterm显示屏幕，高度
      default: 100,
    },
  },
  data() {
    return {
      term: null,
      socket: null,
    }
  },
  computed: {   // 动态设置xterm显示屏幕高度
    styleVar() {
      return {
        '--terminal-height': this.height + "vh"
      }
    }
  },
  mounted() {  // 初始化链接
    this.init()
    this.initSocket()
  },
  beforeDestroy() {  // 退出销毁链接
    this.socket.close()
    this.term.dispose()
  },
  methods: {
    init() {  // 初始化Terminal
      this.term = new Terminal({
        fontSize: 18,
        convertEol: true, // 启用时，光标将设置为下一行的开头
        rendererType: 'canvas', // 渲染类型
        cursorBlink: true, // 光标闪烁
        cursorStyle: 'bar', // 光标样式 underline
        theme: {
          background: '#060101', // 背景色
          cursor: 'help' // 设置光标
        }
      })
    },
    initSocket() {  // 初始化Websocket
      const fitPlugin = new FitAddon()
      this.term.loadAddon(fitPlugin)

      const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
      this.socket = new WebSocket(`${protocol}//${window.location.host}/socket/ws/ssh/${this.ip}`)

      this.socket.onmessage = e => {
        const reader = new window.FileReader()
        reader.onload = () => this.term.write(reader.result)
        reader.readAsText(e.data, 'utf-8')
      }

      this.socket.onopen = () => {
        this.term.open(this.$refs.xterm)
        this.term.focus()
        fitPlugin.fit()
      }

      this.socket.onclose = e => {
        if (e.code === 1234) {  // 结束标记
          window.location.href = 'about:blank'
          window.close()
        } else {
          setTimeout(() => this.term.write('\r\nConnection is closed.\r\n'), 200)
        }
      }

      this.term.onData(data => this.socket.send(JSON.stringify({ data })))
      this.term.onResize(({ cols, rows }) => {
        this.socket.send(JSON.stringify({ resize: [cols, rows] }))
      })

      window.onresize = () => fitPlugin.fit()
    }
  }
}
</script>
<style lang="scss" scoped>
.terminal {
  display: flex;
  width: 100%;
  min-height: var(--terminal-height);
  flex: 1;
  background-color: #000;
}
.terminal > div {
  flex: 1;
}
</style>
```

​ 父组件引用：

```
<template>
  <SSH :ip="ip" :height="100" />
</template>

<script>

export default {
  name: 'Xterm',
  components: {
    SSH: () => import('@/components/SSH')
  },
  data() {
    return {
      ip: ''
    }
  },
  mounted() {
    this.ip = this.$route.query.ip  //http://localhost:8011/ssh?ip=xxx
  }
}
</script>
```

***

## 后台：

​ Django==3.1.10 daphne==3.0.2 paramiko==2.8.0 channels==3.0.4

​ 假定目录结构如下：

​ (1) 修改asgi.py

​ [Tutorial Part 1: Basic Setup — Channels 3.0.4 documentation](https://channels.readthedocs.io/en/stable/tutorial/part_1.html)

```
import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')

application = ProtocolTypeRouter({  # 区分http or ws请求
    "http": get_asgi_application(),
})
```

​ (2) 修改settings.py

```
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels'  # 添加
]


ASGI_APPLICATION = 'project.asgi.application'
CHANNEL_LAYERS = {
    'default': {
        "BACKEND": "channels.layers.InMemoryChannelLayer"  # 默认用内存
    },
}
```

信道中间层用redis 可参加 <https://channels.readthedocs.io/en/stable/topics/channel\\_layers.html>

​ （3）配置路由

​ settings.py

```
import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter

from consumer import routing  # 添加路由

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    'websocket': routing.ws_router  # ws请求入口
})
```

​ routing.py

```
from channels.auth import AuthMiddlewareStack
from channels.routing import URLRouter
from django.urls import path

from consumer.consumers import SSHConsumer

ws_router = AuthMiddlewareStack(
    URLRouter([
        path(r'ws/ssh/<str:ip>', SSHConsumer.as_asgi()),
    ])
)
```

​ consumers.py

```
import json
from threading import Thread

from channels.generic.websocket import WebsocketConsumer
import paramiko


class SSHConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.ip = None
        self.chan = None
        self.ssh = None

    def connect(self):
        self.ip = self.scope['url_route']['kwargs']['ip']

        self.accept()
        self._init()

    def disconnect(self, close_code):
        self.chan.close()
        self.ssh.close()

    def get_client(self):
        p_key = paramiko.RSAKey.from_private_key_file("/root/.ssh/id_rsa")  # ssh免密登录私钥
        ssh = paramiko.SSHClient()
        ssh.load_system_host_keys()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(hostname=self.ip, port=22, username='root', pkey=p_key)

        return ssh

    def loop_read(self):
        while True:
            data = self.chan.recv(32 * 1024)
            if not data:
                self.close(1234)
                break

            self.send(bytes_data=data)

    def _init(self):
        self.send(bytes_data=b'Connecting ...\r\n')

        try:
            self.ssh = self.get_client()
        except Exception as e:
            self.send(bytes_data=f'Exception: {e}\r\n'.encode())
            self.close()
            return

        self.chan = self.ssh.invoke_shell(term='xterm')
        self.chan.transport.set_keepalive(30)

        Thread(target=self.loop_read).start()

    def receive(self, text_data=None, bytes_data=None):
        data = text_data or bytes_data
        if data:
            data = json.loads(data)

            resize = data.get('resize')
            if resize and len(resize) == 2:
                self.chan.resize_pty(*resize)
            else:
                self.chan.send(data['data'])
```

***

## Nginx配置：

```
upstream app{
    server ip:port;
}

server{
        listen 80;
        server_name _;

        location / {
                root   /usr/share/nginx/html;
                try_files $uri $uri/ /index.html;
        }

        location ~/app/ { # http入口
                rewrite ^/app/(.*) /$1 break;
                proxy_pass http://app;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Host $server_name;
        }
        location ~/socket/ {  # websocket入口
                rewrite ^/socket/(.*) /$1 break;
                proxy_pass http://app;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Host $server_name;
        }
}
```

扩展

* > 如果使用的是状态框，可参考下面内容

```html
<template>
  <div id="terminal"></div>
</template>


<script>
import "xterm/dist/xterm.css";
import { Terminal } from "xterm"; //初始化id="terminal"
import * as attach from "xterm/lib/addons/attach/attach"; //黑窗口绑定
import * as fit from "xterm/lib/addons/fit/fit"; //自动化调节

Terminal.applyAddon(attach); //Addon插件
Terminal.applyAddon(fit);

export default {
  name: "xterm",
  mounted() {
    let terminalContainer = document.getElementById("terminal");
    this.term = new Terminal(this.terminal); //初始化终端
    this.term.open(terminalContainer); //把div为terminal的标签绑定到初始化号的黑窗口

    this.terminalSocket = new WebSocket("ws://10.11.9.246:5000/webssh/");

    this.term.attach(this.terminalSocket); //绑定黑窗口到websocket上

    this.terminalSocket; //连接对象

    //  提示语发送成功，失败，等信息语。
    this.terminalSocket.onopen = function () {
      console.log("websocket is Connected...");
    };
    this.terminalSocket.onclose = function () {
      console.log("websocket is Closed...");
    };
    this.terminalSocket.onerror = function () {
      console.log("damn websocket is broken!");
    };
  },

  //   当浏览器关闭时，也代表着客户端关闭，此时主动断开连接，交给vue的钩子函数来处理这个问题
  beforeDestroy() {
    this.terminalSocket.close(); //先关闭websocket
    this.term.destroy(); //关闭黑窗口
  },
};
</script>
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://close.gitbook.io/yun-wei-bi-ji/python/django/vue+websocket+django-shi-xian-webssh-demo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
