Zoo

← back to the zoo

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}