热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Django3使用WebSocket实现WebShell

△点击上方“Python猫”关注,回复“1”领取电子书剧照:《眷思量》作者:从零开始的程序员生活来源:https:www.c

△点击上方“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 osfrom django.core.asgi import get_asgi_application
from 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.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':breakif 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 = scopeself._receive = receiveself._send = sendself._client_state = State.CONNECTINGself._app_state = State.CONNECTING@propertydef headers(self):return Headers(self._scope)@propertydef scheme(self):return self._scope["scheme"]@propertydef path(self):return self._scope["path"]@propertydef query_params(self):return QueryParams(self._scope["query_string"].decode())@propertydef query_string(self) -> str:return self._scope["query_string"]@propertydef scope(self):return self._scopeasync 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.DISCONNECTEDelse:self._app_state = State.CONNECTEDelif 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.DISCONNECTEDawait 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.CONNECTEDelif 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.DISCONNECTEDreturn message

缝合怪

做为合格的代码搬运工,为了提高搬运效率还是要造点轮子填点坑的,如何将上面的 WebSocket 类与 paramiko 结合起来,实现从前端接受字符传递给远程主机,并同时接受返回呢?

import asyncio
import traceback
import paramiko
from webshell.ssh import Base, RemoteSSH
from 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_sessionself.ssh_session = ssh_sessionself.chanel_session = chanel_sessiondef 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_sessionself.chanel_session = chanel_sessionasync 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(&#39;utf-8&#39;)if not message:returnawait self.ws_session.send_text(message)async def web_to_ssh(self):# print(&#39;--------web_to_ssh------->&#39;)while True:# print(&#39;--------------->&#39;)if not self.chanel_session.active or not self.ws_session.status:returnawait asyncio.sleep(0.01)shell = await self.ws_session.receive_text()# print(&#39;-------shell-------->&#39;, shell)if self.chanel_session.active and self.chanel_session.send_ready():self.chanel_session.send(bytes(shell, &#39;utf-8&#39;))# print(&#39;--------------->&#39;, "end")async def ssh_to_web(self):# print(&#39;<--------ssh_to_web-----------&#39;)while True:# print(&#39;<-------------------&#39;)if not self.chanel_session.active:await self.ws_session.send_text(&#39;ssh closed&#39;)returnif not self.ws_session.status:returnawait asyncio.sleep(0.01)if self.chanel_session.recv_ready():message = self.chanel_session.recv(2048).decode(&#39;utf-8&#39;)# print(&#39;<---------message----------&#39;, message)if not len(message):continueawait self.ws_session.send_text(message)# print(&#39;<-------------------&#39;, "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("" + cols + "," + rows)});window.addEventListener(&#39;resize&#39;, this.onResize);}componentWillUnmount() {window.removeEventListener(&#39;resize&#39;, this.onResize);}onResize = () => {this.fitAddon.fit();}render() {return  this.terminal = ref as HTMLDivElement}>

;}
}

好了,废话不多少了,代码我放这里了webshell (https://github.com/aleimu/webshell) 欢迎 star/fork!

Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~

还不过瘾?试试它们

▲Python 协程与 Javascript 协程的对比

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

▲我在 GitHub 上读清华

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

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

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

如果你觉得本文有帮助

请慷慨分享点赞,感谢啦



推荐阅读
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 怀疑是每次都在新建文件,具体代码如下 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 程序员_阿里Antd藏圣诞节彩蛋 程序员被离职
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了阿里Antd藏圣诞节彩蛋程序员被离职相关的知识,希望对你有一定的参考价值。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
author-avatar
Amandadahl
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有