הראה קוד מקור ל terminal_commands

"""
Terminal commands: sandboxed command execution via Docker containers.
"""
import asyncio
import logging
import shlex
import shutil
from html import escape as html_escape
from typing import Tuple

from telegram import Update, ReplyKeyboardMarkup
from telegram.constants import ParseMode
from telegram.ext import (Application, CommandHandler, MessageHandler, ConversationHandler,
                          ContextTypes, filters)

logger = logging.getLogger(__name__)

# Conversation states
TERMINAL_ACTIVE = 1

# UI
TERMINAL_KEYBOARD = ReplyKeyboardMarkup([["🚪 יציאה מטרמינל"]], resize_keyboard=True)

DOCKER_IMAGE = "alpine:3.20"

async def _check_docker_available() -> bool:
    """Return True if docker CLI is available and usable."""
    if not shutil.which("docker"):
        return False
    # quick version check with small timeout
    try:
        proc = await asyncio.create_subprocess_shell(
            "docker version --format '{{.Server.Version}}'",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        try:
            await asyncio.wait_for(proc.communicate(), timeout=3)
            return proc.returncode == 0
        except asyncio.TimeoutError:
            proc.kill()
            return False
    except Exception:
        return False

[תיעוד] async def run_in_sandbox(command: str, timeout_sec: int = 10, max_output_chars: int = 3500) -> Tuple[int, str]: """Run a shell command inside a locked-down Docker container and return (rc, output).""" # Use a restricted container with no network, non-root, read-only FS, tmpfs for /tmp and /sandbox docker_cmd = ( "docker run --rm --network none --cpus=0.5 --memory=256m --pids-limit=128 " "--read-only --tmpfs /tmp:rw,size=64m --tmpfs /sandbox:rw,size=64m " "--workdir /sandbox --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges " f"{DOCKER_IMAGE} sh -lc {shlex.quote(command)}" ) proc = await asyncio.create_subprocess_shell( docker_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec) output = (stdout.decode() + stderr.decode()) truncated = False if len(output) > max_output_chars: output = output[:max_output_chars] truncated = True if truncated: output += "\n\n[פלט קוצר]" # Guard: mypy thinks returncode can be Optional[int]; after communicate it's int assert isinstance(proc.returncode, int) return proc.returncode, output except asyncio.TimeoutError: try: proc.kill() except Exception: pass return 124, "⏱️ פקודה חרגה ממגבלת הזמן"
[תיעוד] async def terminal_enter(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Enter terminal mode if Docker is available.""" if not await _check_docker_available(): await update.message.reply_text( "❌ מצב טרמינל לא זמין: Docker לא זמין בשרת", parse_mode=ParseMode.HTML ) return ConversationHandler.END await update.message.reply_text( "💻 נכנסת למצב טרמינל מוגבל\n\n" "מגבלות אבטחה ומשאבים:\n" "• ללא רשת (--network none)\n" "• זמן ריצה מקסימלי: 10 שניות\n" "• זיכרון: 256MB, CPU: 0.5 vCPU\n" "• מערכת קבצים לקריאה בלבד, עם /tmp ו-/sandbox זמניים\n" "• פלט מקסימלי: 3500 תווים (נחתך אוטומטית)\n\n" "הקלד פקודה להרצה, או לחץ '🚪 יציאה מטרמינל' כדי לצאת.", reply_markup=TERMINAL_KEYBOARD, parse_mode=ParseMode.HTML ) return TERMINAL_ACTIVE
[תיעוד] async def terminal_exit(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Exit terminal mode.""" await update.message.reply_text( "🔒 יצאת ממצב טרמינל.", reply_markup=ReplyKeyboardMarkup(context.application.bot_data.get('MAIN_KEYBOARD', [["🏠 תפריט ראשי"]]), resize_keyboard=True) ) return ConversationHandler.END
[תיעוד] async def terminal_run_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Run a command string inside the sandbox and send the output.""" cmd = (update.message.text or "").strip() if not cmd: return TERMINAL_ACTIVE # guard: prevent long inputs if len(cmd) > 500: await update.message.reply_text("❌ פקודה ארוכה מדי") return TERMINAL_ACTIVE await update.message.reply_text("🔄 מריץ...", reply_to_message_id=update.message.message_id) try: rc, output = await run_in_sandbox(cmd) header = f"✅ קוד יציאה: {rc}" if rc == 0 else f"⚠️ קוד יציאה: {rc}" # escape and wrap output safe_output = html_escape(output) if output else "(ללא פלט)" await update.message.reply_text( f"{header}\n\n<pre>{safe_output}</pre>", parse_mode=ParseMode.HTML, reply_markup=TERMINAL_KEYBOARD ) except Exception as e: logger.error(f"Terminal command failed: {e}") await update.message.reply_text("❌ שגיאה בהרצת הפקודה", reply_markup=TERMINAL_KEYBOARD) return TERMINAL_ACTIVE
[תיעוד] async def terminal_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """/terminal command to enter terminal mode.""" return await terminal_enter(update, context)
[תיעוד] def setup_terminal_handlers(application: Application): """Register terminal conversation handlers and commands (disabled on Render).""" # Disabled: no handlers or buttons registered when Docker is unavailable return