Django使用Channels实现WebSSH网页终端,实现SSH堡垒机雏形

更多内容请点击 我的博客 查看,欢迎来访。

本教程基于《Django使用Channels实现WebSocket消息通知功能》

逻辑简述

xterm.js : 前端模拟 shell 终端的一个库,当用户每输入一个键,就向后端发送该数据

paramiko : Python 下对 ssh2 封装的一个库,可以使用他来远程连接主机

  1. 用户首先通过网页上的WebSocket连接到后端,后端再通过SSH连接到远程主机,实现一个长连接;
  2. 连接成功后, xterm.js 在浏览器中模拟shell终端,监听用户按键,将每次按键输入通过已连接好的WebSocket;
  3. 后端通过WebSocket收到数据后,将用户输入的内容通过 paramiko 建立的 SSH 通道连接到远程主机上执行;
  4. paramiko 将远程主机上的处理结果返回给后端;
  5. 后端将返回结果通过WebSocket返回用户页面;
  6. xterm.js 将从WebSocket接收的数据显示到到模拟终端中;

创建webssh app

创建app。名为webssh

将应用添加到 settings.py

INSTALLED_APPS = [
    # 。。。。
    'webssh.apps.WebsshConfig',  # 网页远程ssh终端
]

修改应用下的 apps.py

from django.apps import AppConfig


class WebsshConfig(AppConfig):
    name = 'webssh'
    verbose_name = '远程终端'

修改应用下的 __init__.py

default_app_config = 'webssh.apps.WebsshConfig'

webssh逻辑代码

前端页面链接webssh

{% extends 'pxectrl/base-pxectrl.html' %}
{% load static %}
{% block title %}远程终端{% endblock %}

{% block css %}
    <link href="{% static 'xterm/xterm.css' %}" rel="stylesheet">
    <link href="{% static 'hadmin/css/plugins/toastr/toastr.min.css' %}" rel="stylesheet">
{% endblock %}

{% block content %}
    <div class="wrapper wrapper-content animated fadeInRight">
        <div class="row">
            <div class="col-sm-12">
                <div class="ibox float-e-margins" id="id-box">
                    <div class="ibox-title">
                        <h5>在线终端</h5>
                    </div>
                    <div class="ibox-content" id="id-content">
                        <div class="row">
                            <form role="form" id="id-form" autocomplete="off">
                                <div class="col-md-4">
                                    <div class="form-group">
                                        <label>用户名</label>
                                        <input type="text" placeholder="User" id="id-user" class="form-control"
                                               autocomplete="off" value="user">
                                    </div>
                                </div>
                                <div class="col-sm-4">
                                    <div class="form-group">
                                        <label>主机地址</label>
                                        <input type="text" placeholder="Host" id="id-host" class="form-control" required
                                               autocomplete="off" value="192.168.96.20">
                                    </div>

                                </div>
                                <div class="col-sm-4">
                                    <div class="form-group">
                                        <label>端口</label>
                                        <input type="text" placeholder="Port" id="id-port" class="form-control"
                                               value="22" required autocomplete="off">
                                    </div>

                                </div>
                                <div class="col-sm-12">
                                    <div class="form-group">
                                        <label>认证类型</label> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                                        <label class="radio-inline">
                                            <input type="radio" name="auth" id="id-use-pwd" value="pwd" checked> 密码认证
                                        </label>
                                        <label class="radio-inline">
                                            <input type="radio" name="auth" id="id-use-key" value="key"> 秘钥认证
                                        </label>

                                    </div>
                                </div>
                                <div class="col-sm-6">
                                    <div class="form-group">
                                        <label id="id-auth-type">登录密码</label>
                                        <input type="password" placeholder="Pwd" id="id-pwd" class="form-control"
                                               required autocomplete="off" value="">
                                    </div>
                                </div>
                                <div class="col-sm-6 hide" id="id-show-upload">
                                    <div class="form-group">
                                        <label>密钥文件</label>
                                        <input class="filestyle" id="id-key-file" type="file">
                                    </div>
                                </div>
                                <div class="col-sm-12">
                                    注意:
                                    <p style="color: red; font-size: 10px">1、当认证类型为密码认证时, 秘钥文件上传将不可用</p>
                                    <p style="color: red; font-size: 10px">2、当认证类型为秘钥认证时, 如果密码输入框不为空,
                                        则密码输入框的内容将作为秘钥的解密密码</p>
                                    <button class="btn btn-sm btn-info" type="button" onclick="connectWebSocket()">
                                        <strong>连接</strong>
                                    </button>
                                </div>
                            </form>

                        </div>
                        <br>
                        <hr>
                        <br>
                        <table class="table table-striped">
                            <thead>
                            <tr>
                                <th>#</th>
                                <th>服务器</th>
                                <th>用途</th>
                                <th>SSH连接</th>
                            </tr>
                            </thead>
                            <tbody>
                            <tr>
                                <td>1</td>
                                <td>192.168.96.20</td>
                                <td>PXE服务器</td>
                                <td>
                                    <button type="button" class="btn btn-info btn-sm"
                                            onclick="connectWebSocket(host_id=1)">连接
                                    </button>
                                    <button type="button" class="btn btn-primary btn-sm"
                                            onclick="connectWebSocket(host_id=1, team=true)">协作连接
                                    </button>
                                </td>
                            </tr>
                            <tr>
                                <td>2</td>
                                <td>192.168.96.21</td>
                                <td>Samba服务器:镜像从服务器</td>
                                <td>
                                    <button type="button" class="btn btn-info btn-sm"
                                            onclick="connectWebSocket(host_id=2)">连接
                                    </button>
                                    <button type="button" class="btn btn-primary btn-sm"
                                            onclick="connectWebSocket(host_id=2, team=true)">协作连接
                                    </button>
                                </td>
                            </tr>
                            </tbody>
                        </table>

                    </div>
                    <div class="ibox-content hide" id="id-ssh-content" style="padding: 0;">
                        <p style="position:absolute; padding: 0; right:0; margin-right:17px; text-align: center; z-index: 999">
                            <button class="btn btn-danger btn-lg" id="id-close-conn">关闭连接</button>
                        </p>
                        <div id="terminal"></div>
                    </div>
                </div>
            </div>

        </div>


    </div>
{% endblock %}

