#!/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()