网络编程
位置:首页>> 网络编程>> Python编程>> Django3基于WebSocket实现WebShell的详细过程

Django3基于WebSocket实现WebShell的详细过程

作者:从零开始的程序员生活  发布时间:2021-12-01 21:34:31 

标签:Django,WebShell,WebSocket

前言

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

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

0
投稿

猜你喜欢

手机版 网络编程 asp之家 www.aspxhome.com