This commit is contained in:
Morgan 'ARR\!' Allen 2026-05-08 22:00:50 -07:00
commit acae46d111
3 changed files with 133 additions and 0 deletions

11
pyproject.toml Normal file
View file

@ -0,0 +1,11 @@
[project]
name = 'django-webshell'
description = 'Django Channels based SSH client'
version = '0.0.0'
dependencies = [
'asyncssh>=2',
'channels>=4',
'channels_redis>=4',
'django>=4.2',
]

0
webshell/__init__.py Normal file
View file

122
webshell/consumers.py Normal file
View file

@ -0,0 +1,122 @@
import asyncio
import asyncssh
from asgiref.sync import async_to_sync
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from channels.consumer import AsyncConsumer
from channels.layers import get_channel_layer
class WebShellSocketConsumer(AsyncJsonWebsocketConsumer):
def __init__(self):
super().__init__()
self.conn = None
async def connect(self):
super().connect()
if self.scope['session'].session_key is None:
await self.scope['session'].acreate()
await self.scope['session'].asave()
await self.accept()
print('accepted connection as', self.scope['session'].session_key)
async def receive_json(self, data):
data['owner'] = self.scope['session'].session_key
await self.channel_layer.group_add(data['owner'], self.channel_name)
await self.channel_layer.send('terminal', data)
async def notify(self, event):
await self.send_json({
'type': 'stdout',
'stdout': event['data']
})
class WebShellClient(asyncssh.SSHClient):
pass
class WebShellClientSession(asyncssh.SSHClientSession):
def data_received(self, data, datatype):
channel = get_channel_layer()
asyncio.ensure_future(channel.group_send(self.channel_name, {
'type': 'notify',
'data': data,
}))
def eof_received(self):
print('ssh connection done')
class WebShellWorker(AsyncConsumer):
def __init__(self):
self.sessions = {}
async def ssh(self, event):
host = event['host']
user = event['username']
pw = event['password']
if not host:
await self.send_json({
'error': 'no IP provided'
})
return
if not user:
await self.send_json({
'error': 'no username provided'
})
return
if not pw:
await self.send_json({
'error': 'no password provided'
})
return
# TODO
# Track these on Consumer
if not event['owner'] in self.sessions:
print('creating new session')
conn, client = await asyncssh.create_connection(
WebShellClient,
host,
username=user,
password=pw,
known_hosts=None,
options=asyncssh.SSHClientConnectionOptions(
#server_host_key_algs='ssh-rsa'
)
)
chan, session = await conn.create_session(
WebShellClientSession,
env={
'channel_name': event['owner'],
},
term_type='xterm'
)
session.channel_name = event['owner']
session.worker = self
self.sessions[event['owner']] = {
'connection': conn,
'client': client,
'channel': chan,
'session': session,
}
async def key(self, event):
self.sessions[event['owner']]['channel'].write(event['key'])