Skip to content

Commit

Permalink
webssh add zmodem(sz,rz) support
Browse files Browse the repository at this point in the history
  • Loading branch information
leffss committed Apr 10, 2020
1 parent a1862ba commit cafe7af
Show file tree
Hide file tree
Showing 14 changed files with 657 additions and 293 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 配置**

Expand Down Expand Up @@ -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 逻辑:允许指定组;

Expand Down Expand Up @@ -370,7 +378,7 @@ linux 平台下使用 celery 任务保存终端会话日志与录像(windows
尝试加入 celery 实现异步任务;

### ver1.1.0
新增 ssh 和 telnet 协议连接远程主机
新增 ssh 和 telnet(明文传输的古老协议,不推荐使用)连接远程主机

### ver1.0.0
初始版本
Expand Down Expand Up @@ -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
```


更多新功能不断探索发现中.
180 changes: 95 additions & 85 deletions apps/webssh/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'] = '由于长时间没有操作或者没有数据返回,连接已断开!'
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion apps/webssh/sshd/sshinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
52 changes: 34 additions & 18 deletions apps/webssh/websocket_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
Loading

0 comments on commit cafe7af

Please sign in to comment.