前言

最近工作中需要开发前端操作远程虚拟机的功能,简称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 就是一个很好的实例

, 但过于简单........:

思路

  1. # asgi.py
  2. import os
  3. from django.core.asgi import get_asgi_application
  4. from websocket_app.websocket import websocket_application
  5. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'websocket_app.settings')
  6. django_application = get_asgi_application()
  7. async def application(scope, receive, send):
  8. if scope['type'] == 'http':
  9. await django_application(scope, receive, send)
  10. elif scope['type'] == 'websocket':
  11. await websocket_application(scope, receive, send)
  12. else:
  13. raise NotImplementedError(f"Unknown scope type {scope['type']}")
  14. # websocket.py
  15. async def websocket_application(scope, receive, send):
  16. pass
  1. # websocket.py
  2. async def websocket_application(scope, receive, send):
  3. while True:
  4. event = await receive()
  5. if event['type'] == 'websocket.connect':
  6. await send({
  7. 'type': 'websocket.accept'
  8. })
  9. if event['type'] == 'websocket.disconnect':
  10. break
  11. if event['type'] == 'websocket.receive':
  12. if event['text'] == 'ping':
  13. await send({
  14. 'type': 'websocket.send',
  15. 'text': 'pong!'
  16. })

实现

上面的代码提供了思路,比较完整的可以参考这里 websockets-in-django-3-1 基本可以复用了

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

  1. class WebSocket:
  2. def __init__(self, scope, receive, send):
  3. self._scope = scope
  4. self._receive = receive
  5. self._send = send
  6. self._client_state = State.CONNECTING
  7. self._app_state = State.CONNECTING
  8. @property
  9. def headers(self):
  10. return Headers(self._scope)
  11. @property
  12. def scheme(self):
  13. return self._scope["scheme"]
  14. @property
  15. def path(self):
  16. return self._scope["path"]
  17. @property
  18. def query_params(self):
  19. return QueryParams(self._scope["query_string"].decode())
  20. @property
  21. def query_string(self) -> str:
  22. return self._scope["query_string"]
  23. @property
  24. def scope(self):
  25. return self._scope
  26. async def accept(self, subprotocol: str = None):
  27. """Accept connection.
  28. :param subprotocol: The subprotocol the server wishes to accept.
  29. :type subprotocol: str, optional
  30. """
  31. if self._client_state == State.CONNECTING:
  32. await self.receive()
  33. await self.send({"type": SendEvent.ACCEPT, "subprotocol": subprotocol})
  34. async def close(self, code: int = 1000):
  35. await self.send({"type": SendEvent.CLOSE, "code": code})
  36. async def send(self, message: t.Mapping):
  37. if self._app_state == State.DISCONNECTED:
  38. raise RuntimeError("WebSocket is disconnected.")
  39. if self._app_state == State.CONNECTING:
  40. assert message["type"] in {SendEvent.ACCEPT, SendEvent.CLOSE}, (
  41. 'Could not write event "%s" into socket in connecting state.'
  42. % message["type"]
  43. )
  44. if message["type"] == SendEvent.CLOSE:
  45. self._app_state = State.DISCONNECTED
  46. else:
  47. self._app_state = State.CONNECTED
  48. elif self._app_state == State.CONNECTED:
  49. assert message["type"] in {SendEvent.SEND, SendEvent.CLOSE}, (
  50. 'Connected socket can send "%s" and "%s" events, not "%s"'
  51. % (SendEvent.SEND, SendEvent.CLOSE, message["type"])
  52. )
  53. if message["type"] == SendEvent.CLOSE:
  54. self._app_state = State.DISCONNECTED
  55. await self._send(message)
  56. async def receive(self):
  57. if self._client_state == State.DISCONNECTED:
  58. raise RuntimeError("WebSocket is disconnected.")
  59. message = await self._receive()
  60. if self._client_state == State.CONNECTING:
  61. assert message["type"] == ReceiveEvent.CONNECT, (
  62. 'WebSocket is in connecting state but received "%s" event'
  63. % message["type"]
  64. )
  65. self._client_state = State.CONNECTED
  66. elif self._client_state == State.CONNECTED:
  67. assert message["type"] in {ReceiveEvent.RECEIVE, ReceiveEvent.DISCONNECT}, (
  68. 'WebSocket is connected but received invalid event "%s".'
  69. % message["type"]
  70. )
  71. if message["type"] == ReceiveEvent.DISCONNECT:
  72. self._client_state = State.DISCONNECTED
  73. return message

