文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Django3 使用 WebSocket 实现 WebShell

2024-12-02 17:34

关注

大致看了下觉得这不够有趣,翻了翻 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运维内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