215 lines
7.7 KiB
Python
Executable File
215 lines
7.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Linux 命令沙盒练习平台 Server(增强版)
|
||
- 集成 sandbox.py 沙盒引擎
|
||
- 提供课程、命令执行、任务判定、学习建议等 API
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import http.server
|
||
import json
|
||
import os
|
||
import urllib.parse
|
||
from typing import Any
|
||
|
||
from sandbox import LinuxSandbox
|
||
|
||
USERS = {
|
||
"admin": hashlib.sha256(b"safe_linux_2026").hexdigest(),
|
||
}
|
||
|
||
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()
|
||
|
||
|
||
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 {"meta": {}, "levels": []}
|
||
|
||
|
||
TASKS = load_tasks()
|
||
|
||
|
||
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 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(raw)
|
||
|
||
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(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
|
||
|
||
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", "")
|
||
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
|
||
|
||
if path == "/":
|
||
self._send_file(HTML_FILE, "text/html; charset=utf-8")
|
||
return
|
||
|
||
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
|
||
|
||
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)
|
||
return
|
||
task = find_task(task_id)
|
||
if not task:
|
||
self.send_json({"error": "Task not found"}, 404)
|
||
return
|
||
success, reason = self.evaluate_task(task, sandbox_state)
|
||
self.send_json({
|
||
"task_id": task_id,
|
||
"success": success,
|
||
"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
|
||
|
||
if path == "/api/tasks":
|
||
self.send_json(TASKS)
|
||
return
|
||
|
||
self.send_response(404)
|
||
self.end_headers()
|
||
self.wfile.write(b"Not Found")
|
||
|
||
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 "{}"
|
||
|
||
if path == "/api/login":
|
||
try:
|
||
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
|
||
|
||
if path == "/api/logout":
|
||
self.send_json({"success": True, "message": "👋 已退出登录"})
|
||
return
|
||
|
||
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 启动中... http://127.0.0.1:{PORT}")
|
||
print("📚 地址: https://linux.xiaoxiaoluohao.indevs.in")
|
||
print("🔑 Account: admin / safe_linux_2026")
|
||
http.server.ThreadingHTTPServer(("127.0.0.1", PORT), LinuxSandboxHandler).serve_forever()
|