• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    Django3基于WebSocket实现WebShell的详细过程

    前言

    最近工作中需要开发前端操作远程虚拟机的功能,简称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
    # 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!'
                    })

    实现

    上面的代码提供了思路,比较完整的可以参考这里 websockets-in-django-3-1 基本可以复用了
    其中最核心的实现部分我放下面:

    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

    缝合怪

    做为合格的代码搬运工,为了提高搬运效率还是要造点轮子填点坑的,如何将上面的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>;
        }
    }

    好了,废话不多少了,代码我放这里了webshell 欢迎star/fork!

    参考资料

    webshell

    django文档

    graphene-django文档

    django 异步视图

    websockets-in-django-3-1

    How to Add Websockets to a Django App without Extra Dependencies

    到此这篇关于Django3使用WebSocket实现WebShell的文章就介绍到这了,更多相关Django3实现WebShell内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    您可能感兴趣的文章:
    • Django websocket原理及功能实现代码
    • 详解Django3中直接添加Websockets方式
    • Django通过dwebsocket实现websocket的例子
    • 详解Django-channels 实现WebSocket实例
    • Django使用Channels实现WebSocket的方法
    上一篇:分析总结Python数据化运营KMeans聚类
    下一篇:如何使用python数据处理解决数据冲突和样本的选取
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

    Django3基于WebSocket实现WebShell的详细过程 Django3,基于,WebSocket,实现,