From 24730ebd0f90b2441851b6ccb2259e0dd71d3a88 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sat, 7 Nov 2015 13:38:50 +0800 Subject: [PATCH] =?UTF-8?q?web=20terminal=E8=AE=B0=E5=BD=95=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- connect.py | 265 +++++++++++++++++++-------------- jumpserver/settings.py | 2 +- run_websocket.py | 86 +++++++---- templates/jlog/log_online.html | 28 ++-- 4 files changed, 218 insertions(+), 163 deletions(-) diff --git a/connect.py b/connect.py index 063708ad3..f8dad935b 100644 --- a/connect.py +++ b/connect.py @@ -201,26 +201,6 @@ def deal_command(str_r, ssh): else: return '' -def remove_control_char(str_r): - """ - 处理日志特殊字符 - """ - control_char = re.compile(r""" - \x1b[ #%()*+\-.\/]. | - \r | #匹配 回车符(CR) - (?:\x1b\[|\x9b) [ -?]* [@-~] | #匹配 控制顺序描述符(CSI)... Cmd - (?:\x1b\]|\x9d) .*? (?:\x1b\\|[\a\x9c]) | \x07 | #匹配 操作系统指令(OSC)...终止符或振铃符(ST|BEL) - (?:\x1b[P^_]|[\x90\x9e\x9f]) .*? (?:\x1b\\|\x9c) | #匹配 设备控制串或私讯或应用程序命令(DCS|PM|APC)...终止符(ST) - \x1b. #匹配 转义过后的字符 - [\x80-\x9f] #匹配 所有控制字符 - """, re.X) - backspace = re.compile(r"[^\b][\b]") - line_filtered = control_char.sub('', str_r.rstrip()) - while backspace.search(line_filtered): - line_filtered = backspace.sub('', line_filtered) - - return line_filtered - def newline_code_in(strings): for i in ['\r', '\r\n', '\n']: @@ -230,17 +210,139 @@ def newline_code_in(strings): return False -class Jtty(object): +class Tty(object): + """ + A virtual tty class + 一个虚拟终端类,实现连接ssh和记录日志,基类 + """ + def __init__(self, username, asset_name): + self.username = username + self.asset_name = asset_name + self.ip = None + self.port = 22 + self.channel = None + self.user = None + self.asset = None + self.role = None + self.ssh = None + self.connect_info = None + self.login_type = 'ssh' + + @staticmethod + def is_output(strings): + newline_char = ['\n', '\r', '\r\n'] + for char in newline_char: + if char in strings: + return True + return False + + @staticmethod + def remove_control_char(str_r): + """ + 处理日志特殊字符 + """ + control_char = re.compile(r""" + \x1b[ #%()*+\-.\/]. | + \r | #匹配 回车符(CR) + (?:\x1b\[|\x9b) [ -?]* [@-~] | #匹配 控制顺序描述符(CSI)... Cmd + (?:\x1b\]|\x9d) .*? (?:\x1b\\|[\a\x9c]) | \x07 | #匹配 操作系统指令(OSC)...终止符或振铃符(ST|BEL) + (?:\x1b[P^_]|[\x90\x9e\x9f]) .*? (?:\x1b\\|\x9c) | #匹配 设备控制串或私讯或应用程序命令(DCS|PM|APC)...终止符(ST) + \x1b. #匹配 转义过后的字符 + [\x80-\x9f] #匹配 所有控制字符 + """, re.X) + backspace = re.compile(r"[^\b][\b]") + line_filtered = control_char.sub('', str_r.rstrip()) + while backspace.search(line_filtered): + line_filtered = backspace.sub('', line_filtered) + + return line_filtered + + def get_log_file(self): + """ + Logging user command and output. + 记录用户的日志 + """ + tty_log_dir = os.path.join(log_dir, 'tty') + timestamp_start = int(time.time()) + date_start = time.strftime('%Y%m%d', time.localtime(timestamp_start)) + time_start = time.strftime('%H%M%S', time.localtime(timestamp_start)) + today_connect_log_dir = os.path.join(tty_log_dir, date_start) + log_file_path = os.path.join(today_connect_log_dir, '%s_%s_%s' % (self.username, self.asset_name, time_start)) + + if self.login_type == 'ssh': + pid = os.getpid() + remote_ip = os.popen("who -m | awk '{ print $5 }'").read().strip('()\n') + else: + pid = 0 + remote_ip = 'Web' + + try: + is_dir(today_connect_log_dir, mode=0777) + except OSError: + raise ServerError('Create %s failed, Please modify %s permission.' % (today_connect_log_dir, tty_log_dir)) + + try: + log_file_f = open(log_file_path + '.log', 'a') + log_time_f = open(log_file_path + '.time', 'a') + except IOError: + raise ServerError('Create logfile failed, Please modify %s permission.' % today_connect_log_dir) + + log = Log(user=self.username, host=self.asset_name, remote_ip=remote_ip, + log_path=log_file_path, start_time=datetime.datetime.now(), pid=pid) + log_file_f.write('Start at %s\n' % datetime.datetime.now()) + log.save() + return log_file_f, log_time_f, log + + def get_connect_info(self): + """ + 获取需要登陆的主机的信息和映射用户的账号密码 + """ + + # 1. get ip, port + # 2. get 映射用户 + # 3. get 映射用户的账号,密码或者key + # self.connect_info = {'user': '', 'asset': '', 'ip': '', 'port': 0, 'role_name': '', 'role_pass': '', 'role_key': ''} + self.connect_info = {'user': 'a', 'asset': 'b', 'ip': '127.0.0.1', 'port': 22, 'role_name': 'root', 'role_pass': 'redhat', 'role_key': ''} + return self.connect_info + + def get_connection(self): + """ + 获取连接成功后的ssh + """ + connect_info = self.get_connect_info() + + # 发起ssh连接请求 Make a ssh connection + ssh = paramiko.SSHClient() + ssh.load_system_host_keys() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if connect_info.get('role_pass'): + ssh.connect(connect_info.get('ip'), + port=connect_info.get('port'), + username=connect_info.get('role_name'), + password=connect_info.get('role_pass'), + look_for_keys=False) + else: + ssh.connect(connect_info.get('ip'), + port=connect_info.get('port'), + username=connect_info.get('role_name'), + key_filename=connect_info.get('role_key'), + look_for_keys=False) + + except paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException: + raise ServerError('认证失败 Authentication Error.') + except socket.error: + raise ServerError('端口可能不对 Connect SSH Socket Port Error, Please Correct it.') + else: + self.ssh = ssh + return ssh + + +class SshTty(Tty): """ A virtual tty class 一个虚拟终端类,实现连接ssh和记录日志 """ - def __init__(self, username, ip): - self.chan = None - self.username = username - self.ip = ip - # self.user = user - # self.asset = asset @staticmethod def get_win_size(): @@ -263,49 +365,16 @@ class Jtty(object): """ try: win_size = self.get_win_size() - self.chan.resize_pty(height=win_size[0], width=win_size[1]) + self.channel.resize_pty(height=win_size[0], width=win_size[1]) except Exception: pass - def log_record(self): - """ - Logging user command and output. - 记录用户的日志 - """ - tty_log_dir = os.path.join(log_dir, 'tty') - timestamp_start = int(time.time()) - date_start = time.strftime('%Y%m%d', time.localtime(timestamp_start)) - time_start = time.strftime('%H%M%S', time.localtime(timestamp_start)) - today_connect_log_dir = os.path.join(tty_log_dir, date_start) - log_file_path = os.path.join(today_connect_log_dir, '%s_%s_%s' % (self.username, self.ip, time_start)) - pid = os.getpid() - pts = os.popen("ps axu | grep %s | grep -v grep | awk '{ print $7 }'" % pid).read().strip() - ip_list = os.popen("who | grep %s | awk '{ print $5 }'" % pts).read().strip('()\n') - - try: - is_dir(today_connect_log_dir) - except OSError: - raise ServerError('Create %s failed, Please modify %s permission.' % (today_connect_log_dir, tty_log_dir)) - - try: - # log_file_f = open('/opt/jumpserver/logs/tty/20151102/a_b_191034.log', 'a') - log_file_f = open(log_file_path + '.log', 'a') - log_time_f = open(log_file_path + '.time', 'a') - except IOError: - raise ServerError('Create logfile failed, Please modify %s permission.' % today_connect_log_dir) - - log = Log(user=self.username, host=self.ip, remote_ip=ip_list, - log_path=log_file_path, start_time=datetime.datetime.now(), pid=pid) - log_file_f.write('Start time is %s\n' % datetime.datetime.now()) - log.save() - return log_file_f, log_time_f, ip_list, log - - def posix_shell(self,ssh): + def posix_shell(self): """ Use paramiko channel connect server interactive. 使用paramiko模块的channel,连接后端,进入交互式 """ - log_file_f, log_time_f, ip_list, log = self.log_record() + log_file_f, log_time_f, log = self.get_log_file() old_tty = termios.tcgetattr(sys.stdin) pre_timestamp = time.time() input_r = '' @@ -314,29 +383,29 @@ class Jtty(object): try: tty.setraw(sys.stdin.fileno()) tty.setcbreak(sys.stdin.fileno()) - self.chan.settimeout(0.0) + self.channel.settimeout(0.0) while True: try: - r, w, e = select.select([self.chan, sys.stdin], [], []) + r, w, e = select.select([self.channel, sys.stdin], [], []) except Exception: pass - if self.chan in r: + if self.channel in r: try: - x = self.chan.recv(1024) + x = self.channel.recv(1024) if len(x) == 0: break sys.stdout.write(x) sys.stdout.flush() - log_file_f.write(x) now_timestamp = time.time() log_time_f.write('%s %s\n' % (round(now_timestamp-pre_timestamp, 4), len(x))) + log_file_f.write(x) pre_timestamp = now_timestamp log_file_f.flush() log_time_f.flush() - if input_mode and not newline_code_in(x): + if input_mode and not self.is_output(x): input_r += x except socket.timeout: @@ -348,14 +417,16 @@ class Jtty(object): input_mode = True if str(x) in ['\r', '\n', '\r\n']: - input_r = deal_command(input_r,ssh) + # input_r = deal_command(input_r,ssh) + input_r = self.remove_control_char(input_r) + TtyLog(log=log, datetime=datetime.datetime.now(), cmd=input_r).save() input_r = '' input_mode = False if len(x) == 0: break - self.chan.send(x) + self.channel.send(x) finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) @@ -365,42 +436,6 @@ class Jtty(object): log.end_time = datetime.datetime.now() log.save() - def get_connect_item(self): - """ - get args for connect: ip, port, username, passwd - 获取连接需要的参数,也就是服务ip, 端口, 用户账号和密码 - """ - # if not self.asset.is_active: - # raise ServerError('该主机被禁用 Host %s is not active.' % self.ip) - # - # if not self.user.is_active: - # raise ServerError('该用户被禁用 User %s is not active.' % self.username) - - # password = CRYPTOR.decrypt(self.]) - # return self.username, password, self.ip, int(self.asset.port) - return 'root', 'redhat', '127.0.0.1', 22 - - def get_connection(self): - """ - Get the ssh connection for reuse - 获取连接套接字 - """ - username, password, ip, port = self.get_connect_item() - logger.debug("username: %s, password: %s, ip: %s, port: %s" % (username, password, ip, port)) - - # 发起ssh连接请求 Make a ssh connection - ssh = paramiko.SSHClient() - ssh.load_system_host_keys() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - ssh.connect(ip, port=port, username=username, password=password) - except paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException: - raise ServerError('认证错误 Authentication Error.') - except socket.error: - raise ServerError('端口可能不对 Connect SSH Socket Port Error, Please Correct it.') - else: - return ssh - def connect(self): """ Connect server. @@ -415,7 +450,7 @@ class Jtty(object): # 获取连接的隧道并设置窗口大小 Make a channel and set windows size global channel win_size = self.get_win_size() - self.chan = channel = ssh.invoke_shell(height=win_size[0], width=win_size[1]) + self.channel = channel = ssh.invoke_shell(height=win_size[0], width=win_size[1], term='xterm') try: signal.signal(signal.SIGWINCH, self.set_win_size) except: @@ -424,17 +459,17 @@ class Jtty(object): # 设置PS1并提示 Set PS1 and msg it #channel.send(ps1) #channel.send(login_msg) - channel.send('echo ${SSH_TTY}\n') - global SSH_TTY - while not channel.recv_ready(): - time.sleep(1) - tmp = channel.recv(1024) + # channel.send('echo ${SSH_TTY}\n') + # global SSH_TTY + # while not channel.recv_ready(): + # time.sleep(1) + # tmp = channel.recv(1024) #print 'ok'+tmp+'ok' # SSH_TTY = re.search(r'(?<=/dev/).*', tmp).group().strip() - SSH_TTY = '' + # SSH_TTY = '' channel.send('clear\n') # Make ssh interactive tunnel - self.posix_shell(ssh) + self.posix_shell() # Shutdown channel socket channel.close() diff --git a/jumpserver/settings.py b/jumpserver/settings.py index 23908986d..fa64dde1e 100644 --- a/jumpserver/settings.py +++ b/jumpserver/settings.py @@ -15,7 +15,7 @@ import getpass config = ConfigParser.ConfigParser() -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) config.read(os.path.join(BASE_DIR, 'jumpserver.conf')) DB_HOST = config.get('db', 'host') diff --git a/run_websocket.py b/run_websocket.py index 32083aa5f..62d62998b 100644 --- a/run_websocket.py +++ b/run_websocket.py @@ -1,6 +1,7 @@ # coding: utf-8 import time +import datetime import json import os import sys @@ -25,6 +26,8 @@ from pyinotify import WatchManager, Notifier, ProcessEvent, IN_DELETE, IN_CREATE import struct, fcntl, signal, socket, select, fnmatch import paramiko +from connect import Tty +from connect import TtyLog try: import simplejson as json @@ -124,12 +127,6 @@ class MonitorHandler(tornado.websocket.WebSocketHandler): MonitorHandler.threads.append(thread) self.stream.set_nodelay(True) - print len(MonitorHandler.threads), len(MonitorHandler.clients) - - def on_message(self, message): - self.write_message('Connect WebSocket Success.
') - # 监控日志,发生变动发向客户端 - try: for t in MonitorHandler.threads: if t.is_alive(): @@ -143,6 +140,12 @@ class MonitorHandler(tornado.websocket.WebSocketHandler): MonitorHandler.clients.remove(self) MonitorHandler.threads.remove(MonitorHandler.threads[client_index]) + print len(MonitorHandler.threads), len(MonitorHandler.clients) + + def on_message(self, message): + # 监控日志,发生变动发向客户端 + pass + def on_close(self): # 客户端主动关闭 # self.close() @@ -153,28 +156,34 @@ class MonitorHandler(tornado.websocket.WebSocketHandler): MonitorHandler.threads.remove(MonitorHandler.threads[client_index]) +class WebTty(Tty): + def __init__(self, *args, **kwargs): + super(WebTty, self).__init__(*args, **kwargs) + self.login_type = 'web' + self.ws = None + self.input_r = '' + self.input_mode = False + + class WebTerminalHandler(tornado.websocket.WebSocketHandler): tasks = [] def __init__(self, *args, **kwargs): - self.chan = None - self.ssh = None + self.term = None + self.channel = None + self.log_file_f = None + self.log_time_f = None + self.log = None super(WebTerminalHandler, self).__init__(*args, **kwargs) def check_origin(self, origin): return True def open(self): - self.ssh = paramiko.SSHClient() - self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - self.ssh.connect('127.0.0.1', 22, 'root', 'redhat') - except: - self.write_message(json.loads({'data': 'Connect server Error'})) - self.close() - - self.chan = self.ssh.invoke_shell(term='xterm') - WebTerminalHandler.tasks.append(threading.Thread(target=self._forward_outbound)) + self.term = WebTty('a', 'b') + self.term.get_connection() + self.channel = self.term.ssh.invoke_shell(term='xterm') + WebTerminalHandler.tasks.append(MyThread(target=self.forward_outbound)) for t in WebTerminalHandler.tasks: if t.is_alive(): @@ -186,37 +195,50 @@ class WebTerminalHandler(tornado.websocket.WebSocketHandler): data = json.loads(message) if not data: return - if 'resize' in data: - self.chan.resize_pty( - data['resize'].get('width', 80), - data['resize'].get('height', 24)) - if 'data' in data: - self.chan.send(data['data']) + if data.get('data'): + self.term.input_mode = True + if str(data['data']) in ['\r', '\n', '\r\n']: + TtyLog(log=self.log, datetime=datetime.datetime.now(), cmd=self.term.remove_control_char(self.term.input_r)).save() + self.term.input_r = '' + self.term.input_mode = False + self.channel.send(data['data']) def on_close(self): - self.write_message(json.dumps({'data': 'close websocket'})) + print 'On_close' + self.log_file_f.write('End time is %s' % datetime.datetime.now()) + self.log.is_finished = True + self.log.end_time = datetime.datetime.now() + self.log.save() + self.close() - def _forward_outbound(self): - """ Forward outbound traffic (ssh -> websockets) """ + def forward_outbound(self): + self.log_file_f, self.log_time_f, self.log = self.term.get_log_file() try: data = '' + pre_timestamp = time.time() while True: - r, w, e = select.select([self.chan, sys.stdin], [], []) - if self.chan in r: - recv = self.chan.recv(1024) - print recv + r, w, e = select.select([self.channel, sys.stdin], [], []) + if self.channel in r: + recv = self.channel.recv(1024) if not len(recv): return data += recv try: self.write_message(json.dumps({'data': data})) + now_timestamp = time.time() + self.log_time_f.write('%s %s\n' % (round(now_timestamp-pre_timestamp, 4), len(data))) + self.log_file_f.write(data) + pre_timestamp = now_timestamp + self.log_file_f.flush() + self.log_time_f.flush() + if self.term.input_mode and not self.term.is_output(data): + self.term.input_r += data data = '' except UnicodeDecodeError: pass finally: self.close() - if __name__ == '__main__': tornado.options.parse_command_line() app = Application() diff --git a/templates/jlog/log_online.html b/templates/jlog/log_online.html index 23ee55647..2f855b084 100644 --- a/templates/jlog/log_online.html +++ b/templates/jlog/log_online.html @@ -127,8 +127,20 @@ var file_path = obj.attr('file_path'); var wsUri = '{{ web_monitor_uri }}'; var socket = new WebSocket(wsUri + '?file_path=' + file_path); + + var term = new Terminal({ + cols: 80, + rows: 24, + screenKeys: false + }); + + var tag = $('
'); + term.open(); + term.resize(80, 24); + socket.onopen = function(evt){ - socket.send(file_path) + socket.send('hello'); + term.write('~.~ Connect WebSocket Success.~.~ \r\n'); }; window.onbeforeunload = function(){ @@ -138,29 +150,15 @@ var username = obj.closest('tr').find('#username').text(); var ip = obj.closest('tr').find('#ip').text(); - - BootstrapDialog.show({message: function(){ //服务器端认证 {# socket.send('login', {userid:message.id, filename:message.filename,username:username,seed:seed});#} - var term = new Terminal({ - cols: 80, - rows: 24, - screenKeys: false - }); - var tag = $('
'); - term.open(); - term.resize(80, 24); - window.setTimeout(function(){ $('.terminal').detach().appendTo('#term'); socket.onmessage = function(evt){ term.write(evt.data); }}, 1000); - - - tag[0].style.color = "#00FF00"; return tag[0]; } , title:'Jumpserver实时监控 '+' 登录用户名: '+''+username+''+' 登录主机: '+''+ip,