diff --git a/class/core/ssh_terminal.py b/class/core/ssh_terminal.py new file mode 100644 index 000000000..9dcdc590a --- /dev/null +++ b/class/core/ssh_terminal.py @@ -0,0 +1,446 @@ +# coding: utf-8 + +# --------------------------------------------------------------------------------- +# MW-Linux面板 +# --------------------------------------------------------------------------------- +# copyright (c) 2018-∞(https://github.com/midoks/mdserver-web) All rights reserved. +# --------------------------------------------------------------------------------- +# Author: midoks +# --------------------------------------------------------------------------------- + +# --------------------------------------------------------------------------------- +# 公共操作 +# --------------------------------------------------------------------------------- + +import json +import time +import os +import sys +import socket +import threading +import re + +from io import BytesIO, StringIO + +import mw +import paramiko + +from flask_socketio import SocketIO, emit, send + + +class ssh_terminal: + + __debug_file = 'logs/terminal.log' + __log_type = 'SSH终端' + + # websocketio 唯一标识 + __sid = '' + + __host = None + __port = 22 + __user = None + __pass = None + __pkey = None + __key_passwd = None + + __rep_ssh_config = False + __rep_ssh_service = False + __sshd_config_backup = None + + __connecting = False + __ssh = None + __tp = None + __ps = None + + def __init__(self): + pass + + def debug(self, msg): + msg = "{} - {}:{} => {} \n".format(mw.formatDate(), + self.__host, self.__port, msg) + # self.history_send(msg) + mw.writeFile(self.__debug_file, msg, 'a+') + + def returnMsg(self, status, msg): + return {'status': status, 'msg': msg} + + def restartSsh(self, act='reload'): + ''' + 重启ssh 无参数传递 + ''' + version = mw.readFile('/etc/redhat-release') + if not os.path.exists('/etc/redhat-release'): + mw.execShell('service ssh ' + act) + elif version.find(' 7.') != -1 or version.find(' 8.') != -1: + mw.execShell("systemctl " + act + " sshd.service") + else: + mw.execShell("/etc/init.d/sshd " + act) + + def isRunning(self, rep=False): + try: + if rep and self.__rep_ssh_service: + self.restartSsh('stop') + return True + + status = self.getSshStatus() + if not status: + self.restartSsh('start') + self.__rep_ssh_service = True + return True + return False + except: + return False + + def setSshdConfig(self, rep=False): + self.isRunning(rep) + if rep and not self.__rep_ssh_config: + return False + + try: + sshd_config_file = '/etc/ssh/sshd_config' + if not os.path.exists(sshd_config_file): + return False + + sshd_config = mw.readFile(sshd_config_file) + if not sshd_config: + return False + + if rep: + if self.__sshd_config_backup: + mw.writeFile(sshd_config_file, self.__sshd_config_backup) + self.restartSsh() + return True + + pin = r'^\s*PubkeyAuthentication\s+(yes|no)' + pubkey_status = re.findall(pin, sshd_config, re.I) + if pubkey_status: + if pubkey_status[0] == 'yes': + pubkey_status = True + else: + pubkey_status = False + + pin = r'^\s*RSAAuthentication\s+(yes|no)' + rsa_status = re.findall(pin, sshd_config, re.I) + if rsa_status: + if rsa_status[0] == 'yes': + rsa_status = True + else: + rsa_status = False + + self._sshd_config_backup = sshd_config + is_write = False + if not pubkey_status: + sshd_config = re.sub( + r'\n#?PubkeyAuthentication\s\w+', '\nPubkeyAuthentication yes', sshd_config) + is_write = True + if not rsa_status: + sshd_config = re.sub( + r'\n#?RSAAuthentication\s\w+', '\nRSAAuthentication yes', sshd_config) + is_write = True + + if is_write: + mw.writeFile(sshd_config_file, sshd_config) + self.__rep_ssh_config = True + self.restartSsh() + else: + self.__sshd_config_backup = None + return True + except: + return False + + def setSid(self, sid): + self.__sid = sid + + def connect(self): + # self.connectBySocket() + if self.__host in ['127.0.0.1', 'localhost']: + return self.connectLocalSsh() + else: + return self.connectBySocket() + + def connectLocalSsh(self): + self.createSshInfo() + self.__ps = paramiko.SSHClient() + self.__ps.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + self.__port = mw.getSSHPort() + try: + self.__ps.connect(self.__host, self.__port, timeout=60) + except Exception as e: + self.__ps.connect('127.0.0.1', self.__port) + except Exception as e: + self.__ps.connect('localhost', self.__port) + except Exception as e: + self.setSshdConfig(True) + self.__ps.close() + e = str(e) + if e.find('websocket error!') != -1: + return self.returnMsg(True, '连接成功') + if e.find('Authentication timeout') != -1: + self.debug("认证超时{}".format(e)) + return self.returnMsg(False, '认证超时,请按回车重试!{}'.format(e)) + if e.find('Connection reset by peer') != -1: + self.debug('目标服务器主动拒绝连接') + return self.returnMsg(False, '目标服务器主动拒绝连接') + if e.find('Error reading SSH protocol banner') != -1: + self.debug('协议头响应超时') + return self.returnMsg(False, '协议头响应超时,与目标服务器之间的网络质量太糟糕:' + e) + if not e: + self.debug('SSH协议握手超时') + return self.returnMsg(False, "SSH协议握手超时,与目标服务器之间的网络质量太糟糕") + err = mw.getTracebackInfo() + self.debug(err) + return self.returnMsg(False, "未知错误: {}".format(err)) + + self.debug('local-ssh:认证成功,正在构建会话通道') + self.__ssh = self.__ps.invoke_shell(term='xterm', width=83, height=21) + self.__ssh.setblocking(0) + self.__connect_time = time.time() + self.__last_send = [] + mw.writeLog(self.__log_type, '成功登录到SSH服务器 [{}:{}]'.format( + self.__host, self.__port)) + self.debug('local-ssh:通道已构建') + return self.returnMsg(True, '连接成功!') + + def connectBySocket(self): + if not self.__host: + return self.returnMsg(False, '错误的连接地址') + if not self.__user: + self.__user = 'root' + if not self.__port: + self.__port = 22 + + if self.__host in ['127.0.0.1', 'localhost']: + self.__port = mw.getSSHPort() + + self.setSshdConfig(True) + num = 0 + while num < 5: + num += 1 + try: + self.debug('正在尝试第{}次连接'.format(num)) + if self.__rep_ssh_config: + time.sleep(0.1) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(2 + num) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192) + sock.connect((self.__host, self.__port)) + break + except Exception as e: + if num == 5: + self.setSshdConfig(True) + self.debug('重试连接失败,{}'.format(e)) + if self.__host in ['127.0.0.1', 'localhost']: + return self.returnMsg(False, '连接目标服务器失败: {}'.format("Authentication failed ," + self.__user + "@" + self.__host + ":" + str(self.__port))) + return self.returnMsg(False, '连接目标服务器失败, {}:{}'.format(self.__host, self.__port)) + else: + time.sleep(0.2) + + # print(sock) + self.__tp = paramiko.Transport(sock) + try: + self.__tp.start_client() + self.__tp.banner_timeout = 60 + if not self.__pass or not self.__pkey: + return self.returnMsg(False, '密码或私钥不能都为空: {}:{}'.format(self.__host, self.__port)) + + if self.__pkey: + self.debug('正在认证私钥') + if sys.version_info[0] == 2: + try: + self.__pkey = self.__pkey.encode('utf-8') + except: + pass + p_file = BytesIO(self.__pkey) + else: + p_file = StringIO(self.__pkey) + + try: + pkey = paramiko.RSAKey.from_private_key(p_file) + except: + try: + p_file.seek(0) # 重置游标 + pkey = paramiko.Ed25519Key.from_private_key(p_file) + except: + try: + p_file.seek(0) + pkey = paramiko.ECDSAKey.from_private_key( + p_file) + except: + p_file.seek(0) + pkey = paramiko.DSSKey.from_private_key(p_file) + + self.__tp.auth_publickey(username=self.__user, key=pkey) + else: + try: + self.__tp.auth_none(self.__user) + except Exception as e: + e = str(e) + if e.find('keyboard-interactive') >= 0: + self._auth_interactive() + else: + self.debug('正在认证密码') + self.__tp.auth_password( + username=self.__user, password=self.__pass) + except Exception as e: + self.setSshdConfig(True) + self.__tp.close() + e = str(e) + if e.find('websocket error!') != -1: + return self.returnMsg(True, '连接成功') + if e.find('Authentication timeout') != -1: + self.debug("认证超时{}".format(e)) + return self.returnMsg(False, '认证超时,请按回车重试!{}'.format(e)) + if e.find('Authentication failed') != -1: + self.debug('认证失败{}'.format(e)) + return self.returnMsg(False, '帐号或密码错误: {}'.format(e + "," + self.__user + "@" + self.__host + ":" + str(self.__port))) + if e.find('Bad authentication type; allowed types') != -1: + self.debug('认证失败{}'.format(e)) + if self.__host in ['127.0.0.1', 'localhost'] and self.__pass == 'none': + return self.returnMsg(False, '帐号或密码错误: {}'.format("Authentication failed ," + self.__user + "@" + self.__host + ":" + str(self.__port))) + return self.returnMsg(False, '不支持的身份验证类型: {}'.format(e)) + if e.find('Connection reset by peer') != -1: + self.debug('目标服务器主动拒绝连接') + return self.returnMsg(False, '目标服务器主动拒绝连接') + if e.find('Error reading SSH protocol banner') != -1: + self.debug('协议头响应超时') + return self.returnMsg(False, '协议头响应超时,与目标服务器之间的网络质量太糟糕:' + e) + if not e: + self.debug('SSH协议握手超时') + return self.returnMsg(False, "SSH协议握手超时,与目标服务器之间的网络质量太糟糕") + err = mw.getTracebackInfo() + self.debug(err) + return self.returnMsg(False, "未知错误: {}".format(err)) + + self.debug('认证成功,正在构建会话通道') + self.__ssh = self.__tp.open_session() + self.__ssh.get_pty(term='xterm', width=100, height=34) + self.__ssh.invoke_shell() + self.__connect_time = time.time() + self.__last_send = [] + mw.writeLog(self.__log_type, '成功登录到SSH服务器 [{}:{}]'.format( + self.__host, self.__port)) + self.debug('通道已构建') + return self.returnMsg(True, '连接成功.') + + def setAttr(self, info): + self.__host = info['host'].strip() + + if 'port' in info: + self.__port = int(info['port']) + if 'username' in info: + self.__user = info['username'] + if 'pkey' in info: + self.__pkey = info['pkey'] + if 'password' in info: + self.__pass = info['password'] + if 'pkey_passwd' in info: + self.__key_passwd = info['pkey_passwd'] + try: + result = self.connect() + except Exception as ex: + if str(ex).find("NoneType") == -1: + raise ex + return result + + def send(self): + pass + + def close(self): + try: + if self.__ssh: + self.__ssh.close() + if self.__tp: # 关闭宿主服务 + self.__tp.close() + if self.__ps: + self.__ps.close() + except: + pass + + def heartbeat(self): + while True: + time.sleep(0.1) + if self.__tp and self.__tp.is_active(): + self.__tp.send_ignore() + else: + break + + if self.__ps and self.__ps.is_active(): + self.__ps.send_ignore() + else: + break + + def wsSend(self, recv): + try: + t = recv.decode("utf-8") + return emit('server_response', {'data': t}) + except Exception as e: + return emit('server_response', {'data': recv}) + + def run(self, sid, info): + self.__sid = sid + if not self.__sid: + return self.wsSend('WebSocketIO无效') + + if self.__connecting and not 'host' in info: + return + + if not self.__ssh: + self.__connecting = True + result = self.setAttr(info) + self.__connecting = False + else: + result = self.returnMsg(True, '已连接') + + if result['status']: + if type(info) == str: + time.sleep(0.1) + self.__ssh.send(info) + + try: + time.sleep(0.005) + recv = self.__ssh.recv(8192) + return self.wsSend(recv) + except Exception as ex: + return self.wsSend('') + else: + return self.wsSend(result['msg']) + + def getSshDir(self): + if mw.isAppleSystem(): + user = mw.execShell( + "who | sed -n '2, 1p' |awk '{print $1}'")[0].strip() + return '/Users/' + user + '/.ssh' + return '/root/.ssh' + + def createRsa(self): + ssh_dir = self.getSshDir() + ssh_ak = ssh_dir + '/authorized_keys' + if not os.path.exists(ssh_ak): + mw.execShell('touch ' + ssh_ak) + if not os.path.exists(ssh_dir + '/id_rsa.pub') and os.path.exists(ssh_dir + '/id_rsa'): + cmd = 'echo y | ssh-keygen -q -t rsa -P "" -f ' + ssh_dir + '/id_rsa' + mw.execShell(cmd) + else: + cmd = 'ssh-keygen -q -t rsa -P "" -f ' + ssh_dir + '/id_rsa' + mw.execShell(cmd) + cmd = 'cat ' + ssh_dir + '/id_rsa.pub >> ' + ssh_dir + '/authorized_keys' + mw.execShell(cmd) + cmd = 'chmod 600 ' + ssh_dir + '/authorized_keys' + mw.execShell(cmd) + + def createSshInfo(self): + ssh_dir = self.getSshDir() + if not os.path.exists(ssh_dir + '/id_rsa') or not os.path.exists(ssh_dir + '/id_rsa.pub'): + self.createRsa() + # 检查是否写入authorized_keys + cmd = "cat " + ssh_dir + "/id_rsa.pub | awk '{print $3}'" + data = mw.execShell(cmd) + if data[0] != "": + cmd = "cat " + ssh_dir + "/authorized_keys | grep " + data[0] + ak_data = mw.execShell(cmd) + if ak_data[0] == "": + cmd = 'cat ' + ssh_dir + '/id_rsa.pub >> ' + ssh_dir + '/authorized_keys' + mw.execShell(cmd) + cmd = 'chmod 600 ' + ssh_dir + '/authorized_keys' + mw.execShell(cmd) diff --git a/plugins/webssh/js/webssh.js b/plugins/webssh/js/webssh.js index 70565d912..c71e3dfa4 100755 --- a/plugins/webssh/js/webssh.js +++ b/plugins/webssh/js/webssh.js @@ -1,6 +1,6 @@ - - +//全局 +var host_ssh_list = []; function appPost(method,args,callback){ var _args = null; @@ -110,8 +110,30 @@ function webShell_Load(){ webShell_cmd(); }); + // 切换服务器终端视图 + $('.term_item_tab .list').on('click', 'span.item', function (ev) { + var index = $(this).index(), data = $(this).data(); + if ($(this).hasClass('addServer')) { + } else if ($(this).hasClass('tab_tootls')) { + } else { + $(this).addClass('active').siblings().removeClass('active'); + $('.term_content_tab .term_item:eq(' + index + ')').addClass('active').siblings().removeClass('active'); + var item = host_ssh_list[data.id]; + // item.term.focus(); + // item.term.FitAddon.fit(); + // item.resize({ cols: item.term.cols, rows: item.term.rows }); + } + }); + + $('.term_item_tab').on('click', '.icon-trem-close', function () { + var id = $(this).parent().data('id'); + webShell_removeTermView(id); + }) + //服务器列表和常用命令 - webShell_getHostList();//默认调用 + webShell_getHostList(); + + //服务器列表和命令切换 $('.term_tootls .tab-nav span').click(function(){ var list_type = $(this).attr('data-type'); if (!$(this).hasClass('on')){ @@ -243,155 +265,46 @@ function webShell_getCmdList(){ function webShell_Menu2(){ - new Terms('#ECFEfRWM8', { ssh_info: { host: "127.0.0.1", ps: "22", id: 'ECFEfRWM8' } }); + var random = 'localhost'; + host_ssh_list[random] = new Terms_WebSocketIO('#'+random, { ssh_info: { host: "127.0.0.1", ps: "22", id: random } }); } -function webShell_Menu() { - var termCols = 83; - var termRows = 20; - var sendTotal = 0; - if(!socket)socket = io.connect(); - var term = new Terminal({ cols: termCols, rows: termRows, screenKeys: true, useStyle: true}); - - term.open(); - term.setOption('cursorBlink', true); - term.setOption('fontSize', 14); - gterm = term; - - socket.on('server_response', function (data) { - term.write(data.data); - if (data.data == '\r\n登出\r\n' || - data.data == '登出\r\n' || - data.data == '\r\nlogout\r\n' || - data.data == 'logout\r\n') { - setTimeout(function () { - layer.closeAll(); - term.destroy(); - clearInterval(interval); - }, 500); - } - }); - - $(window).unload(function(){ -   term.destroy(); - clearInterval(interval); - }); - - if (socket) { - socket.emit('connect_event', ''); - interval = setInterval(function () { - socket.emit('connect_event', ''); - }, 1000); +function webShell_openTermView(info) { + console.log(info); + if (typeof info === "undefined") { + info = { host: '127.0.0.1', ps: '本地服务器' } } - - term.on('data', function (data) { - socket.emit('webssh', data); - }); - - $(".shell_btn_close").click(function(){ - layer.close(term_box); - term.destroy(); - clearInterval(interval); - }) - - setTimeout(function () { - $('.terminal').detach().appendTo('#ECFEfRWM8'); - $("#ECFEfRWM8").show(); - socket.emit('webssh', "\n"); - term.focus(); - - // 鼠标右键事件 - var can = $("#term"); - can.contextmenu(function (e) { - var winWidth = can.width(); - var winHeight = can.height(); - var mouseX = e.pageX; - var mouseY = e.pageY; - var menuWidth = $(".contextmenu").width(); - var menuHeight = $(".contextmenu").height(); - var minEdgeMargin = 10; - if (mouseX + menuWidth + minEdgeMargin >= winWidth && - mouseY + menuHeight + minEdgeMargin >= winHeight) { - menuLeft = mouseX - menuWidth - minEdgeMargin + "px"; - menuTop = mouseY - menuHeight - minEdgeMargin + "px"; - } - else if (mouseX + menuWidth + minEdgeMargin >= winWidth) { - menuLeft = mouseX - menuWidth - minEdgeMargin + "px"; - menuTop = mouseY + minEdgeMargin + "px"; - } - else if (mouseY + menuHeight + minEdgeMargin >= winHeight) { - menuLeft = mouseX + minEdgeMargin + "px"; - menuTop = mouseY - menuHeight - minEdgeMargin + "px"; - } - else { - menuLeft = mouseX + minEdgeMargin + "px"; - menuTop = mouseY + minEdgeMargin + "px"; - }; - - var selectText = term.getSelection() - var style_str = ''; - var paste_str = ''; - if (!selectText) { - if (!getCookie('shell_copy_body')) { - paste_str = 'style="color: #bbb;" disable'; - } - style_str = 'style="color: #bbb;" disable'; - } else { - setCookie('ssh_selection', selectText); - } - - - var menudiv = ''; - $("body").append(menudiv); - $(".contextmenu").css({ - "left": menuLeft, - "top": menuTop - }); - return false; - }); - can.click(function () { - remove_ssh_menu(); - }); + var random = getRandomString(9); + var tab_content = $('.term_content_tab'); + var item_list = $('.term_item_tab .list'); + tab_content.find('.term_item').removeClass('active').siblings().removeClass('active'); + tab_content.append('
'); + item_list.find('.item').removeClass('active'); + if (info.ps == ''){ + info.ps = info.host; + } + item_list.append('
' + info.ps + '
'); + host_ssh_list[random] = new Terms_WebSocketIO('#' + random, { ssh_info: { host: info.host, ps: info.ps, id: random } }); +} - clipboard = new ClipboardJS('.shell_copy_btn'); - clipboard.on('success', function (e) { - layer.msg('复制成功!'); - setCookie('shell_copy_body', e.text) - remove_ssh_menu(); - term.focus(); - }); - clipboard.on('error', function (e) { - layer.msg('复制失败,浏览器不兼容!'); - setCookie('shell_copy_body', e.text) - remove_ssh_menu(); - term.focus(); - }); - - $(".shellbutton").click(function () { - var tobj = $("textarea[name='ssh_copy']"); - var ptext = tobj.val(); - tobj.val(''); - if ($(this).text().indexOf('Alt') != -1) { - ptext +="\n"; - } - socket.emit('webssh', ptext); - term.focus(); - }) - $("textarea[name='ssh_copy']").keydown(function (e) { - - if (e.ctrlKey && e.keyCode == 13) { - $(".shell_btn_1").click(); - } else if (e.altKey && e.keyCode == 13) { - $(".shell_btn_1").click(); - } - }); +function webShell_removeTermView(id){ + var item = $('[data-id="' + id + '"]'), next = item.next(), prev = item.prev(); + $('#' + id).remove(); + item.remove(); + try { + host_ssh_list[id].close(); + } catch (error) { + } - }, 100); + delete host_ssh_list[id]; + if (item.hasClass('active')) { + if (next.length > 0) { + next.click(); + } else { + prev.click(); + } + } } function webShell_getHostList(info){ @@ -401,7 +314,7 @@ function webShell_getHostList(info){ var tli = ''; for (var i = 0; i < alist.length; i++) { - tli+='
  • \ + tli+='
  • \ \ '+alist[i]['host']+'\ \ @@ -430,11 +343,42 @@ function webShell_getHostList(info){ }); }); + + $('.tootls_host_list').on('click', 'li', function (e) { + var index = $(this).index(); + var host = $(this).data('host'); + $(this).find('i').addClass('active'); + if ($('.item[data-host="' + host + '"]').length > 0) { + layer.msg('已经打开!', { icon: 0, time: 3000 }) + // layer.msg('如需多会话窗口,请右键终端标题复制会话!', { icon: 0, time: 3000 }); + } else { + webShell_openTermView(alist[index]); + } + }); + + // tab切换 + $('.term_tootls .tab-nav span').click(function () { + if ($(this).hasClass('on')) return; + var index = $(this).index(); + $(this).siblings('.on').removeClass('on'); + $(this).addClass('on'); + $('.term_tootls .tab-con .tab-block').removeClass('on'); + $('.term_tootls .tab-con .tab-block').eq(index).addClass('on'); + var type = $(this).attr('data-type'); + switch (type) { + case 'host': + that.reader_host_list(); + break; + case 'shell': + that.reader_command_list(); + break; + } + }); + }); } function webShell_addServer(info=[]){ - console.log(info); layer.open({ type: 1, title: '添加主机信息', diff --git a/plugins/webssh/menu/index.css b/plugins/webssh/menu/index.css index 35f25dc81..249c427b7 100644 --- a/plugins/webssh/menu/index.css +++ b/plugins/webssh/menu/index.css @@ -10,6 +10,10 @@ /* menu end */ +.xterm{ + position: inherit; +} + .tab-nav { border-bottom: #cacad9 1px solid; } diff --git a/plugins/webssh/menu/index.html b/plugins/webssh/menu/index.html index 460321320..f7f8a4958 100755 --- a/plugins/webssh/menu/index.html +++ b/plugins/webssh/menu/index.html @@ -7,7 +7,7 @@
    - +
    本地服务器
    @@ -23,7 +23,7 @@
    -
    +
    diff --git a/route/__init__.py b/route/__init__.py index 3c9cabfc4..ff55188c4 100755 --- a/route/__init__.py +++ b/route/__init__.py @@ -12,8 +12,7 @@ import socket # reload(sys) # sys.setdefaultencoding('utf-8') - - +import paramiko from datetime import timedelta from flask import Flask @@ -612,7 +611,7 @@ do ps -t /dev/$i |grep -v TTY | awk '{print $1}' | xargs kill -9 done -#getHostAddr +# getHostAddr PLIST=`who | grep "${ip}" | awk '{print $2}'` for i in $PLIST do @@ -647,45 +646,32 @@ def connect_ssh(): return True -# methods=["GET", "OPTIONS", "HEAD"] -# pip3 install flask==1.1.2 -# pip3 install Werkzeug==1.0.1 -@sockets.route('/webssh', methods=["GET", "OPTIONS", "HEAD"]) -def webssh(ws): - print("connection start") - while not ws.closed: - msg = ws.receive() # 同步阻塞 - print(msg) - now = datetime.datetime.now().isoformat() - ws.send(now) # 发送数据 - time.sleep(1) +shell_client = None -# def handle_route_websocket(app_socket): -# @app_socket.route('/webssh') -# def page_websocket_test(ws): -# now = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time())) -# while not ws.closed: -# # 回传给clicent -# message = ws.receive() # 接收到消息 -# if message is not None: -# print("client says(%s): %s" % (now, message)) -# ws.send(str("回执:server已收到消息!-- %s " % now)) -# ws.send(str(json.dumps(message))) # 回传给clicent -# else: -# print(now, "no receive") +@socketio.on('webssh_websocketio') +def webssh_websocketio(data): + if not isLogined(): + emit('server_response', {'data': '会话丢失,请重新登陆面板!\r\n'}) + return + global shell_client + if not shell_client: + import ssh_terminal + shell_client = ssh_terminal.ssh_terminal() -# handle_route_websocket(sockets) + print("request.sid", request.sid) + shell_client.run(request.sid, data) + return @socketio.on('webssh') def webssh(msg): - print('webssh cmd:' + msg) + global shell, ssh if not isLogined(): emit('server_response', {'data': '会话丢失,请重新登陆面板!\r\n'}) return None - global shell, ssh + ssh_success = True if not shell: ssh_success = connect_ssh() @@ -729,7 +715,6 @@ def connected_msg(msg): # print 'connected_msg:' + str(e) try: - import paramiko ssh = paramiko.SSHClient() # 启动尝试时连接 diff --git a/route/static/js/term-websocketio.js b/route/static/js/term-websocketio.js new file mode 100644 index 000000000..517d3ce56 --- /dev/null +++ b/route/static/js/term-websocketio.js @@ -0,0 +1,143 @@ +function Terms_WebSocketIO (el, config) { + + console.log(config); + if (typeof config == "undefined") { + config = {}; + } + this.el = el; + this.id = config.ssh_info.id || ''; + this.ws = null; //websocket对象 + this.route = 'webssh_websocketio'; // 访问的方法 + this.term = null; //term对象 + this.info = null; // 请求数据 + this.last_body = null; + this.fontSize = 14; //终端字体大小 + this.ssh_info = config.ssh_info; + this.term_timer = null; + this.run(); +} + +var socket, gterm; + +Terms_WebSocketIO.prototype = { + // websocket持久化连接 + connect: function (callback) { + if(!this.ws){ + this.ws = io.connect(); + } + }, + + //连接服务器成功 + on_open: function (ws_event) { + var http_token = $("#request_token_head").attr('token'); + this.ssh_info['x-http-token'] = http_token; + + this.send(JSON.stringify(this.ssh_info || { 'x-http-token': http_token })) + this.term.FitAddon.fit(); + this.resize({ cols: this.term.cols, rows: this.term.rows }); + }, + + //服务器消息事件 + on_message: function (ws_event) { + // console.log(ws_event.data); + this.term.write(ws_event.data); + if (ws_event.data == '\r\n登出\r\n' || ws_event.data == '登出\r\n' || + ws_event.data == '\r\nlogout\r\n' || ws_event.data == 'logout\r\n'|| + ws_event.data == '\r\nexit\r\n' || ws_event.data == 'exit\r\n') { + this.term.destroy(); + clearInterval(this.term_timer); + } + }, + + //websocket关闭事件 + on_close: function (ws_event) { + this.ws = null; + }, + + //websocket错误事件 + on_error: function (ws_event) { + if (ws_event.target.readyState === 3) { + // var msg = '错误: 无法创建WebSocket连接,请在面板设置页面关闭【开发者模式】'; + // layer.msg(msg,{time:5000}) + // if(Term.state === 3) return + // Term.term.write(msg) + // Term.state = 3; + } else { + console.log(ws_event) + } + }, + //发送数据 + //@param event 唯一事件名称 + //@param data 发送的数据 + //@param callback 服务器返回结果时回调的函数,运行完后将被回收 + send: function (data, num) { + var that = this; + //如果没有连接,则尝试连接服务器 + if (!this.ws || this.bws.readyState == 3 || this.bws.readyState == 2) { + this.connect(); + } + //判断当前连接状态,如果!=1,则100ms后尝试重新发送 + if (this.ws.readyState === 1) { + this.ws.send(data); + } else { + if (this.state === 3) return; + if (!num) num = 0; + if (num < 5) { + num++; + setTimeout(function () { that.send(data, num++); }, 100) + } + } + }, + + //关闭连接 + close: function () { + this.ws.close(); + this.set_term_icon(0); + }, + resize: function (size) { + if (this.ws) { + size['resize'] = 1; + this.send(JSON.stringify(size)); + } + + }, + run: function (ssh_info) { + this.connect(); + + var that = this; + var termCols = 83; + var termRows = 21; + this.term = new Terminal({ cols: termCols, rows: termRows, screenKeys: true, useStyle: true}); + + this.term.open($('#'+this.id)[0]); + this.term.setOption('cursorBlink', true); + this.term.setOption('fontSize', this.fontSize); + + // console.log(this.term.cols,this.term.rows); + // this.term.fit(); + // console.log(this.term.cols,this.term.rows); + + // this.term.FitAddon = new window.FitAddon(); + // this.term.loadAddon(this.term.FitAddon); + // this.term.WebLinksAddon = new WebLinksAddon.WebLinksAddon(); + // this.term.loadAddon(this.term.WebLinksAddon); + + this.ws.on('server_response', function (ev) { that.on_message(ev)}); + + if (this.ws) { + that.ws.emit('webssh_websocketio', ''); + this.term_timer = setInterval(function () { + that.ws.emit('webssh_websocketio', ''); + }, 500); + } + + this.term.on('data', function (data) { + that.ws.emit('webssh_websocketio', data); + }); + + // $('.terminal').detach().appendTo('#'+this.id); + this.ws.emit('webssh_websocketio', this.ssh_info); + this.ws.emit('webssh_websocketio', '\n'); + this.term.focus(); + } +} diff --git a/route/static/js/term.js b/route/static/js/term.js index 53cb7935d..47fb7ac9c 100644 --- a/route/static/js/term.js +++ b/route/static/js/term.js @@ -57,6 +57,7 @@ Terms.prototype = { } }, + //websocket关闭事件 on_close: function (ws_event) { this.set_term_icon(0); @@ -65,7 +66,6 @@ Terms.prototype = { /** * @name 设置终端标题状态 - * @author chudong<2020-08-10> * @param {number} status 终端状态 * @return void */ diff --git a/route/templates/default/layout.html b/route/templates/default/layout.html index fd92b8308..2d55fe8ac 100755 --- a/route/templates/default/layout.html +++ b/route/templates/default/layout.html @@ -75,7 +75,7 @@ - +