Building a Django WebShell with WebSockets, ASGI, and Paramiko
This guide walks through creating a WebShell that lets a React front‑end control a remote virtual machine via Django‑based WebSocket services, covering ASGI setup, custom WebSocket handling, integration with Paramiko for SSH, and a lightweight xterm.js terminal client.
Preface
In a recent project we needed a front‑end feature to operate a remote virtual machine, called WebShell. The stack is React + Django, and most backend implementations use django‑channels for WebSocket support.
Django itself does not support WebSocket natively, but since Django 3 it supports the ASGI protocol, allowing us to implement our own WebSocket service.
We chose the combination gunicorn + uvicorn + ASGI + WebSocket + Django 3.2 + Paramiko to build the WebShell.
Implementing the WebSocket service
Django’s project scaffold generates asgi.py and wsgi.py. For this case we use asgi.py to handle WebSocket connections, implementing the typical connect, send, receive, and disconnect callbacks.
Idea
# 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):
while True:
event = await receive()
if event['type'] == 'websocket.connect':
await send({'type': 'websocket.accept'})
if event['type'] == 'websocket.disconnect':
break
if event['type'] == 'websocket.receive':
if event['text'] == 'ping':
await send({'type': 'websocket.send', 'text': 'pong!'})Implementation
The core WebSocket class is shown below; it wraps the ASGI scope and provides methods such as accept, close, send, and receive.
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."""
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 messageCombining WebSocket with Paramiko
To bridge the front‑end and the remote host we create a WebShell class that couples the WebSocket instance with a Paramiko SSH channel.
import asyncio
import traceback
import paramiko
from webshell.ssh import Base, RemoteSSH
from webshell.connection import WebSocket
class WebShell:
"""Tie WebSocket and paramiko.Channel together for bidirectional data flow."""
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):
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()Front‑end
On the client side we use xterm.js to render the terminal and connect to the Django WebSocket endpoint.
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());
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>;
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
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.
