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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Django使用Channels实现WebSSH网页终端,实现SSH堡垒机雏形相关的知识,希望对你有一定的参考价值。

参考技术A

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

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

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

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

创建app。名为 webssh

将应用添加到 settings.py

修改应用下的 apps.py

修改应用下的 __init__.py

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

apps/webssh/consumers.py

apps/webssh/routing.py

合并多个应用的url

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

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

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

代码发布项目——django实现websocket(使用channels)基于channels实现群聊功能gojs插件paramiko模块

一、django实现websocket

django默认是不支持websocket,只支持http协议

在django中如果想要基于websocket开发项目 你需要安装模块:channles
    pip3 install channels==2.3
    版本不要使用最新的,如果安装最新的可能会自动把你的django版本升级到最新版
    对应的解释器环境建议使用3.6(官网的说法:3.5可能有问题,3.7可能也有问题...具体原因没有给解释)
    
    channels模块内部已经帮我们封装好了
        握手/加密/解密
        
面试点:
注意:不是所有的服务端都支持websocket
    django
        -默认不支持
        -但是有第三方的模块:channles来实现
    
    flask
        -默认不支持
        -但是也有第三方的模块:geventwebsocket
    
    tronado
        -默认支持

创建django项目测试channels模块

1.需要在配置文件中注册channels应用

INSTALLED_APPS = [
    # 1.需要先注册channels
    channels
]

2.还需要在配置文件配置以下参数

# ‘与项目名同名的文件夹名.routing文件名.文件内的变量名application
ASGI_APPLICATION = channels_demo.routing.application

3.创建routing.py文件(和settings.py同级),文件内容如下

from channels.routing import ProtocolTypeRouter,URLRouter

application = ProtocolTypeRouter({
    websocket:URLRouter([
        # 路由与视图函数对应关系
    ])
})

注意配置完成后,django就会即支持http协议也支持websocket协议

强调

正常的http协议还是按照之前的写法 在urls中写路由与视图函数对应关系,正常的访问页面还是需要在urls中设置路径
而针对websocket协议则在当前文件内书写路由与视图函数对应关系,这个设置的路径在前端验证是否支持websocket使用

在routing文件中书写路由与视图函数对应关系

application = ProtocolTypeRouter({
    websocket:URLRouter([
        url(r^chat/,consumers.ChatConsumer)
    ])
})

在项目根目录创建consumer.py文件用来专门处理websocket请求的视图函数

from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        """
        客户端发来链接请求之后就会自动触发
        """
    def websocket_receive(self, message):
        """
        客户端向服务端发送消息就会自动触发
      message包括前端发送的消息
""" def websocket_disconnect(self, message): """ 客户端主动断开链接之后自动触发 """

二、基于channels实现多人聊天室

http协议
    index                    index函数 
    浏览器发送请求即可访问,设置url路径访问

websocket协议
    chat                    ChatConsumer类 内部有三个方法
    通过前端创建new WebScoket对象才能访问

后端

# 该文件内是专门用来写处理websocket请求的视图函数

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer

consumer_object_list = []

class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        """
        客户端发来链接请求之后就会自动触发
        """
        self.accept()  # 向服务端发送加密字符串,在前一章中讲的shankhands中的加密
        # self就是每一个客户端对象
        # 链接成功 我就将当前对象放入全局的列表中
        consumer_object_list.append(self)

    def websocket_receive(self, message):
        """
        客户端向服务端发送消息就会自动触发
        """
        print(message)     #接收客户端传递的信息

        # 给列表中所有的对象都发送消息,给客户端返回信息用send,传递信息用text_data
        for obj in consumer_object_list:
            obj.send(text_data=message.get(text))

    def websocket_disconnect(self, message):
        """
        客户端主动断开链接之后自动触发
        """
        print(断开链接了)
        # 服务端断开链接 就去列表中删除对应的客户端对象
        consumer_object_list.remove(self)
        raise StopConsumer   #停止消费

前端

