Backend Development 11 min read

Implementing a WebSocket Service with Django ASGI and Paramiko for a WebShell

This article explains how to build a WebShell by creating a Django ASGI WebSocket service using gunicorn, uvicorn, and Paramiko, and integrates it with a React front‑end component that leverages xterm.js for terminal interaction, providing full code examples and implementation details.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Implementing a WebSocket Service with Django ASGI and Paramiko for a WebShell

Introduction

In a recent project I needed to develop a front‑end feature for operating a remote virtual machine, called a WebShell. The stack consists of React and Django, and after researching I found that most back‑end implementations use django+channels for WebSocket services.

Django itself does not support WebSockets natively, but starting with Django 3 it supports the ASGI protocol, allowing custom WebSocket services.

Therefore I chose gunicorn + uvicorn + ASGI + WebSocket + Django 3.2 + paramiko to implement the WebShell.

Implementing the WebSocket Service

Django's project scaffold automatically generates asgi.py and wsgi.py . Most applications use wsgi.py together with Nginx for deployment, but here we mainly use asgi.py to handle WebSocket connections, implementing the typical connect , send , receive , and disconnect actions.

A useful reference is the article "How to Add WebSockets to a Django App without Extra Dependencies", although it is overly simplistic for our needs.

Idea

<code># asgi.py
import os
from django.core.asgi import get_asgi_application
from websocket_app.websocket import websocket_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'websocket_app.settings')

django_application = get_asgi_application()

async def application(scope, receive, send):
    if scope['type'] == 'http':
        await django_application(scope, receive, send)
    elif scope['type'] == 'websocket':
        await websocket_application(scope, receive, send)
    else:
        raise NotImplementedError(f"Unknown scope type {scope['type']}")

# websocket.py
async def websocket_application(scope, receive, send):
    pass
</code>

Implementation

The code above provides the basic idea.

The core implementation is shown below:

<code>class WebSocket:
    def __init__(self, scope, receive, send):
        self._scope = scope
        self._receive = receive
        self._send = send
        self._client_state = State.CONNECTING
        self._app_state = State.CONNECTING

    @property
    def headers(self):
        return Headers(self._scope)

    @property
    def scheme(self):
        return self._scope["scheme"]

    @property
    def path(self):
        return self._scope["path"]

    @property
    def query_params(self):
        return QueryParams(self._scope["query_string"]).decode()

    @property
    def query_string(self) -> str:
        return self._scope["query_string"]

    @property
    def scope(self):
        return self._scope

    async def accept(self, subprotocol: str = None):
        """Accept connection.
        :param subprotocol: The subprotocol the server wishes to accept.
        :type subprotocol: str, optional
        """
        if self._client_state == State.CONNECTING:
            await self.receive()
        await self.send({"type": SendEvent.ACCEPT, "subprotocol": subprotocol})

    async def close(self, code: int = 1000):
        await self.send({"type": SendEvent.CLOSE, "code": code})

    async def send(self, message: t.Mapping):
        if self._app_state == State.DISCONNECTED:
            raise RuntimeError("WebSocket is disconnected.")
        if self._app_state == State.CONNECTING:
            assert message["type"] in {SendEvent.ACCEPT, SendEvent.CLOSE}, (
                f'Could not write event "{message["type"]}" into socket in connecting state.'
            )
            if message["type"] == SendEvent.CLOSE:
                self._app_state = State.DISCONNECTED
            else:
                self._app_state = State.CONNECTED
        elif self._app_state == State.CONNECTED:
            assert message["type"] in {SendEvent.SEND, SendEvent.CLOSE}, (
                f'Connected socket can send "{SendEvent.SEND}" and "{SendEvent.CLOSE}" events, not "{message["type"]}"'
            )
            if message["type"] == SendEvent.CLOSE:
                self._app_state = State.DISCONNECTED
        await self._send(message)

    async def receive(self):
        if self._client_state == State.DISCONNECTED:
            raise RuntimeError("WebSocket is disconnected.")
        message = await self._receive()
        if self._client_state == State.CONNECTING:
            assert message["type"] == ReceiveEvent.CONNECT, (
                f'WebSocket is in connecting state but received "{message["type"]}" event'
            )
            self._client_state = State.CONNECTED
        elif self._client_state == State.CONNECTED:
            assert message["type"] in {ReceiveEvent.RECEIVE, ReceiveEvent.DISCONNECT}, (
                f'WebSocket is connected but received invalid event "{message["type"]}".'
            )
            if message["type"] == ReceiveEvent.DISCONNECT:
                self._client_state = State.DISCONNECTED
        return message
