#!/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/health", } PUBLIC_POST_PATHS = { "/api/login", "/api/logout", } 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 is_supported_command_token(command: str) -> bool: normalized = command.strip().lower() if not normalized: return False supported = SANDBOX.supported_commands() if normalized in supported: return True if "|" in normalized: segments = [segment.strip() for segment in normalized.split("|") if segment.strip()] return bool(segments) and all(segment in supported for segment in segments) return False 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)}." COMMAND_CATEGORIES = { "navigation": {"pwd", "ls", "cd", "which", "whereis", "clear"}, "filesystem": {"cat", "echo", "mkdir", "touch", "cp", "mv", "rm", "chmod", "stat", "tar"}, "text": {"grep", "find", "head", "tail", "sort", "uniq", "cut", "awk", "sed"}, "observation": {"ps", "top", "uptime", "free", "df", "du", "lsof", "last", "w", "history", "date", "cal"}, "service": {"systemctl", "journalctl", "nohup", "crontab"}, "network": {"ip", "ping", "ss", "curl", "wget", "netstat", "traceroute", "dig"}, "identity": {"whoami", "id", "env", "export", "alias"}, "package": {"apt", "yum", "rpm", "dpkg"}, } CATEGORY_ROLES = { "navigation": "Use this command family to regain orientation before acting.", "filesystem": "Use this command family to inspect, create, move, or protect files safely.", "text": "Use this command family to filter noisy output into evidence you can act on.", "observation": "Use this command family to understand what the host is doing right now.", "service": "Use this command family to control long-running workloads and read their execution history.", "network": "Use this command family to test reachability, sockets, HTTP responses, and DNS resolution.", "identity": "Use this command family to understand who you are, what your shell knows, and what your environment passes downstream.", "package": "Use this command family to inspect, install, or verify software that powers the host.", "general": "Use the base form first, then add options only when you know what evidence you need.", } CATEGORY_EXTENSION_TIPS = { "navigation": [ "Repeat the lesson from a second directory and explain why path context changes the next command.", "Practice saying the current directory out loud before every file-changing action.", ], "filesystem": [ "Run the read-only form first, then explain what would happen if you switched to a state-changing form.", "Create a small backup-and-verify routine so file work becomes reversible instead of risky.", ], "text": [ "Turn one noisy log or command output into a smaller evidence set with filters and field extraction.", "Compare broad search output with a narrower variant and explain which one is safer under pressure.", ], "observation": [ "Capture the one output field that matters most before jumping into repair actions.", "Explain how this observation command changes your next diagnostic step instead of stopping at raw output.", ], "service": [ "Link service state, listening port, and logs into a three-step incident playbook.", "Practice one failed-start scenario and one healthy-state verification sequence.", ], "network": [ "Distinguish host reachability, open port, HTTP response, and DNS lookup as separate layers.", "Run one command per network layer and explain what each result rules in or rules out.", ], "identity": [ "Check identity and environment before using sudo, editing config, or launching a process.", "Practice spotting the difference between shell state and system-wide state.", ], "package": [ "Trace one tool from package name to installed files to executable path.", "Explain how package inspection helps before upgrades, rollbacks, or incident recovery.", ], } COMMAND_ORIGINS = { "pwd": "pwd is short for 'print working directory'.", "ls": "ls is the compact Unix shorthand for 'list'.", "grep": "grep comes from the old ed command g/re/p, meaning globally search a regular expression and print matches.", "awk": "awk is named after Aho, Weinberger, and Kernighan, the language's original authors.", "sed": "sed stands for 'stream editor', highlighting that it edits text as a flowing stream.", "ps": "ps expands to 'process status'.", "df": "df expands to 'disk free'.", "du": "du expands to 'disk usage'.", "ss": "ss expands to 'socket statistics' and was designed as a faster successor to parts of netstat.", "lsof": "lsof expands to 'list open files'.", "nohup": "nohup literally means 'no hangup' because it ignores the hangup signal when a terminal closes.", "crontab": "crontab means 'cron table', the per-user schedule file for cron jobs.", "apt": "apt is short for 'Advanced Package Tool'.", "yum": "yum originally expanded to 'Yellowdog Updater, Modified'.", "dpkg": "dpkg is the low-level Debian package management tool.", "rpm": "rpm refers to the RPM package manager format and toolchain.", "dig": "dig is short for 'Domain Information Groper', a famously odd but memorable DNS tool name.", "curl": "curl is a client-side URL transfer tool; the name is intentionally short and terminal-friendly.", "wget": "wget historically comes from the idea of 'web get'.", "netstat": "netstat expands to 'network statistics'.", "systemctl": "systemctl is the systemd control command for units such as services, timers, and sockets.", "journalctl": "journalctl is the control/query tool for the systemd journal.", "whoami": "whoami reads like a sentence because it answers the most practical shell identity question directly.", "whereis": "whereis focuses on where a binary, source tree, and manual page live.", "which": "which answers which executable the shell would run first.", "ping": "ping was named after sonar ping sounds; the common acronym explanation is popular but not official.", } COMMAND_FUN_FACTS = { "pwd": "Strong operators use pwd almost like a seatbelt: quick, boring, and essential before riskier actions.", "ls": "Many incidents start with a wrong assumption about what exists, which is why ls remains a first-response habit.", "grep": "The command name looks cryptic until you learn its editor history, and then you start seeing Unix culture in miniature.", "awk": "awk is tiny in name but surprisingly powerful; many shell one-liners quietly depend on it for structured text work.", "sed": "sed shines when you need a quick surgical text transform without opening an editor.", "ss": "On modern Linux hosts, ss is often the better first stop than netstat for live socket checks.", "curl": "curl often becomes the bridge between 'the service is up' and 'the application is actually responding correctly'.", "systemctl": "systemctl teaches a crucial operations lesson: a process existing is not the same as a service being healthy.", "journalctl": "journalctl turns logs into a timeline, which is exactly what you need when a deployment fails under time pressure.", "nohup": "nohup is a good reminder that shell sessions and process lifetime are related, but not the same thing.", "dig": "dig is one of the most useful commands for proving that a problem is DNS, or proving that it is not DNS.", "history": "history is not only memory; it is a personal operations notebook showing how you solved similar problems before.", } COMMAND_VARIANTS = { "pwd": [ {"syntax": "pwd", "purpose": "Print the current logical working directory.", "example": "pwd"}, {"syntax": "pwd -P", "purpose": "Resolve symbolic links and show the physical path.", "example": "pwd -P"}, ], "ls": [ {"syntax": "ls -l", "purpose": "Show a long listing with permissions, owner, size, and time.", "example": "ls -l /var/log"}, {"syntax": "ls -la", "purpose": "Include hidden entries such as dotfiles.", "example": "ls -la ~"}, {"syntax": "ls -lh", "purpose": "Format file sizes in human-readable units.", "example": "ls -lh build"}, ], "cd": [ {"syntax": "cd /path", "purpose": "Jump to a specific directory.", "example": "cd /var/log"}, {"syntax": "cd -", "purpose": "Return to the previous directory quickly.", "example": "cd -"}, {"syntax": "cd ~", "purpose": "Go back to the current user's home directory.", "example": "cd ~"}, ], "cat": [ {"syntax": "cat file", "purpose": "Print the full contents of a file.", "example": "cat /etc/os-release"}, {"syntax": "cat -n file", "purpose": "Show line numbers while printing.", "example": "cat -n app.log"}, ], "echo": [ {"syntax": "echo text", "purpose": "Print a string or quick note.", "example": "echo hello"}, {"syntax": "echo $PATH", "purpose": "Inspect shell expansion and environment values.", "example": "echo $PATH"}, ], "mkdir": [ {"syntax": "mkdir dir", "purpose": "Create one directory.", "example": "mkdir logs"}, {"syntax": "mkdir -p a/b/c", "purpose": "Create parent directories in one run.", "example": "mkdir -p releases/2026/03"}, ], "touch": [ {"syntax": "touch file", "purpose": "Create an empty file or update its timestamp.", "example": "touch notes.txt"}, ], "cp": [ {"syntax": "cp src dest", "purpose": "Copy one file to another path.", "example": "cp app.conf app.conf.bak"}, {"syntax": "cp -r src dest", "purpose": "Copy a directory tree recursively.", "example": "cp -r site site.bak"}, {"syntax": "cp -a src dest", "purpose": "Preserve attributes while copying.", "example": "cp -a /etc/nginx nginx.backup"}, ], "mv": [ {"syntax": "mv old new", "purpose": "Rename or move a file or directory.", "example": "mv access.log archive.log"}, ], "rm": [ {"syntax": "rm file", "purpose": "Remove one file.", "example": "rm debug.log"}, {"syntax": "rm -r dir", "purpose": "Remove a directory tree recursively.", "example": "rm -r old_build"}, {"syntax": "rm -i file", "purpose": "Prompt before deleting.", "example": "rm -i important.txt"}, ], "chmod": [ {"syntax": "chmod 644 file", "purpose": "Set common read/write file permissions.", "example": "chmod 644 app.conf"}, {"syntax": "chmod +x script.sh", "purpose": "Add execute permission.", "example": "chmod +x deploy.sh"}, ], "stat": [ {"syntax": "stat file", "purpose": "Inspect inode, timestamps, and file metadata.", "example": "stat access.log"}, ], "grep": [ {"syntax": "grep pattern file", "purpose": "Search matching lines.", "example": "grep ERROR app.log"}, {"syntax": "grep -n pattern file", "purpose": "Show line numbers with matches.", "example": "grep -n timeout app.log"}, {"syntax": "grep -i pattern file", "purpose": "Ignore letter case.", "example": "grep -i failed auth.log"}, ], "find": [ {"syntax": "find /path -name pattern", "purpose": "Search by file or directory name.", "example": "find /var/log -name '*.log'"}, {"syntax": "find /path -type f", "purpose": "Restrict the search to files.", "example": "find . -type f"}, {"syntax": "find /path -mtime -1", "purpose": "Filter by recent modification time.", "example": "find /backup -mtime -1"}, ], "head": [ {"syntax": "head file", "purpose": "Show the first 10 lines.", "example": "head app.log"}, {"syntax": "head -n 20 file", "purpose": "Show a custom number of leading lines.", "example": "head -n 20 app.log"}, ], "tail": [ {"syntax": "tail file", "purpose": "Show the last 10 lines.", "example": "tail app.log"}, {"syntax": "tail -n 50 file", "purpose": "Show more trailing lines.", "example": "tail -n 50 app.log"}, {"syntax": "tail -f file", "purpose": "Follow a log while it grows.", "example": "tail -f /var/log/nginx/access.log"}, ], "sort": [ {"syntax": "sort file", "purpose": "Sort text lines.", "example": "sort hosts.txt"}, {"syntax": "sort -u file", "purpose": "Sort and deduplicate in one step.", "example": "sort -u users.txt"}, ], "uniq": [ {"syntax": "uniq file", "purpose": "Collapse adjacent duplicates.", "example": "uniq users.txt"}, {"syntax": "sort file | uniq -c", "purpose": "Count repeated values after sorting.", "example": "sort status.txt | uniq -c"}, ], "cut": [ {"syntax": "cut -d: -f1 file", "purpose": "Pick fields using a delimiter.", "example": "cut -d: -f1 /etc/passwd"}, ], "awk": [ {"syntax": "awk '{print $1}' file", "purpose": "Print one or more whitespace-separated fields.", "example": "awk '{print $1}' access.log"}, {"syntax": "awk -F: '{print $1}' file", "purpose": "Set a custom field separator.", "example": "awk -F: '{print $1}' /etc/passwd"}, ], "sed": [ {"syntax": "sed -n '1,10p' file", "purpose": "Print only selected line ranges.", "example": "sed -n '1,10p' app.log"}, {"syntax": "sed 's/old/new/' file", "purpose": "Replace text in the output stream.", "example": "sed 's/http/https/' app.conf"}, ], "ps": [ {"syntax": "ps -ef", "purpose": "Show a full process list.", "example": "ps -ef"}, {"syntax": "ps -ef | grep name", "purpose": "Filter for one process family.", "example": "ps -ef | grep nginx"}, ], "top": [ {"syntax": "top", "purpose": "Open an interactive live process view.", "example": "top"}, {"syntax": "top -b -n 1", "purpose": "Capture one batch snapshot for logs or pipes.", "example": "top -b -n 1 | head -20"}, ], "ip": [ {"syntax": "ip addr", "purpose": "Show interfaces and assigned IP addresses.", "example": "ip addr"}, {"syntax": "ip route", "purpose": "Inspect the routing table.", "example": "ip route"}, ], "ping": [ {"syntax": "ping host", "purpose": "Test reachability continuously.", "example": "ping 8.8.8.8"}, {"syntax": "ping -c 4 host", "purpose": "Send a fixed number of probes.", "example": "ping -c 4 example.com"}, ], "ss": [ {"syntax": "ss -lntp", "purpose": "Show listening TCP ports and owning processes.", "example": "ss -lntp"}, {"syntax": "ss -s", "purpose": "Print a compact socket summary.", "example": "ss -s"}, ], "curl": [ {"syntax": "curl URL", "purpose": "Fetch a URL quickly from the terminal.", "example": "curl http://localhost:8080/health"}, {"syntax": "curl -I URL", "purpose": "Request response headers only.", "example": "curl -I https://example.com"}, {"syntax": "curl -X POST URL", "purpose": "Send a request with an explicit HTTP method.", "example": "curl -X POST http://localhost:8080/api/login"}, ], "wget": [ {"syntax": "wget URL", "purpose": "Download a file to disk.", "example": "wget https://example.com/file.tar.gz"}, {"syntax": "wget -O name URL", "purpose": "Choose the output filename.", "example": "wget -O app.tar.gz https://example.com/latest.tar.gz"}, ], "systemctl": [ {"syntax": "systemctl status name", "purpose": "Inspect service state and recent status text.", "example": "systemctl status nginx"}, {"syntax": "systemctl restart name", "purpose": "Restart a service after config or code changes.", "example": "systemctl restart nginx"}, {"syntax": "systemctl enable name", "purpose": "Enable a unit at boot time.", "example": "systemctl enable nginx"}, ], "journalctl": [ {"syntax": "journalctl -u name", "purpose": "Read logs for one systemd unit.", "example": "journalctl -u nginx"}, {"syntax": "journalctl -xe", "purpose": "Show recent errors with context.", "example": "journalctl -xe"}, {"syntax": "journalctl -u name -n 50", "purpose": "Read the latest lines for one unit.", "example": "journalctl -u nginx -n 50"}, ], "df": [ {"syntax": "df -h", "purpose": "Show filesystem usage in human units.", "example": "df -h"}, {"syntax": "df -i", "purpose": "Check inode exhaustion, not just bytes.", "example": "df -i"}, ], "du": [ {"syntax": "du -sh path", "purpose": "Summarize directory size.", "example": "du -sh /var/log"}, {"syntax": "du -h path", "purpose": "Break size down recursively.", "example": "du -h /var/log | sort -h | tail"}, ], "whoami": [ {"syntax": "whoami", "purpose": "Print the current account name.", "example": "whoami"}, ], "id": [ {"syntax": "id", "purpose": "Show UID, GID, and groups.", "example": "id"}, {"syntax": "id user", "purpose": "Inspect another account if the environment allows it.", "example": "id nginx"}, ], "env": [ {"syntax": "env", "purpose": "Print environment variables.", "example": "env"}, {"syntax": "env | grep NAME", "purpose": "Filter the environment for one variable.", "example": "env | grep PATH"}, ], "export": [ {"syntax": "export VAR=value", "purpose": "Set an environment variable for child processes.", "example": "export APP_ENV=prod"}, {"syntax": "export PATH=$PATH:/opt/bin", "purpose": "Extend PATH temporarily in the current shell.", "example": "export PATH=$PATH:/opt/bin"}, ], "alias": [ {"syntax": "alias", "purpose": "List current shell aliases.", "example": "alias"}, {"syntax": "alias ll='ls -alF'", "purpose": "Create a shortcut for a repeated command.", "example": "alias ll='ls -alF'"}, ], "which": [ {"syntax": "which cmd", "purpose": "Show the executable the shell would run first.", "example": "which python"}, ], "whereis": [ {"syntax": "whereis cmd", "purpose": "Locate binary, source, and manual page paths.", "example": "whereis nginx"}, ], "apt": [ {"syntax": "apt search name", "purpose": "Search repositories for packages.", "example": "apt search nginx"}, {"syntax": "apt install name", "purpose": "Install a package.", "example": "apt install nginx"}, {"syntax": "apt remove name", "purpose": "Remove a package.", "example": "apt remove nginx"}, ], "yum": [ {"syntax": "yum search name", "purpose": "Search repositories.", "example": "yum search nginx"}, {"syntax": "yum install name", "purpose": "Install a package.", "example": "yum install nginx"}, ], "rpm": [ {"syntax": "rpm -qa", "purpose": "List installed RPM packages.", "example": "rpm -qa | grep nginx"}, {"syntax": "rpm -qi name", "purpose": "Inspect one package in detail.", "example": "rpm -qi nginx"}, ], "dpkg": [ {"syntax": "dpkg -l", "purpose": "List installed Debian packages.", "example": "dpkg -l | grep nginx"}, {"syntax": "dpkg -L name", "purpose": "List files installed by one package.", "example": "dpkg -L nginx"}, ], "tar": [ {"syntax": "tar -tf file.tar", "purpose": "List archive contents without extracting.", "example": "tar -tf backup.tar"}, {"syntax": "tar -czf out.tar.gz dir", "purpose": "Create a gzip-compressed archive.", "example": "tar -czf logs.tar.gz /var/log/myapp"}, {"syntax": "tar -xzf file.tar.gz", "purpose": "Extract a gzip-compressed archive.", "example": "tar -xzf logs.tar.gz"}, ], "nohup": [ {"syntax": "nohup cmd &", "purpose": "Keep a process alive after terminal disconnect.", "example": "nohup python3 server.py &"}, ], "uptime": [ {"syntax": "uptime", "purpose": "Show uptime and load averages.", "example": "uptime"}, ], "free": [ {"syntax": "free -h", "purpose": "Show memory usage in human units.", "example": "free -h"}, ], "lsof": [ {"syntax": "lsof -i :port", "purpose": "Find the process using a port.", "example": "lsof -i :8080"}, {"syntax": "lsof file", "purpose": "Find which process has a file open.", "example": "lsof /var/log/app.log"}, ], "netstat": [ {"syntax": "netstat -lntp", "purpose": "Legacy view of listening TCP ports.", "example": "netstat -lntp"}, ], "traceroute": [ {"syntax": "traceroute host", "purpose": "Trace network hops to a target host.", "example": "traceroute example.com"}, ], "dig": [ {"syntax": "dig name", "purpose": "Query DNS records with full output.", "example": "dig example.com"}, {"syntax": "dig name +short", "purpose": "Print compact answers only.", "example": "dig example.com +short"}, ], "date": [ {"syntax": "date", "purpose": "Print the current system time.", "example": "date"}, {"syntax": "date '+%F %T'", "purpose": "Format the output explicitly.", "example": "date '+%F %T'"}, ], "cal": [ {"syntax": "cal", "purpose": "Show the current month calendar.", "example": "cal"}, {"syntax": "cal 2026", "purpose": "Show a whole year calendar.", "example": "cal 2026"}, ], "crontab": [ {"syntax": "crontab -l", "purpose": "List current cron jobs.", "example": "crontab -l"}, {"syntax": "crontab -e", "purpose": "Edit the cron table.", "example": "crontab -e"}, ], "history": [ {"syntax": "history", "purpose": "Show previous shell commands.", "example": "history"}, {"syntax": "history | grep word", "purpose": "Search for a prior command pattern.", "example": "history | grep tar"}, ], "last": [ {"syntax": "last", "purpose": "Show recent login records.", "example": "last"}, ], "w": [ {"syntax": "w", "purpose": "Show who is logged in and what they are doing.", "example": "w"}, ], "clear": [ {"syntax": "clear", "purpose": "Clear terminal output without changing shell state.", "example": "clear"}, ], } def command_category(command: str) -> str: normalized = command.strip().lower() for category, commands in COMMAND_CATEGORIES.items(): if normalized in commands: return category return "general" def command_origin(command: str) -> str: normalized = command.strip().lower() return COMMAND_ORIGINS.get( normalized, f"{normalized} follows the classic Unix habit of using short names so repeated terminal work stays fast.", ) def command_fun_fact(command: str) -> str: normalized = command.strip().lower() return COMMAND_FUN_FACTS.get( normalized, f"Treat {normalized} as part of a command chain: its real value appears when it informs the next observation or repair step.", ) def generic_variant(command: str) -> list[dict[str, str]]: normalized = command.strip().lower() if normalized in {"cd", "export", "alias"}: return [ { "syntax": f"help {normalized}", "purpose": "Inspect builtin shell help before using unfamiliar forms.", "example": f"help {normalized}", } ] return [ { "syntax": f"{normalized} --help", "purpose": "Review the supported options before using the command in a new situation.", "example": f"{normalized} --help", } ] def command_variants(command: str) -> list[dict[str, str]]: normalized = command.strip().lower() return COMMAND_VARIANTS.get(normalized, generic_variant(normalized)) def build_command_guides(commands: list[str]) -> list[dict[str, Any]]: guides: list[dict[str, Any]] = [] seen: set[str] = set() for command in commands: normalized = command.strip().lower() if not normalized or normalized in seen: continue seen.add(normalized) guides.append( { "command": normalized, "role": CATEGORY_ROLES.get(command_category(normalized), CATEGORY_ROLES["general"]), "origin": command_origin(normalized), "fun_fact": command_fun_fact(normalized), "variants": command_variants(normalized), } ) return guides def build_ops_extensions(commands: list[str]) -> list[str]: items: list[str] = [] seen: set[str] = set() for command in commands: category = command_category(command) for tip in CATEGORY_EXTENSION_TIPS.get(category, []): if tip not in seen: items.append(tip) seen.add(tip) closing_notes = [ "Run the simplest read-only form first, then explain what stronger or riskier form you would use only if the evidence demands it.", "Write a tiny playbook note for this lesson: goal -> command -> key output -> next step.", ] for note in closing_notes: if note not in seen: items.append(note) seen.add(note) return items[:6] def build_review_prompts(commands: list[str], lesson_goal: str) -> list[str]: label = command_label(commands) return [ f"What system question are you trying to answer with {label} before you touch the system state?", f"Which output line, field, or signal matters most for this goal: {lesson_goal}", f"What narrower or safer syntax variant would you try first if the default output feels too broad or too risky?", f"After {label}, what should the next verification, comparison, or repair step be in a real operations flow?", ] 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", [])] exercises = [exercise for lesson in module.get("lessons", []) for exercise in lesson.get("exercises", [])] operations = [exercise for exercise in exercises if exercise.get("type") == "operation"] reflections = [exercise for exercise in exercises if exercise.get("type") != "operation"] commands = sorted( { command for lesson in module.get("lessons", []) for command in split_commands(lesson.get("command"), lesson.get("id")) if command } ) return { "id": module.get("id"), "display_title": module_title(module), "display_summary": module_summary(module), "lesson_count": len(module.get("lessons", [])), "exercise_count": len(exercises), "operation_count": len(operations), "reflection_count": len(reflections), "command_count": len(commands), "command_tokens": commands, "lessons": lessons, } def extract_solution_commands() -> list[str]: commands: list[str] = [] for module in COURSE.get("modules", []): for lesson in module.get("lessons", []): for exercise in lesson.get("exercises", []): for solution in exercise.get("solution", []): for segment in str(solution).split("|"): head = segment.strip().split(" ")[0].strip() if head: commands.append(head) return commands def build_diagnostics() -> dict[str, Any]: lessons = flatten_lessons() course_commands = sorted( { command for lesson in lessons for command in split_commands(lesson.get("command"), lesson.get("id")) if command } ) solution_commands = sorted(set(extract_solution_commands())) required_commands = sorted(set(course_commands) | set(solution_commands)) supported_commands = sorted(SANDBOX.supported_commands()) unsupported_commands = [command for command in required_commands if not is_supported_command_token(command)] total_exercises = sum(len(lesson.get("exercises", [])) for lesson in lessons) operation_exercises = sum( 1 for lesson in lessons for exercise in lesson.get("exercises", []) if exercise.get("type") == "operation" ) reflection_exercises = total_exercises - operation_exercises return { "supported_command_count": len(supported_commands), "required_command_count": len(required_commands), "covered_command_count": len(required_commands) - len(unsupported_commands), "coverage_rate": 100 if not required_commands else round(((len(required_commands) - len(unsupported_commands)) / len(required_commands)) * 100), "unsupported_commands": unsupported_commands, "course_commands": required_commands, "operation_exercises": operation_exercises, "reflection_exercises": reflection_exercises, "module_checks": [build_module_preview(module) for module in COURSE.get("modules", [])], } 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 goal = safe_text(lesson.get("goal"), default_goal(commands)) 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": goal, "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"]), "command_guides": build_command_guides(commands), "ops_extensions": build_ops_extensions(commands), "review_prompts": build_review_prompts(commands, goal), "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", {}) diagnostics = build_diagnostics() 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, }, "diagnostics": { "coverage_rate": diagnostics["coverage_rate"], "unsupported_count": len(diagnostics["unsupported_commands"]), "operation_exercises": diagnostics["operation_exercises"], "reflection_exercises": diagnostics["reflection_exercises"], }, "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 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 auth_header = self.headers.get("Authorization", "") token = self.headers.get("X-Token", "") if 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"], "coverage_rate": overview["diagnostics"]["coverage_rate"], } ) return if path == "/api/course": self.send_json(COURSE) return if path == "/api/overview": self.send_json(build_overview()) return if path == "/api/diagnostics": self.send_json(build_diagnostics()) 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()