{% block js %}
    <!--首先引入jquery-->
    <!-- 图片上传按钮样式 -->
    <script src="{% static 'hadmin/js/plugins/bootstrap-filestyle/bootstrap-filestyle.min.js' %}"></script>
    <script>
        //上传文件
        $(":file").filestyle({btnClass: "btn-default", text: "更改"});
    </script>
    <script src="{% static 'xterm/xterm.js' %}"></script>
    <script src="{% static 'hadmin/js/plugins/toastr/toastr.min.js' %}"></script>
    <script>
        toastr.options = { // toastr配置
            "closeButton": true,
            "debug": false,
            "progressBar": true,
            "positionClass": "toast-top-center",
            "showDuration": "400",
            "hideDuration": "1000",
            "timeOut": "3000",
            "extendedTimeOut": "1000",
            "showEasing": "swing",
            "hideEasing": "linear",
            "showMethod": "fadeIn",
            "hideMethod": "fadeOut"
        };
    </script>

    <script>
        $("#id-use-pwd").click(function () {
            $('#id-auth-type').html('登录密码');
            $('#id-show-upload').addClass('hide')
        });
        $("#id-use-key").click(function () {
            $('#id-auth-type').html('解密密码');
            $('#id-show-upload').removeClass('hide')
        })
    </script>

    <script>
        function get_box_size() {
            let init_width = 9;
            let init_height = 22;

            let windows_width = $('#id-box').width();
            let windows_height = $(window).height();

            return {
                cols: Math.floor(windows_width / init_width),
                rows: Math.floor(windows_height / init_height),
            }
        }

        function connectWebSocket(host_id = null, team = false) {
            let cols = get_box_size().cols;
            let rows = get_box_size().rows;
            console.log(cols);

            //根据div的大小初始化终端,待WebSocket连接上后,显示终端
            let term = new Terminal(
                {
                    cols: cols,
                    rows: rows,
                    useStyle: true,
                    cursorBlink: true
                }
            );

            //建立WebSocket连接
            if (host_id === null) {
                //获取表单中的信息,并去掉两端空格
                let host = $.trim($('#id-host').val());
                if (host === '') {
                    toastr.warning('主机地址不能为空', '提示');
                    return;
                }
                let port = $.trim($('#id-port').val());
                if (port === '') {
                    toastr.warning('端口不能为空', '提示');
                    return;
                }
                let user = $.trim($('#id-user').val());
                if (user === '') {
                    toastr.warning('用户名不能为空', '提示');
                    return;
                }
                let auth = $("input[name='auth']:checked").val();
                console.log('选择认证方式:' + auth);
                let sshkey_filename = '';
                if (auth === 'key') {
                    //上传密钥文件
                    let key_file = $('#id-key-file')[0].files[0];
                    let csrf = '{{ csrf_token }}';
                    let formData = new FormData();
                    formData.append('key_file', key_file);
                    formData.append('csrfmiddlewaretoken', csrf);
                    $.ajax({
                        url: '{% url "webssh:upload_ssh_key" %}',
                        type: 'post',
                        data: formData,
                        async: false,
                        processData: false,
                        contentType: false,
                        mimeType: 'multipart/form-data',
                        success: function (data) {
                            sshkey_filename = data;  //返回保存文件的名称
                        }
                    });
                }

                let pwd = $.trim($('#id-pwd').val());
                pwd = window.btoa(pwd); //加密密码传输

                //组装为ssh连接参数
                let ssh_args = `user=${user}&host=${host}&port=${port}&auth=${auth}&pwd=${pwd}&sshkey_filename=${sshkey_filename}`;
                console.log(ssh_args);


                let ws_scheme = window.location.protocol === "https:" ? "wss" : "ws"; //获取协议
                let ws_port = (window.location.port) ? (':' + window.location.port) : '';  // 获取端口
                ws = new WebSocket(ws_scheme + '://' + window.location.host + ws_port + '/ws/webssh/?' + ssh_args + `&width=${cols}&height=${rows}`);
            } else {
                //指定服务器id连接
                if (team) {
                    let ws_scheme = window.location.protocol === "https:" ? "wss" : "ws"; //获取协议
                    let ws_port = (window.location.port) ? (':' + window.location.port) : '';  // 获取端口
                    ws = new WebSocket(ws_scheme + '://' + window.location.host + ws_port + `/ws/webssh/${host_id}/` + `?width=${cols}&height=${rows}&team=${team}`);
                } else {
                    let ws_scheme = window.location.protocol === "https:" ? "wss" : "ws"; //获取协议
                    let ws_port = (window.location.port) ? (':' + window.location.port) : '';  // 获取端口
                    ws = new WebSocket(ws_scheme + '://' + window.location.host + ws_port + `/ws/webssh/${host_id}/` + `?width=${cols}&height=${rows}`);
                }

            }

            //打开websocket连接,并打开终端
            ws.onopen = function () {
                console.log('WebSocket建立连接,打开Web终端');
                $('#id-content').addClass('hide');
                $('#id-ssh-content').removeClass('hide');

                term.open(document.getElementById('terminal'));
            };
            ws.onclose = function (e) {
                console.error('WebSocket关闭连接,关闭Web终端');
                toastr.success('SSH连接已关闭', '消息');
                //term.write(message);
                setTimeout(function () {
                    window.location.reload();
                }, 3000);
            };

            //读取服务器发送的数据并写入web终端
            ws.onmessage = function (e) {
                console.log('WebSocket接收消息,ssh交互中');
                let data = JSON.parse(e.data);
                console.log(data);
                let message = data['message'];
                if (data.flag === 'msg') {
                    term.write(message);
                } else if (data.flag === 'fail') {
                    term.write(message);  //连接ssh的异常提示
                    toastr.error(message + "返回登录页", '失败');
                    setTimeout(function () {
                        window.location.reload();
                    }, 5000);
                } else if (data.flag === 'user') {
                    toastr.info(message, '消息');
                } else if (data.flag === 'error') {
                    toastr.error(message, '失败');
                    //term.write(message);
                    setTimeout(function () {
                        window.location.reload();
                    }, 5000);

                }

            };

            //向服务器发送数据,flag=1
            term.on('data', function (data) {
                //data为每个按键输入内容,例如按A,就传递给后端:{'flag': 1, 'data': 'a', 'cols': None, 'rows': None}
                let send_data = JSON.stringify({
                    'flag': 'entered_key',
                    'entered_key': data,
                    'cols': null,
                    'rows': null
                });
                //向WebSocket发送消息,也就是输入的每一个按键
                ws.send(send_data)
            });

            //终端右上角显示的关闭连接安装,当点击是,关闭ws
            $('#id-close-conn').click(function () {
                ws.close();
            });

            // 监听浏览器窗口, 根据浏览器窗口大小修改终端大小
            $(window).resize(function () {
                let cols = get_box_size().cols;
                let rows = get_box_size().rows;
                console.log(`更改显示终端窗口大小,行${rows}${cols}`);
                let send_data = JSON.stringify({'flag': 'resize', 'cols': cols, 'rows': rows});
                ws.send(send_data);
                term.resize(cols, rows) //调整页面终端大小
            })
        }
    </script>
{% endblock %}

