From cafe7afe827fedba2be3be25be8567af7afe9386 Mon Sep 17 00:00:00 2001 From: leffss <348926676@qq.com> Date: Fri, 10 Apr 2020 13:28:50 +0800 Subject: [PATCH] webssh add zmodem(sz,rz) support --- README.md | 18 +- apps/webssh/ssh.py | 180 ++++++++-------- apps/webssh/sshd/sshinterface.py | 2 +- apps/webssh/websocket_layer.py | 52 +++-- apps/webtelnet/telnet.py | 39 ++-- apps/webtelnet/websocket_layer.py | 2 +- requirements.txt | 2 +- static/bootbox/5.4.0/bootbox.min.js | 1 + static/webssh/webssh.clissh.view.js | 21 +- static/webssh/webssh.js | 313 +++++++++++++++++++++------- static/webssh/webssh.view.js | 20 +- static/webtelnet/webtelnet.js | 294 ++++++++++++++++++++------ templates/webssh/terminal.html | 3 + templates/webtelnet/terminal.html | 3 + 14 files changed, 657 insertions(+), 293 deletions(-) create mode 100644 static/bootbox/5.4.0/bootbox.min.js diff --git a/README.md b/README.md index 296e03b..7935bc7 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ docker run --name guacd -p 4822:4822 -d guacamole/guacd # 安装相关库 pip3 install -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt ``` -- -i 指定阿里源,国外源慢得一逼,我大天朝威武 +- -i 指定阿里源,国外源慢得一逼,我大天朝威武,局域网玩得贼6 **修改 devops/settings.py 配置** @@ -248,8 +248,16 @@ systemctl start nginx 有点多,看图,不想描述了。 +# 存在问题 +web 终端(包括 webssh,webtelnet)在使用 chrome 浏览器打开时,很大机率会出现一片空白无法显示 xterm.js 终端的情况。 +解决方法是改变一下 chrome 的缩放比例就好了(ctrl + 鼠标滚轮),在 firefox 下无此问题,具体原因未知。 + + # 升级日志 +### ver1.8.7 +webssh 新增 zmodem(sz, rz) 上传下载文件支持(webtelnet 理论上也可以实现,原理一样,应该淘汰的协议,就懒得做了); + ### ver1.8.6 优化执行 playbook 逻辑:允许指定组; @@ -370,7 +378,7 @@ linux 平台下使用 celery 任务保存终端会话日志与录像(windows 尝试加入 celery 实现异步任务; ### ver1.1.0 -新增 ssh 和 telnet 协议连接远程主机; +新增 ssh 和 telnet(明文传输的古老协议,不推荐使用)连接远程主机; ### ver1.0.0 初始版本 @@ -401,15 +409,17 @@ linux 平台下使用 celery 任务保存终端会话日志与录像(windows ![效果](https://github.com/leffss/devops/blob/master/screenshots/24.PNG?raw=true) ![效果](https://github.com/leffss/devops/blob/master/screenshots/25.PNG?raw=true) + # TODO LISTS - [ ] docker 容器管理 - [ ] k8s 集群管理 -- [ ] 自动化部署CI/CD -- [ ] webssh 与 webtelnet 的 zmodem(sz, rz) 支持 +- [ ] 自动化部署 ci/cd + # MIT License ``` Copyright (c) 2019-2020 leffss ``` + 更多新功能不断探索发现中. diff --git a/apps/webssh/ssh.py b/apps/webssh/ssh.py index f3eb4de..2816a6d 100644 --- a/apps/webssh/ssh.py +++ b/apps/webssh/ssh.py @@ -20,6 +20,11 @@ except Exception: terminal_exipry_time = 60 * 30 +zmodemszstart = b'rz\r**\x18B00000000000000\r\x8a' +zmodemszend = b'**\x18B0800000000022d\r\x8a' +zmodemrzstart = b'rz waiting to receive.**\x18B0100000023be50\r\x8a' +zmodemrzend = b'**\x18B0800000000022d\r\x8a' + class SSH: def __init__(self, websocker, message): @@ -39,6 +44,8 @@ def __init__(self, websocker, message): tmp_date2 + '_' + gen_rand_char(16) + '.txt' self.last_save_time = self.start_time self.res_asciinema = [] + self.zmodem = False + self.zmodemOO = False # term 可以使用 ansi, linux, vt100, xterm, dumb,除了 dumb外其他都有颜色显示 def connect(self, host, user, password=None, ssh_key=None, port=22, timeout=30, @@ -97,8 +104,6 @@ def connect(self, host, user, password=None, ssh_key=None, port=22, timeout=30, # 创建3个线程将服务器返回的数据发送到django websocket(1个线程都可以) Thread(target=self.websocket_to_django).start() - # Thread(target=self.websocket_to_django).start() - # Thread(target=self.websocket_to_django).start() except Exception: print(traceback.format_exc()) self.message['status'] = 2 @@ -128,104 +133,110 @@ def su_root(self, superuser, superpassword, wait_time=1): def django_to_ssh(self, data): try: self.channel.send(data) - if data == '\r': # 记录命令 - data = '\n' - if self.cmd_tmp.strip() != '': - self.cmd_tmp += data - self.cmd += self.cmd_tmp - - # print('-----------------------------------') - # print(self.cmd_tmp) - # print(self.cmd_tmp.encode()) - # print('-----------------------------------') - - self.cmd_tmp = '' - elif data.encode() == b'\x07': - pass - else: - if 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 + if not self.zmodem and not self.zmodemOO: + if data == '\r': # 记录命令 + data = '\n' + if self.cmd_tmp.strip() != '': + self.cmd_tmp += data + self.cmd += self.cmd_tmp + self.cmd_tmp = '' + elif data.encode() == b'\x07': + pass else: - self.cmd_tmp += data + if 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 + else: + self.cmd_tmp += data except Exception: - print(traceback.format_exc()) + self.close() + + def django_bytes_to_ssh(self, data): + try: + self.channel.send(data) + except: self.close() def websocket_to_django(self): try: while 1: - x = b'' - try: - # data = self.channel.recv(4096).decode('utf-8') + 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(4096) + else: x = self.channel.recv(4096) + if not len(x): + return - logger.info(x) - logger.info(x.decode('utf-8', 'ignore')) - - # sz - if b'rz\r**\x18B00000000000000\r\x8a\x11' in x: - # continue - data = "\n\runsupport zmodem sz command\n\r" - - # rz - elif b'rz waiting to receive.**\x18B0100000023be50\r\x8a\x11' in x: - # continue - data = "\n\runsupport zmodem rz command\n\r" + if self.zmodem: + if zmodemszend in x or zmodemrzend in x: + self.zmodem = False + if zmodemszend in x: + self.zmodemOO = True + self.websocker.send(bytes_data=x) + else: + if zmodemszstart in x or zmodemrzstart in x: + self.zmodem = True + self.websocker.send(bytes_data=x) else: - 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: - print(traceback.format_exc()) - data = x.decode('utf-8', 'ignore') # 拼接2次后还是报错则证明结果是乱码,强制转换 - if not len(data): - return + 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: + print(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, - }) + 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])) + 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 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.tab_mode: - 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() - self.tab_mode = False - if self.history_mode: # 不完善,只支持向上翻一个历史命令 - # print(data) - if data.strip() != '': - self.cmd_tmp = data - self.history_mode = False + if self.tab_mode: + 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() + self.tab_mode = False + if self.history_mode: # 不完善,只支持向上翻一个历史命令 + if data.strip() != '': + self.cmd_tmp = data + self.history_mode = False except socket.timeout: self.message['status'] = 1 self.message['message'] = '由于长时间没有操作或者没有数据返回,连接已断开!' @@ -239,7 +250,6 @@ def websocket_to_django(self): }) self.close(send_message=False) except Exception: - print(traceback.format_exc()) self.close() def close(self, send_message=True): diff --git a/apps/webssh/sshd/sshinterface.py b/apps/webssh/sshd/sshinterface.py index 29aee51..bdbfec0 100644 --- a/apps/webssh/sshd/sshinterface.py +++ b/apps/webssh/sshd/sshinterface.py @@ -218,7 +218,7 @@ def bridge(self): self.chan_cli.send(recv_message) continue else: - if b'rz\r**\x18B00000000000000\r\x8a\x11' in recv_message or b'rz waiting to receive.**\x18B0100000023be50\r\x8a\x11' in recv_message : + if b'rz\r**\x18B00000000000000\r\x8a' in recv_message or b'rz waiting to receive.**\x18B0100000023be50\r\x8a' in recv_message: self.zmodem = True # logger.info("zmodem start") self.chan_cli.send(recv_message) diff --git a/apps/webssh/websocket_layer.py b/apps/webssh/websocket_layer.py index f08add2..f346508 100644 --- a/apps/webssh/websocket_layer.py +++ b/apps/webssh/websocket_layer.py @@ -232,24 +232,40 @@ def disconnect(self, close_code): TerminalSession.objects.filter(name=self.channel_name, group=self.group).delete() def receive(self, text_data=None, bytes_data=None): - data = json.loads(text_data) - if type(data) == dict: - if data['data'] and '\r' in data['data']: - self.check_login() - status = data['status'] - if status == 0: - data = data['data'] - if self.lock: - self.message['status'] = 3 - self.message['message'] = '当前会话已被管理员锁定' - message = json.dumps(self.message) - self.send(message) - else: - self.ssh.shell(data) - else: - cols = data['cols'] - rows = data['rows'] - self.ssh.resize_pty(cols=cols, rows=rows) + if text_data is None: + self.ssh.django_bytes_to_ssh(bytes_data) + else: + if not self.ssh.zmodem: # zmodem 模式下不接受 xterm.js 输入的 string 数据 + data = json.loads(text_data) + if type(data) == dict: + if data['data'] and '\r' in data['data']: + self.check_login() + status = data['status'] + if status == 0: + data = data['data'] + if self.lock: + self.message['status'] = 3 + self.message['message'] = '当前会话已被管理员锁定' + message = json.dumps(self.message) + self.send(message) + else: + self.ssh.shell(data) + elif status == 1: + cols = data['cols'] + rows = data['rows'] + self.ssh.resize_pty(cols=cols, rows=rows) + elif status == 2: + delay = round(time.time() - self.ssh.start_time, 6) + self.ssh.res_asciinema.append(json.dumps([delay, 'o', data['data']])) + else: # 兼容 rz 完成后客户端发送过来的信息 + data = json.loads(text_data) + if type(data) == dict: + if data['data'] and '\r' in data['data']: + self.check_login() + status = data['status'] + if status == 2: + delay = round(time.time() - self.ssh.start_time, 6) + self.ssh.res_asciinema.append(json.dumps([delay, 'o', data['data']])) def check_login(self): lasttime = int(self.scope['session']['lasttime']) diff --git a/apps/webtelnet/telnet.py b/apps/webtelnet/telnet.py index fede89d..72fa234 100644 --- a/apps/webtelnet/telnet.py +++ b/apps/webtelnet/telnet.py @@ -15,7 +15,6 @@ logger = logging.getLogger(__name__) from django.utils.encoding import smart_str, force_str - try: terminal_exipry_time = settings.CUSTOM_TERMINAL_EXIPRY_TIME except Exception: @@ -43,9 +42,7 @@ def __init__(self, websocker, message): tmp_date2 + '_' + gen_rand_char(16) + '.txt' self.last_save_time = self.start_time self.res_asciinema = [] - self._buffer = b'' - self.tn = telnetlib.Telnet() def connect(self, host, user, password, port=23, timeout=60 * 30, wait_time=3, user_pre=b"ogin:", password_pre=b"assword:"): @@ -123,12 +120,6 @@ def django_to_telnet(self, data): if self.cmd_tmp.strip() != '': self.cmd_tmp += data self.cmd += self.cmd_tmp - - # print('-----------------------------------') - # print(self.cmd_tmp) - # print(self.cmd_tmp.encode()) - # print('-----------------------------------') - self.cmd_tmp = '' elif data.encode() == b'\x07': pass @@ -140,28 +131,29 @@ def django_to_telnet(self, data): else: self.cmd_tmp += data except Exception: - print(traceback.format_exc()) self.close() def websocket_to_django(self): try: while 1: - data = '' # read_very_eager 方法读取时会是无限循环,性能比较低 # data = self.tn.read_very_eager().decode('utf-8') # if not len(data): # continue # expect 使用正则匹配所有返回内容,还可以实现超时无返回内容断开连接 + if len(self._buffer) >= 1: + data = self._buffer[:4096] + self._buffer = self._buffer[4096:] + else: + x, y, z = self.tn.expect([br'[\s\S]+'], timeout=terminal_exipry_time) + self._buffer += z + data = self._buffer[:4096] # 一次最多截取4096个字符 + self._buffer = self._buffer[4096:] + if not len(data): + raise socket.timeout + try: - if len(self._buffer) >= 1: - data = self._buffer[:4096] - self._buffer = self._buffer[4096:] - else: - x, y, z = self.tn.expect([br'[\s\S]+'], timeout=terminal_exipry_time) - self._buffer += z - data = self._buffer[:4096] # 一次最多截取4096个字符 - self._buffer = self._buffer[4096:] data = data.decode('utf-8') except UnicodeDecodeError: # utf-8中文占3个字符,可能会被截断,需要拼接 try: @@ -192,11 +184,7 @@ def websocket_to_django(self): data += z data = data.decode('utf-8') except UnicodeDecodeError: - print(traceback.format_exc()) data = data.decode('utf-8', 'ignore') # 拼接2次后还是报错则证明结果是乱码,强制转换 - - if not len(data): - raise socket.timeout self.message['status'] = 0 self.message['message'] = data self.res += data @@ -208,7 +196,7 @@ def websocket_to_django(self): "type": "chat.message", "text": message, }) - + delay = round(time.time() - self.start_time, 6) self.res_asciinema.append(json.dumps([delay, 'o', data])) # 指定条结果或者指定秒数或者占用指定大小内存就保存一次 @@ -246,7 +234,6 @@ def websocket_to_django(self): }) self.close(send_message=False) except Exception: - print(traceback.format_exc()) self.close() def close(self, send_message=True): @@ -265,7 +252,7 @@ def close(self, send_message=True): self.websocker.close() self.tn.close() except Exception: - print(traceback.format_exc()) + pass def shell(self, data): self.django_to_telnet(data) diff --git a/apps/webtelnet/websocket_layer.py b/apps/webtelnet/websocket_layer.py index 79efc4b..db897f9 100644 --- a/apps/webtelnet/websocket_layer.py +++ b/apps/webtelnet/websocket_layer.py @@ -264,7 +264,7 @@ def chat_message(self, data): else: pass except Exception: - print(traceback.format_exc()) + pass def lock_message(self, data): if not self.lock: diff --git a/requirements.txt b/requirements.txt index ca79161..016317d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ selectors2==2.0.1 django-redis==4.10.0 pyguacamole==0.8 # ansible==2.8.5 -ansible==2.9.0 +ansible==2.9.2 daphne==2.3.0 # gunicorn==19.9.0 gunicorn==20.0.3 diff --git a/static/bootbox/5.4.0/bootbox.min.js b/static/bootbox/5.4.0/bootbox.min.js new file mode 100644 index 0000000..8b8a019 --- /dev/null +++ b/static/bootbox/5.4.0/bootbox.min.js @@ -0,0 +1 @@ +!function(t,e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e(require("jquery")):t.bootbox=e(t.jQuery)}(this,function e(p,u){"use strict";var r,n,i,l;Object.keys||(Object.keys=(r=Object.prototype.hasOwnProperty,n=!{toString:null}.propertyIsEnumerable("toString"),l=(i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"]).length,function(t){if("function"!=typeof t&&("object"!=typeof t||null===t))throw new TypeError("Object.keys called on non-object");var e,o,a=[];for(e in t)r.call(t,e)&&a.push(e);if(n)for(o=0;o