feat: Linux练习平台
- Web界面Linux命令练习 - Python后端 + sandbox安全沙箱 - 课程和任务管理
This commit is contained in:
581
sandbox.py
Normal file
581
sandbox.py
Normal file
@@ -0,0 +1,581 @@
|
||||
#!/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 /")) # 应该被拦截
|
||||
Reference in New Issue
Block a user