Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ

pull/26/head
liuzheng712 2015-11-09 16:58:55 +08:00
commit 6db7642331
12 changed files with 373 additions and 245 deletions

View File

@ -201,26 +201,6 @@ def deal_command(str_r, ssh):
else: else:
return '' 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): def newline_code_in(strings):
for i in ['\r', '\r\n', '\n']: for i in ['\r', '\r\n', '\n']:
@ -230,17 +210,143 @@ def newline_code_in(strings):
return False 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))
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)
if self.login_type == 'ssh':
pid = os.getpid()
remote_ip = os.popen("who -m | awk '{ print $5 }'").read().strip('()\n')
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)
else:
remote_ip = 'Web'
log = Log(user=self.username, host=self.asset_name, remote_ip=remote_ip,
log_path=log_file_path, start_time=datetime.datetime.now(), pid=0)
log.save()
log.pid = log.id
log.save()
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': '', 'role_key': '/root/.ssh/id_rsa.bak'}
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 A virtual tty class
一个虚拟终端类实现连接ssh和记录日志 一个虚拟终端类实现连接ssh和记录日志
""" """
def __init__(self, username, ip):
self.chan = None
self.username = username
self.ip = ip
# self.user = user
# self.asset = asset
@staticmethod @staticmethod
def get_win_size(): def get_win_size():
@ -263,49 +369,16 @@ class Jtty(object):
""" """
try: try:
win_size = self.get_win_size() 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: except Exception:
pass pass
def log_record(self): def posix_shell(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):
""" """
Use paramiko channel connect server interactive. Use paramiko channel connect server interactive.
使用paramiko模块的channel连接后端进入交互式 使用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) old_tty = termios.tcgetattr(sys.stdin)
pre_timestamp = time.time() pre_timestamp = time.time()
input_r = '' input_r = ''
@ -314,29 +387,29 @@ class Jtty(object):
try: try:
tty.setraw(sys.stdin.fileno()) tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno()) tty.setcbreak(sys.stdin.fileno())
self.chan.settimeout(0.0) self.channel.settimeout(0.0)
while True: while True:
try: try:
r, w, e = select.select([self.chan, sys.stdin], [], []) r, w, e = select.select([self.channel, sys.stdin], [], [])
except Exception: except Exception:
pass pass
if self.chan in r: if self.channel in r:
try: try:
x = self.chan.recv(1024) x = self.channel.recv(1024)
if len(x) == 0: if len(x) == 0:
break break
sys.stdout.write(x) sys.stdout.write(x)
sys.stdout.flush() sys.stdout.flush()
log_file_f.write(x)
now_timestamp = time.time() now_timestamp = time.time()
log_time_f.write('%s %s\n' % (round(now_timestamp-pre_timestamp, 4), len(x))) log_time_f.write('%s %s\n' % (round(now_timestamp-pre_timestamp, 4), len(x)))
log_file_f.write(x)
pre_timestamp = now_timestamp pre_timestamp = now_timestamp
log_file_f.flush() log_file_f.flush()
log_time_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 input_r += x
except socket.timeout: except socket.timeout:
@ -348,14 +421,16 @@ class Jtty(object):
input_mode = True input_mode = True
if str(x) in ['\r', '\n', '\r\n']: 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() TtyLog(log=log, datetime=datetime.datetime.now(), cmd=input_r).save()
input_r = '' input_r = ''
input_mode = False input_mode = False
if len(x) == 0: if len(x) == 0:
break break
self.chan.send(x) self.channel.send(x)
finally: finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
@ -365,42 +440,6 @@ class Jtty(object):
log.end_time = datetime.datetime.now() log.end_time = datetime.datetime.now()
log.save() 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): def connect(self):
""" """
Connect server. Connect server.
@ -415,7 +454,7 @@ class Jtty(object):
# 获取连接的隧道并设置窗口大小 Make a channel and set windows size # 获取连接的隧道并设置窗口大小 Make a channel and set windows size
global channel global channel
win_size = self.get_win_size() 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: try:
signal.signal(signal.SIGWINCH, self.set_win_size) signal.signal(signal.SIGWINCH, self.set_win_size)
except: except:
@ -424,17 +463,17 @@ class Jtty(object):
# 设置PS1并提示 Set PS1 and msg it # 设置PS1并提示 Set PS1 and msg it
#channel.send(ps1) #channel.send(ps1)
#channel.send(login_msg) #channel.send(login_msg)
channel.send('echo ${SSH_TTY}\n') # channel.send('echo ${SSH_TTY}\n')
global SSH_TTY # global SSH_TTY
while not channel.recv_ready(): # while not channel.recv_ready():
time.sleep(1) # time.sleep(1)
tmp = channel.recv(1024) # tmp = channel.recv(1024)
#print 'ok'+tmp+'ok' #print 'ok'+tmp+'ok'
# SSH_TTY = re.search(r'(?<=/dev/).*', tmp).group().strip() # SSH_TTY = re.search(r'(?<=/dev/).*', tmp).group().strip()
SSH_TTY = '' # SSH_TTY = ''
channel.send('clear\n') channel.send('clear\n')
# Make ssh interactive tunnel # Make ssh interactive tunnel
self.posix_shell(ssh) self.posix_shell()
# Shutdown channel socket # Shutdown channel socket
channel.close() channel.close()

