堡壘機的核心武器:WebSSH錄影實現
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操作時間過長時,會導致記憶體的持續增加
兩種方案一種是對磁碟的消耗另一種是對記憶體的消耗,各有利弊,當然你也可以考慮批量寫入,例如每分鐘寫一次檔案,一分鐘之內的儲存在記憶體中,平衡記憶體和磁碟的消耗,期待你的實現
相關文章推薦閱讀:
- Django實現WebSSH操作Kubernetes Pod
- Django實現WebSSH操作物理機或虛擬機器