02: tornado结合Websocket进行WebSSH的实现

1.1 WebSSH介绍

   1、什么是WebSSH?

      1. webssh 泛指一种技术可以在网页上实现一个 SSH 终端。

      2. 从而无需 Xshell 之类的模拟终端工具进行 SSH 连接,将 SSH 这一比较低层的操作也从 C/S 架构扭成了 B/S 架构

      3. 这样的架构常用在运维制作开发一些堡垒机等系统中,或是目前比较新型的在线教育方式

      4. 通过WebSSH 向学生提供一个可以直接使用浏览器进行相关 Linux 操作或代码编写的学习方式

      5. WebSSh 主要是建立客户端与服务端的即时通信

   2、要实现WebSSH技术栈介绍

# 前端
vue
websocket
xterm.js

# 后端
tornado
dwebsocket
paramiko
threading

  3、技术介绍

    1)xterm

        前端通过 xterm 插件进行 shell 黑窗口环境的搭建,

        这个插件会自动解析由后台 paramiko 返回的带有标记样式的命令结果,并渲染到浏览器中,非常酷炫
      
    2)websocket

        这里通过 websocket 进行浏览器与 tornado 的双向通信。

    3)paramiko

        paramiko 此时的角色用来承担 tornado与 Linux 环境的交互,

        将前端发来的命令发送给后台,将后台发来的命令结果返回到前端的 xterm 组件中

1.2 前端实现

        参考代码:https://gitee.com/edushiyanlou/webssh

  1、初始化vue项目

vue init webpack webssh
npm install  --save  xterm@3.1.0
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'

// 引入xterm.css样式
import 'xterm/dist/xterm.css'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})
src\main.js  文件中引入xterm.css文件
<template>
  <div class="console" id="terminal"></div>
</template>
<script>
  import Terminal from './Xterm'
  export default {
    name: 'Console',
    data () {
      return {
        terminal: {
          pid: 1,
          name: 'terminal',
          cols: 400,
          rows: 400
        },
        term: null,
        terminalSocket: null
      }
    },
    methods: {
      runRealTerminal () {
        console.log('webSocket is finished')
      },
      errorRealTerminal () {
        console.log('error')
      },
      closeRealTerminal () {
        console.log('close')
      }
    },
    mounted () {
      console.log('pid : ' + this.terminal.pid + ' is on ready')
      let terminalContainer = document.getElementById('terminal')
      this.term = new Terminal()                     // 创建一个新的Terminal对象
      this.term.open(terminalContainer)              // 将term挂砸到dom节点上
      // open websocket
      this.terminalSocket = new WebSocket('ws://127.0.0.1:3000/terminals/')  // 创建socket连接
      this.terminalSocket.onopen = this.runRealTerminal          // 当连接成功触发此函数
      this.terminalSocket.onclose = this.closeRealTerminal       // 当断开连接调用此函数
      this.terminalSocket.onerror = this.errorRealTerminal       // 当发生错误调用此函数
      this.term.attach(this.terminalSocket)                      // // 绑定xterm到ws流中
      this.term._initialized = true
      console.log('mounted is going on')
    },
    beforeDestroy () {
      this.terminalSocket.close()
      this.term.destroy()
    }
  }
</script>
src\components\WebSSH.vue  创建.vue文件
import { Terminal } from 'xterm'
import * as fit from 'xterm/lib/addons/fit/fit'
import * as attach from 'xterm/lib/addons/attach/attach'
Terminal.applyAddon(fit)
Terminal.applyAddon(attach)

export default Terminal
src\components\Xterm.js  导出xterm相关插件
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import WebSSH from '@/components/WebSSH'

Vue.use(Router)

export default new Router({
  routes: [
    { path: '/', name: 'HelloWorld', component: HelloWorld },
    { path: '/webssh', name: 'WebSSH', component: WebSSH }
  ]
})
src\router\index.js 添加路由

  2、对核心代码解释

# 1. Xterm.js  初始化 xterm 组件并添加两个插件 
attach 可以将终端附加到 websocket 流中
fit 可以调整终端的大小以及行和列适配父级元素

# 2. WebSSH.vue 
构建 websocket 并绑定到终端, websocket 地址为 ws 协议前缀
此时使用的是即将在 tornado 中配置 Websocket 后台视图的路由,这一系列行为将挂载到钩子函数下进行

# 当浏览器关闭时,也代表着客户端关闭,此时主动断开连接,交给 vue 的钩子函数来处理这个问题
beforeDestroy () {
    this.terminalSocket.close()
    this.term.destroy()
}

 1.3 后端tornado代码(方法一)

  1、安装相关包

# requirements.txt
paramiko==2.4.1
tornado==4.5.2
requests==2.18.4
PyJWT==1.6.4

  2、tornado服务端代码

# -*- coding: utf-8 -*-
import tornado
import tornado.websocket
import paramiko
import threading
import time