View File

@ -48,7 +48,8 @@ def log_list(request, offset):
contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request)
web_monitor_uri = '%s/monitor' % web_socket_host web_monitor_uri = 'ws://%s/monitor' % web_socket_host
web_kill_uri = 'http://%s/kill' % web_socket_host
return render_to_response('jlog/log_%s.html' % offset, locals(), context_instance=RequestContext(request)) return render_to_response('jlog/log_%s.html' % offset, locals(), context_instance=RequestContext(request))
@ -103,6 +104,10 @@ def log_record(request):
def web_terminal(request): def web_terminal(request):
web_terminal_uri = '%s/terminal' % web_socket_host #username = get_session.get('username', '')
token = request.COOKIES.get('sessionid')
username = request.user.username
asset_name = '127.0.0.1'
web_terminal_uri = 'ws://%s/terminal?username=%s&asset_name=%s&token=%s' % (web_socket_host, username, asset_name, token)
return render_to_response('jlog/web_terminal.html', locals()) return render_to_response('jlog/web_terminal.html', locals())

View File

@ -23,7 +23,7 @@ root_pw = secret234
[websocket] [websocket]
web_socket_host = ws://192.168.244.129:3000 web_socket_host = 192.168.244.129:3000
[mail] [mail]
@ -32,4 +32,4 @@ email_host = smtp.exmail.qq.com
email_port = 25 email_port = 25
email_host_user = noreply@jumpserver.org email_host_user = noreply@jumpserver.org
email_host_password = jumpserver1234 email_host_password = jumpserver1234
email_use_tls = False email_use_tls = True

View File

