zoukankan      html  css  js  c++  java
  • Django + Nginx + Daphne实现webssh功能

    前言:日常工作中经常要登录服务器,我们最常用的就是用ssh终端软件登录到服务器操作,假如有一天我们电脑没有安装软件,然后又不知道机器IP信息怎么办,确实会不够方便,今天分享下基于django实现前端页面免密码登录服务器操作。
     
    一、关键的技术
    1.WebSocket
    WebSocket是一种在单个TCP连接上进行全双工通讯的协议。WebSocket允许服务端主动向客户端推送数据。在WebSocket协议中,客户端浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并在浏览器和服务器之间进行双向的数据传输。
    WebSocket有什么用?
    WebSocket区别于HTTP协议的一个最为显著的特点是,WebSocket协议可以由服务端主动发起消息,对于浏览器需要及时接收数据变化的场景非常适合,例如在Django中遇到一些耗时较长的任务我们通常会使用Celery来异步执行,那么浏览器如果想要获取这个任务的执行状态,在HTTP协议中只能通过轮训的方式由浏览器不断的发送请求给服务器来获取最新状态,这样发送很多无用的请求不仅浪费资源,还不够优雅,如果使用WebSokcet来实现就很完美了
     
    2.Channels
    Django本身不支持WebSocket,但可以通过集成Channels框架来实现WebSocket
    Channels是针对Django项目的一个增强框架,可以使Django不仅支持HTTP协议,还能支持WebSocket,MQTT等多种协议,同时Channels还整合了Django的auth以及session系统方便进行用户管理及认证。
    要是实现webssh功能要使用到channels模块
     
    二、配置后端Django
    1.环境是Linux(centos6.9),后端语言为python3.6
    pip install channels==2.0.0
    pip install Django==2.1
    pip install uWSGI==2.0.19.1
    pip install paramiko==2.4.1
    pip install daphne==2.2.5
    

    2.打开django项目的setting.py文件,添加以下内容

    INSTALLED_APPS = [
        'channels',
    ]
    
    ASGI_APPLICATION = 'my_project_name.routing.application'
    

    3.在setting.py同级目录下添加routing.py文件,routing.py文件就相当于urls.py意思  

    from channels.auth import AuthMiddlewareStack
    from channels.routing import ProtocolTypeRouter, URLRouter
    from assets.tools.channel import routing
    
    application = ProtocolTypeRouter({
        'websocket': AuthMiddlewareStack(
            URLRouter(
                routing.websocket_urlpatterns
            )
        ),
    })
    

    4.在你的新建一个app应用下面添加一下目录文件

    tools目录
        __init__.py
         ssh.py
         tools.py
    channel目录
        __init__.py
         routing.py
         websocket.py
    

    routing.py文件

    from django.urls import path
    from assets.tools.channel import websocket
    
    websocket_urlpatterns = [
        path('webssh/', websocket.WebSSH) 开头是webssh请求交给websocket.WebSSH处理
    ]
    

    websocket.py文件

    from channels.generic.websocket import WebsocketConsumer
    from assets.tools.ssh import SSH
    from django.http.request import QueryDict
    from django.utils.six import StringIO
    from test_devops.settings import TMP_DIR
    import os
    import json
    import base64
    
    
    class WebSSH(WebsocketConsumer):
        message = {'status': 0, 'message': None}
        """
        status:
            0: ssh 连接正常, websocket 正常
            1: 发生未知错误, 关闭 ssh 和 websocket 连接
    
        message:
            status 为 1 时, message 为具体的错误信息
            status 为 0 时, message 为 ssh 返回的数据, 前端页面将获取 ssh 返回的数据并写入终端页面
        """
    
        def connect(self):
            """
            打开 websocket 连接, 通过前端传入的参数尝试连接 ssh 主机
            :return:
            """
            self.accept()
            query_string = self.scope.get('query_string')
            ssh_args = QueryDict(query_string=query_string, encoding='utf-8')
    
            width = ssh_args.get('width')
            height = ssh_args.get('height')
            port = ssh_args.get('port')
    
            width = int(width)
            height = int(height)
            port = int(port)
    
            auth = ssh_args.get('auth')
            ssh_key_name = ssh_args.get('ssh_key')
            passwd = ssh_args.get('password')
    
            host = ssh_args.get('host')
            user = ssh_args.get('user')
    
            if passwd:
                passwd = base64.b64decode(passwd).decode('utf-8')
            else:
                passwd = None
    
    
            self.ssh = SSH(websocker=self, message=self.message)
    
            ssh_connect_dict = {
                'host': host,
                'user': user,
                'port': port,
                'timeout': 30,
                'pty_width': width,
                'pty_height': height,
                'password': passwd
            }
    
            if auth == 'key':
                ssh_key_file = os.path.join(TMP_DIR, ssh_key_name)
                with open(ssh_key_file, 'r') as f:
                    ssh_key = f.read()
    
                string_io = StringIO()
                string_io.write(ssh_key)
                string_io.flush()
                string_io.seek(0)
    
                ssh_connect_dict['ssh_key'] = string_io
                os.remove(ssh_key_file)
    
            self.ssh.connect(**ssh_connect_dict)
    
        def disconnect(self, close_code):
            try:
                self.ssh.close()
            except:
                pass
    
        def receive(self, text_data=None, bytes_data=None):
            data = json.loads(text_data)
            if type(data) == dict:
                status = data['status']
                if status == 0:
                    data = data['data']
                    self.ssh.shell(data)
                else:
                    cols = data['cols']
                    rows = data['rows']
                    self.ssh.resize_pty(cols=cols, rows=rows)
    

    ssh.py文件

    import paramiko
    from threading import Thread
    from assets.tools.tools import get_key_obj
    import socket
    import json
    
    
    class SSH:
        def __init__(self, websocker, message):
            self.websocker = websocker
            self.message = message
    
        def connect(self, host, user, password=None, ssh_key=None, port=22, timeout=30,
                    term='xterm', pty_width=80, pty_height=24):
            try:
                # 实例化SSHClient
                ssh_client = paramiko.SSHClient()
                # 当远程服务器没有本地主机的密钥时自动添加到本地,这样不用在建立连接的时候输入yes或no进行确认
                ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
                if ssh_key:
                    key = get_key_obj(paramiko.RSAKey, pkey_obj=ssh_key, password=password) or 
                          get_key_obj(paramiko.DSSKey, pkey_obj=ssh_key, password=password) or 
                          get_key_obj(paramiko.ECDSAKey, pkey_obj=ssh_key, password=password) or 
                          get_key_obj(paramiko.Ed25519Key, pkey_obj=ssh_key, password=password)
    
                    # 连接SSH服务器,这里以账号密码的方式进行认证,也可以用key
                    ssh_client.connect(username=user, hostname=host, port=port, pkey=key, timeout=timeout)
                # else:
                #     ssh_client.connect(username=user, password=password, hostname=host, port=port, timeout=timeout)
    
                # 打开ssh通道,建立长连接
                transport = ssh_client.get_transport()
                self.channel = transport.open_session()
    
                # 获取ssh通道,并设置term和终端大小
                self.channel.get_pty(term=term, width=pty_width, height=pty_height)
    
                # 激活终端,这样就可以正常登陆了
                self.channel.invoke_shell()
    
                # 连接建立一次,之后交互数据不会再进入该方法
                for i in range(2):
                    # SSH返回的数据需要转码为utf-8,否则json序列化会失败
                    recv = self.channel.recv(1024).decode('utf-8')
                    self.message['status'] = 0
                    self.message['message'] = recv
                    message = json.dumps(self.message)
                    self.websocker.send(message)
            except socket.timeout:
                self.message['status'] = 1
                self.message['message'] = 'ssh 连接超时'
                message = json.dumps(self.message)
                self.websocker.send(message)
                self.close()
            except Exception as e:
                self.close(e)
    
        # 动态调整终端窗口大小
        def resize_pty(self, cols, rows):
            self.channel.resize_pty(width=cols, height=rows)
    
        def django_to_ssh(self, data):
            try:
                self.channel.send(data)
            except Exception as e:
                self.close(e)
    
        def websocket_to_django(self):
            try:
                while True:
                    data = self.channel.recv(1024).decode('utf-8')
                    if not len(data):
                        return
                    self.message['status'] = 0
                    self.message['message'] = data
                    message = json.dumps(self.message)
                    self.websocker.send(message)
            except Exception as e:
                self.close(e)
    
        def close(self,error=None):
            self.message['status'] = 1
            self.message['message'] = f'{error}'
            message = json.dumps(self.message)
            self.websocker.send(message)
            try:
                self.websocker.close()
                self.channel.close()
            except Exception as e:
                pass
    
        def shell(self, data):
            Thread(target=self.django_to_ssh, args=(data,)).start()
            Thread(target=self.websocket_to_django).start()
    

    tools.py文件

    import time
    import random
    import hashlib
    
    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)
    
    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()
    

    三、前端页面代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>webssh</title>
        <link rel="stylesheet" href="/static/css/ssh/xterm/xterm.css"/>
        <link rel="stylesheet" href="/static/css/ssh/xterm/style.css"/>
        <link rel="stylesheet" href="/static/css/toastr/toastr.min.css">
        <link rel="stylesheet" href="/static/css/bootstrap.min.css"/>
    </head>
    <body>
    
    <div id="django-webssh-terminal">
        <div id="terminal"></div>
    </div>
    
    <script src="/static/js/plugin/jquery.min.js"></script>
    <script src="/static/js/plugin/ssh/xterm/xterm.js"></script>
    <script src="/static/js/plugin/toastr/toastr.min.js"></script>
    <script>
        host_data = {{ data | safe }}
        var port = host_data.port;
        var intrant_ip = host_data.intranet_ip;
        var user_name = host_data.login_user;
        var auth_type = host_data.auth_type;
        var user_key = host_data.ssh_key;
    
        function get_term_size() {
            var init_width = 9;
            var init_height = 17;
    
            var windows_width = $(window).width();
            var windows_height = $(window).height();
            return {
                cols: Math.floor(windows_width / init_width),
                rows: Math.floor(windows_height / init_height),
            }
        }
    
        var cols = get_term_size().cols;
        var rows = get_term_size().rows;
        var connect_info = 'host=' + intrant_ip+ '&port=' + port + '&user=' + user_name + '&auth='  + auth_type + '&password='  + '&ssh_key=' + user_key;
    
    
        var term = new Terminal(
            {
                cols: cols,
                rows: rows,
                useStyle: true,
                cursorBlink: true
            }
            ),
            protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://',
            socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') +
                '/webssh/?' + connect_info + '&width=' + cols + '&height=' + rows;
    
        var sock;
        sock = new WebSocket(socketURL);
    
        // 打开 websocket 连接, 打开 web 终端
        sock.addEventListener('open', function () {
            term.open(document.getElementById('terminal'));
        });
    
        // 读取服务器端发送的数据并写入 web 终端
        sock.addEventListener('message', function (recv) {
            var data = JSON.parse(recv.data);
            var message = data.message;
            var status = data.status;
            if (status === 0) {
                term.write(message)
            } else {
                toastr.error('连接失败,错误:' + data.message)
            }
        });
    
        /*
        * status 为 0 时, 将用户输入的数据通过 websocket 传递给后台, data 为传递的数据, 忽略 cols 和 rows 参数
        * status 为 1 时, resize pty ssh 终端大小, cols 为每行显示的最大字数, rows 为每列显示的最大字数, 忽略 data 参数
        */
        var message = {'status': 0, 'data': null, 'cols': null, 'rows': null};
    
        // 向服务器端发送数据
        term.on('data', function (data) {
            message['status'] = 0;
            message['data'] = data;
            var send_data = JSON.stringify(message);
            sock.send(send_data)
        });
    
        // 监听浏览器窗口, 根据浏览器窗口大小修改终端大小
        $(window).resize(function () {
            var cols = get_term_size().cols;
            var rows = get_term_size().rows;
            message['status'] = 1;
            message['cols'] = cols;
            message['rows'] = rows;
            var send_data = JSON.stringify(message);
            sock.send(send_data);
            term.resize(cols, rows)
        })
    </script>
    </body>
    </html>
    

    四、配置Daphne  

    在生产环境一般用django + nginx + uwsgi,但是uwsgi只处理http协议请求,不处理websocket请求,所以要额外添加文件启动进程,这里使用daphne,在setting.py文件同级目录下添加asgi.py文件

    补充小知识:Daphne 是一个纯Python编写的应用于UNIX环境的由Django项目维护的ASGI服务器。它扮演着ASGI参考服务器的角色。

    """
    ASGI entrypoint. Configures Django and then runs the application
    defined in the ASGI_APPLICATION setting.
    """
    
    import os
    import django
    from channels.routing import get_default_application
    
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_project_name.settings")
    django.setup()
    application = get_default_application()
    

    启动方式

    #你应该在与 manage.py 文件相同的路径中运行这个命令。
    daphne -p 8001 my_project_name.asgi:application
    

    五、配置Nginx  

        upstream wsbackend {
             server 127.0.0.1:8001;
        }
    
        server {
            listen       80;
            server_name  192.168.10.133;
    
            location /webssh {
                   proxy_pass http://wsbackend;
                   proxy_http_version 1.1;
                   proxy_set_header Upgrade $http_upgrade;
                   proxy_set_header Connection "upgrade";
                   proxy_redirect off;
                   proxy_set_header Host $host;
                   proxy_set_header X-Real-IP $remote_addr;
                   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                   proxy_set_header X-Forwarded-Host $server_name;
    
            }
        }
    

    六、效果展示  

    结合自己的开发的后台,实现最终效果
    1.用户添加ssh私钥

    点击登录按钮,如果用户公钥在这台机器上面就可以登录

     2.如果用户没有权限登录就连接失败,关闭窗口连接也会断开

     总结:如果后台配合权限整合webssh功能,对使用者来说带来很多方便,不妨试试~ 

  • 相关阅读:
    信号灯的典型应用
    字符串过滤
    做一些学习的事情一定要坚持下去
    昨天的你造就今天的你,今天的你准备怎么造就明天的你呢?
    vue中计算属性,方法,侦听器
    vue模板语法
    Vue实例的生命周期钩子
    VUE实例
    简单的组件间传值
    前端组件化
  • 原文地址:https://www.cnblogs.com/lucktomato/p/14850595.html
Copyright © 2011-2022 走看看