def __init__(self, websocker, message): self.websocker = websocker self.message = message self.cmd = '' # 记录多行处理过的命令 self.cmd_tmp = '' # 记录一行待处理的命令 self.res = '' self.start_time = time.time() tmp_date1 = time.strftime("%Y-%m-%d", time.localtime(int(self.start_time))) tmp_date2 = time.strftime("%Y%m%d%H%M%S", time.localtime(int(self.start_time))) if not os.path.isdir(os.path.join(settings.RECORD_ROOT, tmp_date1)): os.makedirs(os.path.join(settings.RECORD_ROOT, tmp_date1)) self.res_file = settings.RECORD_DIR + '/' + tmp_date1 + '/' + 'webssh_' + \ tmp_date2 + '_' + gen_rand_char(16) + '.txt' self.last_save_time = self.start_time self.res_asciinema = [] self.zmodem = False self.zmodemOO = False mp_readline.TESTING = True self.rl = mp_readline.MpReadline() self.tab_mode = False # 使用tab命令补全时需要读取返回数据然后添加到当前输入命令后 self.history_mode = False self.enter = False # 是否输入回车 \r, 为 True 时则根据 ssh 服务端返回的数据判断是否是执行的命令或者是编辑文本 self.ctrl_z = False self.ctrl_c = False
def __init__(self): self.event = threading.Event() self.tty_args = ['xterm', 80, 40] # 终端参数(终端, 长, 宽) # self.ssh_args = None # ssh连接参数 self.ssh_args = None self.type = None self.http_user = None # 终端日志 -- http用户 self.hostname = None # 后端主机名称 self.password = None self.hostid = None # 终端日志 -- hostid self.closed = False self.chan_cli = None self.client = None self.client_addr = None self.group = 'session_' + gen_rand_char() self.cmd = '' # 多行命令 self.cmd_tmp = '' # 一行命令 self.start_time = time.time() tmp_date1 = time.strftime("%Y-%m-%d", time.localtime(int(self.start_time))) tmp_date2 = time.strftime("%Y%m%d%H%M%S", time.localtime(int(self.start_time))) if not os.path.isdir(os.path.join(settings.RECORD_ROOT, tmp_date1)): os.makedirs(os.path.join(settings.RECORD_ROOT, tmp_date1)) self.res_file = settings.RECORD_DIR + '/' + tmp_date1 + '/' + 'clissh_' + \ tmp_date2 + '_' + gen_rand_char(16) + '.txt' self.log_start_time = timezone.now() self.last_save_time = self.start_time self.res_asciinema = [] self.width = 80 self.height = 40 self.user_role = False # False 普通用户 True 管理员 self.superusername = None self.superpassword = None self.lock = False # 锁定会话 self.zmodem = False mp_readline.TESTING = True self.rl = mp_readline.MpReadline() self.tab_mode = False # 使用tab命令补全时需要读取返回数据然后添加到当前输入命令后 self.history_mode = False self.enter = False # 是否输入回车 \r, 为 True 时则根据 ssh 服务端返回的数据判断是否是执行的命令或者是编辑文本 self.ctrl_z = False self.ctrl_c = False
def websocket_to_django(self): try: while 1: if self.zmodemOO: self.zmodemOO = False x = self.channel.recv(2) if not len(x): return if x == b'OO': self.websocker.send(bytes_data=x) continue else: x += self.channel.recv(BufferSize) else: x = self.channel.recv(BufferSize) if not len(x): return if self.zmodem: if zmodemszend in x or zmodemrzend in x: self.zmodem = False if zmodemszend in x: self.zmodemOO = True if zmodemcancel in x: self.zmodem = False self.channel.send('\n') self.websocker.send(bytes_data=x) else: if zmodemszstart in x or zmodemrzstart in x or zmodemrzestart in x or zmodemrzsstart in x \ or zmodemrzesstart in x: self.zmodem = True self.websocker.send(bytes_data=x) else: try: data = x.decode('utf-8') except UnicodeDecodeError: # utf-8中文占3个字符,可能会被截断,需要拼接 try: x += self.channel.recv(1) data = x.decode('utf-8') except UnicodeDecodeError: try: x += self.channel.recv(1) data = x.decode('utf-8') except UnicodeDecodeError: logger.error(traceback.format_exc()) data = x.decode( 'utf-8', 'ignore') # 拼接2次后还是报错则证明结果是乱码,强制转换 self.message['status'] = 0 self.message['message'] = data self.res += data message = json.dumps(self.message) if self.websocker.send_flag == 0: self.websocker.send(message) elif self.websocker.send_flag == 1: async_to_sync( self.websocker.channel_layer.group_send)( self.websocker.group, { "type": "chat.message", "text": message, }) delay = round(time.time() - self.start_time, 6) self.res_asciinema.append( json.dumps([delay, 'o', data])) # 指定条结果或者指定秒数或者占用指定内存就保存一次 if len(self.res_asciinema) > 2000 or int(time.time() - self.last_save_time) > 60 or \ sys.getsizeof(self.res_asciinema) > 20971752: tmp = list(self.res_asciinema) self.res_asciinema = [] self.last_save_time = time.time() save_res(self.res_file, tmp) if self.enter: self.enter = False if not data.startswith( "\r\n"): # 回车后结果不以\r\n开头的肯定不是命令 self.cmd_tmp = '' else: if re.match( rb'^\r\n\s+\x1b.*$', x ): # 终端为 xterm,linux 等显示颜色类型时在 vi 编辑模式下回车 self.cmd_tmp = '' # elif x == b'\r\n': # todo 正常模式下 vi 文件会返回 \r\n ,终端为 dumb 类型时在 vi 编辑模式下回车也会返回 \r\n, # self.cmd_tmp = '' else: # 记录真正命令, rl 不支持中文命令 cmd_time = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))) cmd = self.rl.process_line( self.cmd_tmp.encode("utf-8")) if cmd is None: # 有可能 rl 库会返回 None,重试一次 mp_readline.TESTING = True self.rl = mp_readline.MpReadline() cmd = self.rl.process_line( self.cmd_tmp.encode("utf-8")) if cmd: self.cmd += cmd_time + "\t" + remove_control_chars( cmd) + '\n' else: if cmd is None: logger.error( "recv from server: {} \nerror command: {}" .format( x, self.cmd_tmp.encode( "utf-8"))) self.cmd += cmd_time + "\t" + remove_control_chars( self.cmd_tmp) + '\n' self.cmd_tmp = '' else: if self.tab_mode: # todo 兼容有问题 self.tab_mode = False if x == b'\x07': pass tmp = data.split(' ') # tab 只返回一个命令时匹配 if len(tmp) == 2 and tmp[ 1] == '' and tmp[0] != '': self.cmd_tmp = self.cmd_tmp + tmp[ 0].encode().replace(b'\x07', b'').decode() elif len(tmp) == 1 and tmp[0].encode( ) != b'\x07': # \x07 蜂鸣声 self.cmd_tmp = self.cmd_tmp + tmp[ 0].encode().replace(b'\x07', b'').decode() # 多次上下箭头查找历史命令返回数据中可能会包含 \x1b[1P 导致 rl 无法解析命令,具体原因没有深究 if self.history_mode: self.history_mode = False if x != b'' and x != b'\x07': x = re.sub(rb'\x1b\[\d+P', b'', x) self.cmd_tmp += x.decode("utf-8") if self.ctrl_c: # 取消命令 self.ctrl_c = False # if x == b'^C\r\n': if re.match(rb'^\^C\r\n[\s\S]*$', x) or re.match( rb'^\r\n[\s\S]*$', x): self.cmd_tmp = "" if self.ctrl_z: self.ctrl_z = False if re.match( rb'^[\s\S]*\[\d+\]\+\s+Stopped\s+\S+[\s\S]*$', x): self.cmd_tmp = "" except socket.timeout: self.message['status'] = 1 self.message['message'] = '由于长时间没有操作或者没有数据返回,连接已断开!' message = json.dumps(self.message) if self.websocker.send_flag == 0: self.websocker.send(message) elif self.websocker.send_flag == 1: async_to_sync(self.websocker.channel_layer.group_send)( self.websocker.group, { "type": "chat.message", "text": message, }) self.close(send_message=False) except Exception: logger.info(traceback.format_exc()) self.close()
def bridge(self): # 桥接 客户终端 和 代理服务终端 交互 # transport_keepalive(self.chan_ser.transport) sel = selectors.DefaultSelector( ) # 根据平台自动选择 IO 模式(kqueue, devpoll, epoll, poll, select) sel.register(self.chan_cli, selectors.EVENT_READ) sel.register(self.chan_ser, selectors.EVENT_READ) try: while self.chan_ser and self.chan_cli and not ( self.chan_ser.closed or self.chan_cli.closed): events = sel.select( timeout=terminal_exipry_time) # 指定时间无数据输入或者无数据返回则断开连接 if not events: raise socket.timeout for key, n in events: if key.fileobj == self.chan_ser: try: recv_message = self.chan_ser.recv(BufferSize) if self.zmodem: if zmodemszend in recv_message or zmodemrzend in recv_message: self.zmodem = False delay = round( time.time() - self.start_time, 6) self.res_asciinema.append( json.dumps([delay, 'o', '\r\n'])) # logger.info("zmodem end") if zmodemcancel in recv_message: self.zmodem = False self.chan_ser.send(b'\n') # logger.info("zmodem cancel") self.chan_cli.send(recv_message) continue else: if zmodemszstart in recv_message or zmodemrzstart in recv_message or \ zmodemrzestart in recv_message or zmodemrzsstart in recv_message or \ zmodemrzesstart in recv_message: self.zmodem = True # logger.info("zmodem start") self.chan_cli.send(recv_message) continue if len(recv_message) == 0: self.chan_cli.send( "\r\n\033[31m服务端已断开连接....\033[0m\r\n") time.sleep(1) break else: message = dict() message['status'] = 0 try: # 发送数据给查看会话的 websocket 组 message['message'] = recv_message.decode( 'utf-8') except UnicodeDecodeError: try: recv_message += self.chan_ser.recv(1) message[ 'message'] = recv_message.decode( 'utf-8') except UnicodeDecodeError: try: recv_message += self.chan_ser.recv( 1) message[ 'message'] = recv_message.decode( 'utf-8') except UnicodeDecodeError: logger.error( traceback.format_exc()) # 拼接2次后还是报错则证明结果是乱码,强制转换 message[ 'message'] = recv_message.decode( 'utf-8', 'ignore') self.chan_cli.send(recv_message) channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( self.group, { "type": "chat.message", "text": message, }) delay = round(time.time() - self.start_time, 6) self.res_asciinema.append( json.dumps([ delay, 'o', recv_message.decode('utf-8') ])) # 250条结果或者指定秒数就保存一次,这个任务可以优化为使用 celery if len(self.res_asciinema) > 2000 or int(time.time() - self.last_save_time) > 60 \ or sys.getsizeof(self.res_asciinema) > 2097152: tmp = list(self.res_asciinema) self.res_asciinema = [] self.last_save_time = time.time() res(self.res_file, tmp) try: data = recv_message.decode('utf-8') if self.enter: self.enter = False if not data.startswith( "\r\n" ): # 回车后结果不以\r\n开头的肯定不是命令 self.cmd_tmp = '' else: if re.match( rb'^\r\n\s+\x1b.*$', recv_message ): # 终端为 xterm,linux 等显示颜色类型时在 vi 编辑模式下回车 self.cmd_tmp = '' # elif x == b'\r\n': # todo 正常模式下 vi 文件会返回 \r\n ,终端为 dumb 类型时在 vi 编辑模式下回车也会返回 \r\n, # self.cmd_tmp = '' else: # 记录真正命令, rl 不支持中文命令 cmd_time = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime( int(time.time()))) cmd = self.rl.process_line( self.cmd_tmp.encode( "utf-8")) if not cmd: # 有可能 rl 库会返回 None,重试一次 mp_readline.TESTING = True self.rl = mp_readline.MpReadline( ) cmd = self.rl.process_line( self.cmd_tmp.encode( "utf-8")) if cmd: self.cmd += cmd_time + "\t" + remove_control_chars( cmd) + '\n' else: logger.error( "recv from server: {} \nerror command: {}" .format( recv_message, self.cmd_tmp. encode("utf-8"))) self.cmd += cmd_time + "\t" + remove_control_chars( self.cmd_tmp) + '\n' self.cmd_tmp = '' else: if self.tab_mode: # todo 兼容有问题 self.tab_mode = False tmp = data.split(' ') # tab 只返回一个命令时匹配 # print(tmp) if len(tmp) == 2 and tmp[ 1] == '' and tmp[0] != '': self.cmd_tmp = self.cmd_tmp + tmp[ 0].encode().replace( b'\x07', b'').decode() elif len(tmp ) == 1 and tmp[0].encode( ) != b'\x07': # \x07 蜂鸣声 self.cmd_tmp = self.cmd_tmp + tmp[ 0].encode().replace( b'\x07', b'').decode() # 多次上下箭头查找历史命令返回数据中可能会包含 \x1b[1P 导致 rl 无法解析命令,具体原因没有深究 if self.history_mode: self.history_mode = False if recv_message != b'' and recv_message != b'\x07': recv_message = re.sub( rb'\x1b\[\d+P', b'', recv_message) self.cmd_tmp += recv_message.decode( "utf-8") if self.ctrl_c: # 取消命令 self.ctrl_c = False # if x == b'^C\r\n': if re.match( rb'^\^C\r\n[\s\S]*$', recv_message) or re.match( rb'^\r\n[\s\S]*$', recv_message): self.cmd_tmp = "" if self.ctrl_z: self.ctrl_z = False if re.match( rb'^[\s\S]*\[\d+\]\+\s+Stopped\s+\S+[\s\S]*$', recv_message): self.cmd_tmp = "" except Exception: logger.error(traceback.format_exc()) except socket.timeout: logger.error(traceback.format_exc()) if key.fileobj == self.chan_cli: try: send_message = self.chan_cli.recv(BufferSize) if len(send_message) == 0: logger.info('客户端断开了连接 {}....'.format( self.client_addr)) # time.sleep(1) break else: if not self.lock: self.chan_ser.send(send_message) if not self.zmodem: try: data = send_message.decode('utf-8') if data == '\r': # 回车,开始根据服务端返回判断是否是命令,这种判断方式的特性就导致了无法是否禁止命令功能,当然想绝对禁止命令本身就是一个伪命题 if self.cmd_tmp.strip() != '': self.enter = True elif data.encode( ) == b'\x07': # 响铃 pass elif data == '\t' or data.encode( ) == b'\x1b': # \x1b 点击2下esc键也可以补全 self.tab_mode = True elif data.encode( ) == b'\x1b[A' or data.encode( ) == b'\x1b[B': self.history_mode = True elif data.encode( ) == b'\x03': # 输入命令后先 ctrl + v,然后 ctrl + c 需要两次才能取消 self.ctrl_c = True elif data.encode( ) == b'\x1a': # ctrl + z self.ctrl_z = True else: self.cmd_tmp += data except Exception: logger.error( traceback.format_exc()) else: # 红色提示文字 self.chan_cli.send( "\r\n\033[31m当前会话已被管理员锁定\033[0m\r\n") self.check_channel_window_change_request( self.chan_cli, self.width - 1, self.height, 0, 0) self.check_channel_window_change_request( self.chan_cli, self.width + 1, self.height, 0, 0) except socket.timeout: logger.error(traceback.format_exc()) except Exception: logger.error(traceback.format_exc()) break except socket.timeout: self.chan_cli.send( "\r\n\033[31m由于长时间没有操作或者没有数据返回,连接已断开!\033[0m\r\n") logger.info("后端主机 (%s@%s) 会话由于长时间没有操作或者没有数据返回,连接断开!" % (self.ssh_args[2], self.ssh_args[0])) except Exception: logger.error(traceback.format_exc())
def websocket_to_django(self): try: while 1: # read_very_eager 方法读取时会是无限循环,性能比较低 # data = self.tn.read_very_eager().decode('utf-8') # if not len(data): # continue # expect 使用正则匹配所有返回内容,还可以实现超时无返回内容断开连接 if len(self._buffer) >= 1: x = self._buffer[:BufferSize] self._buffer = self._buffer[BufferSize:] else: n, y, z = self.tn.expect([br'[\s\S]+'], timeout=terminal_exipry_time) self._buffer += z x = self._buffer[:BufferSize] # 一次最多截取 BufferSize 个字符 self._buffer = self._buffer[BufferSize:] if not len(x): raise socket.timeout try: data = x.decode('utf-8') except UnicodeDecodeError: # utf-8中文占3个字符,可能会被截断,需要拼接 try: if len(self._buffer) >= 1: x += self._buffer[:1] self._buffer = self._buffer[1:] else: n, y, z = self.tn.expect( [br'[\s\S]+'], timeout=terminal_exipry_time) if len(z) > 1: self._buffer += z x += self._buffer[:1] self._buffer = self._buffer[1:] else: x += z data = x.decode('utf-8') except UnicodeDecodeError: try: if len(self._buffer) >= 1: x += self._buffer[:1] self._buffer = self._buffer[1:] else: n, y, z = self.tn.expect( [br'[\s\S]+'], timeout=terminal_exipry_time) if len(z) > 1: self._buffer += z x += self._buffer[:1] self._buffer = self._buffer[1:] else: x += z data = x.decode('utf-8') except UnicodeDecodeError: data = x.decode('utf-8', 'ignore') # 拼接2次后还是报错则证明结果是乱码,强制转换 self.message['status'] = 0 self.message['message'] = data self.res += data message = json.dumps(self.message) if self.websocker.send_flag == 0: self.websocker.send(message) elif self.websocker.send_flag == 1: async_to_sync(self.websocker.channel_layer.group_send)( self.websocker.group, { "type": "chat.message", "text": message, }) delay = round(time.time() - self.start_time, 6) self.res_asciinema.append(json.dumps([delay, 'o', data])) # 指定条结果或者指定秒数或者占用指定大小内存就保存一次 if len(self.res_asciinema) > 2000 or int(time.time() - self.last_save_time) > 60 or \ sys.getsizeof(self.res_asciinema) > 2097152: tmp = list(self.res_asciinema) self.res_asciinema = [] self.last_save_time = time.time() save_res(self.res_file, tmp) if self.enter: self.enter = False if not data.startswith("\r\n"): # 回车后结果不以\r\n开头的肯定不是命令 self.cmd_tmp = '' else: if re.match(rb'^\r\n\s+\x1b.*$', x): # 终端为 xterm,linux 等显示颜色类型时在 vi 编辑模式下回车 self.cmd_tmp = '' # elif x == b'\r\n': # todo 正常模式下 vi 文件会返回 \r\n ,终端为 dumb 类型时在 vi 编辑模式下回车也会返回 \r\n, # self.cmd_tmp = '' else: # 记录真正命令, rl 不支持中文命令 cmd_time = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))) cmd = self.rl.process_line( self.cmd_tmp.encode("utf-8")) if not cmd: # 有可能 rl 库会返回 None,重试一次 mp_readline.TESTING = True self.rl = mp_readline.MpReadline() cmd = self.rl.process_line( self.cmd_tmp.encode("utf-8")) if cmd: self.cmd += cmd_time + "\t" + remove_control_chars( cmd) + '\n' else: logger.error( "recv from server: {} \nerror command: {}". format(x, self.cmd_tmp.encode("utf-8"))) self.cmd += cmd_time + "\t" + remove_control_chars( self.cmd_tmp) + '\n' self.cmd_tmp = '' else: if self.tab_mode: # todo 兼容有问题 self.tab_mode = False tmp = data.split(' ') # tab 只返回一个命令时匹配 # print(tmp) if len(tmp) == 2 and tmp[1] == '' and tmp[0] != '': self.cmd_tmp = self.cmd_tmp + tmp[0].encode( ).replace(b'\x07', b'').decode() elif len(tmp) == 1 and tmp[0].encode( ) != b'\x07': # \x07 蜂鸣声 self.cmd_tmp = self.cmd_tmp + tmp[0].encode( ).replace(b'\x07', b'').decode() # 多次上下箭头查找历史命令返回数据中可能会包含 \x1b[1P 导致 rl 无法解析命令,具体原因没有深究 if self.history_mode: self.history_mode = False if x != b'' and x != b'\x07': x = re.sub(rb'\x1b\[\d+P', b'', x) self.cmd_tmp += x.decode("utf-8") if self.ctrl_c: # 取消命令 self.ctrl_c = False # if x == b'^C\r\n': if re.match(rb'^\^C\r\n[\s\S]*$', x) or re.match( rb'^\r\n[\s\S]*$', x): self.cmd_tmp = "" if self.ctrl_z: self.ctrl_z = False if re.match( rb'^[\s\S]*\[\d+\]\+\s+Stopped\s+\S+[\s\S]*$', x): self.cmd_tmp = "" except socket.timeout: self.message['status'] = 1 self.message['message'] = '由于长时间没有操作或者没有数据返回,连接已断开!' message = json.dumps(self.message) if self.websocker.send_flag == 0: self.websocker.send(message) elif self.websocker.send_flag == 1: async_to_sync(self.websocker.channel_layer.group_send)( self.websocker.group, { "type": "chat.message", "text": message, }) self.close(send_message=False) except Exception: logger.error(traceback.format_exc()) self.close()