Initial commit.

This commit is contained in:
retoor 2025-10-10 01:17:47 +02:00
commit 9a0d120ba4
6 changed files with 371 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__

324
demo.py Executable file
View File

@ -0,0 +1,324 @@
#!/usr/bin/env python3
import asyncio
import shutil
import uuid
import os
import pty
import struct
import fcntl
import termios
import signal
import json
from pathlib import Path
from fastapi import FastAPI, WebSocket, HTTPException
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse
import uvicorn
app = FastAPI()
active_sessions = {}
@app.get("/")
async def root():
return HTMLResponse("""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Preparing Environment</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box;-ms-overflow-style: none;
scrollbar-width: none;
}
*::-webkit-scrollbar {
display: none;
}
body {
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
height: 100vh;
font-family: Arial, sans-serif;
background: #1e1e1e;
color: #fff;
}
.container { text-align: center; }
.spinner {
border: 4px solid rgba(255,255,255,0.1);
border-top: 4px solid #fff;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<div class="spinner"></div>
<h1>Preparing Environment</h1>
<p>Please wait while we set up your workspace...</p>
</div>
<script>
fetch('/prepare').then(r => r.json()).then(data => {
window.location.href = '/terminal/' + data.session_id;
});
</script>
</body>
</html>
""")
@app.get("/prepare")
async def prepare():
session_id = str(uuid.uuid4())
system_directory = Path("system")
workspace_directory = Path(f"workspace/{session_id}")
if not system_directory.exists():
raise HTTPException(status_code=500, detail="System directory not found")
shutil.copytree(system_directory, workspace_directory)
active_sessions[session_id] = {"workspace": workspace_directory}
return {"session_id": session_id}
@app.get("/terminal/{session_id}")
async def terminal(session_id: str):
if session_id not in active_sessions:
return RedirectResponse(url="/")
return HTMLResponse(f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Terminal</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ overflow: hidden; }}
#terminal {{ width: 100vw; height: 100vh; }}
</style>
</head>
<body>
<div id="terminal"></div>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<script>
const term = new Terminal({{
cursorBlink: true,
fontSize: 14,
theme: {{
background: '#1e1e1e',
foreground: '#f0f0f0'
}}
}});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
fitAddon.fit();
const ws = new WebSocket('ws://' + window.location.host + '/ws/{session_id}');
ws.onopen = () => {{
const dims = {{ cols: term.cols, rows: term.rows }};
ws.send(JSON.stringify({{ type: 'resize', data: dims }}));
}};
ws.onmessage = (event) => {{
term.write(event.data);
}};
ws.onclose = () => {{
window.location.href = window.location.href.replace('terminal', 'files');
term.writeln('\\r\\nThat went smooth huh, that\s sum Molodetz for ya!');
}};
term.onData((data) => {{
ws.send(JSON.stringify({{ type: 'input', data: data }}));
}});
term.onResize(({{ cols, rows }}) => {{
ws.send(JSON.stringify({{ type: 'resize', data: {{ cols, rows }} }}));
}});
window.addEventListener('resize', () => {{
fitAddon.fit();
}});
document.addEventListener('DOMContentLoaded', function() {{
const elements = document.querySelectorAll('.xterm .xterm-viewport');
elements.forEach(element => {{
element.style.setProperty('overflow-y', 'hidden', 'important');
}});
}});
</script>
</body>
</html>
""")
def set_winsize(fd, rows, cols):
winsize = struct.pack("HHHH", rows, cols, 0, 0)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
@app.websocket("/ws/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
if session_id not in active_sessions:
await websocket.close(code=1008)
return
await websocket.accept()
workspace_directory = active_sessions[session_id]["workspace"]
master_fd, slave_fd = pty.openpty()
event_loop = asyncio.get_event_loop()
process = await asyncio.create_subprocess_exec(
"./manage.py", "start",
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
cwd=str(workspace_directory),
start_new_session=True
)
os.close(slave_fd)
async def read_output():
try:
while True:
data = await event_loop.run_in_executor(None, os.read, master_fd, 1024)
if not data:
break
await websocket.send_text(data.decode('utf-8', errors='replace'))
except Exception:
await websocket.close()
async def handle_input():
try:
while True:
message = await websocket.receive_text()
parsed_message = json.loads(message)
if parsed_message['type'] == 'input':
await event_loop.run_in_executor(None, os.write, master_fd, parsed_message['data'].encode('utf-8'))
elif parsed_message['type'] == 'resize':
set_winsize(master_fd, parsed_message['data']['rows'], parsed_message['data']['cols'])
except Exception:
await websocket.close()
try:
await asyncio.gather(read_output(), handle_input(), return_exceptions=True)
except Exception as e:
print(e)
await websocket.close()
try:
os.close(master_fd)
except Exception:
pass
try:
process.kill()
except Exception:
pass
await websocket.close()
@app.get("/files/{session_id}/{path:path}")
async def browse_files(session_id: str, path: str = ""):
if session_id not in active_sessions:
raise HTTPException(status_code=404, detail="Session not found")
workspace_directory = active_sessions[session_id]["workspace"]
target_path = (workspace_directory / path).resolve()
if not str(target_path).startswith(str(workspace_directory.resolve())):
raise HTTPException(status_code=403, detail="Access denied")
if not target_path.exists():
raise HTTPException(status_code=404, detail="Path not found")
if target_path.is_file():
return FileResponse(target_path)
items = []
for item in sorted(target_path.iterdir()):
rel_path = item.absolute().relative_to(workspace_directory.absolute())
items.append({
"name": item.name,
"is_dir": item.is_dir(),
"path": str(rel_path)
})
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>File Browser</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; }}
h1 {{ margin-bottom: 20px; }}
.path {{ background: #fff; padding: 10px; margin-bottom: 20px; border-radius: 4px; border: 1px solid #ddd; }}
table {{ width: 100%; background: #fff; border-collapse: collapse; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
th {{ background: #333; color: #fff; }}
a {{ color: #0066cc; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
.dir {{ font-weight: bold; }}
.file {{ color: #333; }}
</style>
</head>
<body>
<h1>File Browser</h1>
<div class="path">Path: /{path or ''}</div>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>
"""
if path:
parent = str(Path(path).parent) if Path(path).parent != Path('.') else ''
html += f'<tr><td><a href="/files/{session_id}/{parent}">..</a></td><td>Directory</td></tr>'
for item in items:
name_display = item['name'] + ('/' if item['is_dir'] else '')
item_type = 'Directory' if item['is_dir'] else 'File'
css_class = 'dir' if item['is_dir'] else 'file'
html += f'<tr><td><a class="{css_class}" href="/files/{session_id}/{item["path"]}">{name_display}</a></td><td>{item_type}</td></tr>'
html += """
</tbody>
</table>
</body>
</html>
"""
return HTMLResponse(html)
if __name__ == "__main__":
signal.signal(signal.SIGINT, lambda s, f: os._exit(0))
import argparse
parser = argparse.ArgumentParser(description="Run the application server.")
parser.add_argument("--port", type=int, default=8240, help="Port number to run the server on.")
arguments = parser.parse_args()
uvicorn.run(app, host="0.0.0.0", port=arguments.port)

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
websockets
python-multipart
aiofiles

16
system/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM alpine:latest
# Install bash
RUN apk add --no-cache bash
# Create non-root user
RUN adduser -D -s /bin/bash myuser
# Switch to non-root user
USER myuser
# Set working directory
WORKDIR /home/myuser
# Default command
CMD ["/bin/bash"]

21
system/docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
services:
secure-bash:
build: .
stdin_open: true
tty: true
read_only: true
tmpfs:
- /tmp
- /home/myuser
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
cpus: '0.5'
memory: 128M
reservations:
cpus: '0.25'
memory: 64M

4
system/manage.py Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env python3
import os
os.system("docker compose run secure-bash /bin/bash")
exit(0)