19. Show me how
Every exhibit shows the code that makes it work.
An app's code is plain files in its checkout; the zoo serves its own exhibits' files as they are on disk, so what you read is what runs.
This is the code serving the zoo right now: read from disk for this request, from commit
0c1c124a02 (live). The zoo's own self-check fetches this
page and compares it byte for byte with the file it runs.
exhibits/19-show-me-how.php sha256 e4c7e90c31de · raw
1<?php 2// Exhibit 19. Every card's "how" link opens /how/<n>, which reads the exhibit's files from the 3// checkout serving the request (lib/how.php). The check fetches every one of those pages over 4// HTTPS and compares it byte for byte with the file the zoo is running. 5return [ 6 "n" => 19, "wing" => "Building", 7 "title" => "Show me how", 8 "promise" => "Every exhibit shows the code that makes it work.", 9 "block" => "An app's code is plain files in its checkout; the zoo serves its own exhibits' files as they are on disk, so what you read is what runs.", 10 "files" => ["lib/how.php", "lib/zoo.php"], 11 "show" => function (?array $me): string { 12 return '<p>Every card has a <b>how</b> link. Try <a href="/how/10">the counter\'s</a> or <a href="/how/9">the doors\'</a>: the actual file serving this page, with its sha256.</p>'; 13 }, 14 "check" => function (): array { 15 require_once ZOO_ROOT . "/lib/how.php"; 16 $b = new Browser(); 17 $n = 0; 18 foreach (exhibits() as $e) { 19 foreach (how_files($e) as $f) { 20 [$code, , $body] = $b->get("https://" . env("RC_HOST") . "/how/{$e['n']}?raw=" . rawurlencode($f)); 21 $body = is_array($body) ? json_encode($body) : $body; 22 if ($code !== 200) return [false, "how for exhibit {$e['n']} ($f) answered $code"]; 23 if ($body !== file_get_contents(ZOO_ROOT . "/$f")) return [false, "how for exhibit {$e['n']} shows a $f that differs from the running one"]; 24 $n++; 25 } 26 } 27 return [true, "$n how pages fetched over HTTPS, each identical to the file running"]; 28 }, 29];
lib/how.php sha256 d4447e7364bd · raw
1<?php 2// Exhibit 19, "show me how": an exhibit's source, read at request time from the checkout that is 3// serving this very page, so it cannot differ from what is live. ?raw=<file> gives the bytes. 4 5function how_files(array $e): array { 6 return array_values(array_unique([$e["file"], ...$e["files"]])); 7} 8 9function how(int $n): void { 10 $e = exhibits()[$n] ?? null; 11 if (!$e) { http_response_code(404); echo "no such exhibit\n"; return; } 12 $files = how_files($e); 13 if (isset($_GET["raw"])) { 14 // Only the files this exhibit names: never anything else in the checkout. 15 if (!in_array($_GET["raw"], $files, true)) { http_response_code(404); echo "not one of this exhibit's files\n"; return; } 16 header("Content-Type: text/plain; charset=utf-8"); 17 header("Cache-Control: no-store"); 18 readfile(ZOO_ROOT . "/" . $_GET["raw"]); 19 return; 20 } 21 $deploy = fact("deploy.json")[env("RC_ENV")] ?? []; 22 header("Content-Type: text/html; charset=utf-8"); 23 header("Cache-Control: no-store"); 24 ?> 25<!doctype html> 26<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> 27<title>How: <?= h($e["title"]) ?></title><meta name="color-scheme" content="light dark"> 28<?= rc_head(false) ?> 29<link rel="stylesheet" href="/zoo.css?<?= substr(md5_file(ZOO_ROOT . "/public/zoo.css"), 0, 8) ?>"></head> 30<body class="how"> 31<?= rc_header("Zoo", auth_user(), "/api/3/signout", false) ?> 32<main class="wrap"> 33 <p><a href="/#e<?= $n ?>">← back to the zoo</a></p> 34 <h1><?= $n ?>. <?= h($e["title"]) ?></h1> 35 <p class="promise"><?= h($e["promise"]) ?></p> 36 <p class="block"><?= h($e["block"]) ?></p> 37 <p class="muted">This is the code serving the zoo right now: read from disk for this request, from commit 38 <code><?= h(substr($deploy["commit"] ?? "unknown", 0, 10)) ?></code> (<?= h(env("RC_ENV")) ?>). The zoo's own self-check fetches this 39 page and compares it byte for byte with the file it runs.</p> 40 <?php foreach ($files as $f): $src = (string)file_get_contents(ZOO_ROOT . "/$f"); ?> 41 <section class="src"> 42 <h2><code><?= h($f) ?></code> <small class="muted">sha256 <?= substr(hash("sha256", $src), 0, 12) ?> · <a href="/how/<?= $n ?>?raw=<?= h(rawurlencode($f)) ?>">raw</a></small></h2> 43 <pre><?php foreach (explode("\n", rtrim($src, "\n")) as $i => $line) echo '<span class="ln">', $i + 1, "</span>", h($line), "\n"; ?></pre> 44 </section> 45 <?php endforeach ?> 46</main> 47</body></html> 48<?php 49}
lib/zoo.php sha256 0804ac0756f0 · raw
1<?php 2/* 3 * The zoo's shared parts: its database, its exhibits and their lights, and a few helpers. 4 * 5 * Every path and host comes from the environment rc gives every app (RC_DATA, RC_FACTS, RC_HOST, 6 * RC_PUBLISH, ...). There are no fallbacks: an unset variable is an error, not a guess. 7 */ 8 9require_once getenv("RC_LIB") . "/rc-auth.php"; 10require_once __DIR__ . "/web.php"; 11 12define("ZOO_ROOT", dirname(__DIR__)); // the running checkout, which "how" shows 13const WINGS = ["Identity", "Deploy", "Isolation", "Realtime", "Storage", "Intelligence", "Notifications", "Building", "Operations"]; 14const HUMAN_FRESH_DAYS = 30; // a human-only exhibit goes amber after this 15const CHECK_STALE_MIN = 15; // the self-check runs every 5 minutes 16 17function env(string $k): string { 18 $v = getenv($k); 19 if ($v === false || $v === "") throw new RuntimeException("$k is not set"); 20 return $v; 21} 22 23// --- the database: $RC_DATA/zoo.db, the only place the zoo writes ------------------------------- 24 25function db(): PDO { 26 static $db = null; 27 if ($db) return $db; 28 $db = new PDO("sqlite:" . env("RC_DATA") . "/zoo.db", null, null, 29 [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); 30 $db->exec("PRAGMA busy_timeout = 8000"); 31 $db->exec("PRAGMA journal_mode = WAL"); 32 $db->exec(" 33 CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT); 34 CREATE TABLE IF NOT EXISTS guestbook (user_id INTEGER PRIMARY KEY, note TEXT NOT NULL, at INTEGER NOT NULL); 35 CREATE TABLE IF NOT EXISTS lights (n INTEGER PRIMARY KEY, ok INTEGER NOT NULL, detail TEXT NOT NULL, 36 data TEXT, at INTEGER NOT NULL, last_ok INTEGER); 37 CREATE TABLE IF NOT EXISTS seen (id INTEGER PRIMARY KEY AUTOINCREMENT, n INTEGER NOT NULL, 38 user_id INTEGER NOT NULL, at INTEGER NOT NULL);"); 39 // Which database this is. Live and staging each make their own, so comparing them proves 40 // the two environments share no data (exhibit 7). 41 if (meta("instance") === null) meta_set("instance", bin2hex(random_bytes(8))); 42 return $db; 43} 44 45function q(string $sql, array $args = []): PDOStatement { 46 $s = db()->prepare($sql); 47 $s->execute($args); 48 return $s; 49} 50function meta(string $k): ?string { 51 $v = q("SELECT v FROM meta WHERE k = ?", [$k])->fetchColumn(); 52 return $v === false ? null : $v; 53} 54function meta_set(string $k, string $v): void { 55 q("INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v", [$k, $v]); 56} 57 58// --- facts: what root tells the zoo, read-only, in RC_FACTS --------------------------------------- 59 60function fact(string $name): ?array { 61 $j = json_decode((string)@file_get_contents(env("RC_FACTS") . "/$name"), true); 62 return is_array($j) ? $j : null; 63} 64 65// --- exhibits ---------------------------------------------------------------------------------- 66// 67// One file per exhibit in exhibits/, returning an array: 68// n, wing, title, promise the exhibit as ZOO.md states it 69// block what the building block is and how an app uses it, in a sentence 70// kind "self" (the self-check proves it), "human" (a person presses and 71// looks) or "waiting" (its building block does not exist yet) 72// show(?$me): string the live part of the card, HTML 73// api($do, ?$me): array JSON at /api/<n>/<do> (POSTs are CSRF-checked before this runs) 74// check(): [ok, detail, data] self-running exhibits: what the self-check runs 75// channels ["name" => fn($id, ?$me): bool], who may listen (auth channels) 76// files other files "how" should show besides the exhibit's own 77 78function exhibits(): array { 79 static $all = null; 80 if ($all !== null) return $all; 81 $all = []; 82 foreach (glob(ZOO_ROOT . "/exhibits/*.php") as $f) { 83 $e = require $f; 84 $e["file"] = "exhibits/" . basename($f); 85 $all[$e["n"]] = $e + ["kind" => "self", "files" => [], "show" => null, "api" => null, "check" => null, "channels" => [], "try" => null]; 86 } 87 ksort($all); 88 return $all; 89} 90 91// Run one exhibit's check and store the result as its light. The light is data: the page reads 92// it, it never makes live requests to draw itself. 93function run_check(array $e): array { 94 $t = microtime(true); 95 try { 96 $r = ($e["check"])(); 97 } catch (Throwable $x) { 98 $r = [false, "the check itself failed: " . $x->getMessage()]; 99 } 100 [$ok, $detail, $data] = $r + [false, "", null]; 101 $now = time(); 102 q("INSERT INTO lights (n, ok, detail, data, at, last_ok) VALUES (:n, :ok, :d, :data, :at, :lo) 103 ON CONFLICT(n) DO UPDATE SET ok = excluded.ok, detail = excluded.detail, data = excluded.data, 104 at = excluded.at, last_ok = COALESCE(excluded.last_ok, lights.last_ok)", 105 ["n" => $e["n"], "ok" => $ok ? 1 : 0, "d" => $detail, "data" => json_encode($data), "at" => $now, "lo" => $ok ? $now : null]); 106 return ["n" => $e["n"], "ok" => (bool)$ok, "detail" => $detail, "ms" => (int)((microtime(true) - $t) * 1000)]; 107} 108 109// True for exactly one caller per $every seconds per exhibit (an upsert that only wins when the 110// last claim is old enough). 111function claim_check(int $n, int $every): bool { 112 $now = time(); 113 $s = q("INSERT INTO meta (k, v) VALUES (:k, :now) ON CONFLICT(k) DO UPDATE SET v = excluded.v 114 WHERE CAST(meta.v AS INTEGER) <= :old", ["k" => "check-claim.$n", "now" => (string)$now, "old" => $now - $every]); 115 return $s->rowCount() === 1; 116} 117 118function stored(int $n): ?array { 119 $r = q("SELECT * FROM lights WHERE n = ?", [$n])->fetch(); 120 if (!$r) return null; 121 $r["data"] = json_decode((string)$r["data"], true); 122 return $r; 123} 124 125// green / red / amber / grey, and the words that go with it. 126function light(array $e): array { 127 if ($e["kind"] === "waiting") return ["color" => "grey", "text" => "not built yet: waiting on " . $e["waiting_on"]]; 128 if ($e["kind"] === "human") { 129 $s = q("SELECT user_id, at FROM seen WHERE n = ? ORDER BY at DESC LIMIT 1", [$e["n"]])->fetch(); 130 if (!$s) return ["color" => "amber", "text" => "nobody has seen this work yet"]; 131 $who = rc_users([$s["user_id"]])[$s["user_id"]]["username"] ?? "a deleted account"; 132 $fresh = time() - $s["at"] < HUMAN_FRESH_DAYS * 86400; 133 return ["color" => $fresh ? "green" : "amber", "text" => "last seen working by $who", "at" => (int)$s["at"]]; 134 } 135 $s = stored($e["n"]); 136 if (!$s) return ["color" => "grey", "text" => "not checked yet"]; 137 if (time() - $s["at"] > CHECK_STALE_MIN * 60) return ["color" => "red", "text" => "not checked since", "at" => (int)$s["at"]]; 138 // A check may pass with a warning (data.amber): its detail says what is getting old. 139 if ($s["ok"] && ($s["data"]["amber"] ?? false)) return ["color" => "amber", "text" => $s["detail"], "at" => (int)$s["at"]]; 140 if ($s["ok"]) return ["color" => "green", "text" => "last worked", "at" => (int)$s["at"]]; 141 return ["color" => "red", "text" => $s["detail"], "at" => $s["last_ok"] ? (int)$s["last_ok"] : null, "prefix" => "last worked"]; 142} 143 144// A human-only exhibit's "this worked for me". 145function seen(int $n, array $me): void { 146 q("INSERT INTO seen (n, user_id, at) VALUES (?, ?, ?)", [$n, $me["id"], time()]); 147} 148 149// --- helpers ------------------------------------------------------------------------------------- 150 151function h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8"); } 152 153function ago(?int $t): string { 154 if (!$t) return "never"; 155 $d = max(0, time() - $t); 156 $s = match (true) { 157 $d < 60 => "just now", 158 $d < 3600 => intdiv($d, 60) . " minute" . (intdiv($d, 60) === 1 ? "" : "s") . " ago", 159 $d < 172800 => intdiv($d, 3600) . " hour" . (intdiv($d, 3600) === 1 ? "" : "s") . " ago", 160 default => intdiv($d, 86400) . " days ago", 161 }; 162 return '<time datetime="' . gmdate("c", $t) . '">' . $s . "</time>"; 163} 164 165// The other environment's address: zoo <-> zoo-staging. Derived from our own RC_HOST, the way 166// rc_app_url derives another app's. 167function other_env_host(): string { 168 [$label, $rest] = explode(".", env("RC_HOST"), 2); 169 $label = str_ends_with($label, "-staging") ? substr($label, 0, -8) : "$label-staging"; 170 return "$label.$rest"; 171} 172 173// Publish on one of our declared channels. RC_PUBLISH is our private publish listener; rc fixes 174// the channel ids there as zoo.<env>.<name>.<id>, so we can only ever reach our own streams. 175function publish(string $channel, string $id, array $msg): bool { 176 $c = curl_init(env("RC_PUBLISH") . "/pub/$channel/" . rawurlencode($id)); 177 curl_setopt_array($c, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($msg), CURLOPT_RETURNTRANSFER => true, 178 CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_TIMEOUT => 3]); 179 curl_exec($c); 180 $code = curl_getinfo($c, CURLINFO_HTTP_CODE); 181 return $code >= 200 && $code < 300; 182} 183 184// Call another app the way any app does: HTTPS, with an account assertion from rc_app_headers. 185// $user true = for the signed-in person (needs a live sign-in in this request). [status, body, content-type]. 186function call_app(string $app, string $path, bool $user, string $method = "GET", ?string $body = null, array $headers = [], int $timeout = 30): array { 187 $c = curl_init(rc_app_url($app) . $path); 188 curl_setopt_array($c, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, 189 CURLOPT_CONNECTTIMEOUT => 4, CURLOPT_HTTPHEADER => [...rc_app_headers($app, $user), ...$headers]]); 190 if ($body !== null) curl_setopt($c, CURLOPT_POSTFIELDS, $body); 191 $out = (string)curl_exec($c); 192 return [(int)curl_getinfo($c, CURLINFO_HTTP_CODE), $out, (string)curl_getinfo($c, CURLINFO_CONTENT_TYPE)]; 193} 194 195// A card's "this worked for me" button, for human-only exhibits. 196function seen_button(int $n, ?array $me, string $what = "This worked for me"): string { 197 if (!$me) return '<p class="muted">Sign in to record that this worked for you.</p>'; 198 return '<button class="seen" data-seen="' . $n . '">' . h($what) . "</button>"; 199} 200 201// One user's card: picture, name, username. Pictures are account's URLs, cached forever by 202// the browser, and always the current one (exhibit 4). 203function person(?array $u, string $class = ""): string { 204 if (!$u) return '<div class="person ' . $class . '"><span class="pic none">?</span><span><b>visitor</b><small>not signed in</small></span></div>'; 205 if (($u["picture"] ?? "") === "") return '<div class="person ' . $class . '"><span class="pic none">?</span><span><b>' . h($u["name"]) . "</b><small>no longer here</small></span></div>"; 206 return '<div class="person ' . $class . '"><img class="pic" src="' . h($u["picture"]) . '" alt=""><span><b>' . h($u["name"]) 207 . "</b><small>@" . h($u["username"]) . " · #" . (int)$u["id"] . "</small></span></div>"; 208}