前言
最近工作中需要开发前端操作远程虚拟机的功能,简称WebShell。基于当前的技术栈为反应django,调研了一会发现大部分的后端实现都是django频道来实现websocket服务。
大致看了下觉得这不够有趣,翻了翻django的官方文档发现django原生是不支持websocket的,但django3之后支持了asgi协议可以自己实现websocket服务。
于是选定古泥玉米紫外线图标asgi网络插座django 3.2 paramiko来实现WebShell。
实现 websocket 服务
使用django自带的脚手架生成的项目会自动生成asgi.py和wsgi.py两个文件,普通应用大部分用的都是wsgi.py配合nginx部署线上服务。
这次主要使用asgi.py实现websocket服务的思路大致网上搜一下就能找到,主要就是实现
连接/发送/接收/断开这个几个动作的处理方法。
这里如何在没有额外依赖的情况下将网络套接字添加到Django应用程序(
https://jaydenwindle。com/writing/django-web sockets-zero-dependencies/)就是一个很好的实例,但过于简单……
思路
# asgi.py
导入操作系统
来自django.core.asgi导入get_asgi_application
从websocket_app.websocket导入websocket _应用程序
操作系统。环境。setdefault(' DJANGO _ SETTINGS _ MODULE ',' websocket_app.settings ')
django _ application=get _ asgi _ application()
异步定义应用程序(范围、接收、发送):
如果作用域['type']=='http':
等待django_application(范围、接收、发送)
否则如果作用域['type']=='websocket':
等待websocket_application(作用域、接收、发送)
else:
引发notimplementerror(f)未知的作用域类型{作用域['type']}”)
# websocket.py
异步def websocket_application(作用域、接收、发送):
及格
# websocket.py
异步def websocket_application(作用域、接收、发送):
而True:
事件=等待接收()
如果事件[' type ']==' web套接字。connect ' :
等待发送({ 0
键入“:”网络插座。接受
})
如果事件[' type ']==' web套接字。断开' :
破裂
如果事件[' type ']==' web套接字。接收' :
如果事件['text']=='ping':
等待发送({ 0
键入: 'websocket.send ',
文字':'乒乓!
})
实现
上面的代码提供了思路,比较完整的可以参考这里django web套接字-3-1(
https://aliashkevich.com/websockets-in-django-3-1/)基本可以复用了。
其中最核心的实现部分我放下面:
WebSocket:类
def __init__(
self, scope, receive, send): self._scope = scope self._receive = receive self._send = send self._client_state = State.CONNECTING self._app_state = State.CONNECTING @property def headers(self): return Headers(self._scope) @property def scheme(self): return self._scope["scheme"] @property def path(self): return self._scope["path"] @property def query_params(self): return QueryParams(self._scope["query_string"].decode()) @property def query_string(self) -> str: return self._scope["query_string"] @property def scope(self): return self._scope async def accept(self, subprotocol: str = None): """Accept connection. :param subprotocol: The subprotocol the server wishes to accept. :type subprotocol: str, optional """ if self._client_state == State.CONNECTING: await self.receive() await self.send({"type": SendEvent.ACCEPT, "subprotocol": subprotocol}) async def close(self, code: int = 1000): await self.send({"type": SendEvent.CLOSE, "code": code}) async def send(self, message: t.Mapping): if self._app_state == State.DISCONNECTED: raise RuntimeError("WebSocket is disconnected.") if self._app_state == State.CONNECTING: assert message["type"] in {SendEvent.ACCEPT, SendEvent.CLOSE}, ( 'Could not write event "%s" into socket in connecting state.' % message["type"] ) if message["type"] == SendEvent.CLOSE: self._app_state = State.DISCONNECTED else: self._app_state = State.CONNECTED elif self._app_state == State.CONNECTED: assert message["type"] in {SendEvent.SEND, SendEvent.CLOSE}, ( 'Connected socket can send "%s" and "%s" events, not "%s"' % (SendEvent.SEND, SendEvent.CLOSE, message["type"]) ) if message["type"] == SendEvent.CLOSE: self._app_state = State.DISCONNECTED await self._send(message) async def receive(self): if self._client_state == State.DISCONNECTED: raise RuntimeError("WebSocket is disconnected.") message = await self._receive() if self._client_state == State.CONNECTING: assert message["type"] == ReceiveEvent.CONNECT, ( 'WebSocket is in connecting state but received "%s" event' % message["type"] ) self._client_state = State.CONNECTED elif self._client_state == State.CONNECTED: assert message["type"] in {ReceiveEvent.RECEIVE, ReceiveEvent.DISCONNECT}, ( 'WebSocket is connected but received invalid event "%s".' % message["type"] ) if message["type"] == ReceiveEvent.DISCONNECT: self._client_state = State.DISCONNECTED return message缝合怪
做为合格的代码搬运工,为了提高搬运效率还是要造点轮子填点坑的,如何将上面的 WebSocket 类与 paramiko 结合起来,实现从前端接受字符传递给远程主机,并同时接受返回呢?
import asyncio
import traceback
import paramiko
from webshell.ssh import Base, RemoteSSH
from webshell.connection import WebSocket
class WebShell:
"""整理 WebSocket 和 paramiko.Channel,实现两者的数据互通"""
def __init__(self, ws_session: WebSocket,
ssh_session: paramiko.SSHClient = None,
chanel_session: paramiko.Channel = None
):
self.ws_session = ws_session
self.ssh_session = ssh_session
self.chanel_session = chanel_session
def init_ssh(self, host=None, port=22, user="admin", passwd="admin@123"):
self.ssh_session, self.chanel_session = RemoteSSH(host, port, user, passwd).session()
def set_ssh(self, ssh_session, chanel_session):
self.ssh_session = ssh_session
self.chanel_session = chanel_session
async def ready(self):
await self.ws_session.accept()
async def welcome(self):
# 展示Linux欢迎相关内容
for i in range(2):
if self.chanel_session.send_ready():
message = self.chanel_session.recv(2048).decode('utf-8')
if not message:
return
await self.ws_session.send_text(message)
async def web_to_ssh(self):
# print('--------web_to_ssh------->')
while True:
# print('--------------->')
if not self.chanel_session.active or not self.ws_session.status:
return
await asyncio.sleep(0.01)
shell = await self.ws_session.receive_text()
# print('-------shell-------->', shell)
if self.chanel_session.active and self.chanel_session.send_ready():
self.chanel_session.send(bytes(shell, 'utf-8'))
# print('--------------->', "end")
async def ssh_to_web(self):
# print('<--------ssh_to_web-----------')
while True:
# print('<-------------------')
if not self.chanel_session.active:
await self.ws_session.send_text('ssh closed')
return
if not self.ws_session.status:
return
await asyncio.sleep(0.01)
if self.chanel_session.recv_ready():
message = self.chanel_session.recv(2048).decode('utf-8')
# print('<---------message----------', message)
if not len(message):
continue
await self.ws_session.send_text(message)
# print('<-------------------', "end")
async def run(self):
if not self.ssh_session:
raise Exception("ssh not init!")
await self.ready()
await asyncio.gather(
self.web_to_ssh(),
self.ssh_to_web()
)
def clear(self):
try:
self.ws_session.close()
except Exception:
traceback.print_stack()
try:
self.ssh_session.close()
except Exception:
traceback.print_stack()
前端
xterm.js 完全满足,搜索下找个看着简单的就行。
export class Term extends React.Component {
private terminal!: HTMLDivElement;
private fitAddon = new FitAddon();
componentDidMount() {
const xterm = new Terminal();
xterm.loadAddon(this.fitAddon);
xterm.loadAddon(new WebLinksAddon());
// using wss for https
// const socket = new WebSocket("ws://" + window.location.host + "/api/v1/ws");
const socket = new WebSocket("ws://localhost:8000/webshell/");
// socket.onclose = (event) => {
// this.props.onClose();
// }
socket.onopen = (event) => {
xterm.loadAddon(new AttachAddon(socket));
this.fitAddon.fit();
xterm.focus();
}
xterm.open(this.terminal);
xterm.onResize(({ cols, rows }) => {
socket.send("<RESIZE>" + cols + "," + rows)
});
window.addEventListener('resize', this.onResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
}
onResize = () => {
this.fitAddon.fit();
}
render() {
return <div className="Terminal" ref={(ref) => this.terminal = ref as HTMLDivElement}></div>;
}
}