How to Add Real‑Time User Monitoring and Forced Disconnect to Django WebSSH

This article explains how to extend a Django Channels‑based WebSSH tool with real‑time operation monitoring, group‑based messaging, and a forced‑disconnect feature, detailing the necessary layer configuration, consumer modifications, and WebSocket message handling for both operators and observers.

Efficient Ops
Efficient Ops
Efficient Ops
How to Add Real‑Time User Monitoring and Forced Disconnect to Django WebSSH
这个功能我可以不用,但你不能没有。

Previous articles implemented WebSSH operations for physical machines, virtual machines, and Kubernetes Pods, supporting full‑session recording for later review and audit.

This article adds a seemingly flashy but essential feature: real‑time monitoring of user actions and the ability to kick users offline when needed.

Real‑time Operation View

Django Channels uses a concept called a layer to combine multiple channels into a group, allowing messages sent to the group to be received by every channel within it.

For background on Channels, see the earlier articles on implementing a chatroom and a web‑based tail‑f feature.

The original WebSSH used a single connection without a layer. To enable monitoring, we must merge the operator’s and the monitor’s channels into a group so that all operator actions are broadcast to the monitor in real time. The flow change is illustrated below:

Implementation steps (based on the previous article):

Enable the layer in settings.py:

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

Modify the existing SSHConsumer to support the layer. Key changes include creating a group_name during connection, adding the channel to the group, and recording connection details.

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()
        self.ssh = SSHBridge(self.therecord, websocket=self)
        self.ssh.connect(**ssh_connect_args)

    def disconnect(self, close_code):
        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']))

During connection, a record stores host, user, group_name, channel_name, and initial terminal size, marking is_connecting as True. The group_name follows the same naming rule as the recording file defined in the earlier WebSSH recording article.

When the connection closes, is_connecting is set to False, allowing the front‑end to toggle between monitoring/force‑stop buttons and playback/extract‑command buttons.

Next, create a MonitorConsumer to handle monitoring connections. It joins the same group as the operator and only receives messages without sending any.

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()
        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']))

The differences from SSHConsumer are: the group name is retrieved from the existing record, and the monitor does not send terminal data back to the server.

Finally, adjust SSHBridge to broadcast messages to the group instead of a single WebSocket:

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

With this change, all channels in the group receive the same output, enabling real‑time monitoring.

Kick User Offline

To force‑stop a user, the front‑end sends a request with the record ID to a view, which retrieves the corresponding group_name and sends a disconnect message to the group. All channels in that group then close.

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'}
)

Demo and Explanation

All components interlock; reading the entire series will give you a solid grasp of WebSockets and Django Channels, enabling you to build a simple bastion host with powerful monitoring capabilities.

The original goal was to add WebSSH to the Alodi system for quick debugging during development, but it evolved into a feature‑rich series exploring many interesting aspects of real‑time communication.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

real-time monitoringWebSocketDjangoUser ManagementChannels
Efficient Ops
Written by

Efficient Ops

This public account is maintained by Xiaotianguo and friends, regularly publishing widely-read original technical articles. We focus on operations transformation and accompany you throughout your operations career, growing together happily.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.