# 配置服务器信息
HOSTS = '1.1.1.3'
PORT = 22
USERNAME = 'root'
PASSWORD = 'chnsys@2016'


class MyThread(threading.Thread):
    def __init__(self, id, chan):
        '''
        :param id:  线程id这里没有用
        :param chan:  webSSHServer对象
        '''
        threading.Thread.__init__(self)
        self.chan = chan

    def run(self):
        thread_num = len(threading.enumerate())
        print("线程数量:", thread_num)
        while not self.chan.chan.exit_status_ready():
            time.sleep(0.1)
            try:
                data = self.chan.chan.recv(1024)
                self.chan.write_message(data)
            except Exception as ex:
                print(str(ex))
        self.chan.sshclient.close()
        return False


class webSSHServer(tornado.websocket.WebSocketHandler):
    # 连接websocket服务器时进行的event
    def open(self):
        self.sshclient = paramiko.SSHClient()     # 1 创建SSH对象
        # 通过known_hosts 方式进行认证可以用这个,如果known_hosts 文件未定义还需要定义 known_hosts
        self.sshclient.load_system_host_keys()
        self.sshclient.set_missing_host_key_policy(paramiko.AutoAddPolicy())  # 2 允许连接不在know_hosts文件中的主机
        self.sshclient.connect(HOSTS, PORT, USERNAME, PASSWORD)              # 3 连接服务器
        self.chan = self.sshclient.invoke_shell(term='xterm')               # 4 建立交互式shell连接
        self.chan.settimeout(0)
        t1 = MyThread(999, self)  # 传入webSSHServer对象
        t1.setDaemon(True)       # 守护线程,主线程退出时,需要子线程随主线程退出
        t1.start()

    # 收到信息的时候进行的动作(每次从前端收到执行的命令就会执行此函数)
    def on_message(self, message):
        try:
            self.chan.send(message)
        except Exception as ex:
            print(str(ex))

    # 主动调用close()函数可以关闭这个连接(关闭浏览器时会触发函数,关闭这个线程)
    def on_close(self):
        self.sshclient.close()

    # 每次有浏览器触发新连接调用此函数
    def check_origin(self, origin):
        # 允许跨域访问
        return True


if __name__ == '__main__':
    # 定义路由
    app = tornado.web.Application([
        (r"/terminals/", webSSHServer),
    ],
        debug=True
    )

    # 启动服务器
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(3000)
    tornado.ioloop.IOLoop.current().start()
server.py tornado服务代码

 1.4 后端django代码(方法二)

         基于django的WebSSH: https://blog.51cto.com/hequan/2145007

   1、安装相关包

# requirements.txt
dwebsocket==0.5.10
Django==2.0.4
paramiko==2.4.1

  2、django端代码

from django.contrib import admin
from django.urls import path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('terminals/', views.webssh),  # 前端的 ws 连接地址
]
urls.py
from django.shortcuts import render
import paramiko
import threading
from dwebsocket.decorators import accept_websocket,require_websocket


def _ssh(host,username,password,port=22):
    sh = paramiko.SSHClient()  # 1 创建SSH对象
    sh.set_missing_host_key_policy(paramiko.AutoAddPolicy())    # 2 允许连接不在know_hosts文件中的主机
    sh.connect(host, username=username, password=password)      # 3 连接服务器
    channle = sh.invoke_shell(term='xterm')                    # 4 建立交互式shell连接
    return channle


def recv_ssh_msg(channle,ws):
    '''
    channle: 建立好的SSH连接通道
    这个函数会不停的接收ssh通道返回的命令
    返回到前端的ws套接字里
    '''
    while not channle.exit_status_ready():
        try:
            buf = channle.recv(1024)   # 接收命令的执行结果
            ws.send(buf)               #发送消息到客户端
        except:
            break


@accept_websocket
def webssh(request):
    '''
    1: 接收前端(ws)的命令,发给后台(ssh)
    2: 接收后台的返回结果,给到前端
    '''
    # request.is_websocket: 如果是个websocket请求返回True,如果是个普通的http请求返回False, 可以用这个方法区分它们
    if request.is_websocket:
        host = '1.1.1.3'
        username = 'root'
        password = 'chnsys@2016'
        channle = _ssh(host, username=username, password=password)  # 返回交互式shell连接对象
        ws = request.websocket
        t = threading.Thread(target=recv_ssh_msg,args=(channle,ws))  #
        t.setDaemon(True)
        t.start() # 线程开启
        while 1:
            cmd = ws.wait() # 阻塞接收前端发来的命令
            if cmd:
                channle.send(cmd) # 由SSH通道转交给Linux环境
            else: # 连接断开 跳出循环
                break
        ws.close() # 释放对应套接字资源
        channle.close()
app01/views.py

  3、效果图

      

 

posted @ 2020-01-30 16:18  不做大哥好多年  阅读(1588)  评论(0编辑  收藏  举报