#!/usr/bin/env python3 """Offline server for the Trivy documentation mirror (Python 3.9+ stdlib only). python3 serve.py [--host 127.0.0.1] [--port 8080] Then open http://127.0.0.1:8080/ in a browser. """ import argparse import http.server import os import posixpath import sys import urllib.parse ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "site") HOME = "/docs/latest/guide/" NOT_FOUND = os.path.join(ROOT, "docs", "latest", "404.html") # Mirrored release; tools/mirror.sh updates this line. VERSION = "v0.74" # Same behaviour as www.trivy.dev: the bare docs roots redirect to the guide. REDIRECTS = {"/", "/docs", "/docs/", "/docs/latest", "/docs/latest/"} # The version selector links to /docs//; only the latest version is mirrored. VERSION_ALIASES = (f"/docs/{VERSION}",) # 404.html loads its CSS/JS from absolute //assets/... paths. INTERNAL_PREFIX = f"/{VERSION}/" class Handler(http.server.SimpleHTTPRequestHandler): extensions_map = { **http.server.SimpleHTTPRequestHandler.extensions_map, ".js": "text/javascript", ".json": "application/json", ".woff2": "font/woff2", ".woff": "font/woff", ".svg": "image/svg+xml", ".xml": "application/xml", } def __init__(self, *args, **kwargs): super().__init__(*args, directory=ROOT, **kwargs) def _redirect(self, location): self.send_response(302) self.send_header("Location", location) self.send_header("Content-Length", "0") self.end_headers() def _route(self): """Return True if the request was fully handled here.""" parts = urllib.parse.urlsplit(self.path) path = urllib.parse.unquote(parts.path) query = f"?{parts.query}" if parts.query else "" if path in REDIRECTS: self._redirect(HOME) return True # On an unknown top-level URL the 404 page resolves these against "/". if path == "/versions.json": self.path = "/docs/versions.json" elif path.startswith("/search/"): self.path = "/docs/latest" + self.path elif path.startswith(INTERNAL_PREFIX): self.path = "/docs/latest/" + self.path[len(INTERNAL_PREFIX):] parts = urllib.parse.urlsplit(self.path) path = urllib.parse.unquote(parts.path) for alias in VERSION_ALIASES: if path == alias or path.startswith(alias + "/"): self._redirect("/docs/latest" + path[len(alias):] + query) return True fs_path = self.translate_path(parts.path) if os.path.isdir(fs_path): if not path.endswith("/"): self._redirect(parts.path + "/" + query) return True fs_path = os.path.join(fs_path, "index.html") if not os.path.isfile(fs_path): self._send_404() return True return False def _send_404(self): body = b"404 Not Found" ctype = "text/plain; charset=utf-8" if os.path.isfile(NOT_FOUND): with open(NOT_FOUND, "rb") as f: body = f.read() ctype = "text/html; charset=utf-8" self.send_response(404) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.end_headers() if self.command != "HEAD": self.wfile.write(body) def do_GET(self): if not self._route(): super().do_GET() def do_HEAD(self): if not self._route(): super().do_HEAD() def list_directory(self, path): # never expose directory listings self._send_404() return None def log_message(self, fmt, *args): if not self.server.quiet: super().log_message(fmt, *args) def main(): ap = argparse.ArgumentParser(description="Serve the offline Trivy docs mirror.") ap.add_argument("--host", default="127.0.0.1", help="bind address (default 127.0.0.1; 0.0.0.0 for LAN)") ap.add_argument("--port", type=int, default=8080, help="port (default 8080)") ap.add_argument("--quiet", action="store_true", help="do not log each request") args = ap.parse_args() if not os.path.isfile(os.path.join(ROOT, "docs", "latest", "guide", "index.html")): sys.exit(f"error: mirror not found under {ROOT}") server = http.server.ThreadingHTTPServer((args.host, args.port), Handler) server.quiet = args.quiet shown = "127.0.0.1" if args.host in ("0.0.0.0", "") else args.host print(f"Serving Trivy docs from {ROOT}") print(f"Open http://{shown}:{args.port}/ (Ctrl+C to stop)") try: server.serve_forever() except KeyboardInterrupt: print("\nstopped") finally: server.server_close() if __name__ == "__main__": main()