Django3 使用 WebSocket 實(shí)現(xiàn) WebShell
前言
最近工作中需要開發(fā)前端操作遠(yuǎn)程虛擬機(jī)的功能,簡稱 WebShell?;诋?dāng)前的技術(shù)棧為 react+django,調(diào)研了一會(huì)發(fā)現(xiàn)大部分的后端實(shí)現(xiàn)都是 django+channels 來實(shí)現(xiàn) websocket 服務(wù)。
大致看了下覺得這不夠有趣,翻了翻 django 的官方文檔發(fā)現(xiàn) django 原生是不支持 websocket 的,但 django3 之后支持了 asgi 協(xié)議可以自己實(shí)現(xiàn) websocket 服務(wù)。
于是選定 gunicorn+uvicorn+asgi+websocket+django3.2+paramiko 來實(shí)現(xiàn) WebShell。
實(shí)現(xiàn) websocket 服務(wù)
使用 django 自帶的腳手架生成的項(xiàng)目會(huì)自動(dòng)生成 asgi.py 和 wsgi.py 兩個(gè)文件,普通應(yīng)用大部分用的都是 wsgi.py 配合 nginx 部署線上服務(wù)。
這次主要使用 asgi.py 實(shí)現(xiàn) websocket 服務(wù)的思路大致網(wǎng)上搜一下就能找到,主要就是實(shí)現(xiàn) connect/send/receive/disconnect 這個(gè)幾個(gè)動(dòng)作的處理方法。
這里 How to Add Websockets to a Django App without Extra Dependencies就是一個(gè)很好的實(shí)例,但過于簡單……
思路
- # asgi.py
 - import os
 - from django.core.asgi import get_asgi_application
 - from websocket_app.websocket import websocket_application
 - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'websocket_app.settings')
 - django_application = get_asgi_application()
 - async def application(scope, receive, send):
 - if scope['type'] == 'http':
 - await django_application(scope, receive, send)
 - elif scope['type'] == 'websocket':
 - await websocket_application(scope, receive, send)
 - else:
 - raise NotImplementedError(f"Unknown scope type {scope['type']}")
 - # websocket.py
 - async def websocket_application(scope, receive, send):
 - pass
 
- # websocket.py
 - async def websocket_application(scope, receive, send):
 - while True:
 - event = await receive()
 - if event['type'] == 'websocket.connect':
 - await send({
 - 'type': 'websocket.accept'
 - })
 - if event['type'] == 'websocket.disconnect':
 - break
 - if event['type'] == 'websocket.receive':
 - if event['text'] == 'ping':
 - await send({
 - 'type': 'websocket.send',
 - 'text': 'pong!'
 - })
 
實(shí)現(xiàn)
上面的代碼提供了思路
其中最核心的實(shí)現(xiàn)部分我放下面:
- class 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
 
縫合怪
做為合格的代碼搬運(yùn)工,為了提高搬運(yùn)效率還是要造點(diǎn)輪子填點(diǎn)坑的,如何將上面的 WebSocket 類與 paramiko 結(jié)合起來,實(shí)現(xiàn)從前端接受字符傳遞給遠(yuǎn)程主機(jī),并同時(shí)接受返回呢?
- import asyncio
 - import traceback
 - import paramiko
 - from webshell.ssh import Base, RemoteSSH
 - from webshell.connection import WebSocket
 - class WebShell:
 - """整理 WebSocket 和 paramiko.Channel,實(shí)現(xiàn)兩者的數(shù)據(jù)互通"""
 - 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歡迎相關(guān)內(nèi)容
 - 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 完全滿足,搜索下找個(gè)看著簡單的就行。
- 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>;
 - }
 - }
 















 
 
 













 
 
 
 