@ -7,6 +7,7 @@ class Setting(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
default_user = models.CharField(max_length=100, null=True, blank=True) default_user = models.CharField(max_length=100, null=True, blank=True)
default_port = models.IntegerField(max_length=10, null=True, blank=True) default_port = models.IntegerField(max_length=10, null=True, blank=True)
default_password = models.CharField(max_length=100, null=True, blank=True)
default_pri_key_path = models.CharField(max_length=100, null=True, blank=True) default_pri_key_path = models.CharField(max_length=100, null=True, blank=True)
class Meta: class Meta:

View File

@ -15,7 +15,7 @@ import getpass
config = ConfigParser.ConfigParser() 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')) config.read(os.path.join(BASE_DIR, 'jumpserver.conf'))
DB_HOST = config.get('db', 'host') DB_HOST = config.get('db', 'host')
@ -25,11 +25,13 @@ DB_PASSWORD = config.get('db', 'password')
DB_DATABASE = config.get('db', 'database') DB_DATABASE = config.get('db', 'database')
AUTH_USER_MODEL = 'juser.User' AUTH_USER_MODEL = 'juser.User'
# mail config # mail config
MAIL_ENABLE = config.get('mail', 'mail_enable')
EMAIL_HOST = config.get('mail', 'email_host') EMAIL_HOST = config.get('mail', 'email_host')
EMAIL_PORT = config.get('mail', 'email_port') EMAIL_PORT = config.get('mail', 'email_port')
EMAIL_HOST_USER = config.get('mail', 'email_host_user') EMAIL_HOST_USER = config.get('mail', 'email_host_user')
EMAIL_HOST_PASSWORD = config.get('mail', 'email_host_password') EMAIL_HOST_PASSWORD = config.get('mail', 'email_host_password')
EMAIL_USE_TLS = config.getboolean('mail', 'email_use_tls') EMAIL_USE_TLS = config.getboolean('mail', 'email_use_tls')
EMAIL_TIMEOUT = 5
# ======== Log ========== # ======== Log ==========
LOG = False LOG = False
@ -41,8 +43,6 @@ KEY = config.get('base', 'key')
LOGIN_NAME = getpass.getuser() LOGIN_NAME = getpass.getuser()
# LDAP_ENABLE = CONF.getint('ldap', 'ldap_enable') # LDAP_ENABLE = CONF.getint('ldap', 'ldap_enable')
URL = config.get('base', 'url') URL = config.get('base', 'url')
MAIL_ENABLE = config.get('mail', 'mail_enable')
MAIL_FROM = config.get('mail', 'email_host_user')
log_dir = os.path.join(BASE_DIR, 'logs') log_dir = os.path.join(BASE_DIR, 'logs')
log_level = config.get('base', 'log') log_level = config.get('base', 'log')
web_socket_host = config.get('websocket', 'web_socket_host') web_socket_host = config.get('websocket', 'web_socket_host')

View File

@ -246,27 +246,39 @@ def Logout(request):
def setting(request): def setting(request):
header_title, path1 = '项目设置', '设置' header_title, path1 = '项目设置', '设置'
setting_r = get_object(Setting, name='default') setting_default = get_object(Setting, name='default')
if request.method == "POST": if request.method == "POST":
username = request.POST.get('username', '') setting_raw = request.POST.get('setting', '')
port = request.POST.get('port', '') if setting_raw == 'default':
private_key = request.POST.get('key', '') username = request.POST.get('username', '')
port = request.POST.get('port', '')
password = request.POST.get('password', '')
private_key = request.POST.get('key', '')
if '' in [username, port, private_key]: if '' in [username, port] and ('' in password or '' in private_key):
return HttpResponse('所填内容不能为空') return HttpResponse('所填内容不能为空, 且密码和私钥填一个')
else:
settings = get_object(Setting, id=1)
private_key_path = os.path.join(BASE_DIR, 'keys', 'default', 'default_private_key.pem')
with open(private_key_path, 'w') as f:
f.write(private_key)
os.chmod(private_key_path, 0600)
if settings:
Setting.objects.filter(name='default').update(default_user=username, default_port=port,
default_pri_key_path=private_key_path)
else: else:
setting_r = Setting(name='default', default_user=username, default_port=port, private_key_path = os.path.join(BASE_DIR, 'keys', 'default', 'default_private_key.pem')
default_pri_key_path=private_key_path).save() if private_key:
with open(private_key_path, 'w') as f:
f.write(private_key)
os.chmod(private_key_path, 0600)
if setting_default:
if password != setting_default.default_password:
password_encode = CRYPTOR.encrypt(password)
else:
password_encode = password
Setting.objects.filter(name='default').update(default_user=username, default_port=port,
default_password=password_encode,
default_pri_key_path=private_key_path)
else:
password_encode = CRYPTOR.encrypt(password)
setting_r = Setting(name='default', default_user=username, default_port=port,
default_password=password_encode,
default_pri_key_path=private_key_path).save()
msg = "设置成功" msg = "设置成功"
return my_render('setting.html', locals(), request) return my_render('setting.html', locals(), request)

View File

@ -5,7 +5,7 @@ from subprocess import call
from juser.models import AdminGroup from juser.models import AdminGroup
from jumpserver.api import * from jumpserver.api import *
from jumpserver.settings import BASE_DIR from jumpserver.settings import BASE_DIR, EMAIL_HOST_USER as MAIL_FROM
def group_add_user(group, user_id=None, username=None): def group_add_user(group, user_id=None, username=None):

View File

@ -10,10 +10,11 @@ from django.contrib.auth.decorators import login_required
from django.db.models import Q from django.db.models import Q
from django.template import RequestContext from django.template import RequestContext
from django.db.models import ObjectDoesNotExist from django.db.models import ObjectDoesNotExist
from jumpserver.settings import MAIL_FROM, MAIL_ENABLE from jumpserver.settings import EMAIL_HOST_USER
from juser.user_api import * from juser.user_api import *
from jperm.perm_api import _public_perm_api, perm_user_api, user_permed from jperm.perm_api import _public_perm_api, perm_user_api, user_permed
MAIL_FROM = EMAIL_HOST_USER
def chg_role(request): def chg_role(request):
role = {'SU': 2, 'GA': 1, 'CU': 0} role = {'SU': 2, 'GA': 1, 'CU': 0}

View File

@ -1,11 +1,13 @@
# coding: utf-8 # coding: utf-8
import time import time
import datetime
import json import json
import os import os
import sys import sys
import os.path import os.path
import threading import threading
import urllib
import tornado.ioloop import tornado.ioloop
import tornado.options import tornado.options
@ -13,6 +15,7 @@ import tornado.web
import tornado.websocket import tornado.websocket
import tornado.httpserver import tornado.httpserver
import tornado.gen import tornado.gen
import tornado.httpclient
from tornado.websocket import WebSocketClosedError from tornado.websocket import WebSocketClosedError
from tornado.options import define, options from tornado.options import define, options
@ -21,9 +24,12 @@ from pyinotify import WatchManager, Notifier, ProcessEvent, IN_DELETE, IN_CREATE
# from gevent import monkey # from gevent import monkey
# monkey.patch_all() # monkey.patch_all()
# import gevent # import gevent
from gevent.socket import wait_read, wait_write # from gevent.socket import wait_read, wait_write
import struct, fcntl, signal, socket, select, fnmatch
import paramiko import paramiko
from connect import Tty
from connect import TtyLog, Log
try: try:
import simplejson as json import simplejson as json
@ -35,6 +41,20 @@ define("port", default=3000, help="run on the given port", type=int)
define("host", default='0.0.0.0', help="run port on", type=str) define("host", default='0.0.0.0', help="run port on", type=str)
def require_auth(func):
def _deco(request, *args, **kwargs):
username = request.get_argument('username', '')
asset_name = request.get_argument('asset_name', '')
token = request.get_argument('token', '')
print username, asset_name, token
client = tornado.httpclient.HTTPClient()
# response = client.fetch('http://some/url') + urllib.urlencode({'username': username,
# 'asset_name': asset_name, 'token': token})
# return request.close()
return func(request, *args, **kwargs)
return _deco
class MyThread(threading.Thread): class MyThread(threading.Thread):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MyThread, self).__init__(*args, **kwargs) super(MyThread, self).__init__(*args, **kwargs)
@ -92,6 +112,7 @@ class Application(tornado.web.Application):
handlers = [ handlers = [
(r'/monitor', MonitorHandler), (r'/monitor', MonitorHandler),
(r'/terminal', WebTerminalHandler), (r'/terminal', WebTerminalHandler),
(r'/kill', WebTerminalKillHandler),
] ]
setting = { setting = {
@ -115,6 +136,7 @@ class MonitorHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin): def check_origin(self, origin):
return True return True
@require_auth
def open(self): def open(self):
# 获取监控的path # 获取监控的path
self.file_path = self.get_argument('file_path', '') self.file_path = self.get_argument('file_path', '')
@ -123,12 +145,6 @@ class MonitorHandler(tornado.websocket.WebSocketHandler):
MonitorHandler.threads.append(thread) MonitorHandler.threads.append(thread)
self.stream.set_nodelay(True) self.stream.set_nodelay(True)
print len(MonitorHandler.threads), len(MonitorHandler.clients)
def on_message(self, message):
self.write_message('Connect WebSocket Success. <br/>')
# 监控日志,发生变动发向客户端
try: try:
for t in MonitorHandler.threads: for t in MonitorHandler.threads:
if t.is_alive(): if t.is_alive():
@ -142,6 +158,12 @@ class MonitorHandler(tornado.websocket.WebSocketHandler):
MonitorHandler.clients.remove(self) MonitorHandler.clients.remove(self)
MonitorHandler.threads.remove(MonitorHandler.threads[client_index]) MonitorHandler.threads.remove(MonitorHandler.threads[client_index])
print len(MonitorHandler.threads), len(MonitorHandler.clients)
def on_message(self, message):
# 监控日志,发生变动发向客户端
pass
def on_close(self): def on_close(self):
# 客户端主动关闭 # 客户端主动关闭
# self.close() # self.close()
@ -152,28 +174,55 @@ class MonitorHandler(tornado.websocket.WebSocketHandler):
MonitorHandler.threads.remove(MonitorHandler.threads[client_index]) 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 WebTerminalKillHandler(tornado.web.RequestHandler):
def get(self):
ws_id = self.get_argument('id')
Log.objects.filter(id=ws_id).update(is_finished=True)
for ws in WebTerminalHandler.clients:
print ws.id
if ws.id == int(ws_id):
print "killed"
ws.log.save()
ws.close()
print len(WebTerminalHandler.clients)
class WebTerminalHandler(tornado.websocket.WebSocketHandler): class WebTerminalHandler(tornado.websocket.WebSocketHandler):
tasks = [] tasks = []
clients = []
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.chan = None self.term = None
self.ssh = None self.channel = None
self.log_file_f = None
self.log_time_f = None
self.log = None
self.id = 0
super(WebTerminalHandler, self).__init__(*args, **kwargs) super(WebTerminalHandler, self).__init__(*args, **kwargs)
def check_origin(self, origin): def check_origin(self, origin):
return True return True
@require_auth
def open(self): def open(self):
self.ssh = paramiko.SSHClient() asset_name = self.get_argument('asset_name', '')
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) username = self.get_argument('username', '')
try: token = self.get_argument('token', '')
self.ssh.connect('127.0.0.1', 22, 'root', 'redhat') print asset_name, username, token
except: self.term = WebTty('a', 'b')
self.write_message(json.loads({'data': 'Connect server Error'})) self.term.get_connection()
self.close() self.channel = self.term.ssh.invoke_shell(term='xterm')
WebTerminalHandler.tasks.append(MyThread(target=self.forward_outbound))
self.chan = self.ssh.invoke_shell(term='xterm') WebTerminalHandler.clients.append(self)
WebTerminalHandler.tasks.append(threading.Thread(target=self._forward_outbound))
for t in WebTerminalHandler.tasks: for t in WebTerminalHandler.tasks:
if t.is_alive(): if t.is_alive():
@ -185,35 +234,56 @@ class WebTerminalHandler(tornado.websocket.WebSocketHandler):
data = json.loads(message) data = json.loads(message)
if not data: if not data:
return return
if 'resize' in data: if data.get('data'):
self.chan.resize_pty( self.term.input_mode = True
data['resize'].get('width', 80), if str(data['data']) in ['\r', '\n', '\r\n']:
data['resize'].get('height', 24)) TtyLog(log=self.log, datetime=datetime.datetime.now(), cmd=self.term.remove_control_char(self.term.input_r)).save()
if 'data' in data: self.term.input_r = ''
self.chan.send(data['data']) self.term.input_mode = False
self.channel.send(data['data'])
def on_close(self): def on_close(self):
self.write_message(json.dumps({'data': 'close websocket'})) print 'On_close'
if self in WebTerminalHandler.clients:
WebTerminalHandler.clients.remove(self)
try:
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()
except AttributeError:
pass
def _forward_outbound(self): def forward_outbound(self):
""" Forward outbound traffic (ssh -> websockets) """ self.log_file_f, self.log_time_f, self.log = self.term.get_log_file()
self.id = self.log.id
try: try:
data = '' data = ''
pre_timestamp = time.time()
while True: while True:
wait_read(self.chan.fileno()) r, w, e = select.select([self.channel, sys.stdin], [], [])
recv = self.chan.recv(1024) if self.channel in r:
if not len(recv): recv = self.channel.recv(1024)
return if not len(recv):
data += recv return
try: data += recv
self.write_message(json.dumps({'data': data})) try:
data = '' self.write_message(json.dumps({'data': data}))
except UnicodeDecodeError: now_timestamp = time.time()
pass 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: finally:
self.close() self.close()
if __name__ == '__main__': if __name__ == '__main__':
tornado.options.parse_command_line() tornado.options.parse_command_line()
app = Application() app = Application()

