堡壘機WebSSH進階之實時監控和強制下線


這個功能我可以不用,但你不能沒有

前幾篇文章實現了對物理機虛擬機以及Kubernetes中Pod的WebSSH操作,可以方便的在web端對系統進行管理,同時也支持對所有操作進行全程錄像,以方便后續的查看與審計

這一篇文章接着實現一個看起來很炫酷,但實際上你可能不會經常使用,又必須要存在的功能:實時監控用戶操作,在必要的時候將用戶踢下線

實時查看操作

django通過channels實現websocket中有一個非常重要的概念叫layer,layer可以將多個channel合並成一個group,我們可以發送消息給group,那么group里的每個channel都能收到

關於Channel我有寫過兩篇文章結合聊天室web端實現tail-f功能這兩個案例來詳細介紹,兩篇文章是『Django使用Channels實現WebSocket--上篇』『Django使用Channels實現WebSocket--下篇』,對上邊提到的名詞一臉懵逼的朋友可以通過這兩篇文章來學習

之前的WebSSH僅是單連接,只需要客戶端和服務器建立長連接,然后處理指令就ok了,我們並沒有啟用channel的layer,實際上也可以看作是單channel,但要實現監控的功能,就需要將操作者和管理員(監控者)的多個channel合並在一起組成一個group,這樣操作者的所有操作都可以發送給這個group,同處於這個group內監控者就能實時收到消息了,大概流程變化如下圖所示

接下來看下具體實現,以下所有代碼均是在這篇文章的基礎上進行說明講解:『Django實現WebSSH操作物理機或虛擬機』

首先我們要啟用layer,這個需要在settings.py中添加如下配置

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('ops-coffee.cn', 6379)],
        },
    },
}

然后將處理WebSSH連接名為SSHConsumer的Consumer做改造,以使其支持layer,代碼如下

class SSHConsumer(WebsocketConsumer):
    def connect(self):
        # 格式化參數
        ssh_connect_args = args(self.scope)

        # 新建錄像記錄
        self.host = Host.objects.get(host=ssh_connect_args.get('host'))
        self.group_name = '%s-%s-%d' % (
            ssh_connect_args.get('host'), ssh_connect_args.get('username'), time.time())

        self.therecord = Record.objects.create(
            host=self.host,
            user=self.scope['user'],
            group=self.group_name,
            channel=self.channel_name,
            cols=ssh_connect_args.get('cols'),
            rows=ssh_connect_args.get('rows'),
            is_connecting=True
        )

        async_to_sync(self.channel_layer.group_add)(
            self.group_name,
            self.channel_name
        )

        self.accept()

        # WebSocket連接成功后,連接ssh
        self.ssh = SSHBridge(self.therecord, websocket=self)
        self.ssh.connect(**ssh_connect_args)

    def disconnect(self, close_code):
        # 將連接狀態置為False
        self.therecord.is_connecting = False
        self.therecord.save()

        async_to_sync(self.channel_layer.group_discard)(
            self.group_name,
            self.channel_name
        )

        self.ssh.close()

    def receive(self, text_data=None):
        text_data = json.loads(text_data)

        if text_data.get('flag') == 'resize':
            self.ssh.resize_pty(cols=text_data['cols'], rows=text_data['rows'])
        else:
            self.ssh.shell(data=text_data.get('data', ''))

    def ssh_message(self, event):
        self.send(text_data=json.dumps(
            event['message']
        ))

在connect連接建立時新建一條記錄,存儲主機、用戶、group_namechannel_name以及初始窗口的colsrows信息,同時標記is_connecting為True,這里的group_name命名與文章『堡壘機的核心武器:WebSSH錄像實現』中我們定義的錄像文件名規則一致,另外將這篇文章中新建錄像記錄的操作從SSHBridge.record中給轉到了連接建立的connect中來,更合理也更方便

在disconnect連接關閉時,將is_connecting標記為False,這樣我們在前端頁面上就可以根據這個標記來判斷WebSSH是否正在連接,如果連接則展示監控強制結束按鈕,否則展示播放命令提取按鈕

同時添加個ssh_message方法,用來接收發送到組的數據

到這里,我們已經將WebSSH改造成了支持layer的模式,那么接下來就是要在用戶點擊監控的時候將用戶與服務端建立的連接channel加入到上述group中

新建一個名為MonitorConsumer的consumer,主要用來處理監控連接

class MonitorConsumer(WebsocketConsumer):
    def connect(self):
        pk = self.scope['url_route']['kwargs'].get('id')
        self.group_name = Record.objects.get(id=pk).group

        async_to_sync(self.channel_layer.group_add)(
            self.group_name,
            self.channel_name
        )

        self.accept()

        # 判斷用戶已經結束了這個webssh連接時就關閉監控
        self.connecting = Record.objects.get(id=pk).is_connecting
        if not self.connecting:
            self.close()

    def disconnect(self, close_code):
        async_to_sync(self.channel_layer.group_discard)(
            self.group_name,
            self.channel_name
        )

        self.close()

    def receive(self, text_data=None):
        pass

    def ssh_message(self, event):
        self.send(text_data=json.dumps(
            event['message']
        ))

MonitorConsumer與SSHConsumer有兩個地方不一樣,其一是SSHConsumer中我們直接新生成了個group_name,而MonitorConsumer中需要在connect時獲取到要監控的ID,然后通過ID拿到group_name,將monitor連接加入到這個group,其二是監控只能看,不能操作,所以也不需要前端發送數據的term.on和Consumer的receive處理數據

最后需要修改SSHBridge方法中發送給websocket的指令,從self.websocket.send改為發送到group的模式,如下

async_to_sync(self.websocket.channel_layer.group_send)(
    self.group_name,
    {
        'type': 'ssh.message',
        'message': message
    }
)

至此,監控功能就算完成了,什么?前端頁面怎么弄?參考下之前的WebSSH界面,幾乎可以完全復制

踢用戶下線

踢用戶下線就比較簡單了,邏輯是點擊頁面上的強制結束按鈕,給后端view發送個請求帶上這條記錄的ID,view拿到ID后,通過ID找到group_name,然后向group發送disconnect消息,這個group里的所有channel在收到disconnect消息后就會斷開連接了

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

async_to_sync(get_channel_layer().group_send)(
    Record.objects.get(id=pk).group,
    {'type': 'disconnect'}
)

演示與說明

所有實現環環相扣,單看這一篇文章可能雲里霧里,不知所雲,但你如果能把這個系列文章都給看下的話,我想實現個簡單的堡壘機應該沒有問題吧,更重要的是你會對websocket以及django中的Channels有着更加深刻的理解和運用

原本只是想給我最牛X的Alodi系統添加個WebSSH,可以方便開發或測試在項目運行過程中出現問題時提供一個快速的調試途徑,沒想到這竟然寫了一個系列,實現了這么多有趣好玩兒的功能

最后想起了這句成語:有意栽花花不開,無心插柳柳成蔭,真是奇妙~


掃碼關注公眾號查看更多實用文章

相關文章推薦閱讀:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM