Zoo

staging

← back to the zoo

12. Your files

You can upload a file, see it listed, and download it again unchanged.

storage keeps each person's files (B2-backed) behind their sign-in; an app links to it and never holds storage's keys. The check here uploads random bytes, lists them, downloads them and compares.

This is the code serving the zoo right now: read from disk for this request, from commit 77ff74344d (staging). The zoo's own self-check fetches this page and compares it byte for byte with the file it runs.

exhibits/12-your-files.php sha256 1b6381289153 · raw

1<?php
2// Exhibit 12. A person uploads in storage's own page. The zoo proves the same path works: it asks
3// storage's /app/roundtrip to run the browser's upload (begin, presigned PUT to B2, commit, list),
4// then downloads what storage hands back and compares the bytes with what it sent.
5//     call_app("storage", "/app/roundtrip", true, "POST", $bytes)      // rc_app_headers("storage", true)
6// The check does this as the robot, through our own /api/12/roundtrip.
7
8function roundtrip_once(): array {
9    $bytes = random_bytes(4096 + random_int(0, 4096));
10    [$s, $out] = call_app("storage", "/app/roundtrip?path=" . rawurlencode("zoo-roundtrip.bin"), true, "POST", $bytes, ["Content-Type: application/octet-stream"]);
11    $r = json_decode($out, true);
12    if ($s !== 200 || !is_array($r)) return ["error" => "storage's roundtrip answered $s: " . substr($out, 0, 160), "status" => 502];
13    if (!($r["listed"] ?? false)) return ["error" => "uploaded, but the file was not in the listing", "status" => 502];
14    // The download is a presigned B2 URL, fetched like a browser would: no assertion on it.
15    $c = curl_init($r["download"]);
16    curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 30]);
17    $back = (string)curl_exec($c);
18    $code = (int)curl_getinfo($c, CURLINFO_HTTP_CODE);
19    if ($code !== 200) return ["error" => "the download answered $code", "status" => 502];
20    $same = hash_equals(hash("sha256", $bytes), hash("sha256", $back));
21    return ["size" => strlen($bytes), "got" => strlen($back), "matched" => $same, "sha256" => substr(hash("sha256", $bytes), 0, 12)];
22}
23
24return [
25    "n" => 12, "wing" => "Storage", "kind" => "self",
26    "title" => "Your files",
27    "promise" => "You can upload a file, see it listed, and download it again unchanged.",
28    "block" => "storage keeps each person's files (B2-backed) behind their sign-in; an app links to it and never holds storage's keys. The check here uploads random bytes, lists them, downloads them and compares.",
29    "show" => function (?array $me): string {
30        $s = stored(12);
31        $last = $s && ($s["data"]["matched"] ?? false)
32            ? '<p>Last roundtrip, ' . ago((int)$s["at"]) . ': <b>' . (int)$s["data"]["size"] . ' bytes</b> uploaded, listed and downloaded again: <span class="tag ok">matched</span></p>'
33            : '<p class="muted">No matching roundtrip recorded yet.</p>';
34        return $last . '<p><a href="' . h(rc_app_url("storage")) . '/" target="_blank" rel="noopener">Open storage</a>, upload something, and download it again.</p>';
35    },
36    "api" => function (string $do, ?array $me, array $in, bool $post): ?array {
37        if ($do !== "roundtrip" || !$post) return null;
38        if (!$me) return ["error" => "sign in first", "status" => 401];
39        return roundtrip_once();
40    },
41    // The robot asks the zoo to run the roundtrip as it (so the assertion names the robot).
42    "check" => function (): array {
43        require_once ZOO_ROOT . "/lib/robot.php";
44        [$code, , $j] = robot_at_zoo()->post("https://" . env("RC_HOST") . "/api/12/roundtrip");
45        if ($code !== 200 || !is_array($j)) return [false, "the roundtrip failed ($code): " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))];
46        if (!$j["matched"]) return [false, "the downloaded copy differs from what was uploaded ({$j['size']} bytes up, {$j['got']} down)", $j];
47        return [true, "uploaded {$j['size']} random bytes, listed them, downloaded them again: matched (sha256 {$j['sha256']})", $j];
48    },
49];