View File

@ -50,7 +50,7 @@
<div class="col-lg-12"> <div class="col-lg-12">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div id="ibox-content" class="ibox-title"> <div id="ibox-content" class="ibox-title">
<h5> 用户日志详细信息列表 </h5> <h5> 用户日志详细信息列表 <input type="button" id="test_connect" class="btn btn-primary" value="测试连接 web terminal" /> </h5>
<div class="ibox-tools"> <div class="ibox-tools">
<a class="collapse-link"> <a class="collapse-link">
<i class="fa fa-chevron-up"></i> <i class="fa fa-chevron-up"></i>
@ -97,7 +97,7 @@
{% ifnotequal session_role_id 0 %} {% ifnotequal session_role_id 0 %}
<td class="text-center"><a href="/jlog/history/?id={{ post.id }}" class="log_command"> 命令统计 </a></td> <td class="text-center"><a href="/jlog/history/?id={{ post.id }}" class="log_command"> 命令统计 </a></td>
<td class="text-center"><a class="monitor" file_path="{{ post.log_path }}"> 监控 </a></td> <td class="text-center"><a class="monitor" file_path="{{ post.log_path }}"> 监控 </a></td>
<td class="text-center"><input type="button" id="cut" class="btn btn-danger btn-xs" name="cut" value="阻断" onclick='cut("{{ post.pid }}")' /></td> <td class="text-center"><input type="button" id="cut" class="btn btn-danger btn-xs" name="cut" value="阻断" onclick='cut("{{ post.pid }}", "{{ post.remote_ip }}")' /></td>
{% endifnotequal %} {% endifnotequal %}
<td class="text-center" id="start_time"> {{ post.start_time|date:"Y-m-d H:i:s" }} </td> <td class="text-center" id="start_time"> {{ post.start_time|date:"Y-m-d H:i:s" }} </td>
</tr> </tr>
@ -127,8 +127,20 @@
var file_path = obj.attr('file_path'); var file_path = obj.attr('file_path');
var wsUri = '{{ web_monitor_uri }}'; var wsUri = '{{ web_monitor_uri }}';
var socket = new WebSocket(wsUri + '?file_path=' + file_path); var socket = new WebSocket(wsUri + '?file_path=' + file_path);
var term = new Terminal({
cols: 80,
rows: 24,
screenKeys: false
});
var tag = $('<div id="term" style="height:500px; overflow: auto;background-color: rgba(0, 0, 0, 0);border: none"></div>');
term.open();
term.resize(80, 24);
socket.onopen = function(evt){ socket.onopen = function(evt){
socket.send(file_path) socket.send('hello');
term.write('~.~ Connect WebSocket Success.~.~ \r\n');
}; };
window.onbeforeunload = function(){ window.onbeforeunload = function(){
@ -138,29 +150,15 @@
var username = obj.closest('tr').find('#username').text(); var username = obj.closest('tr').find('#username').text();
var ip = obj.closest('tr').find('#ip').text(); var ip = obj.closest('tr').find('#ip').text();
BootstrapDialog.show({message: function(){ BootstrapDialog.show({message: function(){
//服务器端认证 //服务器端认证
{# socket.send('login', {userid:message.id, filename:message.filename,username:username,seed:seed});#} {# 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 = $('<div id="term" style="height:500px; overflow: auto;background-color: rgba(0, 0, 0, 0);border: none"></div>');
term.open();
term.resize(80, 24);
window.setTimeout(function(){ window.setTimeout(function(){
$('.terminal').detach().appendTo('#term'); $('.terminal').detach().appendTo('#term');
socket.onmessage = function(evt){ socket.onmessage = function(evt){
term.write(evt.data); term.write(evt.data);
}}, 1000); }}, 1000);
tag[0].style.color = "#00FF00";
return tag[0]; return tag[0];
} , } ,
title:'Jumpserver实时监控 '+' 登录用户名: '+'<span class="text-info">'+username+'</span>'+' 登录主机: '+'<span class="text-info">'+ip, title:'Jumpserver实时监控 '+' 登录用户名: '+'<span class="text-info">'+username+'</span>'+' 登录主机: '+'<span class="text-info">'+ip,
@ -190,6 +188,10 @@
}}); }});
return false; return false;
}); });
$('#test_connect').click(function(){
window.open('/jlog/web_terminal/?asset_name="hello', '播放', 'height=400, width=600, top=89px, left=99px,toolbar=no,menubar=no,scrollbars=auto,resizeable=no,location=no,status=no');
});
}); });
{# function log_search(){#} {# function log_search(){#}
@ -204,8 +206,14 @@
{# }#} {# }#}
function cut(num){ function cut(num, host){
var g_url = "/jlog/log_kill/?id="+num; console.log(host);
if (host=='Web'){
var g_url = '{{ web_kill_uri }}' + '?id=' + num;
} else {
g_url = "/jlog/log_kill/?id=" + num;
}
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: g_url, url: g_url,

View File

@ -1,72 +1,56 @@
<div class="col-sm-6"> <div class="col-sm-6">
<div class="dataTables_paginate paging_simple_numbers" id="editable_paginate"> <div class="dataTables_paginate paging_simple_numbers" id="editable_paginate">
<ul class="pagination" style="margin-top: 0; float: right"> <ul class="pagination" style="margin-top: 0; float: right">
{% if keyword %}
{% if contacts.has_previous %} {% if contacts.has_previous %}
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="editable_previous"> <li class="paginate_button previous" aria-controls="editable" tabindex="0" id="editable_previous">
<a href="?keyword={{ keyword }}&page={{ contacts.previous_page_number }}">Previous</a> <a class="page" href="?page={{ contacts.previous_page_number }}">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="paginate_button previous disabled" aria-controls="editable" tabindex="0" id="editable_previous"> <li class="paginate_button previous disabled" aria-controls="editable" tabindex="0" id="editable_previous">
<a href="#">Previous</a> <a class="page" href="#">Previous</a>
</li> </li>
{% endif %} {% endif %}
{% ifequal show_first 1 %} {% ifequal show_first 1 %}
<li class="paginate_button" aria-controls="editable" tabindex="0"><a href="?keyword={{ keyword }}&page=1" title="第1页">1...</a></li> <li class="paginate_button" aria-controls="editable" tabindex="0"><a class="page" href="?page=1" title="第1页">1...</a></li>
{% endifequal %} {% endifequal %}
{% for page in page_range %} {% for page in page_range %}
{% ifequal current_page page %} {% ifequal current_page page %}
<li class="paginate_button active" aria-controls="editable" tabindex="0"><a href="?keyword={{ keyword }}&page={{ page }}" title="第{{ page }}页">{{ page }}</a></li> <li class="paginate_button active" aria-controls="editable" tabindex="0"><a class="page" href="?page={{ page }}" title="第{{ page }}页">{{ page }}</a></li>
{% else %} {% else %}
<li class="paginate_button" aria-controls="editable" tabindex="0"><a href="?keyword={{ keyword }}&page={{ page }}" title="第{{ page }}页">{{ page }}</a></li> <li class="paginate_button" aria-controls="editable" tabindex="0"><a class="page" href="?page={{ page }}" title="第{{ page }}页">{{ page }}</a></li>
{% endifequal %} {% endifequal %}
{% endfor %} {% endfor %}
{% ifequal show_end 1 %} {% ifequal show_end 1 %}
<li class="paginate_button" aria-controls="editable" tabindex="0"><a href="?keyword={{ keyword }}&page={{ p.num_pages }}" title="第{{ page }}页">...{{ p.num_pages }}</a></li> <li class="paginate_button" aria-controls="editable" tabindex="0"><a class="page" href="?page={{ p.num_pages }}" title="第{{ page }}页">...{{ p.num_pages }}</a></li>
{% endifequal %} {% endifequal %}
{% if contacts.has_next %} {% if contacts.has_next %}
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="editable_next"> <li class="paginate_button next" aria-controls="editable" tabindex="0" id="editable_next">
<a href="?keyword={{ keyword }}&page={{ contacts.next_page_number }}">Next</a> <a class="page" href="?page={{ contacts.next_page_number }}">Next</a>
</li> </li>
{% else %} {% else %}
<li class="paginate_button next disabled" aria-controls="editable" tabindex="0" id="editable_next"> <li class="paginate_button next disabled" aria-controls="editable" tabindex="0" id="editable_next">
<a href="#">Next</a> <a class="page" href="#">Next</a>
</li> </li>
{% endif %} {% endif %}
{% else %}
{% if contacts.has_previous %}
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="editable_previous">
<a href="?page={{ contacts.previous_page_number }}">Previous</a>
</li>
{% else %}
<li class="paginate_button previous disabled" aria-controls="editable" tabindex="0" id="editable_previous">
<a href="#">Previous</a>
</li>
{% endif %}
{% ifequal show_first 1 %}
<li class="paginate_button" aria-controls="editable" tabindex="0"><a href="?page=1" title="第1页">1...</a></li>
{% endifequal %}
{% for page in page_range %}
{% ifequal current_page page %}
<li class="paginate_button active" aria-controls="editable" tabindex="0"><a href="?page={{ page }}" title="第{{ page }}页">{{ page }}</a></li>
{% else %}
<li class="paginate_button" aria-controls="editable" tabindex="0"><a href="?page={{ page }}" title="第{{ page }}页">{{ page }}</a></li>
{% endifequal %}
{% endfor %}
{% ifequal show_end 1 %}
<li class="paginate_button" aria-controls="editable" tabindex="0"><a href="?page={{ p.num_pages }}" title="第{{ page }}页">...{{ p.num_pages }}</a></li>
{% endifequal %}
{% if contacts.has_next %}
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="editable_next">
<a href="?page={{ contacts.next_page_number }}">Next</a>
</li>
{% else %}
<li class="paginate_button next disabled" aria-controls="editable" tabindex="0" id="editable_next">
<a href="#">Next</a>
</li>
{% endif %}
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
<script>
$(document).ready(function(){
$('.page').click(function(){
var searchStr = location.search;
var old_href = $(this).attr('href').replace('?', '');
var searchArray = searchStr.split('&');
if (searchStr.indexOf('page')){
searchArray.pop();
}
searchArray.push(old_href);
if (searchArray.length > 2){
$(this).attr('href', searchArray.join('&'));
}
})
});
</script>

View File

@ -46,20 +46,28 @@
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<label for="username" class="col-sm-2 control-label">默认用户名<span class="red-fonts">*</span></label> <label for="username" class="col-sm-2 control-label">默认用户名<span class="red-fonts">*</span></label>
<input name="setting" value="default" style="display: none">
<div class="col-sm-8"> <div class="col-sm-8">
<input id="username" name="username" placeholder="Username" type="text" value="{{ setting_r.default_user }}" class="form-control"> <input id="username" name="username" placeholder="Username" type="text" value="{{ setting_default.default_user }}" class="form-control">
</div> </div>
</div> </div>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<div class="form-group"> <div class="form-group">
<label for="port" class="col-sm-2 control-label">默认ssh端口<span class="red-fonts">*</span></label> <label for="port" class="col-sm-2 control-label">默认ssh端口<span class="red-fonts">*</span></label>
<div class="col-sm-8"> <div class="col-sm-8">
<input id="port" name="port" placeholder="Port" type="text" value="{{ setting_r.default_port }}" class="form-control"> <input id="port" name="port" placeholder="Port" type="text" value="{{ setting_default.default_port }}" class="form-control">
</div> </div>
</div> </div>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<div class="form-group"> <div class="form-group">
<label for="key" class="col-sm-2 control-label">默认密钥<span class="red-fonts">*</span></label> <label for="key" class="col-sm-2 control-label">默认密码</label>
<div class="col-sm-8">
<input id="password" name="password" placeholder="Password" type="password" value="{{ setting_default.default_password }}" class="form-control">
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="form-group">
<label for="key" class="col-sm-2 control-label">默认密钥</label>
<div class="col-sm-8"> <div class="col-sm-8">
<textarea class="form-control" name="key" placeholder="请复制粘贴私钥(原来的因为安全原因不被显示)" rows="10" style="font-size: 9px;"></textarea> <textarea class="form-control" name="key" placeholder="请复制粘贴私钥(原来的因为安全原因不被显示)" rows="10" style="font-size: 9px;"></textarea>
</div> </div>