缝合怪

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

  1. import asyncio
  2. import traceback
  3. import paramiko
  4. from webshell.ssh import Base, RemoteSSH
  5. from webshell.connection import WebSocket
  6. class WebShell:
  7. """整理 WebSocket 和 paramiko.Channel,实现两者的数据互通"""
  8. def __init__(self, ws_session: WebSocket,
  9. ssh_session: paramiko.SSHClient = None,
  10. chanel_session: paramiko.Channel = None
  11. ):
  12. self.ws_session = ws_session
  13. self.ssh_session = ssh_session
  14. self.chanel_session = chanel_session
  15. def init_ssh(self, host=None, port=22, user="admin", passwd="admin@123"):
  16. self.ssh_session, self.chanel_session = RemoteSSH(host, port, user, passwd).session()
  17. def set_ssh(self, ssh_session, chanel_session):
  18. self.ssh_session = ssh_session
  19. self.chanel_session = chanel_session
  20. async def ready(self):
  21. await self.ws_session.accept()
  22. async def welcome(self):
  23. # 展示Linux欢迎相关内容
  24. for i in range(2):
  25. if self.chanel_session.send_ready():
  26. message = self.chanel_session.recv(2048).decode('utf-8')
  27. if not message:
  28. return
  29. await self.ws_session.send_text(message)
  30. async def web_to_ssh(self):
  31. # print('--------web_to_ssh------->')
  32. while True:
  33. # print('--------------->')
  34. if not self.chanel_session.active or not self.ws_session.status:
  35. return
  36. await asyncio.sleep(0.01)
  37. shell = await self.ws_session.receive_text()
  38. # print('-------shell-------->', shell)
  39. if self.chanel_session.active and self.chanel_session.send_ready():
  40. self.chanel_session.send(bytes(shell, 'utf-8'))
  41. # print('--------------->', "end")
  42. async def ssh_to_web(self):
  43. # print('<--------ssh_to_web-----------')
  44. while True:
  45. # print('<-------------------')
  46. if not self.chanel_session.active:
  47. await self.ws_session.send_text('ssh closed')
  48. return
  49. if not self.ws_session.status:
  50. return
  51. await asyncio.sleep(0.01)
  52. if self.chanel_session.recv_ready():
  53. message = self.chanel_session.recv(2048).decode('utf-8')
  54. # print('<---------message----------', message)
  55. if not len(message):
  56. continue
  57. await self.ws_session.send_text(message)
  58. # print('<-------------------', "end")
  59. async def run(self):
  60. if not self.ssh_session:
  61. raise Exception("ssh not init!")
  62. await self.ready()
  63. await asyncio.gather(
  64. self.web_to_ssh(),
  65. self.ssh_to_web()
  66. )
  67. def clear(self):
  68. try:
  69. self.ws_session.close()
  70. except Exception:
  71. traceback.print_stack()
  72. try:
  73. self.ssh_session.close()
  74. except Exception:
  75. traceback.print_stack()

前端

xterm.js 完全满足,搜索下找个看着简单的就行.

  1. export class Term extends React.Component {
  2. private terminal!: HTMLDivElement;
  3. private fitAddon = new FitAddon();
  4. componentDidMount() {
  5. const xterm = new Terminal();
  6. xterm.loadAddon(this.fitAddon);
  7. xterm.loadAddon(new WebLinksAddon());
  8. // using wss for https
  9. // const socket = new WebSocket("ws://" + window.location.host + "/api/v1/ws");
  10. const socket = new WebSocket("ws://localhost:8000/webshell/");
  11. // socket.onclose = (event) => {
  12. // this.props.onClose();
  13. // }
  14. socket.onopen = (event) => {
  15. xterm.loadAddon(new AttachAddon(socket));
  16. this.fitAddon.fit();
  17. xterm.focus();
  18. }
  19. xterm.open(this.terminal);
  20. xterm.onResize(({ cols, rows }) => {
  21. socket.send("<RESIZE>" + cols + "," + rows)
  22. });
  23. window.addEventListener('resize', this.onResize);
  24. }
  25. componentWillUnmount() {
  26. window.removeEventListener('resize', this.onResize);
  27. }
  28. onResize = () => {
  29. this.fitAddon.fit();
  30. }
  31. render() {
  32. return <div className="Terminal" ref={(ref) => this.terminal = ref as HTMLDivElement}></div>;
  33. }
  34. }

