sweedworks

← all sources

checklive.py

Compares served bytes against what was generated

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

  1. 1"""Compare what I generate against what is actually served.
  2. 2 
  3. 3Cloudflare injects a bot-detection script into HTML in transit. I missed it for
  4. 4days because I only ever checked the files I wrote — the injection is invisible
  5. 5from disk. This script diffs the served bytes against the local file and reports
  6. 6anything added, so a change in what sits in front of this domain shows up as a
  7. 7build finding rather than as a false claim on the privacy page.
  8. 8 
  9. 9Needs real network access to the live domain, so it is not part of build.sh:
  10. 10run it after deploying.
  11. 11 python3 checklive.py
  12. 12"""
  13. 13 
  14. 14import os
  15. 15import re
  16. 16import subprocess
  17. 17import sys
  18. 18 
  19. 19HERE = os.path.dirname(os.path.abspath(__file__))
  20. 20ROOT = os.path.abspath(os.path.join(HERE, ".."))
  21. 21BASE = "https://sweedworks.com"
  22. 22 
  23. 23PAGES = [("/", "index.html"),
  24. 24 ("/vocabulary/", "vocabulary/index.html"),
  25. 25 ("/predict/", "predict/index.html"),
  26. 26 ("/cost/", "cost/index.html"),
  27. 27 ("/learn/", "learn/index.html"),
  28. 28 ("/attention/", "attention/index.html"),
  29. 29 ("/tokens/", "tokens/index.html"),
  30. 30 ("/about/", "about/index.html"),
  31. 31 ("/changes/", "changes/index.html"),
  32. 32 ("/checking/", "checking/index.html")]
  33. 33 
  34. 34# Injections we already know about and have disclosed on /about/.
  35. 35KNOWN = [
  36. 36 (r"__CF\$cv\$params", "Cloudflare bot-detection script (disclosed)"),
  37. 37 (r"cdn-cgi/challenge-platform", "Cloudflare challenge platform (disclosed)"),
  38. 38]
  39. 39 
  40. 40 
  41. 41def fetch(path):
  42. 42 """Headers and body to separate files.
  43. 43 
  44. 44 Do not go back to `-D -` with text=True: universal-newline translation turns
  45. 45 the CRLF header/body separator into LF, the partition silently fails, the
  46. 46 body comes back empty, and every comparison below then "passes" against
  47. 47 nothing. This check reported all-clear that way while the injected script
  48. 48 was plainly there.
  49. 49 """
  50. 50 import tempfile
  51. 51 with tempfile.TemporaryDirectory() as tmp:
  52. 52 hp = os.path.join(tmp, "h")
  53. 53 bp = os.path.join(tmp, "b")
  54. 54 subprocess.run(
  55. 55 ["curl", "-sS", "-D", hp, "-o", bp, f"{BASE}{path}?livecheck=1"],
  56. 56 capture_output=True, timeout=60, check=True)
  57. 57 head = open(hp, encoding="utf-8", errors="replace").read()
  58. 58 body = open(bp, encoding="utf-8", errors="replace").read()
  59. 59 if not body:
  60. 60 raise RuntimeError(f"empty body for {path} — the check would be vacuous")
  61. 61 return head, body
  62. 62 
  63. 63 
  64. 64def main():
  65. 65 problems, notes = [], []
  66. 66 
  67. 67 for url, local in PAGES:
  68. 68 head, served = fetch(url)
  69. 69 mine = open(os.path.join(ROOT, local), encoding="utf-8").read()
  70. 70 
  71. 71 # Anything in the served copy that is not in mine.
  72. 72 extra = served
  73. 73 for line in mine.splitlines():
  74. 74 extra = extra.replace(line, "", 1)
  75. 75 extra = extra.strip()
  76. 76 
  77. 77 if extra:
  78. 78 explained = False
  79. 79 for pattern, label in KNOWN:
  80. 80 if re.search(pattern, extra):
  81. 81 notes.append(f"{url}: {label}, {len(extra):,} bytes added")
  82. 82 explained = True
  83. 83 break
  84. 84 if not explained:
  85. 85 problems.append(
  86. 86 f"{url}: UNEXPLAINED content injected in transit "
  87. 87 f"({len(extra):,} bytes): {extra[:200]!r}")
  88. 88 else:
  89. 89 notes.append(f"{url}: served bytes match what I generated")
  90. 90 
  91. 91 if re.search(r"(?im)^set-cookie:", head):
  92. 92 problems.append(f"{url}: a cookie is being set — /about/ says none are")
  93. 93 
  94. 94 for m in re.finditer(r"(?im)^(nel|report-to):\s*(.*)$", head):
  95. 95 if "cloudflare" in m.group(2).lower():
  96. 96 notes.append(f"{url}: {m.group(1)} header points at Cloudflare "
  97. 97 f"(disclosed)")
  98. 98 
  99. 99 # Every internal link must actually be reachable. Checking that files exist
  100. 100 # on disk is not enough: the web server denies some paths, so a link can be
  101. 101 # perfectly valid locally and 403 to the public. That happened.
  102. 102 seen, checked = set(), 0
  103. 103 for url, local in PAGES:
  104. 104 src = open(os.path.join(ROOT, local), encoding="utf-8").read()
  105. 105 for attr in ("href", "src"):
  106. 106 for target in re.findall(rf'{attr}="([^"]+)"', src):
  107. 107 if target.startswith(("http://", "https://", "#", "mailto:",
  108. 108 "data:")):
  109. 109 continue
  110. 110 clean = target.split("#")[0]
  111. 111 if not clean.startswith("/") or clean in seen:
  112. 112 continue
  113. 113 seen.add(clean)
  114. 114 out = subprocess.run(
  115. 115 ["curl", "-sS", "-o", "/dev/null", "-w", "%{http_code}",
  116. 116 f"{BASE}{clean}"],
  117. 117 capture_output=True, text=True, timeout=60)
  118. 118 code = out.stdout.strip()
  119. 119 checked += 1
  120. 120 if code != "200":
  121. 121 problems.append(f"{url}: links to {clean} which returns {code}")
  122. 122 notes.append(f"{checked} internal links checked, all reachable"
  123. 123 if not problems else f"{checked} internal links checked")
  124. 124 
  125. 125 for n in notes:
  126. 126 print(f" note {n}")
  127. 127 print()
  128. 128 if problems:
  129. 129 print(f"{len(problems)} problem(s):")
  130. 130 for p in problems:
  131. 131 print(f" - {p}")
  132. 132 return 1
  133. 133 print("Live pages match what I generated, apart from disclosed injections.")
  134. 134 return 0
  135. 135 
  136. 136 
  137. 137if __name__ == "__main__":
  138. 138 sys.exit(main())