#!/usr/bin/env python3 """ Linux 命令沙盒模拟器 - 零风险:不真实执行系统命令 - 模拟文件系统:虚拟内存字典结构 - 命令白名单:只允许安全命令 - 场景匹配:检查命令是否答对 """ import re # ============================== # 1. 虚拟文件系统(内存树结构) # ============================== SANDBOX_FS = { "/": { "type": "dir", "perm": "r-x", "children": ["users", "projects", "logs", "sandbox_tips"], }, "/users": { "type": "dir", "perm": "r-x", "children": ["alice.txt", "bob.txt", "charlie"], }, "/users/alice.txt": { "type": "file", "perm": "r--", "content": "Hi, I'm Alice. I love Linux!", }, "/users/bob.txt": { "type": "file", "perm": "r--", "content": "Hello from Bob. Learning Linux daily.", }, "/users/charlie": { "type": "dir", "perm": "r-x", "children": ["profile.md", "skills.txt"], }, "/users/charlie/profile.md": { "type": "file", "perm": "r--", "content": "# Charlie\nLoves open source and Linux commands.", }, "/users/charlie/skills.txt": { "type": "file", "perm": "r--", "content": "Linux\nPython\nGit\nDocker", }, "/projects": { "type": "dir", "perm": "r-x", "children": ["web", "backend"], }, "/projects/web": { "type": "dir", "perm": "r-x", "children": ["index.html", "style.css"], }, "/projects/web/index.html": { "type": "file", "perm": "r--", "content": "Hello Linux Sandbox", }, "/projects/web/style.css": { "type": "file", "perm": "r--", "content": "body { font-family: sans-serif; }", }, "/projects/backend": { "type": "dir", "perm": "r-x", "children": ["app.py", "model.py", "utils.py"], }, "/projects/backend/app.py": { "type": "file", "perm": "rw-", "content": "def main():\n print('Hello, Linux!')\n\nif __name__ == '__main__':\n main()", }, "/projects/backend/model.py": { "type": "file", "perm": "rw-", "content": "class User:\n def __init__(self, name):\n self.name = name", }, "/projects/backend/utils.py": { "type": "file", "perm": "rw-", "content": "def log(msg):\n print(f'[LOG] {msg}')", }, "/logs": { "type": "dir", "perm": "r-x", "children": ["access.log", "error.log"], }, "/logs/access.log": { "type": "file", "perm": "r--", "content": "2024-01-01 10:00 GET /index.html 200\n2024-01-01 10:01 POST /api/login 200\n2024-01-01 10:02 GET /about 404", }, "/logs/error.log": { "type": "file", "perm": "r--", "content": "2024-01-01 10:02 ERROR Page not found\n2024-01-01 10:03 WARNING High memory usage", }, "/sandbox_tips": { "type": "dir", "perm": "r-x", "children": ["welcome.txt"], }, "/sandbox_tips/welcome.txt": { "type": "file", "perm": "r--", "content": "Welcome to Linux Sandbox!\n\nYou can practice commands safely here.\nTry 'ls', 'cat', 'grep', 'find' and more!", }, } # ============================== # 2. 白名单命令定义 # ============================== # 命令 → 参数验证规则 ALLOWED_CMDS = { "ls": { "flags": ["-l", "-a", "-h", "-R", "-t", "-S"], "path": True, }, "cd": {"path": True}, "pwd": {}, "echo": {"rest": True}, "cat": {"path": True}, "head": {"flags": ["-n"], "path": True}, "tail": {"flags": ["-n"], "path": True}, "grep": {"flags": ["-i", "-r", "-n"], "pattern": True, "path": True}, "find": { "flags": ["-type", "-name", "-size"], "path": True, }, "du": {"flags": ["-h", "-s"], "path": True}, "sort": {"flags": ["-r", "-n", "-h"]}, "wc": {"flags": ["-l", "-w", "-c"], "path": True}, "mkdir": {"path": True}, "touch": {"path": True}, "cp": {"path": True}, # 模拟副本 "mv": {"path": True}, # 模拟重命名 "whoami": {}, "history": {"flags": ["-n"]}, "stat": {"path": True}, } # ============================== # 3. 沙盒核心类 # ============================== class LinuxSandbox: def __init__(self): self.cwd = "/" # 当前目录 self.history = [] # 命令历史 self.current_task = None def check_permissions(self, cmd_name: str, args: list) -> tuple[bool, str]: """验证命令是否在白名单 + 参数是否合法""" if cmd_name not in ALLOWED_CMDS: return False, f"❌ 拒绝执行 '{cmd_name}'(不在允许列表)" rule = ALLOWED_CMDS[cmd_name] # 检查非法参数 for arg in args: if arg.startswith("-"): if "flags" in rule and arg not in rule["flags"]: return False, f"❌ 不支持参数: {arg}" return True, "" def resolve_path(self, path: str) -> str: """解析相对/绝对路径(简化版)""" if path.startswith("/"): return path if path == ".": return self.cwd if path == "..": parent = "/".join(self.cwd.split("/")[:-1]) or "/" return parent if parent else "/" return f"{self.cwd.rstrip('/')}/{path}" def get_fspath(self, path: str) -> dict | None: """从虚拟 FS 中获取节点""" return SANDBOX_FS.get(path) def _simulate_ls(self, args: list) -> str: """模拟 ls 命令""" if not args: # ls 默认列出当前目录 node = SANDBOX_FS.get(self.cwd) if not node or node.get("type") != "dir": return f"ls: {self.cwd}: Not a directory" children = node.get("children", []) return " ".join(children) # 处理路径参数 path = args[0] resolved = self.resolve_path(path) node = self.get_fspath(resolved) if not node: return f"ls: cannot access '{path}': No such file or directory" if node["type"] != "dir": return f"ls: cannot access '{path}': Not a directory" return " ".join(node.get("children", [])) def _simulate_cat(self, args: list) -> str: """模拟 cat 命令""" if not args: return "cat: missing operand" path = args[0] resolved = self.resolve_path(path) node = self.get_fspath(resolved) if not node: return f"cat: {path}: No such file or directory" if node["type"] != "file": return f"cat: {path}: Is a directory" return node.get("content", "") def _simulate_cd(self, args: list) -> str: """模拟 cd 命令""" if not args or args[0] == "~": self.cwd = "/home" return "" path = args[0] resolved = self.resolve_path(path) node = self.get_fspath(resolved) if not node or node["type"] != "dir": return f"cd: {path}: No such file or directory" self.cwd = resolved return "" def _simulate_pwd(self) -> str: """模拟 pwd 命令""" return self.cwd def _simulate_echo(self, args: list) -> str: """模拟 echo 命令""" return " ".join(args) def _simulate_grep(self, args: list) -> str: """模拟 grep 命令""" # grep pattern file or grep -r pattern dir if len(args) < 1: return "grep: missing file operand" pattern = "" paths = [] # 简化解析:第一个非 flag 参数是 pattern i = 0 while i < len(args): if args[i] == "-r" or args[i] == "-i" or args[i].startswith("-"): i += 1 else: if not pattern: pattern = args[i] else: paths.append(args[i]) i += 1 if not pattern or not paths: return "grep: pattern or file missing" # 支持通配符匹配 import re results = [] for path_arg in paths: # 处理 * 通配符 if "*" in path_arg: # 简单 glob 模拟 prefix = path_arg.split("/")[0] if "/" in path_arg else self.cwd.rstrip("/") pattern_part = path_arg.replace("*", ".*") for key in SANDBOX_FS: if key.startswith(prefix) and SANDBOX_FS[key]["type"] == "file": if re.match(pattern_part, key.split("/")[-1]): node = self.get_fspath(key) if node and node["type"] == "file": lines = node.get("content", "").split("\n") for line in lines: if pattern in line: results.append(f"{key}:{line}") else: resolved = self.resolve_path(path_arg) node = self.get_fspath(resolved) if not node: return f"grep: {path_arg}: No such file or directory" if node["type"] != "file": return f"grep: {path_arg}: Is a directory" lines = node.get("content", "").split("\n") for line in lines: if pattern in line: results.append(line) return "\n".join(results) if results else "" def _simulate_find(self, args: list) -> str: """模拟 find 命令(简化版)""" # find path -type f -name "*.ext" # 兼容不同顺序:find /path -type f -name "*.py" or find /path -name "*.py" path = self.cwd file_type = None name_pattern = None i = 0 while i < len(args): arg = args[i] if arg.startswith("-"): if arg == "-type" and i + 1 < len(args): file_type = args[i + 1] i += 2 elif arg == "-name" and i + 1 < len(args): name_pattern = args[i + 1] i += 2 else: i += 1 else: path = self.resolve_path(arg) i += 1 # 如果没有指定文件类型,默认查找所有文件 if file_type is None: file_type = "f" # 如果没有指定 -name,返回所有文件 if name_pattern is None: name_pattern = "*" # 匹配逻辑 results = [] for key in SANDBOX_FS: if key.startswith(path) and SANDBOX_FS[key]["type"] == file_type: # 简单 glob 匹配 filename = key.split("/")[-1] if name_pattern == "*" or filename.endswith(name_pattern.replace("*", "")): results.append(key) return "\n".join(results) if results else "" def _simulate_du(self, args: list) -> str: """模拟 du 命令""" # du -sh * or du -h path path = self.cwd show_all = False # 解析参数 i = 0 while i < len(args): if args[i] == "-sh" or args[i] == "-h": show_all = True i += 1 elif args[i].startswith("-"): i += 1 else: path = self.resolve_path(args[i]) i += 1 node = self.get_fspath(path) if not node or node["type"] != "dir": return f"du: {path}: No such file or directory" # 模拟大小(KB)- 简化版 size_map = {"file": 1, "dir": 4} total = 0 children = node.get("children", []) for child in children: child_path = f"{path.rstrip('/')}/{child}" child_node = self.get_fspath(child_path) if child_node: total += size_map.get(child_node["type"], 1) return f"{total}K\t{path}" def _simulate_sort(self, args: list, input_text: str) -> str: """模拟 sort 命令""" lines = input_text.strip().split("\n") reverse = "-r" in args numeric = "-n" in args if numeric: try: lines = sorted(lines, key=lambda x: float(x.split()[0]), reverse=reverse) except: pass else: lines.sort(reverse=reverse) return "\n".join(lines) def _simulate_wc(self, args: list, input_text: str = "") -> str: """模拟 wc 命令""" lines = input_text.count("\n") + 1 if input_text else 0 words = len(input_text.split()) if input_text else 0 chars = len(input_text) # 从文件计算 if args and args[-1] not in ["-l", "-w", "-c"]: path = args[-1] resolved = self.resolve_path(path) node = self.get_fspath(resolved) if node and node["type"] == "file": content = node.get("content", "") lines = content.count("\n") + 1 words = len(content.split()) chars = len(content) flag = args[0] if args and args[0].startswith("-") else "" if flag == "-l": return str(lines) elif flag == "-w": return str(words) elif flag == "-c": return str(chars) else: return f" {lines} {words} {chars}" def _simulate_mkdir(self, args: list) -> str: """模拟 mkdir -p""" if not args: return "mkdir: missing operand" path = self.resolve_path(args[0]) # 简化:只允许在 sandbox 下创建 if not path.startswith("/sandbox"): return "mkdir: permission denied: only sandbox paths allowed" return f"mkdir: created directory '{path}' (simulated)" def _simulate_touch(self, args: list) -> str: """模拟 touch""" if not args: return "touch: missing file operand" path = self.resolve_path(args[0]) # 只读沙盒,不允许创建 return f"touch: cannot touch '{path}': Read-only file system" def _simulate_cp(self, args: list) -> str: """模拟 cp""" if len(args) < 2: return "cp: missing file operand" src = self.resolve_path(args[0]) dst = args[-1] # 简化:目标总是最后一个参数 if not self.get_fspath(src): return f"cp: cannot stat '{args[0]}': No such file or directory" return f"cp: copied (simulated) '{src}' → '{dst}'" def _simulate_mv(self, args: list) -> str: """模拟 mv""" if len(args) < 2: return "mv: missing file operand" return f"mv: moved (simulated) '{args[0]}' → '{args[1]}'" def _simulate_stat(self, args: list) -> str: """模拟 stat""" if not args: return "stat: missing operand" path = self.resolve_path(args[0]) node = self.get_fspath(path) if not node: return f"stat: cannot stat '{path}': No such file or directory" return f""" File: {path} Size: {len(node.get('content', ''))} Blocks: 1 IO Block: 4096 {'directory' if node['type'] == 'dir' else 'regular file'} Access: ({node['perm']}/0{'' if node['type']=='dir' else '0' }44) uid=1000 gid=1000 Access: 2026-03-04 00:00:00.000000000 Modify: 2026-03-04 00:00:00.000000000 Change: 2026-03-04 00:00:00.000000000 Birth: -""" # ============================== # 主执行入口 # ============================== def execute(self, cmd: str) -> dict: """执行命令,返回结果 & 社交化反馈""" cmd = cmd.strip() if not cmd: return {"output": "", "success": True, "message": ""} # 记录历史 self.history.append(cmd) # 解析命令 parts = cmd.split() cmd_name = parts[0] args = parts[1:] # 安全检查 # 1. 关键危险命令拦截 if any(x in cmd for x in ["rm -rf", "sudo", "chmod 777", "wget", "curl"]): return { "output": "", "success": False, "message": "❌ 拒绝执行危险命令!这是沙盒环境,为安全起见禁止危险指令。", } # 2. 白名单验证 allowed, err = self.check_permissions(cmd_name, args) if not allowed: return {"output": "", "success": False, "message": err} # 执行模拟命令 output = "" try: if cmd_name == "ls": output = self._simulate_ls(args) elif cmd_name == "cd": msg = self._simulate_cd(args) output = msg if msg else self.cwd elif cmd_name == "pwd": output = self._simulate_pwd() elif cmd_name == "echo": output = self._simulate_echo(args) elif cmd_name == "cat": output = self._simulate_cat(args) elif cmd_name == "grep": output = self._simulate_grep(args) elif cmd_name == "find": output = self._simulate_find(args) elif cmd_name == "du": output = self._simulate_du(args) elif cmd_name == "sort": output = self._simulate_sort(args, "") elif cmd_name == "wc": output = self._simulate_wc(args) elif cmd_name == "mkdir": output = self._simulate_mkdir(args) elif cmd_name == "touch": output = self._simulate_touch(args) elif cmd_name == "cp": output = self._simulate_cp(args) elif cmd_name == "mv": output = self._simulate_mv(args) elif cmd_name == "whoami": output = "sandbox_user" elif cmd_name == "history": n = int(args[0].lstrip("-n")) if args and args[0].startswith("-n") else 5 output = "\n".join(f" {i+1} {h}" for i, h in enumerate(self.history[-n:])) elif cmd_name == "stat": output = self._simulate_stat(args) else: output = f"{cmd_name}: command not found (sandbox mode)" except Exception as e: output = f"Error: {e}" return {"output": output, "success": True, "message": "✅ 命令执行成功"} # ============================== # 4. 单元测试(可选) # ============================== if __name__ == "__main__": sb = LinuxSandbox() print("=== Linux Sandbox Test ===") print(sb.execute("ls")) print(sb.execute("pwd")) print(sb.execute("cat /users/alice.txt")) print(sb.execute("grep Linux /users/*")) print(sb.execute("find /projects -name '*.py'")) print(sb.execute("du -sh /projects")) print(sb.execute("rm -rf /")) # 应该被拦截