File size: 4,080 Bytes
80a4a65
 
 
3c7b4e4
80a4a65
3c7b4e4
 
 
80a4a65
3c7b4e4
80a4a65
 
 
 
 
 
 
 
 
 
 
3c7b4e4
 
 
80a4a65
 
3c7b4e4
 
80a4a65
3c7b4e4
 
 
 
 
 
 
 
 
80a4a65
 
 
3c7b4e4
 
80a4a65
3c7b4e4
 
 
 
80a4a65
 
 
 
3c7b4e4
 
 
 
80a4a65
 
 
 
 
3c7b4e4
80a4a65
3c7b4e4
80a4a65
 
 
 
 
 
 
3c7b4e4
80a4a65
 
 
3c7b4e4
 
80a4a65
3c7b4e4
 
 
 
80a4a65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""``/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}