Django3 使用 WebSocket 实现 WebShell

2021-09-30

前言

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

实现

上面的代码提供了思路

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

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 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>;    }}

原文链接:https://www.cnblogs.com/lgjbky/p/15186188.html

(0)

相关推荐

  • WebSocket

    目录 一.WebSocket -网络通信协议 1-1 简介 二.Websockets servers and clients in Python 2-0 connect 2-0-1 建立一对一短连接 ...

  • asyncio 并发任务,如何限制协程的并发数?

    作者:kingname 来源:未闻Code 有同学问,如果使用 asyncio + httpx 实现并发请求,怎么限制请求的频率呢?怎么限制最多只能有 x 个请求同时发出呢?我们今天给出两种方案. 提 ...

  • [js] 第103天 你是如何更好地处理Async/Await的异常的?

    今日试题: 你是如何更好地处理Async/Await的异常的? 此开源项目四大宗旨:勤思考,多动手,善总结,能坚持 <论语>,曾子曰:"吾日三省吾身"(我每天多次反省自 ...

  • 爬虫神器 Pyppeteer 介绍及爬取某商城实战

    重磅干货,第一时间送达 作者:叶庭云,来自读者投稿 编辑:Lemon 出品:Python数据之道 提起 selenium 想必大家都不陌生,作为一款知名的 Web 自动化测试框架,selenium 支 ...

  • 第101天: Python asyncio

    异步IO之asyncio 异步IO:当发起一个 IO 操作时,并不需要等待它的结束,程序可以去做其他事情,当这个 IO 操作结束时,会发起一个通知. 在 Python 中可以使用 asyncio 模块 ...

  • Python3+WebSockets实现WebSocket通信

    一.说明 1.1 背景说明 前段时间同事说云平台通信使用了个websocket的东西,今天抽空来看一下具体是怎么个通信过程. 从形式上看,websocket是一个应用层协议,socket是数据链路层. ...

  • 第102天: Python异步之aiohttp

    什么是 aiohttp?一个异步的 HTTP 客户端\服务端框架,基于 asyncio 的异步模块.可用于实现异步爬虫,更快于 requests 的同步爬虫. 安装 pip install aioht ...

  • Soul网关websocket同步数据

    websocket同步数据 初始化属性部分 首先启动soul-admin项目,然后启动soul-boostrap项目,可以明显发现websocket连接成功 奇怪的是,使用昨天测试SpringClou ...

  • [软技能] 第80天 你知道什么是websocket吗?它有什么应用场景?

    今日试题: 你知道什么是websocket吗?它有什么应用场景? 此开源项目四大宗旨:勤思考,多动手,善总结,能坚持 <论语>,曾子曰:"吾日三省吾身"(我每天多次反省 ...

  • iNeuOS工业互联平台,.NETCore开发的视频服务组件iNeuVideo,RTSP转WebSocket

    目       录 1.      概述... 2 2.      将来集成到iNeuOS平台演示... 3 3.      iNeuVideo结构... 3 4.      iNeuVideo部署及 ...

  • python测试开发django-3.url配置

    前言 我们在浏览器访问一个网页是通过url地址去访问的,django管理url配置是在urls.py文件.当一个页面数据很多时候,通过会有翻页的情况,那么页数是不固定的,如:page=1. 也就是ur ...

  • python测试开发django-81.dwebsocket实现websocket

    前言 HTTP 协议有一个缺陷:通信只能由客户端发起,做不到服务器主动向客户端推送信息. WebSocket 协议它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是 ...

  • 面试题-websocket 接口如何测试?

    前言 websocket 接口如何测试呢? 简单的可以用在线的网页测试,也可以自己写个web客户端测,也可以用python代码测. 什么是 websocket 接口? 我们平常接触最多的是 http ...

  • Django实战: channels+celery+websocket打造聊天机器人(附源码)

    原创 大江狗 Python Web与Django开发 Channels是Django团队研发的一个给Django提供websocket支持的框架,使用它我们可以轻松开发需要长链接的实时通讯应用.本文在 ...

  • 防守方攻略:四大主流WebShell管理工具分析

    前言 在网络安全实战攻防演练中,只有了解攻击方的攻击思路和运用武器,防守方才能有效应对.以WebShell 为例,由于企业对外提供服务的应用通常以Web形式呈现,因此Web站点经常成为攻击者的攻击目标 ...

  • WebSocket是什么原理?为什么可以实现持久连接?

    很高兴能够看到和回答这个问题! WebSocket是什么原理? WebSocket通过常见的HTTP协议进行数据连接,一般走的是TCP通道,WebSocket是一个允许单TCP连接之间全双工通信的协议 ...