Django3 使用 WebSocket 实现 WebShell

系统 Linux
大致看了下觉得这不够有趣,翻了翻 django 的官方文档发现 django 原生是不支持 websocket 的,但 django3 之后支持了 asgi 协议可以自己实现 websocket 服务。

[[435235]]

 前言

最近工作中需要开发前端操作远程虚拟机的功能,简称 WebShell。基于当前的技术栈为 react+django,调研了一会发现大部分的后端实现都是 django+channels 来实现 websocket 服务。

大致看了下觉得这不够有趣,翻了翻 django 的官方文档发现 django 原生是不支持 websocket 的,但 django3 之后支持了 asgi 协议可以自己实现 websocket 服务。

于是选定 gunicorn+uvicorn+asgi+websocket+django3.2+paramiko 来实现 WebShell。

实现 websocket 服务

使用 django 自带的脚手架生成的项目会自动生成 asgi.py 和 wsgi.py 两个文件,普通应用大部分用的都是 wsgi.py 配合 nginx 部署线上服务。

这次主要使用 asgi.py 实现 websocket 服务的思路大致网上搜一下就能找到,主要就是实现 connect/send/receive/disconnect 这个几个动作的处理方法。

这里 How to Add Websockets to a Django App without Extra Dependencies就是一个很好的实例,但过于简单……

思路 

  1. # asgi.py   
  2. import os  
  3. from django.core.asgi import get_asgi_application  
  4. from websocket_app.websocket import websocket_application  
  5. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'websocket_app.settings')  
  6. django_application = get_asgi_application()  
  7. async def application(scope, receive, send):  
  8.     if scope['type'] == 'http':  
  9.         await django_application(scope, receive, send)  
  10.     elif scope['type'] == 'websocket':  
  11.         await websocket_application(scope, receive, send)  
  12.     else:  
  13.         raise NotImplementedError(f"Unknown scope type {scope['type']}")  
  14. # websocket.py  
  15. async def websocket_application(scope, receive, send):  
  16.     pass  
  1. # websocket.py  
  2. async def websocket_application(scope, receive, send):  
  3.     while True:  
  4.         event = await receive()  
  5.         if event['type'] == 'websocket.connect':  
  6.             await send({  
  7.                 'type': 'websocket.accept'  
  8.             })  
  9.         if event['type'] == 'websocket.disconnect':  
  10.             break  
  11.         if event['type'] == 'websocket.receive':  
  12.             if event['text'] == 'ping':  
  13.                 await send({  
  14.                     'type': 'websocket.send',  
  15.                     'text': 'pong!'  
  16.                 }) 

实现

上面的代码提供了思路

