#!/usr/bin/env python3 """Chrome Cache Viewer - A web tool for browsing Chrome's disk cache.""" import http.server import json import mimetypes import os import struct import sys import urllib.parse from datetime import datetime, timezone from pathlib import Path CACHE_DIR = Path.home() / "Library/Caches/Google/Chrome/Default/Cache/Cache_Data" SIMPLE_CACHE_MAGIC = 0xFCFB6D1BA7725C30 EOF_MAGIC = 0xF4FA6F45970D41D8 BODY_PREFIX_SIZE = 4 # Chrome adds a 4-byte prefix before the body data PORT = 8088 def parse_cache_entry(filepath): """Parse a Chrome Simple Cache entry file and return metadata.""" try: with open(filepath, "rb") as f: data = f.read() except OSError: return None if len(data) < 24: return None magic, version, key_len, key_hash = struct.unpack(" len(data): return None key_raw = data[20 : 20 + key_len] key_str = key_raw.lstrip(b"\x00").decode("utf-8", errors="replace") # Extract the actual URL from the partition key format # Format: "1/0/_dk_ " # or just a plain URL url = key_str if "_dk_" in key_str: parts = key_str.split(" ") url = parts[-1] if len(parts) >= 3 else parts[-1] elif key_str.startswith("1/0/"): url = key_str[4:] # Find the body region (between key end and first EOF magic) body_start = 20 + key_len eof_pos = None for i in range(body_start, min(body_start + 10_000_000, len(data) - 8)): if struct.unpack(" 0: hdr_end = data.find(b"\x00\x00", http_pos) if hdr_end < 0: hdr_end = min(http_pos + 4000, len(data)) for h in data[http_pos:hdr_end].split(b"\x00"): hs = h.decode("utf-8", errors="replace") if hs.startswith("HTTP/"): status_line = hs elif ":" in hs: name, _, value = hs.partition(":") headers[name.strip().lower()] = value.strip() content_type = headers.get("content-type", "") content_length = headers.get("content-length", "") # Body data: skip the 4-byte prefix body_offset = body_start + BODY_PREFIX_SIZE body_end = eof_pos if eof_pos else len(data) body_size = max(0, body_end - body_offset) # File modification time try: mtime = os.path.getmtime(filepath) modified = datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat() except OSError: modified = "" return { "filename": os.path.basename(filepath), "url": url, "full_key": key_str, "status": status_line, "content_type": content_type, "content_length": content_length, "body_size": body_size, "file_size": len(data), "modified": modified, "headers": headers, "body_offset": body_offset, "body_end": body_end, } def get_body_data(filepath, entry): """Extract the body data from a cache entry file.""" try: with open(filepath, "rb") as f: f.seek(entry["body_offset"]) return f.read(entry["body_end"] - entry["body_offset"]) except OSError: return b"" def scan_cache(limit=2000): """Scan the cache directory and return parsed entries.""" entries = [] cache_files = sorted( (f for f in CACHE_DIR.iterdir() if f.name.endswith("_0") and f.name != "index"), key=lambda f: f.stat().st_mtime, reverse=True, ) for fpath in cache_files[:limit]: entry = parse_cache_entry(fpath) if entry and entry["url"]: entries.append(entry) return entries HTML_PAGE = r""" Chrome Cache Viewer

Chrome Cache Viewer

Loading...
Type URL Status Size Modified
Select a cache entry to view details
""" class CacheViewerHandler(http.server.BaseHTTPRequestHandler): _entries_cache = None _entries_cache_time = 0 def log_message(self, fmt, *args): # Quiet logging pass def _set_headers(self, content_type="text/html", status=200): self.send_response(status) self.send_header("Content-Type", content_type) self.send_header("Cache-Control", "no-cache") self.end_headers() def do_GET(self): parsed = urllib.parse.urlparse(self.path) path = parsed.path if path == "/" or path == "": self._set_headers("text/html") self.wfile.write(HTML_PAGE.encode()) elif path == "/api/entries": # Cache the scan results for 30 seconds now = __import__("time").time() if ( CacheViewerHandler._entries_cache is None or now - CacheViewerHandler._entries_cache_time > 30 ): entries = scan_cache() CacheViewerHandler._entries_cache = entries CacheViewerHandler._entries_cache_time = now else: entries = CacheViewerHandler._entries_cache self._set_headers("application/json") # Send only what the frontend needs (exclude body_offset/body_end for listing) slim = [] for e in entries: slim.append( { "filename": e["filename"], "url": e["url"], "full_key": e["full_key"], "status": e["status"], "content_type": e["content_type"], "body_size": e["body_size"], "file_size": e["file_size"], "modified": e["modified"], "headers": e["headers"], } ) self.wfile.write(json.dumps(slim).encode()) elif path.startswith("/api/body/"): filename = path[len("/api/body/") :] if "/" in filename or ".." in filename: self._set_headers("text/plain", 400) self.wfile.write(b"Invalid filename") return filepath = CACHE_DIR / filename if not filepath.exists(): self._set_headers("text/plain", 404) self.wfile.write(b"Not found") return entry = parse_cache_entry(filepath) if not entry: self._set_headers("text/plain", 500) self.wfile.write(b"Could not parse cache entry") return body = get_body_data(filepath, entry) ct = entry["content_type"] or "application/octet-stream" # Strip charset for binary types mime = ct.split(";")[0].strip() self._set_headers(mime) self.wfile.write(body) else: self._set_headers("text/plain", 404) self.wfile.write(b"Not found") def main(): port = PORT if len(sys.argv) > 1: port = int(sys.argv[1]) server = http.server.HTTPServer(("127.0.0.1", port), CacheViewerHandler) print(f"Chrome Cache Viewer running at http://127.0.0.1:{port}") print(f"Cache directory: {CACHE_DIR}") print("Press Ctrl+C to stop") try: server.serve_forever() except KeyboardInterrupt: print("\nStopped.") server.server_close() if __name__ == "__main__": main()