Python社区  »  Django

Django3 使用 WebSocket 实现 WebShell

Python猫 • 6 天前 • 69 次点击  
△点击上方“Python猫”关注 ,回复“1”领取电子书
剧照:《眷思量》

作者:从零开始的程序员生活

来源:https://www.cnblogs.com/lgjbky/p/15186188.html

前言

最近工作中需要开发前端操作远程虚拟机的功能,简称 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(https://jaydenwindle.com/writing/django-websockets-zero-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 (https://aliashkevich.com/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('
        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('
                if not len(message):
                    continue
                await self.ws_session.send_text(message)
            # print('

    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("" + 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 (https://github.com/aleimu/webshell) 欢迎 star/fork!
Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~


还不过瘾?试试它们




Python 协程与 JavaScript 协程的对比

把 Redis 当作队列用,真的合适吗?

我在 GitHub 上读清华

如何快速从 JSON 中找到特定的 Key?

Python进阶:用websocket构建实时日志跟踪器

吐槽:Python正在从简明转向臃肿,从实用转向媚俗


如果你觉得本文有帮助
请慷慨分享点赞 ,感谢啦
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/119887
 
69 次点击  
分享到微博