688 lines
26 KiB
Python
Executable File
688 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Linux learning lab HTTP server."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import http.server
|
|
import json
|
|
import os
|
|
import re
|
|
import urllib.parse
|
|
from typing import Any
|
|
|
|
from sandbox import LinuxSandbox
|
|
|
|
USERS = {
|
|
"admin": hashlib.sha256(b"safe_linux_2026").hexdigest(),
|
|
}
|
|
|
|
TASKS_FILE = os.path.join(os.path.dirname(__file__), "COURSE_TASKS.json")
|
|
HTML_FILE = os.path.join(os.path.dirname(__file__), "index.html")
|
|
PRIVACY_FILE = os.path.join(os.path.dirname(__file__), "privacy.html")
|
|
SANDBOX = LinuxSandbox()
|
|
|
|
PUBLIC_GET_PATHS = {
|
|
"/",
|
|
"/privacy",
|
|
"/privacy.html",
|
|
"/api/course",
|
|
"/api/course/search",
|
|
"/api/health",
|
|
"/api/lesson",
|
|
"/api/overview",
|
|
}
|
|
PUBLIC_POST_PATHS = {
|
|
"/api/login",
|
|
}
|
|
SAFE_REMOTE_HOST = "xiaoxiaoluohao.indevs.in"
|
|
|
|
|
|
def load_course() -> dict[str, Any]:
|
|
try:
|
|
with open(TASKS_FILE, "r", encoding="utf-8") as file:
|
|
return json.load(file)
|
|
except Exception as exc:
|
|
print(f"Error loading course: {exc}")
|
|
return {"meta": {}, "modules": []}
|
|
|
|
|
|
COURSE = load_course()
|
|
|
|
|
|
def looks_garbled(value: Any) -> bool:
|
|
if not isinstance(value, str) or not value.strip():
|
|
return False
|
|
if "\ufffd" in value:
|
|
return True
|
|
asciiish = sum(ch.isascii() and (ch.isalnum() or ch in " -_/.,:;`()[]{}'\"") for ch in value)
|
|
non_ascii = sum(not ch.isascii() for ch in value)
|
|
return non_ascii >= 3 and non_ascii > asciiish
|
|
|
|
|
|
def safe_text(value: Any, fallback: str) -> str:
|
|
if isinstance(value, str) and value.strip() and not looks_garbled(value):
|
|
return value.strip()
|
|
return fallback
|
|
|
|
|
|
def safe_list(values: Any, fallback: list[str]) -> list[str]:
|
|
if not isinstance(values, list) or not values:
|
|
return fallback
|
|
cleaned = [str(item).strip() for item in values if str(item).strip()]
|
|
if not cleaned:
|
|
return fallback
|
|
garbled = sum(1 for item in cleaned if looks_garbled(item))
|
|
if garbled >= max(1, len(cleaned) // 2):
|
|
return fallback
|
|
merged: list[str] = []
|
|
for index, item in enumerate(cleaned):
|
|
if looks_garbled(item):
|
|
if index < len(fallback):
|
|
merged.append(fallback[index])
|
|
continue
|
|
merged.append(item)
|
|
return merged or fallback
|
|
|
|
|
|
def split_commands(command: str | None, lesson_id: str | None = None) -> list[str]:
|
|
if command:
|
|
tokens = [part.strip() for part in re.split(r"\s*/\s*|\s*,\s*", command) if part.strip()]
|
|
if tokens:
|
|
return tokens
|
|
if lesson_id:
|
|
tail = lesson_id.split("_")[-1].strip()
|
|
if tail:
|
|
return [tail]
|
|
return []
|
|
|
|
|
|
def command_label(commands: list[str]) -> str:
|
|
if not commands:
|
|
return "mixed commands"
|
|
if len(commands) == 1:
|
|
return commands[0]
|
|
return " / ".join(commands[:3])
|
|
|
|
|
|
def module_number(module_id: str | None) -> str | None:
|
|
if not module_id:
|
|
return None
|
|
match = re.search(r"(\d+)", module_id)
|
|
return match.group(1) if match else None
|
|
|
|
|
|
def module_title(module: dict[str, Any]) -> str:
|
|
raw = module.get("title")
|
|
fallback = f"Module {module_number(module.get('id'))}" if module_number(module.get("id")) else "Course module"
|
|
return safe_text(raw, fallback)
|
|
|
|
|
|
def module_summary(module: dict[str, Any]) -> str:
|
|
fallback = "Learn command purpose, expected output, and how each step fits into a Linux troubleshooting flow."
|
|
return safe_text(module.get("summary"), fallback)
|
|
|
|
|
|
def lesson_title(lesson: dict[str, Any]) -> str:
|
|
commands = split_commands(lesson.get("command"), lesson.get("id"))
|
|
fallback = f"Learn {command_label(commands)}"
|
|
return safe_text(lesson.get("title"), fallback)
|
|
|
|
|
|
def default_goal(commands: list[str]) -> str:
|
|
return f"Understand what {command_label(commands)} does, how to run it, and how to read the result."
|
|
|
|
|
|
def default_why(commands: list[str]) -> str:
|
|
return f"In day-to-day operations work, {command_label(commands)} helps you confirm the current state before taking the next step."
|
|
|
|
|
|
def default_concepts(commands: list[str]) -> list[str]:
|
|
label = command_label(commands)
|
|
return [
|
|
f"What problem {label} solves",
|
|
"Typical syntax and common options",
|
|
"How to verify the output",
|
|
"How it fits into a troubleshooting chain",
|
|
]
|
|
|
|
|
|
def default_pitfalls(commands: list[str]) -> list[str]:
|
|
label = command_label(commands)
|
|
return [
|
|
f"Running {label} without checking the context first",
|
|
"Reading the output but not validating the target state",
|
|
"Ignoring current directory, permissions, or file state",
|
|
]
|
|
|
|
|
|
def default_scenarios(commands: list[str]) -> list[str]:
|
|
label = command_label(commands)
|
|
return [
|
|
f"Use {label} to inspect a host before editing files or services",
|
|
"Confirm current state before and after a change",
|
|
"Connect one command to the next action in a real incident flow",
|
|
]
|
|
|
|
|
|
def default_flow(commands: list[str]) -> list[str]:
|
|
label = command_label(commands)
|
|
return [
|
|
"Confirm the goal and current working context",
|
|
f"Run {label} and inspect the output carefully",
|
|
"Decide the next command based on what the output shows",
|
|
]
|
|
|
|
|
|
def default_takeaways(commands: list[str]) -> list[str]:
|
|
label = command_label(commands)
|
|
return [
|
|
f"Explain when to use {label} in a real Linux task",
|
|
"Know what output matters most",
|
|
"Link the command to the next verification or repair step",
|
|
]
|
|
|
|
|
|
def default_after_class(commands: list[str]) -> str:
|
|
return f"Repeat this lesson in the sandbox two or three more times and explain every step out loud using {command_label(commands)}."
|
|
|
|
|
|
def flatten_lessons() -> list[dict[str, Any]]:
|
|
rows: list[dict[str, Any]] = []
|
|
for module in COURSE.get("modules", []):
|
|
for lesson in module.get("lessons", []):
|
|
item = dict(lesson)
|
|
item["module_id"] = module.get("id")
|
|
item["module_title"] = module.get("title")
|
|
item["module_summary"] = module.get("summary")
|
|
rows.append(item)
|
|
return rows
|
|
|
|
|
|
def flatten_exercises() -> list[dict[str, Any]]:
|
|
rows: list[dict[str, Any]] = []
|
|
for module in COURSE.get("modules", []):
|
|
for lesson in module.get("lessons", []):
|
|
for exercise in lesson.get("exercises", []):
|
|
item = dict(exercise)
|
|
item["module_id"] = module.get("id")
|
|
item["module_title"] = module.get("title")
|
|
item["lesson_id"] = lesson.get("id")
|
|
item["lesson_title"] = lesson.get("title")
|
|
item["lesson_command"] = lesson.get("command")
|
|
rows.append(item)
|
|
return rows
|
|
|
|
|
|
def find_lesson(lesson_id: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
for module in COURSE.get("modules", []):
|
|
for lesson in module.get("lessons", []):
|
|
if lesson.get("id") == lesson_id:
|
|
return module, lesson
|
|
return None, None
|
|
|
|
|
|
def find_exercise(exercise_id: str) -> dict[str, Any] | None:
|
|
for exercise in flatten_exercises():
|
|
if exercise.get("id") == exercise_id:
|
|
return exercise
|
|
return None
|
|
|
|
|
|
def build_lesson_preview(module: dict[str, Any], lesson: dict[str, Any]) -> dict[str, Any]:
|
|
commands = split_commands(lesson.get("command"), lesson.get("id"))
|
|
return {
|
|
"id": lesson.get("id"),
|
|
"display_title": lesson_title(lesson),
|
|
"display_goal": safe_text(lesson.get("goal"), default_goal(commands)),
|
|
"command": lesson.get("command") or command_label(commands),
|
|
"command_tokens": commands,
|
|
"exercise_count": len(lesson.get("exercises", [])),
|
|
}
|
|
|
|
|
|
def build_module_preview(module: dict[str, Any]) -> dict[str, Any]:
|
|
lessons = [build_lesson_preview(module, lesson) for lesson in module.get("lessons", [])]
|
|
return {
|
|
"id": module.get("id"),
|
|
"display_title": module_title(module),
|
|
"display_summary": module_summary(module),
|
|
"lesson_count": len(module.get("lessons", [])),
|
|
"exercise_count": sum(len(lesson.get("exercises", [])) for lesson in module.get("lessons", [])),
|
|
"lessons": lessons,
|
|
}
|
|
|
|
|
|
def build_exercise(exercise: dict[str, Any], lesson_stub: dict[str, Any]) -> dict[str, Any]:
|
|
commands = split_commands(lesson_stub.get("command"), lesson_stub.get("id"))
|
|
label = command_label(commands)
|
|
first_solution = ""
|
|
if isinstance(exercise.get("solution"), list) and exercise["solution"]:
|
|
first_solution = str(exercise["solution"][0]).strip()
|
|
|
|
payload = {
|
|
"id": exercise.get("id"),
|
|
"type": exercise.get("type") or "operation",
|
|
"hint": safe_text(exercise.get("hint"), f"Try {first_solution or label} first, then inspect the result."),
|
|
"success_msg": safe_text(exercise.get("success_msg"), f"Nice work. You completed a {label} practice task."),
|
|
"solution": exercise.get("solution", []),
|
|
}
|
|
|
|
if payload["type"] == "operation":
|
|
payload["title"] = safe_text(exercise.get("title"), f"Run {first_solution or label}")
|
|
else:
|
|
payload["question"] = safe_text(
|
|
exercise.get("question"),
|
|
f"Explain when you would use {label} in a real Linux operations flow.",
|
|
)
|
|
payload["answer"] = safe_text(
|
|
exercise.get("answer"),
|
|
f"Focus on purpose, expected output, and the next step after {label}.",
|
|
)
|
|
|
|
return payload
|
|
|
|
|
|
def build_lesson_payload(module: dict[str, Any], lesson: dict[str, Any]) -> dict[str, Any]:
|
|
commands = split_commands(lesson.get("command"), lesson.get("id"))
|
|
all_lessons = flatten_lessons()
|
|
lesson_ids = [item.get("id") for item in all_lessons]
|
|
current_id = lesson.get("id")
|
|
index = lesson_ids.index(current_id) if current_id in lesson_ids else -1
|
|
|
|
previous_lesson = None
|
|
next_lesson = None
|
|
if index > 0:
|
|
prev_module, prev_lesson = find_lesson(lesson_ids[index - 1] or "")
|
|
if prev_module and prev_lesson:
|
|
previous_lesson = build_lesson_preview(prev_module, prev_lesson)
|
|
if index >= 0 and index + 1 < len(lesson_ids):
|
|
next_module, next_lesson_item = find_lesson(lesson_ids[index + 1] or "")
|
|
if next_module and next_lesson_item:
|
|
next_lesson = build_lesson_preview(next_module, next_lesson_item)
|
|
|
|
return {
|
|
"id": lesson.get("id"),
|
|
"display_title": lesson_title(lesson),
|
|
"display_goal": safe_text(lesson.get("goal"), default_goal(commands)),
|
|
"display_why": safe_text(lesson.get("why_it_matters"), default_why(commands)),
|
|
"display_module_title": module_title(module),
|
|
"display_module_summary": module_summary(module),
|
|
"command": lesson.get("command") or command_label(commands),
|
|
"command_tokens": commands,
|
|
"examples": safe_list(lesson.get("examples"), commands or ["pwd"]),
|
|
"concepts": safe_list(lesson.get("concepts"), default_concepts(commands)),
|
|
"pitfalls": safe_list(lesson.get("pitfalls"), default_pitfalls(commands)),
|
|
"scenarios": safe_list(lesson.get("scenarios"), default_scenarios(commands)),
|
|
"troubleshooting_flow": safe_list(lesson.get("troubleshooting_flow"), default_flow(commands)),
|
|
"takeaways": safe_list(lesson.get("takeaways"), default_takeaways(commands)),
|
|
"classic_view": safe_text(
|
|
lesson.get("classic_view"),
|
|
f"Treat {command_label(commands)} as part of a workflow, not as an isolated command to memorize.",
|
|
),
|
|
"after_class": safe_text(lesson.get("after_class"), default_after_class(commands)),
|
|
"related_commands": safe_list(lesson.get("related_commands"), commands or ["pwd"]),
|
|
"exercises": [build_exercise(exercise, lesson) for exercise in lesson.get("exercises", [])],
|
|
"exercise_count": len(lesson.get("exercises", [])),
|
|
"module_id": module.get("id"),
|
|
"previous_lesson": previous_lesson,
|
|
"next_lesson": next_lesson,
|
|
}
|
|
|
|
|
|
def build_overview() -> dict[str, Any]:
|
|
modules = [build_module_preview(module) for module in COURSE.get("modules", [])]
|
|
lessons = flatten_lessons()
|
|
commands = sorted(
|
|
{
|
|
command
|
|
for lesson in lessons
|
|
for command in split_commands(lesson.get("command"), lesson.get("id"))
|
|
if command
|
|
}
|
|
)
|
|
exercise_count = sum(len(lesson.get("exercises", [])) for lesson in lessons)
|
|
meta = COURSE.get("meta", {})
|
|
|
|
return {
|
|
"meta": {
|
|
"version": meta.get("version", "4.0"),
|
|
"title": safe_text(meta.get("title"), "Linux Learning Lab"),
|
|
"description": safe_text(
|
|
meta.get("description"),
|
|
"Searchable Linux lessons with command previews, sandbox practice, and guided troubleshooting.",
|
|
),
|
|
"updated": meta.get("updated", ""),
|
|
"module_count": meta.get("module_count", len(modules)),
|
|
"lesson_count": meta.get("total_lessons", len(lessons)),
|
|
"exercise_count": meta.get("total_exercises", exercise_count),
|
|
"command_count": len(commands),
|
|
},
|
|
"runtime": {
|
|
"cwd": SANDBOX.cwd,
|
|
"user": SANDBOX.user,
|
|
},
|
|
"modules": modules,
|
|
"commands": commands[:24],
|
|
}
|
|
|
|
|
|
def search_course(query: str) -> list[dict[str, Any]]:
|
|
needle = query.strip().lower()
|
|
if not needle:
|
|
return []
|
|
|
|
results: list[dict[str, Any]] = []
|
|
for module in COURSE.get("modules", []):
|
|
module_view = build_module_preview(module)
|
|
module_score = 0
|
|
module_id = str(module.get("id", "")).lower()
|
|
if needle == module_id:
|
|
module_score += 120
|
|
elif needle in module_id:
|
|
module_score += 80
|
|
if needle in module_view["display_title"].lower():
|
|
module_score += 60
|
|
if needle in module_view["display_summary"].lower():
|
|
module_score += 40
|
|
if module_score and module.get("lessons"):
|
|
results.append(
|
|
{
|
|
"type": "module",
|
|
"title": module_view["display_title"],
|
|
"subtitle": module_view["display_summary"],
|
|
"module_id": module.get("id"),
|
|
"lesson_id": module.get("lessons", [{}])[0].get("id"),
|
|
"score": module_score,
|
|
}
|
|
)
|
|
|
|
for lesson in module.get("lessons", []):
|
|
lesson_view = build_lesson_preview(module, lesson)
|
|
lesson_score = 0
|
|
targets = [
|
|
str(lesson.get("id", "")).lower(),
|
|
lesson_view["display_title"].lower(),
|
|
lesson_view["display_goal"].lower(),
|
|
str(lesson_view["command"]).lower(),
|
|
" ".join(token.lower() for token in lesson_view["command_tokens"]),
|
|
]
|
|
for text in targets:
|
|
if not text:
|
|
continue
|
|
if needle == text:
|
|
lesson_score += 110
|
|
elif needle in text:
|
|
lesson_score += 55
|
|
if lesson_score:
|
|
results.append(
|
|
{
|
|
"type": "lesson",
|
|
"title": lesson_view["display_title"],
|
|
"subtitle": f"{module_view['display_title']} | {command_label(lesson_view['command_tokens'])}",
|
|
"module_id": module.get("id"),
|
|
"lesson_id": lesson.get("id"),
|
|
"score": lesson_score,
|
|
}
|
|
)
|
|
|
|
for exercise in lesson.get("exercises", []):
|
|
exercise_view = build_exercise(exercise, lesson)
|
|
exercise_score = 0
|
|
for text in [
|
|
str(exercise.get("id", "")).lower(),
|
|
str(exercise_view.get("title", "")).lower(),
|
|
str(exercise_view.get("question", "")).lower(),
|
|
str(exercise_view.get("hint", "")).lower(),
|
|
]:
|
|
if not text:
|
|
continue
|
|
if needle == text:
|
|
exercise_score += 90
|
|
elif needle in text:
|
|
exercise_score += 40
|
|
if exercise_score:
|
|
results.append(
|
|
{
|
|
"type": "exercise",
|
|
"title": exercise_view.get("title") or exercise_view.get("question") or exercise.get("id", ""),
|
|
"subtitle": f"{lesson_view['display_title']} | {exercise.get('id', '')}",
|
|
"module_id": module.get("id"),
|
|
"lesson_id": lesson.get("id"),
|
|
"exercise_id": exercise.get("id"),
|
|
"score": exercise_score,
|
|
}
|
|
)
|
|
|
|
ranked = sorted(results, key=lambda item: (-item["score"], item["title"]))
|
|
return [{key: value for key, value in item.items() if key != "score"} for item in ranked[:24]]
|
|
|
|
|
|
class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
|
def log_message(self, format, *args):
|
|
pass
|
|
|
|
def send_json(self, data: Any, status: int = 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 file:
|
|
body = file.read()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", content_type)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def is_public_path(self, path: str, method: str) -> bool:
|
|
if method == "GET":
|
|
return path in PUBLIC_GET_PATHS
|
|
if method == "POST":
|
|
return path in PUBLIC_POST_PATHS
|
|
return False
|
|
|
|
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 require_auth_if_needed(self, path: str, method: str) -> bool:
|
|
if self.is_public_path(path, method):
|
|
return True
|
|
host = self.headers.get("Host", "")
|
|
auth_header = self.headers.get("Authorization", "")
|
|
token = self.headers.get("X-Token", "")
|
|
if SAFE_REMOTE_HOST in host and not self.check_auth(auth_header, token):
|
|
self.send_json({"error": "Authentication required"}, 401)
|
|
return False
|
|
return True
|
|
|
|
def do_GET(self):
|
|
parsed = urllib.parse.urlparse(self.path)
|
|
path = parsed.path
|
|
|
|
if not self.require_auth_if_needed(path, "GET"):
|
|
return
|
|
|
|
if path == "/":
|
|
self.send_file(HTML_FILE, "text/html; charset=utf-8")
|
|
return
|
|
|
|
if path in {"/privacy", "/privacy.html"}:
|
|
self.send_file(PRIVACY_FILE, "text/html; charset=utf-8")
|
|
return
|
|
|
|
if path == "/api/health":
|
|
overview = build_overview()
|
|
self.send_json(
|
|
{
|
|
"ok": True,
|
|
"cwd": SANDBOX.cwd,
|
|
"user": SANDBOX.user,
|
|
"module_count": overview["meta"]["module_count"],
|
|
"lesson_count": overview["meta"]["lesson_count"],
|
|
"exercise_count": overview["meta"]["exercise_count"],
|
|
}
|
|
)
|
|
return
|
|
|
|
if path == "/api/course":
|
|
self.send_json(COURSE)
|
|
return
|
|
|
|
if path == "/api/overview":
|
|
self.send_json(build_overview())
|
|
return
|
|
|
|
if path == "/api/lesson":
|
|
lesson_id = urllib.parse.parse_qs(parsed.query).get("id", [""])[0]
|
|
if not lesson_id:
|
|
self.send_json({"error": "id required"}, 400)
|
|
return
|
|
module, lesson = find_lesson(lesson_id)
|
|
if not module or not lesson:
|
|
self.send_json({"error": "Lesson not found"}, 404)
|
|
return
|
|
self.send_json({"lesson": build_lesson_payload(module, lesson)})
|
|
return
|
|
|
|
if path == "/api/course/search":
|
|
query = urllib.parse.parse_qs(parsed.query).get("q", [""])[0]
|
|
self.send_json({"query": query, "results": search_course(query)})
|
|
return
|
|
|
|
if path == "/api/run":
|
|
cmd = urllib.parse.parse_qs(parsed.query).get("cmd", [""])[0]
|
|
if not cmd:
|
|
self.send_json({"error": "No command provided"}, 400)
|
|
return
|
|
self.send_json(SANDBOX.execute(cmd))
|
|
return
|
|
|
|
if path == "/api/check":
|
|
query = urllib.parse.parse_qs(parsed.query)
|
|
exercise_id = query.get("exercise_id", [""])[0]
|
|
cmd = query.get("last_cmd", [""])[0]
|
|
output = query.get("output", [""])[0]
|
|
if not exercise_id:
|
|
self.send_json({"error": "exercise_id required"}, 400)
|
|
return
|
|
exercise = find_exercise(exercise_id)
|
|
if not exercise:
|
|
self.send_json({"error": "Exercise not found"}, 404)
|
|
return
|
|
|
|
state = {
|
|
"cmd": cmd,
|
|
"output": output,
|
|
"cwd": SANDBOX.cwd,
|
|
"exists": SANDBOX.exists,
|
|
"is_executable": SANDBOX.is_executable,
|
|
}
|
|
success, reason = self.evaluate_exercise(exercise, state)
|
|
lesson_stub = {"command": exercise.get("lesson_command"), "id": exercise.get("lesson_id")}
|
|
exercise_view = build_exercise(exercise, lesson_stub)
|
|
self.send_json(
|
|
{
|
|
"exercise_id": exercise_id,
|
|
"success": success,
|
|
"message": exercise_view["success_msg"] if success else reason,
|
|
"hint": exercise_view.get("hint"),
|
|
"lesson_title": safe_text(exercise.get("lesson_title"), lesson_title(lesson_stub)),
|
|
"module_title": safe_text(exercise.get("module_title"), "Course module"),
|
|
"next_suggestion": self.build_next_suggestion(exercise_id) if success else None,
|
|
}
|
|
)
|
|
return
|
|
|
|
self.send_json({"error": "Not found"}, 404)
|
|
|
|
def do_POST(self):
|
|
parsed = urllib.parse.urlparse(self.path)
|
|
path = parsed.path
|
|
|
|
if not self.require_auth_if_needed(path, "POST"):
|
|
return
|
|
|
|
content_length = int(self.headers.get("Content-Length", 0))
|
|
raw = self.rfile.read(content_length).decode("utf-8") 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("utf-8")).hexdigest() == USERS[username]:
|
|
self.send_json({"success": True, "token": "safe_linux_2026", "message": "Login succeeded"})
|
|
return
|
|
|
|
self.send_json({"success": False, "error": "Invalid username or password"}, 401)
|
|
return
|
|
|
|
if path == "/api/logout":
|
|
self.send_json({"success": True, "message": "Logged out"})
|
|
return
|
|
|
|
if path == "/api/reset":
|
|
SANDBOX.reset()
|
|
self.send_json({"success": True, "message": "Sandbox reset"})
|
|
return
|
|
|
|
self.send_json({"error": "Not found"}, 404)
|
|
|
|
def evaluate_exercise(self, exercise: dict[str, Any], state: dict[str, Any]) -> tuple[bool, str]:
|
|
exercise_type = exercise.get("type")
|
|
if exercise_type in {"understanding", "scenario"}:
|
|
return False, "This is a reflection task. Read the reference answer, then explain it in your own words."
|
|
|
|
cmd = state["cmd"].strip()
|
|
for solution in exercise.get("solution", []):
|
|
if cmd == str(solution).strip():
|
|
return True, ""
|
|
|
|
success_test = exercise.get("success_test")
|
|
if not success_test:
|
|
return False, "The command ran, but this task does not have an automated check yet."
|
|
|
|
try:
|
|
ok = bool(eval(success_test, {"__builtins__": {}}, state))
|
|
if ok:
|
|
return True, ""
|
|
return False, "The command ran, but the target state has not been reached yet."
|
|
except Exception:
|
|
return False, "The checker could not validate this attempt. Compare your command with the example and try again."
|
|
|
|
def build_next_suggestion(self, current_exercise_id: str) -> str | None:
|
|
rows = flatten_exercises()
|
|
for index, item in enumerate(rows):
|
|
if item.get("id") != current_exercise_id:
|
|
continue
|
|
if index + 1 >= len(rows):
|
|
return None
|
|
next_item = rows[index + 1]
|
|
lesson_stub = {"command": next_item.get("lesson_command"), "id": next_item.get("lesson_id")}
|
|
exercise_view = build_exercise(next_item, lesson_stub)
|
|
title = exercise_view.get("title") or exercise_view.get("question") or "the next exercise"
|
|
return f"Next up: {lesson_title(lesson_stub)} | {title}"
|
|
return None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
port = 8084
|
|
print(f"Linux Learning Lab is running at http://127.0.0.1:{port}")
|
|
print("Remote host: https://linux.xiaoxiaoluohao.indevs.in")
|
|
http.server.ThreadingHTTPServer(("127.0.0.1", port), LinuxLearningHandler).serve_forever()
|