应用视图

import time
import random
import hashlib
import os
from django.conf import settings
from django.shortcuts import render, HttpResponse


def index(request):
    return render(request, 'webssh/index.html')


def unique():
    ctime = str(time.time())
    salt = str(random.random())
    m = hashlib.md5(bytes(salt, encoding='utf-8'))
    m.update(bytes(ctime, encoding='utf-8'))
    return m.hexdigest()


def upload_ssh_key(request):
    if request.method == 'POST':
        key_file = request.FILES.get('key_file')
        if not key_file:
            return HttpResponse('')
        print(type(key_file.read()))
        ssh_key = key_file.read().decode('utf-8')  # 获取上传文件的内容

        sshkey_filename = unique()
        print('文件保存为唯一名称:', sshkey_filename)

        ssh_key_path = os.path.join(settings.MEDIA_ROOT, 'sshkey')

        if not os.path.exists(ssh_key_path):
            os.mkdir(ssh_key_path)  # 创建保存key文件的文件夹

        with open(os.path.join(ssh_key_path, sshkey_filename), 'w', encoding='utf-8') as f:
            f.write(ssh_key)

        return HttpResponse(sshkey_filename)

应用路由

from django.urls import path
from .views import index, upload_ssh_key
from simpleauth.tools.simpleauth_tool import simple_permission_required

