Django3使用WebSocket实现WebShell
这里 2024-10-19 12:50:13 原文
前言
最近工作中需要开发前端操作远程虚拟机的功能,简称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!'
})
实现
上面的代码提供了思路,比较完整的可以参考这里 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('<--------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>;
}
}
好了,废话不多少了,代码我放这里了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的更多相关文章
- express+websocket+exec+spawn=webshell
var child_process = require('child_process'); var ws = require("nodejs-websocket"); consol ...
- Django3.0 异步通信初体验
此前博主曾经写过一篇博文,介绍了Django3.0的新特性,其中最主要的就是加入对ASGI的支持,实现全双工的异步通信. 2019年12月2日,Django终于正式发布了3.0版本.怀着无比的期待,我 ...
- Django3.0 异步通信初体验(小结)
2019年12月2日,Django终于正式发布了3.0版本.怀着无比的期待,我们来尝试一下吧! (附ASGI官方文档地址:https://asgi.readthedocs.io/en/latest/e ...
- Django3.0 前瞻 支持异步通信
最近两年,Django的版本号提升得特别快,2.0还没有多久,很快就要到3.0了. 让我们先看看官方的路线图和时间表: 版本号 发布日期 停止更新日期 停止维护日期 3.0 2019-12 2020- ...
- WebSocket长连接
WebSocket长连接 1.概述 1.1 定义 1.2 原理 2.Django中配置WebSocket 2.1安装第三方法包 pip install channels 2.2 Django 中的配置 ...
- 一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务
一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务 原文转载自「刘悦的技术博客」https://v3u.cn/a_id_177 就在去年(2019年) ...
- 九、Django3的ASGI
九.Django3的ASGI 9.1.Web应用程序和web服务器 Web应用程序(Web)是一种能完成web业务逻辑,能让用户基于web浏览器访问的应用程序,它可以是一个实现http请求和响应功能的 ...
- 漫扯:从polling到Websocket
Http被设计成了一个单向的通信的协议,即客户端发起一个request,然后服务器回应一个response.这让服务器很为恼火:我特么才是老大,我居然不能给小弟发消息... 轮询 老大发火了,小弟们自 ...
- 细说WebSocket - Node篇
在上一篇提高到了 web 通信的各种方式,包括 轮询.长连接 以及各种 HTML5 中提到的手段.本文将详细描述 WebSocket协议 在 web通讯 中的实现. 一.WebSocket 协议 1. ...
随机推荐
- Java初级面试题整理
先看再点赞,给自己一点思考的时间,如果对自己有帮助,微信搜索[程序职场]关注这个执着的职场程序员. 我有什么:职场规划指导,技能提升方法,讲不完的职场故事,个人成长经验. 1. Java语言有哪些特点 ...
- C:汉字存储
问题 C语言中汉字如何存储?梳理思路! 答案 在计算机中,一个英文字符占1个字节,汉字占两个字节,如果用char字符数组存储字符时,需要在最后面自动加上一个字节的结束符"\0" 汉 ...
- C语言:按行读TXT文件
//搂行读取TXT #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_L ...
- [刘阳Java]_Web前端入门级练习_迅雷首页第一屏设计
今天接着上一篇文章<Web前端入门级练习_迅雷官宣网设计>正式开始迅雷首页第一版的设计.如果完成,则最终的效果图如下 第一步:先完成logo部分的设计 logo设计,我们会使用CSS的定位 ...
- ClickHouse学习系列之八【数据导入迁移&同步】
背景 在介绍了一些ClickHouse相关的系列文章之后,大致对ClickHouse有了比较多的了解.它是一款非常优秀的OLAP数据库,为了更好的来展示其强大的OLAP能力,本文将介绍一些快速导入大量 ...
- Day3 变量 运算符 及运算符的优先级
变量 什么是变量: 可以变化的量 Java 是一种强类型语言,每个变量都必须声明其类型. Java变量是程序中最基本的存储单位,其要素包括变量名,变量类型,作用域. 注意事项: 每个变量都有类型, 类 ...
- jquery性能优化建议-上篇
一.注意定义jQuery变量的时候添加var关键字这个不仅仅是jQuery,所有javascript开发过程中,都需要注意,请一定不要定义成如下:$loading = $('#loading'); / ...
- python3.7 jack棋牌11点实例
# -*- codeing: utf-8 -*- # Project: 棋牌游戏11点 # Author: jack # Version: 2.2 # Start Time: 2021-07-24 i ...
- Java 加载、操作和保存WPS文字文档
本文通过Java程序代码来展示如何来加载.操作及保存WPS格式的文字文档. 一.基本步骤:加载时,通过流加载WPS文字文档,完成相关文字操作后,再将结果文档保存到流,将流写入WPS文档,闭关闭流. 二 ...
- 【递归+树】FBI树
题目描述 我们可以把由"0"和"1"组成的字符串分为三类:全"0"串称为BB串,全"1"串称为I串,既含"0& ...