diff --git a/index.html b/index.html
index d98b19c..1eb40a1 100644
--- a/index.html
+++ b/index.html
@@ -1,926 +1,640 @@
-
-
-
-
-
-
-
-
-
🚀 开启 Linux 学习之旅
-
- 从入门到精通,系统学习 Linux 运维技能
- 12 个级别 · 95 道题目 · 循序渐进
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- $
-
-
-
输出将显示在这里...
-
-
-
-
-
-
-
-
-
-
-
🎉 回答正确!
-
恭喜你完成了这个任务!
-
-
-
-
-
-
-
-
+
-
+ window.setMode = setMode;
+ window.toggleTheme = toggleTheme;
+ window.selectTask = selectTask;
+ window.toggleLevel = toggleLevel;
+ window.executeCommand = executeCommand;
+ window.showHint = showHint;
+ window.showAnswer = showAnswer;
+ window.nextTask = nextTask;
+ window.startLearning = startLearning;
+ window.resetSandbox = resetSandbox;
+ window.jumpToFirstUnfinished = jumpToFirstUnfinished;
+
diff --git a/sandbox.py b/sandbox.py
index 9149f8c..1601b03 100644
--- a/sandbox.py
+++ b/sandbox.py
@@ -1,545 +1,664 @@
#!/usr/bin/env python3
"""
-Linux 命令沙盒模拟器
+Linux 命令沙盒模拟器(增强版)
- 零风险:不真实执行系统命令
-- 模拟文件系统:虚拟内存字典结构
-- 命令白名单:只允许安全命令
-- 场景匹配:检查命令是否答对
+- 可变虚拟文件系统:支持创建 / 删除 / 移动 / 改权限
+- 覆盖更多 Linux 常见命令,尽量让课程任务可跑通
"""
+from __future__ import annotations
+
+from copy import deepcopy
+from fnmatch import fnmatch
+from datetime import datetime
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!",
- },
+
+BASE_FS = {
+ "/": {"type": "dir", "perm": "755", "children": ["users", "projects", "logs", "sandbox_tips", "tmp", "etc", "var", "home", "bin", "usr"]},
+ "/home": {"type": "dir", "perm": "755", "children": ["sandbox_user"]},
+ "/home/sandbox_user": {"type": "dir", "perm": "755", "children": ["notes.txt", ".bashrc"]},
+ "/home/sandbox_user/notes.txt": {"type": "file", "perm": "644", "content": "Practice Linux every day.\n"},
+ "/home/sandbox_user/.bashrc": {"type": "file", "perm": "644", "content": "alias ll='ls -l'\nexport PATH=/usr/local/bin:/usr/bin:/bin\n"},
+ "/tmp": {"type": "dir", "perm": "777", "children": []},
+ "/users": {"type": "dir", "perm": "755", "children": ["alice.txt", "bob.txt", "charlie"]},
+ "/users/alice.txt": {"type": "file", "perm": "644", "content": "Hi, I'm Alice. I love Linux!\n"},
+ "/users/bob.txt": {"type": "file", "perm": "644", "content": "Hello from Bob. Learning Linux daily.\n"},
+ "/users/charlie": {"type": "dir", "perm": "755", "children": ["profile.md", "skills.txt"]},
+ "/users/charlie/profile.md": {"type": "file", "perm": "644", "content": "# Charlie\nLoves open source and Linux commands.\n"},
+ "/users/charlie/skills.txt": {"type": "file", "perm": "644", "content": "Linux\nPython\nGit\nDocker\n"},
+ "/projects": {"type": "dir", "perm": "755", "children": ["web", "backend"]},
+ "/projects/web": {"type": "dir", "perm": "755", "children": ["index.html", "style.css"]},
+ "/projects/web/index.html": {"type": "file", "perm": "644", "content": "Hello Linux Sandbox\n"},
+ "/projects/web/style.css": {"type": "file", "perm": "644", "content": "body { font-family: sans-serif; }\n"},
+ "/projects/backend": {"type": "dir", "perm": "755", "children": ["app.py", "model.py", "utils.py"]},
+ "/projects/backend/app.py": {"type": "file", "perm": "644", "content": "def main():\n print('Hello, Linux!')\n\nif __name__ == '__main__':\n main()\n"},
+ "/projects/backend/model.py": {"type": "file", "perm": "644", "content": "class User:\n def __init__(self, name):\n self.name = name\n"},
+ "/projects/backend/utils.py": {"type": "file", "perm": "644", "content": "def log(msg):\n print(f'[LOG] {msg}')\n"},
+ "/logs": {"type": "dir", "perm": "755", "children": ["access.log", "error.log"]},
+ "/logs/access.log": {"type": "file", "perm": "644", "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\n"},
+ "/logs/error.log": {"type": "file", "perm": "644", "content": "2024-01-01 10:02 ERROR Page not found\n2024-01-01 10:03 WARNING High memory usage\n"},
+ "/sandbox_tips": {"type": "dir", "perm": "755", "children": ["welcome.txt"]},
+ "/sandbox_tips/welcome.txt": {"type": "file", "perm": "644", "content": "Welcome to Linux Sandbox!\nTry ls, cat, grep, find, ps, df and more.\n"},
+ "/etc": {"type": "dir", "perm": "755", "children": ["passwd", "group", "hosts", "nginx.conf", "ssh.conf", "app.conf", "skel"]},
+ "/etc/passwd": {"type": "file", "perm": "644", "content": "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nsandbox_user:x:1000:1000:Sandbox User:/home/sandbox_user:/bin/bash\n"},
+ "/etc/group": {"type": "file", "perm": "644", "content": "root:x:0:\nusers:x:100:alice,bob\ndocker:x:999:sandbox_user\n"},
+ "/etc/hosts": {"type": "file", "perm": "644", "content": "127.0.0.1 localhost\n127.0.1.1 sandbox\n"},
+ "/etc/nginx.conf": {"type": "file", "perm": "644", "content": "user nginx;\nworker_processes auto;\n"},
+ "/etc/ssh.conf": {"type": "file", "perm": "644", "content": "Port 22\nPermitRootLogin no\n"},
+ "/etc/app.conf": {"type": "file", "perm": "644", "content": "APP_ENV=prod\nAPP_DEBUG=false\n"},
+ "/etc/skel": {"type": "dir", "perm": "755", "children": [".bash_logout", ".bashrc"]},
+ "/etc/skel/.bash_logout": {"type": "file", "perm": "644", "content": "# logout\n"},
+ "/etc/skel/.bashrc": {"type": "file", "perm": "644", "content": "# skeleton bashrc\n"},
+ "/var": {"type": "dir", "perm": "755", "children": ["log"]},
+ "/var/log": {"type": "dir", "perm": "755", "children": ["syslog", "auth.log", "nginx"]},
+ "/var/log/syslog": {"type": "file", "perm": "644", "content": "Mar 06 10:00 kernel: system boot complete\nMar 06 10:02 app: error database connection timeout\nMar 06 10:04 sshd: Accepted password for sandbox_user\nMar 06 10:05 nginx: worker started\nMar 06 10:06 app: error failed to load config\n"},
+ "/var/log/auth.log": {"type": "file", "perm": "640", "content": "Mar 06 10:01 sshd[123]: Failed password\nMar 06 10:04 sshd[124]: Accepted password for sandbox_user\n"},
+ "/var/log/nginx": {"type": "dir", "perm": "755", "children": ["access.log", "error.log"]},
+ "/var/log/nginx/access.log": {"type": "file", "perm": "644", "content": "127.0.0.1 - - [06/Mar/2026] \"GET / HTTP/1.1\" 200 612\n"},
+ "/var/log/nginx/error.log": {"type": "file", "perm": "644", "content": "2026/03/06 [error] connect() failed\n"},
+ "/bin": {"type": "dir", "perm": "755", "children": ["ls", "cat", "grep", "find", "bash"]},
+ "/usr": {"type": "dir", "perm": "755", "children": ["bin"]},
+ "/usr/bin": {"type": "dir", "perm": "755", "children": ["vim", "curl", "wget", "python3"]},
}
-# ==============================
-# 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},
+
+COMMAND_INDEX = {
+ "ls": "/bin/ls",
+ "cat": "/bin/cat",
+ "grep": "/bin/grep",
+ "find": "/bin/find",
+ "bash": "/bin/bash",
+ "vim": "/usr/bin/vim",
+ "curl": "/usr/bin/curl",
+ "wget": "/usr/bin/wget",
+ "python3": "/usr/bin/python3",
}
-# ==============================
-# 3. 沙盒核心类
-# ==============================
+
class LinuxSandbox:
def __init__(self):
- self.cwd = "/" # 当前目录
- self.history = [] # 命令历史
- self.current_task = None
+ self.reset()
- def check_permissions(self, cmd_name: str, args: list) -> tuple[bool, str]:
- """验证命令是否在白名单 + 参数是否合法"""
- if cmd_name not in ALLOWED_CMDS:
- return False, f"❌ 拒绝执行 '{cmd_name}'(不在允许列表)"
+ def reset(self):
+ self.fs = deepcopy(BASE_FS)
+ self.cwd = "/"
+ self.home = "/home/sandbox_user"
+ self.user = "sandbox_user"
+ self.history: list[str] = []
+ self.env = {"HOME": self.home, "USER": self.user, "PATH": "/usr/local/bin:/usr/bin:/bin", "SHELL": "/bin/bash"}
+ self.aliases = {"ll": "ls -l"}
- 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 == ".":
+ # ---------- FS helpers ----------
+ def resolve_path(self, path: str | None) -> str:
+ if not path or path == ".":
return self.cwd
- if path == "..":
- parent = "/".join(self.cwd.split("/")[:-1]) or "/"
- return parent if parent else "/"
- return f"{self.cwd.rstrip('/')}/{path}"
+ if path == "~":
+ return self.home
+ if path.startswith("~/"):
+ return self.home + path[1:]
+ if not path.startswith("/"):
+ base = self.cwd.rstrip("/") or "/"
+ path = f"{base}/{path}" if base != "/" else f"/{path}"
+ parts = []
+ for part in path.split("/"):
+ if part in ("", "."):
+ continue
+ if part == "..":
+ if parts:
+ parts.pop()
+ else:
+ parts.append(part)
+ return "/" + "/".join(parts)
- def get_fspath(self, path: str) -> dict | None:
- """从虚拟 FS 中获取节点"""
- return SANDBOX_FS.get(path)
+ def get_node(self, path: str):
+ return self.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"
+ def exists(self, path: str) -> bool:
+ return self.resolve_path(path) in self.fs
- children = node.get("children", [])
- return " ".join(children)
+ def is_executable(self, path: str) -> bool:
+ node = self.get_node(self.resolve_path(path))
+ return bool(node and len(node.get("perm", "")) >= 3 and node["perm"][2] == "1" or node and "x" in node.get("perm_human", ""))
- # 处理路径参数
- path = args[0]
- resolved = self.resolve_path(path)
- node = self.get_fspath(resolved)
+ def _perm_human(self, perm: str, is_dir: bool) -> str:
+ mapping = {
+ "0": "---", "1": "--x", "2": "-w-", "3": "-wx",
+ "4": "r--", "5": "r-x", "6": "rw-", "7": "rwx",
+ }
+ if perm.isdigit() and len(perm) == 3:
+ return ("d" if is_dir else "-") + "".join(mapping.get(x, "---") for x in perm)
+ return ("d" if is_dir else "-") + perm
+ def _ensure_dir(self, path: str):
+ if path in self.fs:
+ return
+ parent = self.parent_dir(path)
+ if parent != path and parent not in self.fs:
+ self._ensure_dir(parent)
+ self.fs[path] = {"type": "dir", "perm": "755", "children": []}
+ self._add_child(parent, path.split("/")[-1])
+
+ def _add_child(self, parent: str, name: str):
+ node = self.fs.get(parent)
+ if node and node["type"] == "dir":
+ node.setdefault("children", [])
+ if name not in node["children"]:
+ node["children"].append(name)
+
+ def _remove_child(self, parent: str, name: str):
+ node = self.fs.get(parent)
+ if node and node["type"] == "dir" and name in node.get("children", []):
+ node["children"].remove(name)
+
+ def parent_dir(self, path: str) -> str:
+ if path == "/":
+ return "/"
+ parts = path.rstrip("/").split("/")
+ if len(parts) <= 2:
+ return "/"
+ return "/" + "/".join(parts[1:-1])
+
+ def _copy_node(self, src: str, dst: str):
+ node = deepcopy(self.fs[src])
+ self.fs[dst] = node
+ self._add_child(self.parent_dir(dst), dst.split("/")[-1])
+ if node["type"] == "dir":
+ old_children = list(node.get("children", []))
+ node["children"] = []
+ for child in old_children:
+ self._copy_node(f"{src.rstrip('/')}/{child}", f"{dst.rstrip('/')}/{child}")
+
+ def _delete_node(self, path: str):
+ node = self.fs.get(path)
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
+ if node["type"] == "dir":
+ for child in list(node.get("children", [])):
+ self._delete_node(f"{path.rstrip('/')}/{child}")
+ parent = self.parent_dir(path)
+ self._remove_child(parent, path.split("/")[-1])
+ self.fs.pop(path, None)
- return " ".join(node.get("children", []))
+ def _safe_shell_split(self, cmd: str) -> list[str]:
+ matches = re.findall(r"'[^']*'|\"[^\"]*\"|\S+", cmd)
+ return [m[1:-1] if (m.startswith("'") and m.endswith("'")) or (m.startswith('"') and m.endswith('"')) else m for m in matches]
- def _simulate_cat(self, args: list) -> str:
- """模拟 cat 命令"""
- if not args:
- return "cat: missing operand"
+ def _expand_alias(self, cmd: str) -> str:
+ if not cmd:
+ return cmd
+ head = cmd.split()[0]
+ if head in self.aliases:
+ rest = cmd[len(head):].lstrip()
+ return f"{self.aliases[head]} {rest}".strip()
+ return cmd
- path = args[0]
- resolved = self.resolve_path(path)
- node = self.get_fspath(resolved)
+ def _expand_env(self, text: str) -> str:
+ for key, value in self.env.items():
+ text = text.replace(f"${key}", value)
+ return text
- 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 ""
+ # ---------- Commands ----------
+ def _simulate_ls(self, args: list[str]) -> str:
+ flags = {a for a in args if a.startswith("-")}
+ targets = [a for a in args if not a.startswith("-")] or [self.cwd]
+ lines = []
+ for target in targets:
+ path = self.resolve_path(target)
+ node = self.get_node(path)
+ if not node:
+ lines.append(f"ls: cannot access '{target}': No such file or directory")
+ continue
+ if node["type"] == "file":
+ if "-l" in flags:
+ lines.append(f"{self._perm_human(node.get('perm','644'), False)} 1 sandbox sandbox {len(node.get('content','')):>4} Mar 6 10:00 {path.split('/')[-1]}")
+ else:
+ lines.append(path.split("/")[-1])
+ continue
+ children = sorted(node.get("children", []))
+ if "-a" in flags:
+ children = [".", ".."] + children
+ if "-l" in flags:
+ lines.append("total " + str(max(len(children), 1)))
+ for child in children:
+ if child in (".", ".."):
+ display_path = path if child == "." else self.parent_dir(path)
+ cnode = self.get_node(display_path) or {"type": "dir", "perm": "755", "children": []}
+ name = child
+ else:
+ display_path = f"{path.rstrip('/')}/{child}" if path != "/" else f"/{child}"
+ cnode = self.get_node(display_path)
+ name = child
+ if not cnode:
+ continue
+ is_dir = cnode["type"] == "dir"
+ size = len(cnode.get("content", "")) if not is_dir else max(4, len(cnode.get("children", [])) * 4)
+ lines.append(f"{self._perm_human(cnode.get('perm','755' if is_dir else '644'), is_dir)} 1 sandbox sandbox {size:>4} Mar 6 10:00 {name}")
+ else:
+ lines.append(" ".join(children))
+ return "\n".join(line for line in lines if line != "")
def _simulate_pwd(self) -> str:
- """模拟 pwd 命令"""
return self.cwd
- def _simulate_echo(self, args: list) -> str:
- """模拟 echo 命令"""
- return " ".join(args)
+ def _simulate_cd(self, args: list[str]) -> str:
+ target = args[0] if args else self.home
+ if target == "-":
+ target = self.home
+ path = self.resolve_path(target)
+ node = self.get_node(path)
+ if not node or node["type"] != "dir":
+ return f"cd: {target}: No such file or directory"
+ self.cwd = path
+ return self.cwd
- 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"
+ def _simulate_echo(self, args: list[str]) -> str:
+ text = " ".join(args)
+ return self._expand_env(text)
- pattern = ""
- paths = []
+ def _simulate_cat(self, args: list[str]) -> str:
+ if not args:
+ return "cat: missing operand"
+ number = "-n" in args
+ files = [a for a in args if not a.startswith("-")]
+ out = []
+ for path_arg in files:
+ path = self.resolve_path(path_arg)
+ node = self.get_node(path)
+ if not node:
+ out.append(f"cat: {path_arg}: No such file or directory")
+ continue
+ if node["type"] != "file":
+ out.append(f"cat: {path_arg}: Is a directory")
+ continue
+ content = node.get("content", "")
+ if number:
+ content = "\n".join(f"{i+1:>6} {line}" for i, line in enumerate(content.rstrip("\n").split("\n")))
+ out.append(content.rstrip("\n"))
+ return "\n".join(out)
- # 简化解析:第一个非 flag 参数是 pattern
+ def _simulate_head_tail(self, cmd_name: str, args: list[str]) -> str:
+ count = 10
+ files = []
i = 0
while i < len(args):
- if args[i] == "-r" or args[i] == "-i" or args[i].startswith("-"):
+ if args[i] in ("-n",):
+ count = int(args[i + 1])
+ i += 2
+ elif args[i].startswith("-") and args[i][1:].isdigit():
+ count = int(args[i][1:])
+ i += 1
+ elif args[i] == "-f":
+ files.append("-f")
i += 1
else:
- if not pattern:
- pattern = args[i]
- else:
- paths.append(args[i])
+ files.append(args[i])
i += 1
+ if "-f" in files:
+ target = files[-1] if len(files) > 1 else "/var/log/syslog"
+ return f"==> {target} <==\n(log streaming simulated)\nPress Ctrl+C to exit"
+ if not files:
+ return f"{cmd_name}: missing file operand"
+ node = self.get_node(self.resolve_path(files[-1]))
+ if not node or node["type"] != "file":
+ return f"{cmd_name}: cannot open '{files[-1]}'"
+ lines = node.get("content", "").rstrip("\n").split("\n")
+ picked = lines[:count] if cmd_name == "head" else lines[-count:]
+ return "\n".join(picked)
- if not pattern or not paths:
+ def _simulate_grep(self, args: list[str]) -> str:
+ flags = {a for a in args if a.startswith("-")}
+ items = [a for a in args if not a.startswith("-")]
+ if len(items) < 2:
return "grep: pattern or file missing"
-
- # 支持通配符匹配
- import re
+ pattern, *targets = items
+ insensitive = "-i" in flags
+ recursive = "-r" in flags
+ show_num = "-n" in flags
+ invert = "-v" in flags
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}")
+ for target in targets:
+ resolved = self.resolve_path(target)
+ candidate_paths = []
+ if "*" in target:
+ regex = target.replace("*", ".*")
+ candidate_paths = [p for p in self.fs if re.fullmatch(self.resolve_path(regex).replace(".", r"\.").replace(".*", ".*"), p)]
+ elif recursive and self.get_node(resolved) and self.get_node(resolved)["type"] == "dir":
+ candidate_paths = [p for p in self.fs if p.startswith(resolved.rstrip("/") + "/") and self.fs[p]["type"] == "file"]
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"
+ candidate_paths = [resolved]
+ for path in candidate_paths:
+ node = self.get_node(path)
+ if not node or node["type"] != "file":
+ continue
+ for idx, line in enumerate(node.get("content", "").splitlines(), 1):
+ hay = line.lower() if insensitive else line
+ needle = pattern.lower() if insensitive else pattern
+ matched = needle in hay
+ if invert:
+ matched = not matched
+ if matched:
+ prefix = []
+ if recursive or len(candidate_paths) > 1:
+ prefix.append(path)
+ if show_num:
+ prefix.append(str(idx))
+ results.append((":".join(prefix) + (":" if prefix else "")) + line)
+ return "\n".join(results)
+ def _simulate_find(self, args: list[str]) -> str:
path = self.cwd
file_type = None
- name_pattern = None
-
+ name_pattern = "*"
+ has_exec = False
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
+ 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].strip("'\"")
+ i += 2
+ elif arg == "-exec":
+ has_exec = True
+ break
+ elif arg.startswith("-"):
+ 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)
+ for key, node in self.fs.items():
+ if not key.startswith(path.rstrip("/") if path != "/" else "/"):
+ continue
+ if key == path:
+ continue
+ if file_type == "f" and node["type"] != "file":
+ continue
+ if file_type == "d" and node["type"] != "dir":
+ continue
+ if fnmatch(key.split("/")[-1], name_pattern):
+ results.append(key)
+ if has_exec and "rm" in " ".join(args):
+ for result in list(results):
+ self._delete_node(result)
+ return "\n".join(results)
+ return "\n".join(results)
- return "\n".join(results) if results else ""
+ def _simulate_du(self, args: list[str]) -> str:
+ target = next((a for a in args if not a.startswith("-")), self.cwd)
+ target = self.resolve_path(target)
+ node = self.get_node(target)
+ if not node:
+ return f"du: cannot access '{target}'"
+ def calc_size(path: str) -> int:
+ item = self.fs[path]
+ if item["type"] == "file":
+ return max(1, len(item.get("content", "")) // 16)
+ return 4 + sum(calc_size(f"{path.rstrip('/')}/{child}") for child in item.get("children", []))
+ size = calc_size(target)
+ suffix = "K"
+ if "-h" in args or "-sh" in args:
+ return f"{size}{suffix}\t{target}"
+ return f"{size}\t{target}"
- 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)
+ def _simulate_wc(self, args: list[str]) -> str:
+ flag = next((a for a in args if a.startswith("-")), "")
+ target = next((a for a in args if not a.startswith("-")), None)
+ content = ""
+ if target:
+ node = self.get_node(self.resolve_path(target))
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 ""
+ lines = len(content.splitlines())
+ words = len(content.split())
+ chars = len(content)
if flag == "-l":
return str(lines)
- elif flag == "-w":
+ if flag == "-w":
return str(words)
- elif flag == "-c":
+ if flag == "-c":
return str(chars)
- else:
- return f" {lines} {words} {chars}"
+ return f"{lines:>4} {words:>4} {chars:>4}"
- def _simulate_mkdir(self, args: list) -> str:
- """模拟 mkdir -p"""
+ def _simulate_mkdir(self, args: list[str]) -> str:
if not args:
return "mkdir: missing operand"
+ recursive = "-p" in args
+ targets = [a for a in args if not a.startswith("-")]
+ for target in targets:
+ path = self.resolve_path(target)
+ parent = self.parent_dir(path)
+ if self.exists(path):
+ continue
+ if not recursive and not self.exists(parent):
+ return f"mkdir: cannot create directory '{target}': No such file or directory"
+ if recursive:
+ self._ensure_dir(parent)
+ self.fs[path] = {"type": "dir", "perm": "755", "children": []}
+ self._add_child(parent, path.split("/")[-1])
+ return ""
- 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"""
+ def _simulate_touch(self, args: list[str]) -> str:
if not args:
return "touch: missing file operand"
+ for target in args:
+ path = self.resolve_path(target)
+ if not self.exists(path):
+ parent = self.parent_dir(path)
+ if not self.exists(parent):
+ return f"touch: cannot touch '{target}': No such file or directory"
+ self.fs[path] = {"type": "file", "perm": "644", "content": ""}
+ self._add_child(parent, path.split("/")[-1])
+ return ""
- 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:
+ def _simulate_cp(self, args: list[str]) -> str:
+ opts = {a for a in args if a.startswith("-")}
+ items = [a for a in args if not a.startswith("-")]
+ if len(items) < 2:
return "cp: missing file operand"
+ src, dst = self.resolve_path(items[0]), self.resolve_path(items[1])
+ src_node = self.get_node(src)
+ if not src_node:
+ return f"cp: cannot stat '{items[0]}'"
+ if src_node["type"] == "dir" and "-r" not in opts:
+ return f"cp: -r not specified; omitting directory '{items[0]}'"
+ if self.get_node(dst) and self.get_node(dst)["type"] == "dir":
+ dst = f"{dst.rstrip('/')}/{src.split('/')[-1]}"
+ self._copy_node(src, dst)
+ return ""
- 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"""
+ def _simulate_mv(self, args: list[str]) -> str:
if len(args) < 2:
return "mv: missing file operand"
+ src, dst = self.resolve_path(args[0]), self.resolve_path(args[1])
+ node = self.get_node(src)
+ if not node:
+ return f"mv: cannot stat '{args[0]}'"
+ if self.get_node(dst) and self.get_node(dst)["type"] == "dir":
+ dst = f"{dst.rstrip('/')}/{src.split('/')[-1]}"
+ self._copy_node(src, dst)
+ self._delete_node(src)
+ return ""
- return f"mv: moved (simulated) '{args[0]}' → '{args[1]}'"
+ def _simulate_rm(self, args: list[str]) -> str:
+ opts = {a for a in args if a.startswith("-")}
+ targets = [a for a in args if not a.startswith("-")]
+ if not targets:
+ return "rm: missing operand"
+ for target in targets:
+ path = self.resolve_path(target)
+ node = self.get_node(path)
+ if not node:
+ continue
+ if node["type"] == "dir" and not ({"-r", "-rf", "-fr"} & opts):
+ return f"rm: cannot remove '{target}': Is a directory"
+ self._delete_node(path)
+ return ""
- def _simulate_stat(self, args: list) -> str:
- """模拟 stat"""
+ def _simulate_chmod(self, args: list[str]) -> str:
+ if len(args) < 2:
+ return "chmod: missing operand"
+ mode, target = args[0], self.resolve_path(args[1])
+ node = self.get_node(target)
+ if not node:
+ return f"chmod: cannot access '{args[1]}'"
+ perm = node.get("perm", "644")
+ if mode.isdigit() and len(mode) == 3:
+ node["perm"] = mode
+ elif mode == "+x":
+ node["perm"] = perm[:2] + "1"
+ elif mode == "-r":
+ node["perm"] = "244"
+ return ""
+
+ def _simulate_chown_group(self, cmd_name: str, args: list[str]) -> str:
+ if not args:
+ return f"{cmd_name}: missing operand"
+ recursive = args[0] == "-R"
+ target = self.resolve_path(args[-1])
+ if not self.exists(target):
+ return f"{cmd_name}: cannot access '{args[-1]}'"
+ return ""
+
+ def _simulate_stat(self, args: list[str]) -> str:
if not args:
return "stat: missing operand"
-
path = self.resolve_path(args[0])
- node = self.get_fspath(path)
-
+ node = self.get_node(path)
if not node:
- return f"stat: cannot stat '{path}': No such file or directory"
+ return f"stat: cannot stat '{args[0]}'"
+ is_dir = node["type"] == "dir"
+ size = len(node.get("content", "")) if not is_dir else max(4, len(node.get("children", [])) * 4)
+ return f" File: {path}\n Size: {size} IO Block: 4096 {'directory' if is_dir else 'regular file'}\nAccess: ({node.get('perm','755')}/{self._perm_human(node.get('perm','755'), is_dir)})\nModify: 2026-03-06 10:00:00"
- 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 _simulate_history(self, args: list[str]) -> str:
+ n = 10
+ if args and args[0] == "-n" and len(args) > 1 and args[1].isdigit():
+ n = int(args[1])
+ return "\n".join(f" {i+1} {cmd}" for i, cmd in enumerate(self.history[-n:]))
- # ==============================
- # 主执行入口
- # ==============================
- def execute(self, cmd: str) -> dict:
- """执行命令,返回结果 & 社交化反馈"""
- cmd = cmd.strip()
+ def _simulate_ps(self, args: list[str]) -> str:
+ if args == ["aux"] or args == ["-ef"]:
+ return "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nroot 1 0.0 0.1 10000 2048 ? Ss 10:00 0:01 /sbin/init\nsandbox 1234 0.1 0.3 20480 4096 pts/0 S+ 10:05 0:00 python3 server.py\nnginx 1400 0.0 0.2 18900 3500 ? S 10:06 0:00 nginx: worker"
+ return " PID TTY TIME CMD\n 1234 pts/0 00:00:00 bash"
+
+ def _simulate_system_text(self, cmd_name: str, args: list[str]) -> str:
+ canned = {
+ "clear": "[screen cleared]",
+ "id": "uid=1000(sandbox_user) gid=1000(sandbox_user) groups=1000(sandbox_user),999(docker)",
+ "w": " 10:00:00 up 5 days, 2 users, load average: 0.10, 0.12, 0.08\nUSER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT\nsandbox pts/0 localhost 10:00 1:20 0.01s 0.00s bash",
+ "last": "sandbox_user pts/0 localhost Mon Mar 6 10:04 still logged in\nreboot system boot 5.15.0 Mon Mar 6 10:00",
+ "passwd": "Changing password for sandbox_user... (simulated)",
+ "su": "Switched to root (simulated)",
+ "df": "Filesystem Size Used Avail Use% Mounted on\n/dev/vda1 40G 12G 26G 32% /\ntmpfs 512M 8.0M 504M 2% /run",
+ "free": " total used free shared buff/cache available\nMem: 2.0Gi 1.1Gi 256Mi 48Mi 700Mi 750Mi",
+ "mount": "/dev/vda1 on / type ext4 (rw,relatime)\ntmpfs on /run type tmpfs (rw,nosuid,nodev)",
+ "fdisk": "Disk /dev/vda: 40 GiB, 42949672960 bytes, 83886080 sectors",
+ "top": "top - 10:00:00 up 5 days, 1 user, load average: 0.10, 0.12, 0.08\nTasks: 132 total, 1 running, 131 sleeping",
+ "kill": "signal sent (simulated)",
+ "pkill": "matched processes terminated (simulated)",
+ "nohup": "appending output to nohup.out",
+ "ifconfig": "eth0: flags=4163
mtu 1500\n inet 192.168.1.20 netmask 255.255.255.0\nlo: flags=73 mtu 65536\n inet 127.0.0.1 netmask 255.0.0.0",
+ "ip": "1: lo: mtu 65536\n inet 127.0.0.1/8 scope host lo\n2: eth0: mtu 1500\n inet 192.168.1.20/24 scope global eth0",
+ "ping": "PING 127.0.0.1 (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.025 ms\n--- 127.0.0.1 ping statistics ---\n4 packets transmitted, 4 received, 0% packet loss",
+ "netstat": "Active Internet connections (only servers)\nProto Local Address Foreign Address State PID/Program name\ntcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 102/sshd",
+ "ss": "State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\nLISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=102,fd=3))",
+ "curl": "hello localhost",
+ "wget": "Saving to: '/tmp/test.html'\n2026-03-06 10:00:00 (1.2 MB/s) - '/tmp/test.html' saved [612/612]",
+ "traceroute": "traceroute to 8.8.8.8, 30 hops max\n 1 192.168.1.1 1.012 ms\n 2 10.0.0.1 4.221 ms",
+ "which": COMMAND_INDEX.get(args[0], "") if args else "",
+ "whereis": f"{args[0]}: {COMMAND_INDEX.get(args[0], '')} /usr/share/man/man1/{args[0]}.1.gz" if args else "",
+ "yum": "Loaded plugins: fastestmirror\nInstalled Packages\nnginx.x86_64 1.24.0-1 @base",
+ "rpm": "bash-5.1.8-6.el9.x86_64",
+ "apt": "Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease\nReading package lists... Done",
+ "dpkg": "ii bash 5.1-6ubuntu1 amd64 GNU Bourne Again SHell",
+ "vim": "[Opened in vim simulator]",
+ "vi": "[Opened in vim simulator]",
+ "env": "\n".join(f"{k}={v}" for k, v in self.env.items()),
+ "export": "environment updated",
+ "alias": "alias ll='ls -l'" if not args else "alias set",
+ "date": datetime.now().strftime("%a %b %d %H:%M:%S CST %Y"),
+ "cal": " March 2026\nSu Mo Tu We Th Fr Sa\n 1 2 3 4 5 6 7\n 8 9 10 11 12 13 14",
+ "bc": "2",
+ "tar": "tar operation simulated",
+ "crontab": "no crontab for sandbox_user",
+ "more": "--More-- (simulated pagination)",
+ "less": "/etc/passwd (simulated pager)",
+ }
+ if cmd_name == "ip" and args[:1] == ["addr"]:
+ return canned["ip"]
+ if cmd_name == "tail" or cmd_name == "head":
+ return self._simulate_head_tail(cmd_name, args)
+ if cmd_name == "which":
+ return canned["which"]
+ if cmd_name == "whereis":
+ return canned["whereis"]
+ if cmd_name == "export" and args and "=" in args[0]:
+ key, value = args[0].split("=", 1)
+ self.env[key] = value
+ return f"exported {key}={value}"
+ if cmd_name == "alias" and args and "=" in args[0]:
+ key, value = args[0].split("=", 1)
+ self.aliases[key] = value.strip("'\"")
+ return f"alias {key}='{self.aliases[key]}'"
+ if cmd_name == "bc":
+ expr = " ".join(args)
+ if "|" in expr:
+ match = re.search(r"'([^']+)'", expr)
+ if match:
+ try:
+ return str(eval(match.group(1), {"__builtins__": {}}, {}))
+ except Exception:
+ return "2"
+ return "2"
+ if cmd_name == "tar":
+ if "-czf" in args and len(args) >= 3:
+ archive = self.resolve_path(args[args.index("-czf") + 1])
+ self.fs[archive] = {"type": "file", "perm": "644", "content": "tarball"}
+ self._add_child(self.parent_dir(archive), archive.split("/")[-1])
+ if "-xzf" in args and "-C" in args:
+ dest = self.resolve_path(args[args.index("-C") + 1])
+ self._ensure_dir(dest)
+ return canned["tar"]
+ return canned.get(cmd_name, f"{cmd_name}: simulated")
+
+ # ---------- Main ----------
+ def execute(self, raw_cmd: str) -> dict:
+ cmd = raw_cmd.strip()
if not cmd:
return {"output": "", "success": True, "message": ""}
- # 记录历史
+ cmd = self._expand_alias(cmd)
self.history.append(cmd)
+ parts = self._safe_shell_split(cmd)
+ if not parts:
+ return {"output": "", "success": True, "message": ""}
+ cmd_name, args = parts[0], parts[1:]
- # 解析命令
- parts = cmd.split()
- cmd_name = parts[0]
- args = parts[1:]
+ if any(x in cmd for x in ["sudo rm -rf /", ":(){:|:&};:"]):
+ return {"output": "", "success": False, "message": "❌ 危险命令已拦截,沙盒环境禁止执行。"}
- # 安全检查
- # 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 == "cd":
+ output = self._simulate_cd(args)
elif cmd_name == "echo":
output = self._simulate_echo(args)
elif cmd_name == "cat":
output = self._simulate_cat(args)
+ elif cmd_name in {"head", "tail"}:
+ output = self._simulate_head_tail(cmd_name, 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":
@@ -550,32 +669,28 @@ Change: 2026-03-04 00:00:00.000000000
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 == "rm":
+ output = self._simulate_rm(args)
+ elif cmd_name == "chmod":
+ output = self._simulate_chmod(args)
+ elif cmd_name in {"chown", "chgrp"}:
+ output = self._simulate_chown_group(cmd_name, args)
elif cmd_name == "stat":
output = self._simulate_stat(args)
+ elif cmd_name == "whoami":
+ output = self.user
+ elif cmd_name == "history":
+ output = self._simulate_history(args)
else:
- output = f"{cmd_name}: command not found (sandbox mode)"
-
+ output = self._simulate_system_text(cmd_name, args)
except Exception as e:
- output = f"Error: {e}"
+ return {"output": f"Error: {e}", "success": False, "message": "❌ 执行失败"}
- return {"output": output, "success": True, "message": "✅ 命令执行成功"}
+ return {"output": output, "success": True, "message": "✅ 命令执行成功", "cwd": self.cwd}
-# ==============================
-# 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 /")) # 应该被拦截
+ print(sb.execute("mkdir -p /tmp/a/b"))
+ print(sb.execute("find /etc -name '*.conf'"))
diff --git a/server.py b/server.py
index 6ed635a..125a78d 100755
--- a/server.py
+++ b/server.py
@@ -1,285 +1,214 @@
#!/usr/bin/env python3
"""
-Linux 命令沙盒练习平台 Server
+Linux 命令沙盒练习平台 Server(增强版)
- 集成 sandbox.py 沙盒引擎
-- 路由:/ → HTML 页面, /api/run → 命令执行, /api/check → 任务检查
+- 提供课程、命令执行、任务判定、学习建议等 API
"""
+from __future__ import annotations
+
+import hashlib
import http.server
-import urllib.parse
import json
import os
-import base64
-import hashlib
+import urllib.parse
+from typing import Any
+
+from sandbox import LinuxSandbox
-# ==============================
-# 认证配置
-# ==============================
-# 预置用户:{username: password_hash}
-# 生成方式:hashlib.sha256(b"password").hexdigest()
USERS = {
- # 默认管理员账户
"admin": hashlib.sha256(b"safe_linux_2026").hexdigest(),
- # 可添加更多用户
}
-# Token 有效期(秒)
-TOKEN_TTL = 86400 # 24 小时
-
-
-def check_auth(request_body: bytes = None) -> tuple[bool, str]:
- """
- 检查 Authorization 请求头
- 支持: Bearer token 或 Basic auth
- """
- # 默认允许访问(未启用强制认证)
- # 启用:true 改为 True
- ENABLE_AUTH = False
-
- if not ENABLE_AUTH:
- return True, "admin"
-
- if not request_body:
- return False, "Missing Authorization header"
-
- # 解析请求头
- # 实际在 do_GET 中通过 headers 获取
- return True, "admin" # 暂时 Allow-All,后续优化
-
-# 导入沙盒模块
-from sandbox import LinuxSandbox
+TOKEN_TTL = 86400
+TASKS_FILE = os.path.join(os.path.dirname(__file__), "COURSE_TASKS.json")
+HTML_FILE = os.path.join(os.path.dirname(__file__), "index.html")
SANDBOX = LinuxSandbox()
-# ==============================
-# 任务数据加载
-# ==============================
-TASKS_FILE = os.path.join(os.path.dirname(__file__), "COURSE_TASKS.json")
-
-def load_tasks():
- """加载课程任务"""
+def load_tasks() -> dict[str, Any]:
try:
with open(TASKS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Error loading tasks: {e}")
- return {"levels": []}
+ return {"meta": {}, "levels": []}
TASKS = load_tasks()
-# ==============================
-# Handler 类
-# ==============================
+
+def find_task(task_id: str) -> dict[str, Any] | None:
+ for level in TASKS.get("levels", []):
+ for task in level.get("challenges", []):
+ if task.get("id") == task_id:
+ task["level_title"] = level.get("title", "")
+ task["level_id"] = level.get("id", "")
+ return task
+ return None
+
+
class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
- def send_json(self, data, status=200):
- """发送 JSON 响应"""
+ def log_message(self, format, *args):
+ pass
+
+ def send_json(self, data: dict[str, Any], status=200):
+ raw = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Content-Length", str(len(raw)))
self.end_headers()
- self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
+ self.wfile.write(raw)
- def send_text(self, text, status=200):
- """发送纯文本响应"""
- self.send_response(status)
- self.send_header("Content-Type", "text/plain; charset=utf-8")
+ def _send_file(self, path: str, content_type: str):
+ with open(path, "rb") as f:
+ content = f.read()
+ self.send_response(200)
+ self.send_header("Content-Type", content_type)
+ self.send_header("Content-Length", str(len(content)))
self.end_headers()
- self.wfile.write(text.encode("utf-8"))
+ self.wfile.write(content)
+
+ def check_auth(self, auth_header: str, token: str) -> bool:
+ if self.client_address[0] == "127.0.0.1":
+ return True
+ if token == "safe_linux_2026":
+ return True
+ if auth_header.startswith("Bearer ") and auth_header[7:] == "safe_linux_2026":
+ return True
+ return False
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
- # 🔐 认证检查(跳过 / 登录页 / API 登录)
- if path not in ["/", "/login.html", "/api/login", "/api/logout"]:
- # 检查 Cookie 或 Authorization
+ if path not in ["/", "/login.html", "/api/login", "/api/logout", "/api/tasks", "/api/health"]:
auth_header = self.headers.get("Authorization", "")
token = self.headers.get("X-Token", "")
-
- # 简化:如果带了有效 token 或直接 local 环境(127.0.0.1)则放行
- if not self.check_auth(auth_header, token):
- # 对公网域名强制认证
- if "xiaoxiaoluohao.indevs.in" in self.headers.get("Host", ""):
- self.send_json({"error": "Authentication required"}, 401)
- return
+ if not self.check_auth(auth_header, token) and "xiaoxiaoluohao.indevs.in" in self.headers.get("Host", ""):
+ self.send_json({"error": "Authentication required"}, 401)
+ return
- # 1. 主页:返回 HTML 页面
if path == "/":
- html_path = os.path.join(os.path.dirname(__file__), "index.html")
- try:
- with open(html_path, "rb") as f:
- self.send_response(200)
- self.send_header("Content-Type", "text/html; charset=utf-8")
- self.end_headers()
- self.wfile.write(f.read())
- except Exception as e:
- self.send_response(500)
- self.send_header("Content-Type", "text/plain")
- self.end_headers()
- self.wfile.write(f"Error: {e}".encode())
+ self._send_file(HTML_FILE, "text/html; charset=utf-8")
+ return
- # 2. 命令执行 API
- elif path == "/api/run":
+ if path == "/api/health":
+ self.send_json({"ok": True, "cwd": SANDBOX.cwd, "user": SANDBOX.user})
+ return
+
+ if path == "/api/run":
query = urllib.parse.parse_qs(parsed.query)
cmd = query.get("cmd", [""])[0]
if not cmd:
self.send_json({"error": "No command provided"}, 400)
return
-
- # 执行沙盒命令
result = SANDBOX.execute(cmd)
self.send_json(result)
+ return
- # 3. 任务检查 API
- elif path == "/api/check":
+ if path == "/api/check":
query = urllib.parse.parse_qs(parsed.query)
task_id = query.get("task_id", [""])[0]
-
+ cmd = query.get("last_cmd", [""])[0]
+ output = query.get("output", [""])[0]
+ sandbox_state = {
+ "cwd": SANDBOX.cwd,
+ "exists": SANDBOX.exists,
+ "is_executable": SANDBOX.is_executable,
+ "cmd": cmd,
+ "output": output,
+ }
if not task_id:
- self.send_json({"error": "-task_id required"}, 400)
+ self.send_json({"error": "task_id required"}, 400)
return
-
- # 查找任务
- task = None
- for level in TASKS.get("levels", []):
- for t in level.get("challenges", []):
- if t.get("id") == task_id:
- task = t
- break
- if task:
- break
-
+ task = find_task(task_id)
if not task:
self.send_json({"error": "Task not found"}, 404)
return
-
- # 获取当前命令执行结果
- current_cmd = query.get("last_cmd", [""])[0]
- current_output = query.get("output", [""])[0]
-
- # 检查是否匹配
- success = False
- message = task.get("fail_message", "❌ 未通过测试")
-
- # 尝试匹配 solution
- for sol in task.get("solution", []):
- if current_cmd.strip() == sol.strip():
- success = True
- message = "✅ 回答正确!🎉"
- break
-
- # 如果没匹配上,检查 success_test 逻辑
- if not success and "success_test" in task:
- if self.evaluate_test(task["success_test"], current_output):
- success = True
- message = "✅ 回答正确!🎉"
-
+ success, reason = self.evaluate_task(task, sandbox_state)
self.send_json({
"task_id": task_id,
"success": success,
- "message": message,
- "hint": task.get("hint", "💡 没有提示"),
+ "message": task.get("success_msg", "✅ 回答正确!") if success else reason or task.get("fail_message", "❌ 未通过测试"),
+ "hint": task.get("hint", "继续尝试,或者看看相关命令示例"),
"title": task.get("title", "未知任务"),
+ "next_suggestion": self.build_next_suggestion(task_id),
})
+ return
- # 4. 获取课程总览
- elif path == "/api/tasks":
+ if path == "/api/tasks":
self.send_json(TASKS)
+ return
- # 404
- else:
- self.send_response(404)
- self.send_header("Content-Type", "text/plain")
- self.end_headers()
- self.wfile.write(b"Not Found")
+ self.send_response(404)
+ self.end_headers()
+ self.wfile.write(b"Not Found")
- def check_auth(self, auth_header: str, token: str) -> bool:
- """认证检查:支持 Bearer token / X-Token header"""
- # 本地环境直接放行
- if self.client_address[0] == "127.0.0.1":
- return True
-
- # 公网场景:验证 token
- if token:
- # 简化版:验证预置 token(生产环境应加密存储)
- valid_tokens = ["safe_linux_2026"]
- return token in valid_tokens
-
- # Bearer token 支持
- if auth_header.startswith("Bearer "):
- token = auth_header[7:]
- return token in ["safe_linux_2026"]
-
- return False
-
- def evaluate_test(self, test_expr: str, output: str) -> bool:
- """简单评估 success_test 表达式"""
- try:
- # 支持简单的 if/contains 语法
- # if output contains 'xxx' then pass
- if "contains" in test_expr:
- parts = test_expr.split("'")
- if len(parts) >= 3:
- keyword = parts[1]
- return keyword in output
- elif "==" in test_expr:
- parts = test_expr.split("==")
- if len(parts) == 2:
- expected = parts[1].strip().strip("'")
- return output.strip() == expected
- return False
- except Exception:
- return False
-
- def log_message(self, format, *args):
- pass # Suppress logging
-
- # ==============================
- # Auth APIs
- # ==============================
def do_POST(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
+ content_length = int(self.headers.get("Content-Length", 0))
+ raw = self.rfile.read(content_length).decode() if content_length else "{}"
- # /api/login
if path == "/api/login":
- content_length = int(self.headers.get("Content-Length", 0))
- body = self.rfile.read(content_length).decode()
try:
- data = json.loads(body)
- username = data.get("username", "")
- password = data.get("password", "")
-
- # 简单验证
- if username in USERS:
- pwd_hash = hashlib.sha256(password.encode()).hexdigest()
- if pwd_hash == USERS[username]:
- # 返回成功 token
- self.send_json({
- "success": True,
- "token": "safe_linux_2026",
- "message": "✅ 登录成功!"
- })
- return
-
- self.send_json({"success": False, "error": "❌ 用户名或密码错误"}, 401)
- except Exception as e:
- self.send_json({"success": False, "error": str(e)}, 400)
+ data = json.loads(raw)
+ except Exception:
+ self.send_json({"success": False, "error": "Invalid JSON"}, 400)
+ return
+ username = data.get("username", "")
+ password = data.get("password", "")
+ if username in USERS and hashlib.sha256(password.encode()).hexdigest() == USERS[username]:
+ self.send_json({"success": True, "token": "safe_linux_2026", "message": "✅ 登录成功!"})
+ return
+ self.send_json({"success": False, "error": "❌ 用户名或密码错误"}, 401)
+ return
- # /api/logout
- elif path == "/api/logout":
+ if path == "/api/logout":
self.send_json({"success": True, "message": "👋 已退出登录"})
+ return
- else:
- self.send_response(404)
- self.end_headers()
- self.wfile.write(b"Not Found")
+ if path == "/api/reset":
+ SANDBOX.reset()
+ self.send_json({"success": True, "message": "♻️ 沙盒环境已重置"})
+ return
+
+ self.send_response(404)
+ self.end_headers()
+ self.wfile.write(b"Not Found")
+
+ def evaluate_task(self, task: dict[str, Any], state: dict[str, Any]) -> tuple[bool, str]:
+ cmd = state["cmd"]
+ output = state["output"]
+ success_test = task.get("success_test")
+ if task.get("solution"):
+ for sol in task["solution"]:
+ if cmd.strip() == sol.strip():
+ return True, ""
+ if not success_test:
+ return False, "❌ 还没达到任务要求"
+ try:
+ ok = bool(eval(success_test, {"__builtins__": {}}, state))
+ return ok, "❌ 命令执行了,但结果还没达到题目要求"
+ except Exception:
+ return False, "❌ 当前判题规则未命中,换个更准确的命令试试"
+
+ def build_next_suggestion(self, current_task_id: str) -> str | None:
+ all_tasks = []
+ for level in TASKS.get("levels", []):
+ all_tasks.extend(level.get("challenges", []))
+ for idx, task in enumerate(all_tasks):
+ if task.get("id") == current_task_id and idx + 1 < len(all_tasks):
+ nxt = all_tasks[idx + 1]
+ return f"下一题:{nxt.get('title', '继续挑战')}"
+ return None
if __name__ == "__main__":
PORT = 8084
- print(f"🌱 Linux Sandbox Practice Server with Auth 启动中... http://127.0.0.1:{PORT}")
+ print(f"🌱 Linux Sandbox Practice Server 启动中... http://127.0.0.1:{PORT}")
print("📚 地址: https://linux.xiaoxiaoluohao.indevs.in")
print("🔑 Account: admin / safe_linux_2026")
- http.server.HTTPServer(("127.0.0.1", PORT), LinuxSandboxHandler).serve_forever()
+ http.server.ThreadingHTTPServer(("127.0.0.1", PORT), LinuxSandboxHandler).serve_forever()