app_name = 'webssh'

urlpatterns = [
    path('', simple_permission_required(permission='it_sys_user')(index), name='index'),  # 终端主页
    path('upload_ssh_key/', simple_permission_required(permission='it_sys_user')(upload_ssh_key), name='upload_ssh_key'),  # 终端主页
]

访问 http://127.0.0.1/webssh/ 可以连接到主页

应用下创建 consumers.py

apps/webssh/consumers.py

from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer, AsyncWebsocketConsumer
import json
import base64
from django.http.request import QueryDict
import paramiko
import socket
from threading import Thread
import time
import os
from django.utils.six import StringIO
from django.conf import settings


def get_key_obj(pkeyobj, pkey_file=None, pkey_obj=None, password=None):
    if pkey_file:
        with open(pkey_file) as fo:
            try:
                pkey = pkeyobj.from_private_key(fo, password=password)
                return pkey
            except:
                pass
    else:
        try:
            pkey = pkeyobj.from_private_key(pkey_obj, password=password)
            return pkey
        except:
            pkey_obj.seek(0)


class SSHBridge(object):
    """
    桥接WebSocket和ssh
    参考:https://blog.51cto.com/hongchen99/2336087
    """

    def __init__(self, websocket, simpleuser):
        self.websocket = websocket
        self.simpleuser = simpleuser

    def connect(self, host, user, pwd=None, key=None, port=22, timeout=6, term='xterm', pty_width=80, pty_height=24):
        """
        建立SSH连接,放在 self.ssh_channel 通道中,之后直接在通道中交互数据
        :param host:
        :param user:
        :param pwd:
        :param key:
        :param port:
        :param timeout:
        :param term:
        :param pty_width:
        :param pty_height:
        :return:
        """
        ssh_client = paramiko.SSHClient()
        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        try:
            if key:
                # 密钥方式认证
                pkey = get_key_obj(paramiko.RSAKey, pkey_obj=key, password=pwd) or \
                       get_key_obj(paramiko.DSSKey, pkey_obj=key, password=pwd) or \
                       get_key_obj(paramiko.ECDSAKey, pkey_obj=key, password=pwd) or \
                       get_key_obj(paramiko.Ed25519Key, pkey_obj=key, password=pwd)
                ssh_client.connect(username=user, hostname=host, port=port, pkey=pkey, timeout=timeout)
            else:
                ssh_client.connect(hostname=host, port=port, username=user, password=pwd, timeout=timeout)
        except Exception as e:
            # pri00nt(e)
            message = json.dumps({'flag': 'fail', 'message': str(e)})
            self.websocket.send_message_or_team(message)
            return

        transport = ssh_client.get_transport()

        """
        另一种方式建立通道
        transport = paramiko.Transport((host, port,))
        transport.start_client()
        transport.auth_password(username=user, password=pwd)
        """

        # 打开一个通道
        self.ssh_channel = transport.open_session()
        # 获取一个终端
        self.ssh_channel.get_pty(term=term, width=pty_width, height=pty_height)
        # 激活终端,这样就可以登录到终端了,就和我们用类似于xshell登录系统一样
        self.ssh_channel.invoke_shell()

        # 获取ssh连接主机后的返回内容,例如Linux,会显示上次登录等信息,把这些信息通过WebSocket显示到Web终端。
        # 连接建立一次,之后交互数据不会再进入该方法
        for i in range(2):
            recv = self.ssh_channel.recv(1024).decode('utf-8')
            message = json.dumps({'flag': 'msg', 'message': recv})
            # pri00nt('【WS  --websocket-->  Web】建立SSH通道后,返回欢迎信息:', message)
            self.websocket.send_message_or_team(message)

    def close(self):
        message = {'flag': 0, 'message': '关闭WebSocket和SSH连接'}
        # 向WebSocket发送一个关闭消息
        self.websocket.send_message_or_team(json.dumps(message))

        try:
            # 关闭ssh通道
            self.ssh_channel.close()
            # 关闭WebSocket连接
            self.websocket.close()
        except BaseException as e:
            # pri00nt('关闭WebSocket和SSH连接产生异常:', e)
            pass

    def _ws_to_ssh(self, data):
        """
        尝试发送数据到ssh通道,产生异常则关闭所有连接
        """
        try:
            # pri00nt('【Func  --paramiko-->  SSH】WebSocket中的数据发送数据到ssh通道:', data)
            self.ssh_channel.send(data)
        except OSError as e:
            # pri00nt(e)
            self.close()

    def _ssh_to_ws(self):
        try:
            # while True:
            while not self.ssh_channel.exit_status_ready():
                data = self.ssh_channel.recv(1024).decode('utf-8')
                # pri00nt('【SSH  --paramiko-->  Func】获取ssh通道返回的数据:', data)
                if len(data) != 0:
                    message = {'flag': 'msg', 'message': data}
                    # pri00nt('【WS --websocket-->  Web】通过WebSocket把信息发回前端,显示到Web终端:', message)
                    self.websocket.send_message_or_team(json.dumps(message))
                else:
                    break

        except:
            self.close()

    def shell(self, data):
        Thread(target=self._ws_to_ssh, args=(data,)).start()
        Thread(target=self._ssh_to_ws).start()
        """
        t1 = Thread(target=self._ws_to_ssh, args=(data,))
        t1.setDaemon(True)
        t1.start()
        t2 = Thread(target=self._ssh_to_ws)
        t2.setDaemon(True)
        t2.start()
        """

    def resize_pty(self, cols, rows):
        self.ssh_channel.resize_pty(width=cols, height=rows)