</code>

Patchwork Integration

To efficiently combine the WebSocket class with paramiko , the following WebShell class bridges the front‑end and the remote SSH channel, handling bidirectional data flow.

<code>import asyncio
import traceback
import paramiko
from webshell.ssh import Base, RemoteSSH
from webshell.connection import WebSocket

class WebShell:
    """Combine WebSocket and paramiko.Channel to enable data exchange"""

    def __init__(self, ws_session: WebSocket, ssh_session: paramiko.SSHClient = None, chanel_session: paramiko.Channel = None):
        self.ws_session = ws_session
        self.ssh_session = ssh_session
        self.chanel_session = chanel_session

    def init_ssh(self, host=None, port=22, user="admin", passwd="admin@123"):
        self.ssh_session, self.chanel_session = RemoteSSH(host, port, user, passwd).session()

    def set_ssh(self, ssh_session, chanel_session):
        self.ssh_session = ssh_session
        self.chanel_session = chanel_session

    async def ready(self):
        await self.ws_session.accept()

    async def welcome(self):
        # Show Linux welcome messages
        for i in range(2):
            if self.chanel_session.send_ready():
                message = self.chanel_session.recv(2048).decode('utf-8')
                if not message:
                    return
                await self.ws_session.send_text(message)

    async def web_to_ssh(self):
        while True:
            if not self.chanel_session.active or not self.ws_session.status:
                return
            await asyncio.sleep(0.01)
            shell = await self.ws_session.receive_text()
            if self.chanel_session.active and self.chanel_session.send_ready():
                self.chanel_session.send(bytes(shell, 'utf-8'))

    async def ssh_to_web(self):
        while True:
            if not self.chanel_session.active:
                await self.ws_session.send_text('ssh closed')
                return
            if not self.ws_session.status:
                return
            await asyncio.sleep(0.01)
            if self.chanel_session.recv_ready():
                message = self.chanel_session.recv(2048).decode('utf-8')
                if not len(message):
                    continue
                await self.ws_session.send_text(message)

    async def run(self):
        if not self.ssh_session:
            raise Exception("ssh not init!")
        await self.ready()
        await asyncio.gather(self.web_to_ssh(), self.ssh_to_web())

    def clear(self):
        try:
            self.ws_session.close()
        except Exception:
            traceback.print_stack()
        try:
            self.ssh_session.close()
        except Exception:
            traceback.print_stack()
</code>

Frontend Component

The front‑end uses xterm.js for terminal emulation. The following React component creates a terminal instance, connects to the WebSocket endpoint, and forwards resize events.

<code>export class Term extends React.Component {
    private terminal!: HTMLDivElement;
    private fitAddon = new FitAddon();

    componentDidMount() {
        const xterm = new Terminal();
        xterm.loadAddon(this.fitAddon);
        xterm.loadAddon(new WebLinksAddon());
        // using wss for https
        const socket = new WebSocket("ws://localhost:8000/webshell/");
        socket.onopen = (event) => {
            xterm.loadAddon(new AttachAddon(socket));
            this.fitAddon.fit();
            xterm.focus();
        };
        xterm.open(this.terminal);
        xterm.onResize(({ cols, rows }) => {
            socket.send("<RESIZE>" + cols + "," + rows);
        });
        window.addEventListener('resize', this.onResize);
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.onResize);
    }

    onResize = () => {
        this.fitAddon.fit();
    }

    render() {
        return <div className="Terminal" ref={(ref) => this.terminal = ref as HTMLDivElement}></div>;
    }
}
</code>

Original article: https://www.cnblogs.com/lgjbky/p/15186188.html

frontendreactWebSocketDjangoParamikoWebShellASGI
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login 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.