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.
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
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.