"""``/api/training/terminal*`` — the in-process OS simulator endpoints.""" import os import re from fastapi import APIRouter, Depends, Request from slowapi import Limiter from slowapi.util import get_remote_address from app.core.auth import get_current_user from app.sandbox.terminal import run_terminal_command from app.sandbox.workdir import get_or_create_workdir, safe_join from app.types import ( TerminalRequest, TerminalWriteRequest, TerminalListRequest, ) router = APIRouter() # Rate limiter: 30 terminal commands per minute per IP limiter = Limiter(key_func=get_remote_address) @router.post("/api/training/terminal") @limiter.limit("30/minute") async def post_terminal(request: Request, req: TerminalRequest, user: dict = Depends(get_current_user)): """Execute a terminal command in the challenge's sandbox for real.""" # IDOR FIX: Use authenticated user's team role from JWT team_role = user.get("team_role", "red")[:10] challenge_id = re.sub(r"[^a-f0-9\-]", "", req.challengeId)[:128] command = (req.command or "")[:500] # Limit command length if not challenge_id or not command: return {"stdout": "", "stderr": "❌ مدخلات غير صالحة", "exitCode": 1} return await run_terminal_command(team_role, challenge_id, command) @router.post("/api/training/terminal/write") @limiter.limit("20/minute") async def post_terminal_write(request: Request, req: TerminalWriteRequest, user: dict = Depends(get_current_user)): """Write/edit a file inside the sandbox workdir (path-traversal safe).""" # IDOR FIX: Use authenticated user's team role from JWT team_role = user.get("team_role", "red")[:10] challenge_id = re.sub(r"[^a-f0-9\-]", "", req.challengeId)[:128] if not challenge_id: return {"ok": False, "error": "❌ challengeId مفقود"} if not req.filename: return {"ok": False, "error": "❌ اسم الملف مفقود"} # Sanitize filename filename = re.sub(r"[^a-zA-Z0-9\.\-_\/]", "", req.filename)[:200] if len(req.content) > 200_000: return {"ok": False, "error": "❌ الملف كبير جداً (الحد 200KB)"} workdir, _row = await get_or_create_workdir(team_role, challenge_id) try: target = safe_join(workdir, filename) except ValueError as e: return {"ok": False, "error": "❌ مسار غير مسموح"} try: os.makedirs(os.path.dirname(target) or workdir, exist_ok=True) with open(target, "w", encoding="utf-8", newline="\n") as f: f.write(req.content) return {"ok": True, "path": os.path.relpath(target, workdir).replace("\\", "/")} except Exception as e: return {"ok": False, "error": "❌ تعذّر الحفظ"} @router.post("/api/training/terminal/list") @limiter.limit("60/minute") async def post_terminal_list(request: Request, req: TerminalListRequest, user: dict = Depends(get_current_user)): """List files currently in the sandbox workdir (for the file explorer).""" # IDOR FIX: Use authenticated user's team role from JWT team_role = user.get("team_role", "red")[:10] challenge_id = re.sub(r"[^a-f0-9\-]", "", req.challengeId)[:128] if not challenge_id: return {"files": []} workdir, _row = await get_or_create_workdir(team_role, challenge_id) files = [] try: for name in sorted(os.listdir(workdir)): if name.startswith("."): continue p = os.path.join(workdir, name) if os.path.isfile(p): try: with open(p, "r", encoding="utf-8", errors="replace") as f: content = f.read() if len(content) > 50_000: content = content[:50_000] + "\n... (truncated)" files.append({"name": name, "content": content}) except Exception: files.append({"name": name, "content": "(binary file)"}) except Exception: pass return {"files": files}