好了,废话不多少了,代码我放这里了webshell 欢迎star/fork!

参考资料

Django3使用WebSocket实现WebShell的更多相关文章

  1. express+websocket+exec+spawn=webshell

    var child_process = require('child_process'); var ws = require("nodejs-websocket"); consol ...

  2. Django3.0 异步通信初体验

    此前博主曾经写过一篇博文,介绍了Django3.0的新特性,其中最主要的就是加入对ASGI的支持,实现全双工的异步通信. 2019年12月2日,Django终于正式发布了3.0版本.怀着无比的期待,我 ...

  3. Django3.0 异步通信初体验(小结)

    2019年12月2日,Django终于正式发布了3.0版本.怀着无比的期待,我们来尝试一下吧! (附ASGI官方文档地址:https://asgi.readthedocs.io/en/latest/e ...

  4. Django3.0 前瞻 支持异步通信

    最近两年,Django的版本号提升得特别快,2.0还没有多久,很快就要到3.0了. 让我们先看看官方的路线图和时间表: 版本号 发布日期 停止更新日期 停止维护日期 3.0 2019-12 2020- ...

  5. WebSocket长连接

    WebSocket长连接 1.概述 1.1 定义 1.2 原理 2.Django中配置WebSocket 2.1安装第三方法包 pip install channels 2.2 Django 中的配置 ...

  6. 一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务

    一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务 原文转载自「刘悦的技术博客」https://v3u.cn/a_id_177 就在去年(2019年) ...

  7. 九、Django3的ASGI

    九.Django3的ASGI 9.1.Web应用程序和web服务器 Web应用程序(Web)是一种能完成web业务逻辑,能让用户基于web浏览器访问的应用程序,它可以是一个实现http请求和响应功能的 ...

  8. 漫扯:从polling到Websocket

    Http被设计成了一个单向的通信的协议,即客户端发起一个request,然后服务器回应一个response.这让服务器很为恼火:我特么才是老大,我居然不能给小弟发消息... 轮询 老大发火了,小弟们自 ...

  9. 细说WebSocket - Node篇

    在上一篇提高到了 web 通信的各种方式,包括 轮询.长连接 以及各种 HTML5 中提到的手段.本文将详细描述 WebSocket协议 在 web通讯 中的实现. 一.WebSocket 协议 1. ...

随机推荐

  1. WSL中使用systemctl报错问题

    Windows10里面自带的wsl中安装docker后不支持systemctl命令.需要更换命令,用Sysvinit的命令代替systemd,命令如下: Systemd command Sysvini ...

  2. 深入理解Java容器——HashMap

    目录 存储结构 初始化 put resize 树化 get 为什么equals和hashCode要同时重写? 为何HashMap的数组长度一定是2的次幂? 线程安全 参考 存储结构 JDK1.8前是数 ...

  3. Hadoop知识总结

    ------------恢复内容开始------------ Hadoop知识点 Hadoop知识点什么是HadoopHadoop和Spark差异Hadoop常见版本,有哪些特点,一般是如何进行选择H ...

  4. PYTHON 连接SQL2008 导出到EXCEL

    #import pymssql from datetime import datetime import pyodbc import os current_dir = os.path.abspath( ...

  5. 深入理解javascript按值传递与按引用传递

    https://segmentfault.com/a/1190000012829900

  6. 【强连通分量】Proving Equivalences

    [题目链接]hdu-2767 [题目描述] Consider the following exercise, found in a generic linear algebra textbook. L ...

  7. odoo12常用的方法

    2019-09-13 今天是中秋节,星期五 #自定义显示名称 def name_get(self): result = [] for order in self: rec_name = "% ...

  8. Docker run 命令参数及使用

    Docker run 命令参数及使用 Docker run :创建一个新的容器并运行一个命令 语法 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] OPTI ...

  9. maven 工程构建 之_____<dependencyManagement>标签

    <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://mave ...

  10. SpringBoot自动装配-Condition

    1. 简介 @Conditional注解在Spring4.0中引入,其主要作用就是判断条件是否满足,从而决定是否初始化并向容器注册Bean. 2. 定义 2.1 @Conditional @Condi ...