您当前的位置:首页 > 电脑百科 > 网络技术 > 网络技术

Django3 使用 WebSocket 实现 WebShell

时间:2021-09-13 11:01:15  来源:cnblog  作者:Python猫

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

来源:
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('<--------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 (
https://github.com/aleimu/webshell) 欢迎star/fork!



Tags:Django3   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
最近工作中需要开发前端操作远程虚拟机的功能,简称 WebShell。基于当前的技术栈为 react+django,调研了一会发现大部分的后端实现都是 django+channels 来实现 websocket 服务。...【详细内容】
2021-09-13  Tags: Django3  点击:(52)  评论:(0)  加入收藏
写在开头这篇文章是我阅读了很多其他人的教程和结合自己踩过的坑所作部分引用:Django快速部署简约版 v3.0Ubuntu安装MySQL8.0 允许远程访问Django项目用Nginx部署上线之后,出...【详细内容】
2020-07-06  Tags: Django3  点击:(50)  评论:(0)  加入收藏
▌简易百科推荐
写一个shell获取本机ip地址、网关地址以及dns信息。经常会遇到取本机ip、网关、dns地址,windows一个命令ipconfig /all全部获取到,但linux系统却并非如此。linux系统都自带ifc...【详细内容】
2021-12-27  K佬食古    Tags:shell   点击:(0)  评论:(0)  加入收藏
步骤1、配置 /etc/sysconfig/network-scripts/ifcfg-eth0 里的文件。it动力的CentOS下的ifcfg-eth0的配置详情:[root@localhost ~]# vim /etc/sysconfig/network-scripts/ifc...【详细内容】
2021-12-24  忆梦如风    Tags:网卡   点击:(9)  评论:(0)  加入收藏
1、查找当前目录下所有以.tar结尾的文件然后移动到指定目录find . -name “*.tar” -execmv {}./backup/ ;注解:find &ndash;name 主要用于查找某个文件名字,-exec 、xargs可...【详细内容】
2021-12-17  郭主任    Tags:运维   点击:(18)  评论:(0)  加入收藏
对于经常上网的朋友来说,除了手机购物上网,pc端玩网页游戏还是很多小伙伴首选的,但是有时候明明宽带链接上了,打开浏览器却出现上不了网的现象,下面小编要来跟大家说说电脑有网络...【详细内容】
2021-12-16  小白系统    Tags:网页无法打开   点击:(28)  评论:(0)  加入收藏
在访问像github、gitlab这样的外国网站时,很有可能会出现页面加载不出来或找不到页面的错误。这时候有的朋友就会以为是网络的问题,于是把Wifi断掉连上自己手机的热点,结果却还...【详细内容】
2021-12-15  启施技术IT狼叔    Tags:外网   点击:(14)  评论:(0)  加入收藏
网络地址来源:获取公网IP地址 https://ipip.yy.com/get_ip_info.phphttp://pv.sohu.com/cityjson?ie=utf-8http://www.ip168.com/json.do?view=myipaddress...【详细内容】
2021-12-15  韦廷华12    Tags:外网ip   点击:(14)  评论:(0)  加入收藏
准备好软件IPOP、用ENSP模拟一下华为交换机 启动交换机 <Huawei>sysEnter system view, return user view with Ctrl+Z.[Huawei]sysname FTPClient[FTPClient]interface vla...【详细内容】
2021-12-15  思源Edward    Tags:交换机   点击:(22)  评论:(0)  加入收藏
我们经常用到netstat命令查看主机连接状况,包括连接ip、端口、状态等,今天就练习下shell分析netsat结果。描述假设netstat命令运行的结果我们存储在nowcoder.txt里,格式如下:Pro...【详细内容】
2021-12-14  K佬食古    Tags:netstat   点击:(19)  评论:(0)  加入收藏
什么是滑动窗口?窗口是操作系统开辟的一块缓存空间,发送方在收到接收方ACK应答之前,必须在缓冲区保留已发送的数据,如果按期收到确认应答,数据就可以从缓冲区移除。什么是滑动窗...【详细内容】
2021-12-14  DifferentJava    Tags:TCP   点击:(28)  评论:(0)  加入收藏
概述日常管理华为路由设备过程中,难为会忘记设备登录密码,那么该如何重置设备登录密码吗?本期文章将全面向各位小伙伴总结分享。重置华为设备登录密码思路先行 采用console登录...【详细内容】
2021-12-10  onme0    Tags:   点击:(26)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条