Browse Source

移动api中关于tty到connect.py

pull/26/head
ibuler 9 years ago
parent
commit
618aed3278
  1. 371
      connect.py
  2. 362
      jumpserver/api.py

371
connect.py

@ -8,18 +8,33 @@ sys.setdefaultencoding('utf8')
import os
import re
import time
import datetime
import textwrap
import getpass
import readline
import django
from multiprocessing import Pool
import paramiko
import struct, fcntl, signal, socket, select, fnmatch
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'
if django.get_version() != '1.6':
django.setup()
from jumpserver.api import ServerError, User, Asset, Jtty, get_object
from jumpserver.api import logger
from jumpserver.api import ServerError, User, Asset, AssetGroup, get_object
from jumpserver.api import logger, is_dir, Log, TtyLog
from jumpserver.settings import log_dir
try:
import termios
import tty
except ImportError:
print '\033[1;31m仅支持类Unix系统 Only unix like supported.\033[0m'
time.sleep(3)
sys.exit()
VIM_FLAG = False
VIM_COMMAND = ''
SSH_TTY = ''
login_user = get_object(User, username=getpass.getuser())
@ -81,6 +96,358 @@ def verify_connect(user, option):
jtty.connect()
def check_vim_status(command, ssh):
global SSH_TTY
print command
if command == '':
return True
else:
command_str= 'ps -ef |grep "%s" | grep "%s"|grep -v grep |wc -l' % (command,SSH_TTY)
print command_str
stdin, stdout, stderr = ssh.exec_command(command_str)
ps_num = stdout.read()
print ps_num
if int(ps_num) == 0:
return True
else:
return False
def deal_command(str_r, ssh):
"""
处理命令中特殊字符
"""
str_r = re.sub('\x07','',str_r) #删除响铃
patch_char = re.compile('\x08\x1b\[C') #删除方向左右一起的按键
while patch_char.search(str_r):
str_r = patch_char.sub('', str_r.rstrip())
result_command = '' #最后的结果
pattern_str = '' #模式中间中的字符串
backspace_num = 0 #光标移动的个数
reach_backspace_flag = False #没有检测到光标键则为true
end_flag = False
while str_r:
tmp = re.match(r'\w', str_r)
if tmp:
if reach_backspace_flag:
pattern_str += str(tmp.group(0))
str_r = str_r[1:]
continue
else:
result_command += str(tmp.group(0))
str_r = str_r[1:]
continue
tmp = re.match(r'\x1b\[K[\x08]*', str_r)
if tmp:
if backspace_num > 0:
if backspace_num > len(result_command) :
result_command += pattern_str
result_command = result_command[0:-backspace_num]
else:
result_command = result_command[0:-backspace_num]
result_command += pattern_str
del_len = len(str(tmp.group(0)))-3
if del_len > 0:
result_command = result_command[0:-del_len]
reach_backspace_flag = False
backspace_num =0
pattern_str=''
str_r = str_r[len(str(tmp.group(0))):]
continue
if re.match(r'\x08', str_r):
backspace_num += 1
reach_backspace_flag = True
str_r = str_r[1:]
if len(str_r) == 0:
end_flag = True
continue
if reach_backspace_flag :
pattern_str += str_r[0]
else :
result_command += str_r[0]
str_r = str_r[1:]
if backspace_num > 0 and not end_flag:
result_command = result_command[:-backspace_num]
result_command += pattern_str
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)
result_command = control_char.sub('', result_command.strip())
global VIM_FLAG
global VIM_COMMAND
if not VIM_FLAG:
if result_command.startswith('vim') or result_command.startswith('vi') :
VIM_FLAG = True
VIM_COMMAND = result_command
return result_command.decode('utf8',"ignore")
else:
if check_vim_status(VIM_COMMAND, ssh):
VIM_FLAG = False
VIM_COMMAND=''
return result_command.decode('utf8',"ignore")
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']:
if i in strings:
#print "new line"
return True
return False
class Jtty(object):
"""
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():
"""
This function use to get the size of the windows!
获得terminal窗口大小
"""
if 'TIOCGWINSZ' in dir(termios):
TIOCGWINSZ = termios.TIOCGWINSZ
else:
TIOCGWINSZ = 1074295912L
s = struct.pack('HHHH', 0, 0, 0, 0)
x = fcntl.ioctl(sys.stdout.fileno(), TIOCGWINSZ, s)
return struct.unpack('HHHH', x)[0:2]
def set_win_size(self, sig, data):
"""
This function use to set the window size of the terminal!
设置terminal窗口大小
"""
try:
win_size = self.get_win_size()
self.chan.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):
"""
Use paramiko channel connect server interactive.
使用paramiko模块的channel连接后端进入交互式
"""
log_file_f, log_time_f, ip_list, log = self.log_record()
old_tty = termios.tcgetattr(sys.stdin)
pre_timestamp = time.time()
input_r = ''
input_mode = False
try:
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())
self.chan.settimeout(0.0)
while True:
try:
r, w, e = select.select([self.chan, sys.stdin], [], [])
except Exception:
pass
if self.chan in r:
try:
x = self.chan.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)))
pre_timestamp = now_timestamp
log_file_f.flush()
log_time_f.flush()
if input_mode and not newline_code_in(x):
input_r += x
except socket.timeout:
pass
if sys.stdin in r:
x = os.read(sys.stdin.fileno(), 1)
if not input_mode:
input_mode = True
if str(x) in ['\r', '\n', '\r\n']:
input_r = deal_command(input_r,ssh)
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)
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
log_file_f.write('End time is %s' % datetime.datetime.now())
log_file_f.close()
log.is_finished = True
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.
连接服务器
"""
ps1 = "PS1='[\u@%s \W]\$ '\n" % self.ip
login_msg = "clear;echo -e '\\033[32mLogin %s done. Enjoy it.\\033[0m'\n" % self.ip
# 发起ssh连接请求 Make a ssh connection
ssh = self.get_connection()
# 获取连接的隧道并设置窗口大小 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])
try:
signal.signal(signal.SIGWINCH, self.set_win_size)
except:
pass
# 设置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)
#print 'ok'+tmp+'ok'
SSH_TTY = re.search(r'(?<=/dev/).*', tmp).group().strip()
channel.send('clear\n')
# Make ssh interactive tunnel
self.posix_shell(ssh)
# Shutdown channel socket
channel.close()
ssh.close()
def execute(self, cmd):
"""
execute cmd on the asset
执行命令
"""
pass
def print_prompt():
"""
Print prompt

362
jumpserver/api.py

@ -8,8 +8,6 @@ import hashlib
import datetime
import random
import subprocess
import paramiko
import struct, fcntl, signal, socket, select, fnmatch
from settings import *
from django.core.paginator import Paginator, EmptyPage, InvalidPage
@ -25,17 +23,7 @@ from django.shortcuts import render_to_response
from django.core.mail import send_mail
import json
import logging
VIM_FLAG = False
VIM_COMMAND = ''
SSH_TTY = ''
try:
import termios
import tty
except ImportError:
print '\033[1;31m仅支持类Unix系统 Only unix like supported.\033[0m'
time.sleep(3)
sys.exit()
def set_log(level):
@ -97,356 +85,6 @@ def pages(post_objects, request):
# 所有对象, 分页器, 本页对象, 所有页码, 本页页码,是否显示第一页,是否显示最后一页
return post_objects, paginator, page_objects, page_range, current_page, show_first, show_end
def check_vim_status(command, ssh):
global SSH_TTY
print command
if command == '':
return True
else:
command_str= 'ps -ef |grep "%s" | grep "%s"|grep -v grep |wc -l' % (command,SSH_TTY)
print command_str
stdin, stdout, stderr = ssh.exec_command(command_str)
ps_num = stdout.read()
print ps_num
if int(ps_num) == 0:
return True
else:
return False
def deal_command(str_r, ssh):
"""
处理命令中特殊字符
"""
str_r = re.sub('\x07','',str_r) #删除响铃
patch_char = re.compile('\x08\x1b\[C') #删除方向左右一起的按键
while patch_char.search(str_r):
str_r = patch_char.sub('', str_r.rstrip())
result_command = '' #最后的结果
pattern_str = '' #模式中间中的字符串
backspace_num = 0 #光标移动的个数
reach_backspace_flag = False #没有检测到光标键则为true
end_flag = False
while str_r:
tmp = re.match(r'\w', str_r)
if tmp:
if reach_backspace_flag:
pattern_str += str(tmp.group(0))
str_r = str_r[1:]
continue
else:
result_command += str(tmp.group(0))
str_r = str_r[1:]
continue
tmp = re.match(r'\x1b\[K[\x08]*', str_r)
if tmp:
if backspace_num > 0:
if backspace_num > len(result_command) :
result_command += pattern_str
result_command = result_command[0:-backspace_num]
else:
result_command = result_command[0:-backspace_num]
result_command += pattern_str
del_len = len(str(tmp.group(0)))-3
if del_len > 0:
result_command = result_command[0:-del_len]
reach_backspace_flag = False
backspace_num =0
pattern_str=''
str_r = str_r[len(str(tmp.group(0))):]
continue
if re.match(r'\x08', str_r):
backspace_num += 1
reach_backspace_flag = True
str_r = str_r[1:]
if len(str_r) == 0:
end_flag = True
continue
if reach_backspace_flag :
pattern_str += str_r[0]
else :
result_command += str_r[0]
str_r = str_r[1:]
if backspace_num > 0 and not end_flag:
result_command = result_command[:-backspace_num]
result_command += pattern_str
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)
result_command = control_char.sub('', result_command.strip())
global VIM_FLAG
global VIM_COMMAND
if not VIM_FLAG:
if result_command.startswith('vim') or result_command.startswith('vi') :
VIM_FLAG = True
VIM_COMMAND = result_command
return result_command.decode('utf8',"ignore")
else:
if check_vim_status(VIM_COMMAND, ssh):
VIM_FLAG = False
VIM_COMMAND=''
return result_command.decode('utf8',"ignore")
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']:
if i in strings:
#print "new line"
return True
return False
class Jtty(object):
"""
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():
"""
This function use to get the size of the windows!
获得terminal窗口大小
"""
if 'TIOCGWINSZ' in dir(termios):
TIOCGWINSZ = termios.TIOCGWINSZ
else:
TIOCGWINSZ = 1074295912L
s = struct.pack('HHHH', 0, 0, 0, 0)
x = fcntl.ioctl(sys.stdout.fileno(), TIOCGWINSZ, s)
return struct.unpack('HHHH', x)[0:2]
def set_win_size(self, sig, data):
"""
This function use to set the window size of the terminal!
设置terminal窗口大小
"""
try:
win_size = self.get_win_size()
self.chan.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):
"""
Use paramiko channel connect server interactive.
使用paramiko模块的channel连接后端进入交互式
"""
log_file_f, log_time_f, ip_list, log = self.log_record()
old_tty = termios.tcgetattr(sys.stdin)
pre_timestamp = time.time()
input_r = ''
input_mode = False
try:
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())
self.chan.settimeout(0.0)
while True:
try:
r, w, e = select.select([self.chan, sys.stdin], [], [])
except Exception:
pass
if self.chan in r:
try:
x = self.chan.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)))
pre_timestamp = now_timestamp
log_file_f.flush()
log_time_f.flush()
if input_mode and not newline_code_in(x):
input_r += x
except socket.timeout:
pass
if sys.stdin in r:
x = os.read(sys.stdin.fileno(), 1)
if not input_mode:
input_mode = True
if str(x) in ['\r', '\n', '\r\n']:
input_r = deal_command(input_r,ssh)
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)
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
log_file_f.write('End time is %s' % datetime.datetime.now())
log_file_f.close()
log.is_finished = True
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.
连接服务器
"""
ps1 = "PS1='[\u@%s \W]\$ '\n" % self.ip
login_msg = "clear;echo -e '\\033[32mLogin %s done. Enjoy it.\\033[0m'\n" % self.ip
# 发起ssh连接请求 Make a ssh connection
ssh = self.get_connection()
# 获取连接的隧道并设置窗口大小 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])
try:
signal.signal(signal.SIGWINCH, self.set_win_size)
except:
pass
# 设置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)
#print 'ok'+tmp+'ok'
SSH_TTY = re.search(r'(?<=/dev/).*', tmp).group().strip()
channel.send('clear\n')
# Make ssh interactive tunnel
self.posix_shell(ssh)
# Shutdown channel socket
channel.close()
ssh.close()
def execute(self, cmd):
"""
execute cmd on the asset
执行命令
"""
pass
class PyCrypt(object):

Loading…
Cancel
Save