diff --git a/.gitignore b/.gitignore index 4272d22..00b62f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ jumpserver .idea .idea/ +examples/ diff --git a/client/jumpcli.go b/client/jumpcli.go index cdb0290..2312302 100644 --- a/client/jumpcli.go +++ b/client/jumpcli.go @@ -85,6 +85,7 @@ func (t *SSHTerminal) updateTerminalSize() { continue } + // 调整窗体大小 t.Session.WindowChange(currTermHeight, currTermWidth) if err != nil { fmt.Printf("Unable to send window-change reqest: %s.", err) @@ -108,12 +109,14 @@ func (t *SSHTerminal) interactiveSession() error { }() fd := int(os.Stdin.Fd()) + // raw模式 cbreak state, err := terminal.MakeRaw(fd) if err != nil { return err } defer terminal.Restore(fd, state) + // 获取当前窗体大小 termWidth, termHeight, err := terminal.GetSize(fd) if err != nil { return err @@ -124,6 +127,7 @@ func (t *SSHTerminal) interactiveSession() error { termType = "xterm-256color" } + // 设置tty err = t.Session.RequestPty(termType, termHeight, termWidth, ssh.TerminalModes{ ssh.ECHO: 1, ssh.TTY_OP_ISPEED: 14400, @@ -133,6 +137,7 @@ func (t *SSHTerminal) interactiveSession() error { return err } + // 动态调整窗体大小 t.updateTerminalSize() t.stdin, err = t.Session.StdinPipe() diff --git a/client/jumpcli.py b/client/jumpcli.py new file mode 100644 index 0000000..34e1932 --- /dev/null +++ b/client/jumpcli.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +import paramiko +import os +import sys +import curses +import termios +import tty +import socket +import signal +import getpass +import platform +import sqlite3 + +KEYS_ENTER = (curses.KEY_ENTER, ord('\n'), ord('\r')) +KEYS_UP = (curses.KEY_UP, ord('k')) +KEYS_DOWN = (curses.KEY_DOWN, ord('j')) +KEYS_SELECT = (curses.KEY_RIGHT, ord(' ')) + +# 数据库文件 +gSqlite3File = "/usr/local/jumpserver/jumpserver.db" + +# ssh_private_path ssh私钥路径 +system_type = platform.system() +if system_type == "Darwin": + ssh_private_path = "/Users/%s/.ssh/id_rsa" +elif system_type == "Linux": + ssh_private_path = "/home/%s/.ssh/id_rsa" +else: + exit(1) + + +class Picker(object): + def __init__(self, options, title=None, indicator='*', default_index=0, multiselect=False, multi_select=False, + min_selection_count=0, options_map_func=None): + + if len(options) == 0: + raise ValueError('options should not be an empty list') + + self.options = options + self.title = title + self.indicator = indicator + self.multiselect = multiselect or multi_select + self.min_selection_count = min_selection_count + self.options_map_func = options_map_func + self.all_selected = [] + + if default_index >= len(options): + raise ValueError('default_index should be less than the length of options') + + if multiselect and min_selection_count > len(options): + raise ValueError( + 'min_selection_count is bigger than the available options, you will not be able to make any selection') + + if options_map_func is not None and not callable(options_map_func): + raise ValueError('options_map_func must be a callable function') + + self.index = default_index + self.custom_handlers = {} + + def register_custom_handler(self, key, func): + self.custom_handlers[key] = func + + def move_up(self): + self.index -= 1 + if self.index < 0: + self.index = len(self.options) - 1 + + def move_down(self): + self.index += 1 + if self.index >= len(self.options): + self.index = 0 + + def mark_index(self): + if self.multiselect: + if self.index in self.all_selected: + self.all_selected.remove(self.index) + else: + self.all_selected.append(self.index) + + def get_selected(self): + """return the current selected option as a tuple: (option, index) + or as a list of tuples (in case multiselect==True) + """ + if self.multiselect: + return_tuples = [] + for selected in self.all_selected: + return_tuples.append((self.options[selected], selected)) + return return_tuples + else: + return self.options[self.index], self.index + + def get_title_lines(self): + if self.title: + return self.title.split('\n') + [''] + return [] + + def get_option_lines(self): + lines = [] + for index, option in enumerate(self.options): + # pass the option through the options map of one was passed in + if self.options_map_func: + option = self.options_map_func(option) + + if index == self.index: + prefix = self.indicator + else: + prefix = len(self.indicator) * ' ' + + if self.multiselect and index in self.all_selected: + format = curses.color_pair(1) + line = ('{0} {1}'.format(prefix, option), format) + else: + line = '{0} {1}'.format(prefix, option) + lines.append(line) + + return lines + + def get_lines(self): + title_lines = self.get_title_lines() + option_lines = self.get_option_lines() + lines = title_lines + option_lines + current_line = self.index + len(title_lines) + 1 + return lines, current_line + + def draw(self): + """draw the curses ui on the screen, handle scroll if needed""" + self.screen.clear() + + x, y = 1, 1 # start point + max_y, max_x = self.screen.getmaxyx() + max_rows = max_y - y # the max rows we can draw + + lines, current_line = self.get_lines() + + # calculate how many lines we should scroll, relative to the top + scroll_top = getattr(self, 'scroll_top', 0) + if current_line <= scroll_top: + scroll_top = 0 + elif current_line - scroll_top > max_rows: + scroll_top = current_line - max_rows + self.scroll_top = scroll_top + + lines_to_draw = lines[scroll_top:scroll_top + max_rows] + + for line in lines_to_draw: + if type(line) is tuple: + self.screen.addnstr(y, x, line[0], max_x - 2, line[1]) + else: + self.screen.addnstr(y, x, line, max_x - 2) + y += 1 + + self.screen.refresh() + + def run_loop(self): + while True: + self.draw() + c = self.screen.getch() + if c in KEYS_UP: + self.move_up() + elif c in KEYS_DOWN: + self.move_down() + elif c in KEYS_ENTER: + if self.multiselect and len(self.all_selected) < self.min_selection_count: + continue + return self.get_selected() + elif c in KEYS_SELECT and self.multiselect: + self.mark_index() + elif c in self.custom_handlers: + ret = self.custom_handlers[c](self) + if ret: + return ret + elif c == ord('q'): + exit(0) + + def config_curses(self): + try: + # use the default colors of the terminal + curses.use_default_colors() + # hide the cursor + curses.curs_set(0) + # add some color for multi_select + # @todo make colors configurable + curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_WHITE) + except: + # Curses failed to initialize color support, eg. when TERM=vt100 + curses.initscr() + + def _start(self, screen): + self.screen = screen + self.config_curses() + return self.run_loop() + + def start(self): + return curses.wrapper(self._start) + + +def updateWindowHandler(signum): + if signum == signal.SIGWINCH: + width, height = os.get_terminal_size() + os.terminal_size((width, height)) + + +def posix_shell(chan): + import select + oldtty = termios.tcgetattr(sys.stdin) + signal.signal(signal.SIGWINCH, updateWindowHandler) + + try: + tty.setraw(sys.stdin.fileno(), termios.TCIOFLUSH) + # tty.setcbreak(sys.stdin.fileno()) + chan.settimeout(0.0) + while True: + r, w, e = select.select([chan, sys.stdin], [], []) + if chan in r: + try: + data = str(chan.recv(1024), encoding='utf-8') + if len(data) == 0: + break + sys.stdout.write(data) + sys.stdout.flush() + except socket.timeout: + pass + if sys.stdin in r: + ch = sys.stdin.read(1) + if len(ch) == 0: + break + chan.send(ch) + finally: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + + +def NewTerminal(host, port, user): + # 建立ssh连接 + ssh = paramiko.SSHClient() + ssh.load_system_host_keys() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + ssh.connect( + host, + port=port, + username=user, + key_filename=ssh_private_path % user, + compress=False) + + # 建立交互式shell连接 + width, height = os.get_terminal_size() + channel = ssh.invoke_shell("xterm-256color", width, height) + + # 建立交互式管道 + posix_shell(channel) + + # 关闭连接 + channel.close() + ssh.close() + + +# 初始化表 +def connect_db(): + # 连接数据库 + conn = sqlite3.connect(gSqlite3File) + if conn == None: + print("sqlite3.connect " + gSqlite3File + "failed!") + exit(1) + return conn + +def get_hosts(user): + db = connect_db() + hosts = db.execute("select name,ip,port from hosts where isdelete=0 and name in (select hostname from hostuser where username='%s') order by id" % user).fetchall() + resp = [] + for host in hosts: + resp.append("%s:%s:%s" % (host[0], host[1], host[2])) + + if len(resp) == 0: + print(user + " no valid hosts") + exit(1) + return resp + +def main(): + user = getpass.getuser() + print("current user: " + user) + + title = "ssh hosts select:" + menu = get_hosts(user) + + while True: + option, index = Picker(menu, title).start() + arr = option.split(":") + if len(arr) == 3: + host = arr[1] + port = int(arr[2]) + NewTerminal(host, port, user) + + +if __name__ == '__main__': + main() diff --git a/client/requirements.txt b/client/requirements.txt new file mode 100644 index 0000000..ada4a36 --- /dev/null +++ b/client/requirements.txt @@ -0,0 +1 @@ +paramiko==2.7.1 diff --git a/examples/cmd-menu.py b/examples/cmd-menu.py new file mode 100644 index 0000000..4e42a7c --- /dev/null +++ b/examples/cmd-menu.py @@ -0,0 +1,24 @@ +# -*- coding: UTF-8 -*- + +from pick import pick +import sshserver + +title = 'Please choose your favorite programming language: ' +# options = ['quit', 'Java', 'JavaScript', 'Python', 'PHP', 'C++', 'Erlang', 'Haskell'] +options = ['ckqas:192.168.1.44:22', 'ckqas129:192.168.1.129:22'] +# option, index = pick(options, title) +# print(option, index) + + +if __name__ == '__main__': + while True: + option, index = pick(options, title) + if option == "quit": + exit(0) + + arr = option.split(":") + if len(arr) == 3: + host = arr[1] + port = int(arr[2]) + sshserver.NewTerminal(host, port, "daniel") + diff --git a/server/manager_user.sh b/server/manager_user.sh index 0e5f7da..7eb7ca6 100644 --- a/server/manager_user.sh +++ b/server/manager_user.sh @@ -82,7 +82,7 @@ del_user() { # unsudo_user - userdel $cmdName && rm -r $USER_HOME_DIR + userdel $cmdName #&& rm -r $USER_HOME_DIR if [ $? -eq 0 ];then echo "${cmdName} is delete successful" else