其中最核心的实现部分我放下面: 

  1. class WebSocket:  
  2.     def __init__(self, scope, receive, send):  
  3.         self._scope = scope  
  4.         self._receive = receive  
  5.         self._send = send  
  6.         self._client_state = State.CONNECTING  
  7.         self._app_state = State.CONNECTING  
  8.     @property  
  9.     def headers(self):  
  10.         return Headers(self._scope)  
  11.     @property  
  12.     def scheme(self): 
  13.        return self._scope["scheme"]  
  14.     @property  
  15.     def path(self): 
  16.        return self._scope["path"]  
  17.     @property  
  18.     def query_params(self):  
  19.         return QueryParams(self._scope["query_string"].decode())  
  20.     @property  
  21.     def query_string(self) -> str:  
  22.         return self._scope["query_string"]  
  23.     @property  
  24.     def scope(self):  
  25.         return self._scope  
  26.     async def accept(self, subprotocol: str = None):  
  27.         """Accept connection.  
  28.         :param subprotocol: The subprotocol the server wishes to accept.  
  29.         :type subprotocol: str, optional  
  30.         """  
  31.         if self._client_state == State.CONNECTING:  
  32.             await self.receive()  
  33.         await self.send({"type": SendEvent.ACCEPT, "subprotocol": subprotocol}) 
  34.     async def close(self, code: int = 1000):  
  35.         await self.send({"type": SendEvent.CLOSE, "code": code})  
  36.     async def send(self, message: t.Mapping):  
  37.         if self._app_state == State.DISCONNECTED: 
  38.             raise RuntimeError("WebSocket is disconnected.")  
  39.         if self._app_state == State.CONNECTING:  
  40.             assert message["type"] in {SendEvent.ACCEPT, SendEvent.CLOSE}, (  
  41.                     'Could not write event "%s" into socket in connecting state.'  
  42.                     % message["type"]  
  43.             )  
  44.             if message["type"] == SendEvent.CLOSE:  
  45.                 self._app_state = State.DISCONNECTED  
  46.             else:  
  47.                 self._app_state = State.CONNECTED  
  48.         elif self._app_state == State.CONNECTED:  
  49.             assert message["type"] in {SendEvent.SEND, SendEvent.CLOSE}, (  
  50.                     'Connected socket can send "%s" and "%s" events, not "%s"'  
  51.                     % (SendEvent.SEND, SendEvent.CLOSE, message["type"])  
  52.             )  
  53.             if message["type"] == SendEvent.CLOSE:  
  54.                 self._app_state = State.DISCONNECTED  
  55.         await self._send(message)  
  56.     async def receive(self):  
  57.         if self._client_state == State.DISCONNECTED:  
  58.             raise RuntimeError("WebSocket is disconnected.")  
  59.         message = await self._receive()  
  60.         if self._client_state == State.CONNECTING:  
  61.             assert message["type"] == ReceiveEvent.CONNECT, (  
  62.                     'WebSocket is in connecting state but received "%s" event'  
  63.                     % message["type"]  
  64.             )  
  65.             self._client_state = State.CONNECTED  
  66.         elif self._client_state == State.CONNECTED:  
  67.             assert message["type"] in {ReceiveEvent.RECEIVE, ReceiveEvent.DISCONNECT}, (  
  68.                     'WebSocket is connected but received invalid event "%s".'  
  69.                     % message["type"]  
  70.             )  
  71.             if message["type"] == ReceiveEvent.DISCONNECT:  
  72.                 self._client_state = State.DISCONNECTED  
  73.         return message 

缝合怪

做为合格的代码搬运工,为了提高搬运效率还是要造点轮子填点坑的,如何将上面的 WebSocket 类与 paramiko 结合起来,实现从前端接受字符传递给远程主机,并同时接受返回呢? 

  1. import asyncio  
  2. import traceback  
  3. import paramiko  
  4. from webshell.ssh import Base, RemoteSSH  
  5. from webshell.connection import WebSocket   
  6. class WebShell:  
  7.     """整理 WebSocket 和 paramiko.Channel,实现两者的数据互通"""  
  8.     def __init__(self, ws_session: WebSocket,  
  9.                  ssh_session: paramiko.SSHClient = None 
  10.                  chanel_session: paramiko.Channel = None  
  11.                  ):  
  12.         self.ws_session = ws_session  
  13.         self.ssh_session = ssh_session  
  14.         self.chanel_session = chanel_session  
  15.     def init_ssh(self, host=Noneport=22user="admin"passwd="admin@123"):  
  16.         self.ssh_session, self.chanel_session = RemoteSSH(host, port, user, passwd).session()  
  17.     def set_ssh(self, ssh_session, chanel_session):  
  18.         self.ssh_session = ssh_session  
  19.         self.chanel_session = chanel_session  
  20.     async def ready(self):  
  21.         await self.ws_session.accept()  
  22.     async def welcome(self):  
  23.         # 展示Linux欢迎相关内容  
  24.         for i in range(2):  
  25.             if self.chanel_session.send_ready():  
  26.                 message = self.chanel_session.recv(2048).decode('utf-8')  
  27.                 if not message:  
  28.                     return  
  29.                 await self.ws_session.send_text(message)  
  30.     async def web_to_ssh(self):  
  31.         # print('--------web_to_ssh------->')  
  32.         while True:  
  33.             # print('--------------->')  
  34.             if not self.chanel_session.active or not self.ws_session.status:  
  35.                 return  
  36.             await asyncio.sleep(0.01)  
  37.             shell = await self.ws_session.receive_text()  
  38.             # print('-------shell-------->', shell)  
  39.             if self.chanel_session.active and self.chanel_session.send_ready():  
  40.                 self.chanel_session.send(bytes(shell, 'utf-8'))  
  41.             # print('--------------->', "end")  
  42.     async def ssh_to_web(self):  
  43.         # print('<--------ssh_to_web-----------')  
  44.         while True:  
  45.             # print('<-------------------')  
  46.             if not self.chanel_session.active:  
  47.                 await self.ws_session.send_text('ssh closed')  
  48.                 return  
  49.             if not self.ws_session.status:  
  50.                 return  
  51.             await asyncio.sleep(0.01)  
  52.             if self.chanel_session.recv_ready():  
  53.                 message = self.chanel_session.recv(2048).decode('utf-8')  
  54.                 # print('<---------message----------', message)  
  55.                 if not len(message):  
  56.                     continue  
  57.                 await self.ws_session.send_text(message)  
  58.             # print('<-------------------', "end")  
  59.     async def run(self):  
  60.         if not self.ssh_session:  
  61.             raise Exception("ssh not init!")  
  62.         await self.ready()  
  63.         await asyncio.gather(  
  64.             self.web_to_ssh(),  
  65.             self.ssh_to_web()  
  66.         ) 
  67.     def clear(self):  
  68.         try:  
  69.             self.ws_session.close()  
  70.         except Exception:  
  71.             traceback.print_stack()  
  72.         try:  
  73.             self.ssh_session.close()  
  74.         except Exception:  
  75.             traceback.print_stack() 

