feat: Linux练习平台

- Web界面Linux命令练习
- Python后端 + sandbox安全沙箱
- 课程和任务管理
This commit is contained in:
likingcode
2026-03-07 05:43:51 +00:00
commit 5686831d9a
22 changed files with 8816 additions and 0 deletions

581
sandbox.py Normal file
View 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 /")) # 应该被拦截