Files
linux-practice/sandbox.py
likingcode 5686831d9a feat: Linux练习平台
- Web界面Linux命令练习
- Python后端 + sandbox安全沙箱
- 课程和任务管理
2026-03-07 05:43:51 +00:00

582 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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": "<html><body>Hello Linux Sandbox</body></html>",
},
"/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 /")) # 应该被拦截