加入收藏 | 设为首页 | 会员中心 | 我要投稿 宜春站长网 (https://www.0795zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 系统 > 正文

Django3 运用 WebSocket 实现 WebShell

发布时间:2021-11-22 13:51:26 所属栏目:系统 来源:互联网
导读:最近工作中需要开发前端操作远程虚拟机的功能,简称 WebShell。基于当前的技术栈为 react+django,调研了一会发现大部分的后端实现都是 django+channels 来实现 websocket 服务。 大致看了下觉得这不够有趣,翻了翻 django 的官方文档发现 django 原生是不支
最近工作中需要开发前端操作远程虚拟机的功能,简称 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!'  
                })
实现
上面的代码提供了思路
 
其中最核心的实现部分我放下面:
 
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>;  
    }  
}  

(编辑:宜春站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    热点阅读