class WebsshConsumer(WebsocketConsumer):
    """
    1、xterm.js 在浏览器端模拟 shell 终端, 监听用户输入通过 websocket 将用户输入的内容上传到 django
    2、django 接受到用户上传的内容, 将用户在前端页面输入的内容通过 paramiko 建立的 ssh 通道上传到远程服务器执行
    3、paramiko 将远程服务器的处理结果返回给 django
    4、django 将 paramiko 返回的结果通过 websocket 返回给用户
    5、xterm.js 接收 django 返回的数据并将其写入前端页面
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.host_id = ''
        self.simple_user = ''
        self.is_team = False
        self.team_name = ''

    def connect(self):
        """
        建立WebSocket连接,并实例化SSHBridge类,在这个对象中建立SSH连接,放在 self.ssh_channel 通道中
        :return:
        """
        self.host_id = self.scope['url_route']['kwargs'].get('host_id')
        self.simple_user = self.scope["session"]["session_simple_nick_name"]  # 获取session中的值
        # pri00nt('【Web  --websocket-->  WS】建立WebSocket通道,当前连接用户:', self.simple_user)

        self.accept()

        # WebSocket连接成功后,连接ssh
        query_string = self.scope.get('query_string')
        ws_args = QueryDict(query_string=query_string, encoding='utf-8')
        # # pri00nt(ws_args)
        # <QueryDict: {'user': ['admin'], 'host': ['192.168.96.20'], 'port': ['22'], 'auth': ['pwd'], 'pwd': ['ZGphbmdvYWRtaW4='], 'key': [''], 'width': ['113'], 'height': ['43']}>
        # 根据参数判断是否是协作
        team = ws_args.get('team')
        if team:
            self.is_team = True
            self.team_name = "team_{}".format(self.host_id)  # 加到这个通道组
            async_to_sync(self.channel_layer.group_add)(
                self.team_name,
                self.channel_name
            )
            # 用户连接时,同一群组发送消息
            self.send_message_or_team(json.dumps({'flag': 'user', 'message': '用户 {} 已连接本终端'.format(self.simple_user)}))

        width = ws_args.get('width')
        height = ws_args.get('height')
        width = int(width)
        height = int(height)  # ssh连接要求int类型:required argument is an integer

        ssh_connect_dict = {}

        if self.host_id:
            # 指定连接
            # pri00nt('连接的服务器id:', self.host_id)
            if int(self.host_id) == 1:
                ssh_connect_dict = {
                    'host': '192.168.96.20',
                    'user': 'user',
                    'port': 22,
                    'timeout': 30,
                    'pty_width': width,
                    'pty_height': height,
                    'pwd': 'user'
                }
            elif int(self.host_id) == 2:
                ssh_connect_dict = {
                    'host': '192.168.96.21',
                    'user': 'user',
                    'port': 22,
                    'timeout': 30,
                    'pty_width': width,
                    'pty_height': height,
                    'pwd': 'user'
                }
            else:
                self.close()
                return

        else:
            user = ws_args.get('user')
            host = ws_args.get('host')
            port = ws_args.get('port')
            port = int(port)
            auth = ws_args.get('auth')
            pwd = ws_args.get('pwd')
            if pwd:
                pwd = base64.b64decode(pwd).decode('utf-8')
            sshkey_filename = ws_args.get('sshkey_filename')

            ssh_connect_dict = {
                'host': host,
                'user': user,
                'port': port,
                'timeout': 30,
                'pty_width': width,
                'pty_height': height,
                'pwd': pwd
            }

            if auth == 'key':
                sshkey_file = os.path.join(settings.MEDIA_ROOT, 'sshkey', sshkey_filename)
                if not os.path.exists(sshkey_file):
                    self.send(json.dumps({'flag': 'error', 'message': '密钥文件不存在'}))

                else:
                    try:
                        f = open(sshkey_file, 'r', encoding='utf-8')
                        key = f.read()
                        string_io = StringIO()
                        string_io.write(key)
                        string_io.flush()
                        string_io.seek(0)
                        ssh_connect_dict['key'] = string_io

                        os.remove(sshkey_file)  # 用完之后删除key文件
                    except BaseException as e:
                        # pri00nt('打开密钥文件出错', e)
                        pass

        # 建立SSH连接
        self.ssh = SSHBridge(websocket=self, simpleuser=self.simple_user)
        # pri00nt('【WS  --SSHBridge-->  SSH】连接SSH参数:', ssh_connect_dict)
        self.ssh.connect(**ssh_connect_dict)

    def disconnect(self, close_code):
        # 断开连接
        # pri00nt('用户 {} 断开WebSocket连接,断开SSH连接'.format(self.simple_user))
        try:
            if self.is_team:
                # 用户连接时,同一群组发送消息
                self.send_message_or_team(json.dumps({'flag': 'user', 'message': '用户 {} 已断开本终端'.format(self.simple_user)}))
                # 退出群组
                async_to_sync(self.channel_layer.group_discard)(
                    self.team_name,
                    self.channel_name
                )
            self.ssh.close()
        except BaseException as e:
            pass

    def receive(self, text_data=None, bytes_data=None):
        # 从WebSocket中接收消息
        text_data = json.loads(text_data)  # json字符串转字典
        # pri00nt('\n\n【Web  --websocket-->  WS】Web终端按键内容通过WebSocket传到后端:', text_data)
        if type(text_data) == dict:
            if text_data.get('flag') == 'entered_key':
                data = text_data.get('entered_key', '')  # 获取前端传过来输入的按键值,并传递给shell
                # pri00nt('【WS  --SSHBridge-->  Func】WebSocket转发SSHBridge:', text_data)
                self.ssh.shell(data=data)
            else:
                cols = text_data['cols']
                rows = text_data['rows']
                # 改变通道中终端大小
                self.ssh.resize_pty(cols=cols, rows=rows)
        else:
            # pri00nt('【!!!】收到的数据不是dict类型')
            pass

    def send_message_or_team(self, message):
        if self.is_team:
            async_to_sync(self.channel_layer.group_send)(
                self.team_name,
                {
                    'type': 'team_message',
                    'message': message
                }
            )
        else:
            self.send(message)

    def team_message(self, event):
        message = event['message']

        # 发送消息到WebSocket
        self.send(message)

应用下创建 routing.py

apps/webssh/routing.py

from django.conf.urls import url

from . import consumers

websocket_urlpatterns = [
    url(r'^ws/webssh/(?P<host_id>\d+)/$', consumers.WebsshConsumer),
    url(r'^ws/webssh/$', consumers.WebsshConsumer),
]

修改项目下 routing.py (主WS路由)

合并多个应用的url

from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.sessions import SessionMiddlewareStack
import pxectrl.routing
import webssh.routing

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    # 【channels】(第6步)添加路由配置指向应用的路由模块
    'websocket': SessionMiddlewareStack(  # 使用Session中间件,可以请求中session的值
        # 一个url
        # URLRouter(
        #     pxectrl.routing.websocket_urlpatterns,
        # ),
        # 多个url合并一起使用,多个子路由列表相加
        URLRouter(
            pxectrl.routing.websocket_urlpatterns + webssh.routing.websocket_urlpatterns,
        ),
    ),
})

整个流程分析

  1. 页面点击连接进入connectWebSocket(host_id = null, team = false)函数,首先获取div的大小,初始化终端大小;
  2. 检测用户输入是否完成,如果是密钥认证将先使用ajax上传文件,这儿只讲下密码验证,组装成ws://IP地址/ws/webssh/?参数1=xxx&参数2=xxx的地址,创建WebSocket连接;
  3. ws.onopen打开WebSocket连接,并隐藏输入表单,显示网页终端,此时通过apps/webssh/routing.py中的url进入WebsshConsumer(WebsocketConsumer).connect(self),接受WebSocket连接,并根据ws的参数,实例化self.ssh = SSHBridge(websocket=self, simpleuser=self.simple_user),连接到SSHself.ssh.connect(**ssh_connect_dict),这儿主要是创建一个SSH通道self.ssh_channel,并激活self.ssh_channel.invoke_shell(),类似xshell功能,后面通过该通道进行SSH请求主机;
  4. SSH通道连接成功后,从该通道中获取返回的数据,并通过WebSocket通道返回到前端,ws.onmessage接到后端返回的数据,显示到网页终端;
  5. 网页终端按键后,通过term.on('data', function (data) {...})实时ws.send(send_data)发送到后端;
  6. 后端def receive(self, text_data=None, bytes_data=None)接收数据,调用self.ssh.shell(data=data)函数,这儿创建一个正向命令和反向接收的线程,主要用于实时交互数据;
  7. 通过SSH通道self.ssh_channel.send(data)传入输入的键,反向获取通道中的内容self.ssh_channel.recv(1024).decode('utf-8'),并通过WebSocket返回前端。

遇到的问题:用协作连接时,用户加入到一个通道组,往这个通道组发送命令,这个通道所有用户都能收到,来实现协作的功能,但是从self.ssh_channel接收返回的数据,可能会存在和预想的不同,特别是top命令。示例如下,不知道怎么解决了!!!

BLOG_20190729_161900_74

使用截图

BLOG_20190729_161853_25

BLOG_20190729_161848_65

BLOG_20190729_161843_50

[外链图片转存失败(img-Z9ACxmep-1564388455352)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190729_161838_38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190729_161838_38.png”)]

参考链接:https://github.com/huyuan1999/django-webssh

https://www.cnblogs.com/52op/articles/9327733.html 【gevent库】

  • 2
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值