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就是一个很好的实例,但过于简单……

思路 

# 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  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
# 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!'  
                }) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

实现

上面的代码提供了思路

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

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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.

缝合怪

做为合格的代码搬运工,为了提高搬运效率还是要造点轮子填点坑的,如何将上面的 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=Noneport=22user="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() 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.

前端

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> 
    }  
 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

 

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

2014-12-16 10:28:49

2021-03-25 08:29:33

SpringBootWebSocket即时消息

2023-08-14 08:01:12

websocket8g用户

2016-03-14 12:33:46

2017-07-11 13:58:10

WebSocket

2022-06-28 08:37:07

分布式服务器WebSocket

2024-03-21 08:34:49

Vue3WebSocketHTTP

2023-11-17 09:35:58

2023-07-26 07:28:55

WebSocket服务器方案

2024-09-02 09:31:19

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服务器

2020-08-02 08:02:26

Webshell样本安全

2023-11-26 09:10:34

WebSocketgreeting​在线用户

2025-01-27 12:31:23

PythonLocustWebSocket

2013-06-03 15:15:51

2024-09-13 09:55:38

RustP2P网

2024-09-11 08:35:54

点赞
收藏

51CTO技术栈公众号