WebSSH终端录像的实现终于来了

前边写了两篇文章『Asciinema:你的所有操作都将被录制』『Asciinema文章勘误及Web端使用介绍』深入介绍了终端录制工具Asciinema,我们已经可以实现在终端下对操作过程的录制,那么在WebSSH中的操作该如何记录并提供后续的回放审计呢?

一种方式是『Asciinema:你的所有操作都将被录制』文章最后介绍的自动录制审计日志的方法,在主机上添加个脚本,每次连接自动进行录制,但这样不仅要在每台远程主机添加脚本,会很繁琐,而且录制的脚本文件都是放在远程主机上的,后续播放也很麻烦

那该如何更好处理呢?下文介绍一种优雅的方式来实现,核心思想是不通过录制命令进行录制,而在Webssh交互执行的过程中直接生成可播放的录像文件

设计思路

通过上边两篇文章的阅读,我们已经知道了Asciinema录像文件主要由两部分组成:header头和IO流数据

header头位于文件的第一行,定义了这个录像的版本、宽高、开始时间、环境变量等参数,我们可以在websocket连接创建时将这些参数按照需要的格式写入到文件

header头数据如下,只有开头一行,是一个字典形式

{"version": 2, "width": 213, "height": 55, "timestamp": 1574155029.1815443, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}, "title": "ops-coffee"}

整个录像文件除了第一行的header头部分,剩下的就都是输入输出的IO流数据,从websocket连接建立开始,随着操作的进行,IO流数据是不断增加的,直到整个websocket长连接的结束,那就需要在整个WebSSH交互的过程中不断的往录像文件追加输入输出的内容

IO流数据如下,每一行一条,列表形式,分别表示操作时间,输入或输出(这里我们为了方便就写固定字符串输出),IO数据

[0.2341010570526123, "o", "Last login: Tue Nov 19 17:11:30 2019 from 192.168.105.91\r\r\n"]

似乎很完美,按照上边的思路录像文件就应该没有问题了,但还有一些细节需要处理

首先是需要历史连接列表,在这个列表里可以看到什么时间,哪个用户连接了哪台主机,当然也需要提供回放功能,新建一张表来记录这些信息

class Record(models.Model):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') host = models.ForeignKey(Host, on_delete=models.CASCADE, verbose_name='主机')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') filename = models.CharField(max_length=128, verbose_name='录像文件名称') def __str__(self):
return self.host

其次还需要考虑的一个问题是header和后续IO数据流要写入同一个文件,这就需要在整个websocket的连接过程中有一个固定的文件名可被读取,这里我使用了主机+用户+当前时间作为文件名,同一用户在同一时间不能多次连接同一主机,这样可保证文件名不重复,同时避免操作写入错误的录像文件,文件名在websocket建立时初始化

def __init__(self, host, user, websocket):
self.host = host
self.user = user self.time = time.time()
self.filename = '%s.%s.%d.cast' % (host, user, self.time)

IO流数据会持续不断的写入文件,这里以一个独立的方法来处理写入

def record(self, type, data):
RECORD_DIR = settings.BASE_DIR + '/static/record/'
if not os.path.isdir(RECORD_DIR):
os.makedirs(RECORD_DIR) if type == 'header':
Record.objects.create(
host=Host.objects.get(id=self.host),
user=self.user,
filename=self.filename
) with open(RECORD_DIR + self.filename, 'w') as f:
f.write(json.dumps(data) + '\n')
else:
iodata = [time.time() - self.time, 'o', data]
with open(RECORD_DIR + self.filename, 'a', buffering=1) as f:
f.write((json.dumps(iodata) + '\n'))

record接收两个参数type和data,type标识本次写入的是header头还是IO流,data则是具体的数据

header只需要执行一次写入,所以将其放在ssh的connect方法中,只在ssh连接建立时执行一次,在执行header写入时同时往数据库插入新的历史记录数据

调用record方法写入header

def connect(self, host, port, username, authtype, password=None, pkey=None,
term='xterm-256color', cols=80, rows=24):
... # 构建录像文件header
self.record('header', {
"version": 2,
"width": cols,
"height": rows,
"timestamp": self.time,
"env": {
"SHELL": "/bin/bash",
"TERM": term
},
"title": "ops-coffee"
})