<script>
    // 验证服务端是否支持websocket
    var ws = new WebSocket(ws://127.0.0.1:8000/chat/);
    //1.握手环节,连接成功自动触发,访问页面成功就会触发
    ws.onopen=function(){
        alert(‘验证成功‘)
    };    
    // 2.客户端给服务端发送消息
    function sendMsg() {
        ws.send($(#d1).val())  // 将用户输入的内容发送给后端
    }
    // 3.接收服务端消息 会自动触发
    ws.onmessage = function (event) {  // event是数据对象 真正的数据在data属性内
        {#alert(event.data)  // 服务端返回的真实数据#}
        // 将消息渲染到html页面上
        var pEle = $(<p>);
        pEle.text(event.data);
        $(#content).append(pEle)

    };
    // 4.断开链接
    function closeLink() {
        ws.close()
    }
</script>

总结:

后端三个方法

websocket_connect
websocket_receive
websocket_disconnect

前端四个方法

var ws = new WebSocket(ws://127.0.0.1:8000/chat/);
ws.onopen
ws.send()
ws.onmessage
ws.close()

上述的群聊功能是我们自己临时想出来的 并不是真正的处理方式

要想实现完美的群聊功能需要借助于channel-layers

三、gojs插件

是一个前端组件,可以动态的创建各种流程图、图表...

基本使用

1.先用div占据页面一块内容,然后在该div内部操作图表

<div id="myDiagramDiv" style="width:500px; height:350px; background-color: #DAE4E4;"></div>
<script src="go.js"></script>
<script>
  var $ = go.GraphObject.make;
  // 第一步:创建图表
  var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图
  // 第二步:创建一个节点,内容为jason
  var node = $(go.Node, $(go.TextBlock, {text: "jason"}));
  // 第三步:将节点添加到图表中
  myDiagram.add(node)
</script>

比较重要概念

  • TextBlock 创建文本

  • Shap 图形

  • Node 节点(将文本与图形结合)

  • Link 箭头

TextBlock

<div id="myDiagramDiv" style="width:500px; height:350px; background-color: #DAE4E4;"></div>
<script src="go.js"></script>
<script>
    var $ = go.GraphObject.make;
    // 第一步:创建图表
    var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图
    var node1 = $(go.Node, $(go.TextBlock, {text: "jason"}));
    myDiagram.add(node1);

    var node2 = $(go.Node, $(go.TextBlock, {text: "jason", stroke: red}));
    myDiagram.add(node2);

    var node3 = $(go.Node, $(go.TextBlock, {text: "jason", background: lightblue}));
    myDiagram.add(node3);
</script>

Shap

gojs只引入go.js默认展示的图形比较少,你如果想展示其他图形,需要再引入Fugures.js

<div id="myDiagramDiv" style="width:500px; height:350px; background-color: #DAE4E4;"></div>
<script src="go.js"></script>
<script src="Figures.js"></script>
<script>
    var $ = go.GraphObject.make;
    // 第一步:创建图表
    var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图
    var node1 = $(go.Node,
        $(go.Shape, {figure: "Ellipse", width: 40, height: 40})
    );
     myDiagram.add(node1);
     var node2 = $(go.Node,
        $(go.Shape, {figure: "RoundedRectangle", width: 40, height: 40, fill: green,stroke:red})
    );
    myDiagram.add(node2);
    var node3 = $(go.Node,
        $(go.Shape, {figure: "Rectangle", width: 40, height: 40, fill: null})
    );
    myDiagram.add(node3);
    var node5 = $(go.Node,
        $(go.Shape, {figure: "Club", width: 40, height: 40, fill: red})
    );
    myDiagram.add(node5);
</script>

Node 节点

<div id="myDiagramDiv" style="width:500px; height:350px; background-color: #DAE4E4;"></div>
<script src="js/go.js"></script>
<script src="js/Figures.js"></script>
<script>
    var $ = go.GraphObject.make;
    // 第一步:创建图表
    var myDiagram = $(go.Diagram, "myDiagramDiv"); // 创建图表,用于在页面上画图

    var node1 = $(go.Node,
         "Vertical",  // 垂直方向
        {
            background: yellow,
            padding: 8
        },
        $(go.Shape, {figure: "Ellipse", width: 40, height: 40,fill:null}),
        $(go.TextBlock, {text: "jason"})
    );
    myDiagram.add(node1);

    var node2 = $(go.Node,
        "Horizontal",  // 水平方向
        {
            background: white,
            padding: 5
        },
        $(go.Shape, {figure: "RoundedRectangle", width: 40, height: 40}),
        $(go.TextBlock, {text: "jason"})
    );
    myDiagram.add(node2);

    var node3 = $(go.Node,
        "Auto",  // 居中
        $(go.Shape, {figure: "Ellipse", width: 80, height: 80, background: green, fill: red}),
        $(go.TextBlock, {text: "jason"})
    );
    myDiagram.add(node3);
</script>

Link 箭头

流程图必备

<div id="myDiagramDiv" style="width:500px; min-height:450px; background-color: #DAE4E4;"></div>
    <script src="js/go-debug.js"></script>
    <script>
        var $ = go.GraphObject.make;

        var myDiagram = $(go.Diagram, "myDiagramDiv",
            {layout: $(go.TreeLayout, {angle: 0})}
        ); // 创建图表,用于在页面上画图

        var startNode = $(go.Node, "Auto",
            $(go.Shape, {figure: "Ellipse", width: 40, height: 40, fill: #79C900, stroke: #79C900}),
            $(go.TextBlock, {text: 开始, stroke: white})
        );
        myDiagram.add(startNode);

        var downloadNode = $(go.Node, "Auto",
            $(go.Shape, {figure: "RoundedRectangle", height: 40, fill: red, stroke: #79C900}),
            $(go.TextBlock, {text: 下载代码, stroke: white})
        );
        myDiagram.add(downloadNode);

        var startToDownloadLink = $(go.Link,
            {fromNode: startNode, toNode: downloadNode},
            $(go.Shape, {strokeWidth: 1}),
            $(go.Shape, {toArrow: "OpenTriangle", fill: null, strokeWidth: 1})
        );
        myDiagram.add(startToDownloadLink);
    </script>

如何结合后端动态生成  (后台传递数据)

拷贝使用即可

<div id="diagramDiv" style="width:100%; min-height:450px; background-color: #DAE4E4;"></div>
    <script src="js/go.js"></script>
    <script>
        var $ = go.GraphObject.make;
        var diagram = $(go.Diagram, "diagramDiv",{
            layout: $(go.TreeLayout, {
                angle: 0,
                nodeSpacing: 20,
                layerSpacing: 70
            })
        });

        diagram.nodeTemplate = $(go.Node, "Auto",
            $(go.Shape, {
                figure: "RoundedRectangle",
                fill: yellow,
                stroke: yellow
            }, new go.Binding("figure", "figure"), new go.Binding("fill", "color"), new go.Binding("stroke", "color")),
            $(go.TextBlock, {margin: 8}, new go.Binding("text", "text"))
        );

        diagram.linkTemplate = $(go.Link,
            {routing: go.Link.Orthogonal},
            $(go.Shape, {stroke: yellow}, new go.Binding(stroke, link_color)),
            $(go.Shape, {toArrow: "OpenTriangle", stroke: yellow}, new go.Binding(stroke, link_color))
        );
        var nodeDataArray = [
            {key: "start", text: 开始, figure: Ellipse, color: "lightgreen"},
            {key: "download", parent: start, text: 下载代码, color: "lightgreen", link_text: 执行中...},
            {key: "compile", parent: download, text: 本地编译, color: "lightgreen"},
            {key: "zip", parent: compile, text: 打包, color: "red", link_color: red},
            {key: "c1", text: 服务器1, parent: "zip"},
            {key: "c11", text: 服务重启, parent: "c1"},
            {key: "c2", text: 服务器2, parent: "zip"},
            {key: "c21", text: 服务重启, parent: "c2"},
            {key: "c3", text: 服务器3, parent: "zip"},
            {key: "c31", text: 服务重启, parent: "c3"}
        ];
        diagram.model = new go.TreeModel(nodeDataArray);

        // 动态控制节点颜色变化
        /*
        var node = diagram.model.findNodeDataForKey("zip");
        diagram.model.setDataProperty(node, "color", "lightgreen");
        */
    </script>

默认所有的页面都有水印,如何除去水印

修改go.js文件中的源码,查找一个字符串7eba17a4ca3b1a8346

先注释

/*a.Cr=b.Z[Wa("7eba17a4ca3b1a8346")][Wa("78a118b7")](b.Z,Zk,4,4);*/

添加新内容

a.kr=function(){return true};

四、paramiko模块

通过ssh远程连接服务器并执行想要命令类似于Xshell

安装

pip install paramiko

使用

paramiko模块支持上面两种连接服务器的方式:

1.用户名和密码连接服务器

2.公钥私钥连接服务器

支持输入命令行

# 用户名和密码的方式
import paramiko

# 创建ssh对象
ssh = paramiko.SSHClient()
# 允许链接不在know_hosts文件中主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

# 链接服务器
ssh.connect(hostname=172.16.219.168,port=22,username=root,password=jason123)

# 执行命令
stdin, stdout, stderr = ssh.exec_command(ip a)

# 获取结果
res = stdout.read()  # 基于网络传输 该结果是一个bytes类型
print(res.decode(utf-8))

# 断开链接
ssh.close()


# 公钥私钥方式
# 首先你要产生你自己的公钥和私钥  然后将你的公钥上传到服务器保存  之后就可以通过私钥来链接
"""
mac本
ssh-keygen -t rsa

ssh-copy-id -i ~/.ssh/id_rsa.pub username@hostname

cat ~/.ssh/id_rsa
"""

# # 公钥和私钥(先讲公钥保存到服务器上)
# import paramiko
#
# # 读取本地私钥
# private_key = paramiko.RSAKey.from_private_key_file(‘a.txt‘)
#
# # 创建SSH对象
# ssh = paramiko.SSHClient()
# # 允许连接不在know_hosts文件中的主机
# ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# # 连接服务器
# ssh.connect(hostname=‘172.16.219.168‘, port=22, username=‘root‘, pkey=private_key)
#
# # 执行命令
# stdin, stdout, stderr = ssh.exec_command(‘ls /‘)
# # 获取命令结果
# result = stdout.read()
# print(result.decode(‘utf-8‘))
# # 关闭连接
# ssh.close()

进行上传下载文件

# 用户名密码方式
# import paramiko
#
# # 用户名和密码
# transport = paramiko.Transport((‘172.16.219.168‘, 22))
# transport.connect(username=‘root‘, password=‘jason123‘)
#
# sftp = paramiko.SFTPClient.from_transport(transport)
#
# # 上传文件
# # sftp.put("a.txt", ‘/data/tmp.txt‘)  # 注意上传文件到远程某个文件下 文件必须存在
#
# # 下载文件
# sftp.get(‘/data/tmp.txt‘, ‘hahahha.txt‘)  # 将远程文件下载到本地并重新命令
# transport.close()

# 公钥私钥方式
#
# import paramiko
# private_key = paramiko.RSAKey.from_private_key_file(‘a.txt‘)
# transport = paramiko.Transport((‘172.16.219.168‘, 22))
# transport.connect(username=‘root‘, pkey=private_key)
# sftp = paramiko.SFTPClient.from_transport(transport)
# # 将location.py 上传至服务器 /tmp/test.py
# sftp.put(‘/tmp/location.py‘, ‘/tmp/test.py‘)
#
# # 将remove_path 下载到本地 local_path
# sftp.get(‘remove_path‘, ‘local_path‘)
# transport.close()

疑问点:如果我想链接服务器执行多条命令并且还想上传下载文件,那么你所能看到的一般做法都是执行一次命令创建一次链接

我们要实现在一个链接上即可以执行命令又可以上传下载文件

我们封装成一个类,类的内部有一系列的方法,并且这些方法都可以在同一个链接下执行多次

import paramiko

class SSHProxy(object):
    def __init__(self, hostname, port, username, password):
        self.hostname = hostname
        self.port = port
        self.username = username
        self.password = password
        self.transport = None

    def open(self):  # 给对象赋值一个上传下载文件对象连接
        self.transport = paramiko.Transport((self.hostname, self.port))
        self.transport.connect(username=self.username, password=self.password)

    def command(self, cmd):  # 正常执行命令的连接  至此对象内容就既有执行命令的连接又有上传下载链接
        ssh = paramiko.SSHClient()
        ssh._transport = self.transport

        stdin, stdout, stderr = ssh.exec_command(cmd)
        result = stdout.read()
        return result

    def upload(self, local_path, remote_path):
        sftp = paramiko.SFTPClient.from_transport(self.transport)
        sftp.put(local_path, remote_path)
        sftp.close()

    def close(self):
        self.transport.close()

    def __enter__(self):
        print(with开始)
        self.open()
        return self  # 该方法返回什么 with ... as 后面就拿到什么

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(with结束)
        self.close()


if __name__ == __main__:
    # obj = SSHProxy(hostname=‘172.16.219.168‘,port=22,username=‘root‘,password=‘jason123‘)
    # # 生成对象之后必须要先执行open方法
    # obj.open()
    # # 你就可以无限制的执行命令或者上传下载文件
    # obj.command(‘ls /‘)
    # obj.command(‘df‘)
    # obj.upload(‘a.txt‘,‘/data/tmp.txt‘)
    # # 断开链接
    # obj.close()
    """
    文件操作
    f = open()
    ...
    f.close()
    
    with上下文管理
    """
    obj = SSHProxy(hostname=172.16.219.168,port=22,username=root,password=jason123)

    with obj as ssh:  # 默认不支持  对象执行with会自动触发内部的__enter__方法
        print(with内部代码)
    # with上下文执行完毕之后 会自动触发__exit__方法

 

以上是关于Django使用Channels实现WebSSH网页终端,实现SSH堡垒机雏形的主要内容,如果未能解决你的问题,请参考以下文章

基于django的webssh实现

Django + Nginx + Daphne实现webssh功能

websockets django nginx webssh

Python Django撸个WebSSH操作Kubernetes Pod(下)- 终端窗口自适应Resize

Django使用Channels实现WebSocket--下篇

kubernetes webssh 管理 (django开发)