From 9a0d120ba44bc22e0ce8f436f4341907bf6025f2 Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 10 Oct 2025 01:17:47 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 1 + demo.py | 324 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 + system/Dockerfile | 16 ++ system/docker-compose.yml | 21 +++ system/manage.py | 4 + 6 files changed, 371 insertions(+) create mode 100644 .gitignore create mode 100755 demo.py create mode 100644 requirements.txt create mode 100644 system/Dockerfile create mode 100644 system/docker-compose.yml create mode 100755 system/manage.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/demo.py b/demo.py new file mode 100755 index 0000000..a426ea6 --- /dev/null +++ b/demo.py @@ -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(""" + + + + + Preparing Environment + + + +
+
+

Preparing Environment

+

Please wait while we set up your workspace...

+
+ + + +""") + +@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""" + + + + + Terminal + + + + +
+ + + + + +""") + +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""" + + + + + File Browser + + + +

File Browser

+
Path: /{path or ''}
+ + + + + + + + +""" + + if path: + parent = str(Path(path).parent) if Path(path).parent != Path('.') else '' + html += f'' + + 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'' + + html += """ + +
NameType
..Directory
{name_display}{item_type}
+ + +""" + + 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) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cdb7f49 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +websockets +python-multipart +aiofiles \ No newline at end of file diff --git a/system/Dockerfile b/system/Dockerfile new file mode 100644 index 0000000..e09755f --- /dev/null +++ b/system/Dockerfile @@ -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"] \ No newline at end of file diff --git a/system/docker-compose.yml b/system/docker-compose.yml new file mode 100644 index 0000000..afd4170 --- /dev/null +++ b/system/docker-compose.yml @@ -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 diff --git a/system/manage.py b/system/manage.py new file mode 100755 index 0000000..b03a361 --- /dev/null +++ b/system/manage.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +import os +os.system("docker compose run secure-bash /bin/bash") +exit(0)