前端

xterm.js 完全满足,搜索下找个看着简单的就行。 

  1. export class Term extends React.Component {  
  2.     private terminal!: HTMLDivElement;  
  3.     private fitAddon = new FitAddon();  
  4.     componentDidMount() {  
  5.         const xterm = new Terminal();  
  6.         xterm.loadAddon(this.fitAddon);  
  7.         xterm.loadAddon(new WebLinksAddon()); 
  8.         // using wss for https  
  9.         //         const socket = new WebSocket("ws://" + window.location.host + "/api/v1/ws");  
  10.         const socket = new WebSocket("ws://localhost:8000/webshell/");  
  11.         // socket.onclose = (event) => {  
  12.         //     this.props.onClose();  
  13.         // }  
  14.         socket.onopen = (event) => {  
  15.             xterm.loadAddon(new AttachAddon(socket));  
  16.             this.fitAddon.fit();  
  17.             xterm.focus();  
  18.         }  
  19.         xterm.open(this.terminal);  
  20.         xterm.onResize(({ cols, rows }) => { 
  21.             socket.send("<RESIZE>" + cols + "," + rows)  
  22.         });  
  23.         window.addEventListener('resize', this.onResize);  
  24.     }  
  25.     componentWillUnmount() {  
  26.         window.removeEventListener('resize', this.onResize);  
  27.     }  
  28.     onResize = () => {  
  29.         this.fitAddon.fit();  
  30.     }  
  31.     render() {  
  32.         return <div className="Terminal" ref={(ref) => this.terminal = ref as HTMLDivElement}></div> 
  33.     }  
  34.  

 

责任编辑:庞桂玉 来源: 马哥Linux运维
相关推荐

2021-03-25 08:29:33

SpringBootWebSocket即时消息

2023-08-14 08:01:12

websocket8g用户

2014-12-16 10:28:49

2017-07-11 13:58:10

WebSocket

2022-06-28 08:37:07

分布式服务器WebSocket

2016-03-14 12:33:46

2024-03-21 08:34:49

Vue3WebSocketHTTP

2023-11-17 09:35:58

2023-07-26 07:28:55

WebSocket服务器方案

2010-08-09 13:37:09

FlexDjango

2023-12-04 07:31:41

Golangwebsocket

2021-02-26 12:37:39

WebSocketOkHttp连接

2021-03-05 11:20:24

HTTPWebshellWeb服务器

2023-11-26 09:10:34

WebSocketgreeting​在线用户

2020-08-02 08:02:26

Webshell样本安全

2013-06-03 15:15:51

2012-04-19 10:04:20

ibmdw

2021-02-05 07:28:11

SpringbootNettyWebsocke

2013-04-12 10:05:49

HTML5WebSocket

2010-09-26 16:46:05

点赞
收藏

51CTO技术栈公众号