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,password=password) or \
                      get_key_obj(paramiko.ECDSAKey,password=password) or \
                      get_key_obj(paramiko.Ed25519Key,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,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功能,对使用者来说带来很多方便,不妨试试~ 

原文地址:https://www.cnblogs.com/lucktomato

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


注:所有源代码均实测运行过。所有源代码均已上传CSDN,请有需要的朋友自行下载。
继承APIView和ViewSetMixin;作用也与APIView基本类似,提供了身份认证、权限校验、流量管理等。ViewSet在开发接口中不经常用。
一、Django介绍Python下有许多款不同的 Web 框架。Django是重量级选手中最有代表性的一位。许多成功的网站和APP都基于Django。Django 是一个开放源代码的 Web 应用框架,由 Python 写成。Django 遵守 BSD 版权,初次发布于 2005 年 7 月, 并于 2008 年 9 月发布了第一个正式版本 1.0 。Django学习线路Django 采用了 MVT 的软件设计模式,即模型(Model),视图(View)和模板(Template)。这个MVT模式并
本文从nginx快速掌握到使用,gunicorn快速掌握到使用,实现小白快速搭建django项目,并对可能出现的报错进行了分析
uniapp微信小程序订阅消息发送服务通知
Django终端打印SQL语句 1 Setting配置: 2 默认python 使用的MysqlDB连接,Python3 支持支持pymysql 所有需要在app里面的__init__加上下面配置:
url: re_path(&#39;authors/$&#39;, views.AuthorView.as_view()), re_path(&#39;book/(?P\d+)/$&#39;, vie
前提 关于html寻找路线: template 如果在各个APP中存在, Django 会优先找全局template 文件下的html文件,如果全局下的template文件没有相关的html Djan
// GET请求request.GET // POST请求request.POST // 处理文件上传请求request.FILES // 处理如checkbox等多选 接受列表request.get
from bs4 import BeautifulSoup#kindeditordef kindeditor(request): s = &#39;&#39;&#39; &lt;li&gt;&lt;s
view.py 配置 html 配置
from django.http import JsonResponse JsonResponse 里面代码会加这一个响应头 kwargs.setdefault(&#39;content_type&#
#下面两种是基于QuerySet查询 也就是说SQL中用的jion连表的方式查询books = models.UserInfo.objects.all() print(type(books)) &gt
return HttpResponse(&quot;OK&quot;) 返回一个字符串 return redirect(&quot;/index/&quot;) 返回URL return render
from django.http import JsonResponse JsonResponse 里面代码会加这一个响应头 kwargs.setdefault(&#39;content_type&#
浏览器有一个很重要的概念——同源策略(Same-Origin Policy)。所谓同源是指,域名,协议,端口相同。不同源的客户端脚本(javascript、ActionScript)在没明确授权的情况
自动发送 &gt; 依赖jQuery文件 实例--&gt;GET请求: 手动发送 &gt; 依赖浏览器XML对象(也叫原生ajax) Ajax主要就是使用 【XmlHttpRequest】对象来完成请
#下面两种是基于QuerySet查询 也就是说SQL中用的jion连表的方式查询books = models.UserInfo.objects.all() print(type(books)) &gt
// GET请求request.GET // POST请求request.POST // 处理文件上传请求request.FILES // 处理如checkbox等多选 接受列表request.get
return HttpResponse(&quot;OK&quot;) 返回一个字符串 return redirect(&quot;/index/&quot;) 返回URL return render