IO流数据则需要与返回给前端的数据保持一致,这样就能保证前端显示什么录像就播放什么了,所以所有需要返回前端数据的地方都同时写入录像文件即可

调用record方法写入io流数据

def connect(self, host, port, username, authtype, password=None, pkey=None,
term='xterm-256color', cols=80, rows=24):
... # 连接建立一次,之后交互数据不会再进入该方法
for i in range(2):
recv = self.ssh_channel.recv(65535).decode('utf-8', 'ignore')
message = json.dumps({'flag': 'success', 'message': recv})
self.websocket.send(message) self.record('iodata', recv) ... def _ssh_to_ws(self):
try:
with self.lock:
while not self.ssh_channel.exit_status_ready():
data = self.ssh_channel.recv(1024).decode('utf-8', 'ignore')
if len(data) != 0:
message = {'flag': 'success', 'message': data}
self.websocket.send(json.dumps(message)) self.record('iodata', data)
else:
break
except Exception as e:
message = {'flag': 'error', 'message': str(e)}
self.websocket.send(json.dumps(message))
self.record('iodata', str(e)) self.close()

由于命令执行与返回都是多线程的操作,这就会导致在写入文件时出现文件乱序影响播放的问题,典型的操作有vim、top等,通过加锁self.lock可以顺利解决

最后历史记录页面,当用户点击播放按钮时,调用js弹出播放窗口

<div class="modal fade" id="modalForm">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-body" id="play">
</div>
</div>
</div>
</div> // 播放录像
function play(host,user,time,file) {
$('#play').html(
'<asciinema-player id="play" title="WebSSH Record" author="ops-coffee.cn" author-url="https://ops-coffee.cn" author-img-url="/static/img/logo.png" src="/static/record/'+file+'" speed="3" '+
'idle-time-limit="2" poster="data:text/plain,\x1b[1;32m'+time+
'\x1b[1;0m用户\x1b[1;32m'+user+
'\x1b[1;0m连接主机\x1b[1;32m'+host+
'\x1b[1;0m的录像记录"></asciinema-player>'
) $('#modalForm').modal('show');
}

asciinema-player标签的详细参数介绍可以看这篇文章『Asciinema文章勘误及Web端使用介绍』

演示与总结

在写入文件的方案中,考虑了实时写入和一次性写入,实时写入就像上边这样,所有的操作都会实时写入录像文件,好处是录像不丢失,且能在操作的过程中进行实时的播放,缺点也很明显,就是会频繁的写文件,造成IO开销

一次性写入可以在用户操作的过程中将录像数据写入内存,在websocket关闭时一次性异步写入到文件中,这种方案在最终写入文件时可能因为种种原因而失败,从而导致录像丢失,还有个缺点是当你WebSSH操作时间过长时,会导致内存的持续增加

两种方案一种是对磁盘的消耗另一种是对内存的消耗,各有利弊,当然你也可以考虑批量写入,例如每分钟写一次文件,一分钟之内的保存在内存中,平衡内存和磁盘的消耗,期待你的实现


相关文章推荐阅读:

堡垒机的核心武器:WebSSH录像实现的更多相关文章

  1. 主机管理+堡垒机系统开发:webssh(十)

    一.安装shellinabox 1.安装依赖工具 yum install git openssl-devel pam-devel zlib-devel autoconf automake libtoo ...

  2. 堡垒机WebSSH进阶之实时监控和强制下线

    这个功能我可以不用,但你不能没有 前几篇文章实现了对物理机.虚拟机以及Kubernetes中Pod的WebSSH操作,可以方便的在web端对系统进行管理,同时也支持对所有操作进行全程录像,以方便后续的 ...

  3. IronFort---基于Django和Websocket的堡垒机

    WebSSH有很多,基于Django的Web服务也有很多,使用Paramiko在Python中进行SSH访问的就更多了.但是通过gevent将三者结合起来,实现通过浏览器访问的堡垒机就很少见了.本文将 ...

  4. 开发基于Django和Websocket的堡垒机

    WebSSH有很多,基于Django的Web服务也有很多,使用Paramiko在Python中进行SSH访问的就更多了.但是通过gevent将三者结合起来,实现通过浏览器访问的堡垒机就很少见了.本文将 ...

  5. 基于python的堡垒机

    一 堡垒机的架构 堡垒机的核心架构通常如下图所示: 二.堡垒机的一般执行流程 管理员为用户在服务器上创建账号(将公钥放置服务器,或者使用用户名密码) 用户登陆堡垒机,输入堡垒机用户名密码,显示当前用户 ...

  6. Python之路——堡垒机原理及其简单实现

    1 堡垒机基本概述 其从功能上讲,它综合了核心系统运维和安全审计管控两大主干功能,从技术实现上讲,通过切断终端计算机对网络和服务器资源的直接访问,而采用协议代理的方式,接管了终端计算机对网络和服务器的 ...

  7. 使用开源软件 jumpserver 搭造自己的堡垒机

    使用开源软件 jumpserver 搭造自己的堡垒机 开软地址:https://github.com/jumpserver/jumpserver 目前版本:1.5.2 测试的时候有少许BUG,但功能却 ...

  8. jumpserver堡垒机(2.4)部署

    jumpserver 2.4.0 部署 jumpserver 官网: https://www.jumpserver.org/ Jumpserver介绍 JumpServer 是全球首款完全开源的堡垒机 ...

  9. 堡垒机环境-jumpserver部署

    1:安装数据库 这里是提前安装,也可以不安装,在安装jumpserver主程序的时候,他会询问你是否安装 yum -y install ncurses-devel cmake echo 'export ...

随机推荐

  1. [POJ2356] Find a multiple 鸽巢原理

    Find a multiple Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 8776   Accepted: 3791   ...

  2. ‎Cocos2d-x 学习笔记(19) Control Invocation

    [Cocos2d-x 学习笔记 目录链接] 1. 简介 control为其子类提供了touch回调函数,当子类触发EventType相关事件时,会调用相关的回调函数. control对象接收到的事件类 ...

  3. mysql 数据分析如何实现日报、周报、月报和年报?

    以天为统计周期,是常见需求.周报.月报更是常见需求.长周期项目,甚至有年报需求.我已经掌握了mysql中按天统计,如何实现按年.按月.按周统计呢? 1.已掌握的技能:按天统计 实现以天为统计周期很简单 ...

  4. python里怎么查看数据类型

    python里怎么查看数据类型? python里可以通过type()函数来查看数据类型. Python 内置函数 Python 内置函数 Python type() 函数如果你只有第一个参数则返回对象 ...

  5. mp-vue实现小程序回顶操作踩坑,wx.pageScrollTo使用无效填坑

    本来项目都写的差不多了,测试测着侧着就冒出了新的想法,我因为做的是问卷,因此会有用户必答题未答完的可能存在,本来市场部给的需求就是做一个弹窗就好了,她说想要做出跳回到用户未答的第一道题,好吧,既然都这 ...

  6. 设计模式(九)Bridge模式

    Bridge模式就是将类的功能层次结构和类的实现层次结构连接起来. 类的功能层次结构就是根据实际非抽象类来说的,也就是父类具有基本功能,然后在子类中增加新功能.用于增加新功能. 类的实现层次结构就是根 ...

  7. LeetCode刷题笔记(6)按照索引计算int[] 数组中的和([Time Limit Exceeded]问题)

    Easy303 Easy633 package easy; public class e303 { private int[] sums; public e303(int[] nums) { sums ...

  8. c#通过libreOffice实现 office文件转pdf文件

    一.安装libreOffice 点击官网下载libreOffice 二.创建一个新的项目LibreOffice 创建一个新的项目,方便后面调用 添加下面代码 public class OfficeCo ...

  9. js如何展示上传的图片

    前言:本文章主要讲的是上传的图片如何展示在页面上. 一般来说,我们会先将本地图片上传到服务器,上传成功后,由后台返回图片的网络地址再在前端显示.但是,我今天讲的是不通过前面说的过程,而是直接使用js将 ...

  10. Azure上MySQL的离线备份:将备份拷贝到Azure Blob上

    公司在Azure的Iaas虚拟机上部署有好几台MySQL数据库,至于没有选择Azure Database for MySQL,是因为预算有限(钱不够啊!说多了也是泪,坑的还是DBA自己).选择了Iaa ...