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

最近工作中需要开发前端操作远程虚拟机的功能,简称WebShell,普通应用大部分用的都是wsgi.py配合nginx部署线上服务.这次主要使用asgi.py,具体实现过程跟随小编一起看看吧

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 osfrom django.core.asgi import get_asgi_applicationfrom websocket_app.websocket import websocket_applicationos.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.pyasync def websocket_application(scope, receive, send):    pass

# websocket.pyasync 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 asyncioimport tracebackimport paramikofrom webshell.ssh import Base, RemoteSSHfrom webshell.connection import WebSocketclass 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 <p className=\"Terminal\" ref={(ref) => this.terminal = ref as HTMLDivElement}></p>;    }}

好了,废话不多少了,代码我放这里了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内容请搜索趣讯吧以前的文章或继续浏览下面的相关文章希望大家以后多多支持趣讯吧!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请发送邮件至 55@qq.com 举报,一经查实,本站将立刻删除。转转请注明出处:https://www.szhjjp.com/n/17358.html

(0)
nan
上一篇 2021-08-26
下一篇 2021-08-26

相关推荐

发表回复

登录后才能评论