sweedworks

← all sources

checklive.py

Compares served bytes against what was generated

136 lines. This is the file the build actually runs, copied verbatim at build time.

"""Compare what I generate against what is actually served.

Cloudflare injects a bot-detection script into HTML in transit. I missed it for
days because I only ever checked the files I wrote — the injection is invisible
from disk. This script diffs the served bytes against the local file and reports
anything added, so a change in what sits in front of this domain shows up as a
build finding rather than as a false claim on the privacy page.

Needs real network access to the live domain, so it is not part of build.sh:
run it after deploying.
    python3 checklive.py
"""

import os
import re
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, ".."))
BASE = "https://sweedworks.com"

PAGES = [("/", "index.html"),
         ("/vocabulary/", "vocabulary/index.html"),
         ("/predict/", "predict/index.html"),
         ("/cost/", "cost/index.html"),
         ("/learn/", "learn/index.html"),
         ("/tokens/", "tokens/index.html"),
         ("/about/", "about/index.html")]

# Injections we already know about and have disclosed on /about/.
KNOWN = [
    (r"__CF\$cv\$params", "Cloudflare bot-detection script (disclosed)"),
    (r"cdn-cgi/challenge-platform", "Cloudflare challenge platform (disclosed)"),
]


def fetch(path):
    """Headers and body to separate files.

    Do not go back to `-D -` with text=True: universal-newline translation turns
    the CRLF header/body separator into LF, the partition silently fails, the
    body comes back empty, and every comparison below then "passes" against
    nothing. This check reported all-clear that way while the injected script
    was plainly there.
    """
    import tempfile
    with tempfile.TemporaryDirectory() as tmp:
        hp = os.path.join(tmp, "h")
        bp = os.path.join(tmp, "b")
        subprocess.run(
            ["curl", "-sS", "-D", hp, "-o", bp, f"{BASE}{path}?livecheck=1"],
            capture_output=True, timeout=60, check=True)
        head = open(hp, encoding="utf-8", errors="replace").read()
        body = open(bp, encoding="utf-8", errors="replace").read()
    if not body:
        raise RuntimeError(f"empty body for {path} — the check would be vacuous")
    return head, body


def main():
    problems, notes = [], []

    for url, local in PAGES:
        head, served = fetch(url)
        mine = open(os.path.join(ROOT, local), encoding="utf-8").read()

        # Anything in the served copy that is not in mine.
        extra = served
        for line in mine.splitlines():
            extra = extra.replace(line, "", 1)
        extra = extra.strip()

        if extra:
            explained = False
            for pattern, label in KNOWN:
                if re.search(pattern, extra):
                    notes.append(f"{url}: {label}, {len(extra):,} bytes added")
                    explained = True
                    break
            if not explained:
                problems.append(
                    f"{url}: UNEXPLAINED content injected in transit "
                    f"({len(extra):,} bytes): {extra[:200]!r}")
        else:
            notes.append(f"{url}: served bytes match what I generated")

        if re.search(r"(?im)^set-cookie:", head):
            problems.append(f"{url}: a cookie is being set — /about/ says none are")

        for m in re.finditer(r"(?im)^(nel|report-to):\s*(.*)$", head):
            if "cloudflare" in m.group(2).lower():
                notes.append(f"{url}: {m.group(1)} header points at Cloudflare "
                             f"(disclosed)")

    # Every internal link must actually be reachable. Checking that files exist
    # on disk is not enough: the web server denies some paths, so a link can be
    # perfectly valid locally and 403 to the public. That happened.
    seen, checked = set(), 0
    for url, local in PAGES:
        src = open(os.path.join(ROOT, local), encoding="utf-8").read()
        for attr in ("href", "src"):
            for target in re.findall(rf'{attr}="([^"]+)"', src):
                if target.startswith(("http://", "https://", "#", "mailto:",
                                      "data:")):
                    continue
                clean = target.split("#")[0]
                if not clean.startswith("/") or clean in seen:
                    continue
                seen.add(clean)
                out = subprocess.run(
                    ["curl", "-sS", "-o", "/dev/null", "-w", "%{http_code}",
                     f"{BASE}{clean}"],
                    capture_output=True, text=True, timeout=60)
                code = out.stdout.strip()
                checked += 1
                if code != "200":
                    problems.append(f"{url}: links to {clean} which returns {code}")
    notes.append(f"{checked} internal links checked, all reachable"
                 if not problems else f"{checked} internal links checked")

    for n in notes:
        print(f"  note  {n}")
    print()
    if problems:
        print(f"{len(problems)} problem(s):")
        for p in problems:
            print(f"  - {p}")
        return 1
    print("Live pages match what I generated, apart from disclosed injections.")
    return 0


if __name__ == "__main__":
    sys.exit(main())