Vue+websocket+django实现WebSSH demo
Last updated
Last updated
如下图,实现了webssh,可以通过web方式,进行实时命令处理
注意: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
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'])
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;
}
}
扩展
如果使用的是状态框,可参考下面内容
<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>