Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 1x 1x 9x 9x 7x 1x 6x 1x 5x 1x 4x 1x 3x 2x 1x 1x 17x 17x 1x 16x 2x 14x 14x 14x 13x 1x 12x 17x 17x 7x 7x 7x 12x 12x 12x 12x 12x 12x 12x 12x 1x 1x | // Image + media serving from R2 (local dev only)
// In deployed environments, assets are served directly from the R2 custom domain.
import { Hono } from "hono";
import { logger } from "../lib/logger";
type Env = { Bindings: CloudflareBindings };
const images = new Hono<Env>();
// Extension → content-type fallback when R2 metadata is missing.
// Avoids serving an MP3 as image/jpeg (silent decode failure in <audio>).
const MIME_BY_EXTENSION: Record<string, string> = {
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
webp: "image/webp",
gif: "image/gif",
avif: "image/avif",
svg: "image/svg+xml",
mp3: "audio/mpeg",
m4a: "audio/mp4",
wav: "audio/wav",
ogg: "audio/ogg",
mp4: "video/mp4",
webm: "video/webm",
};
function inferContentType(path: string): string | null {
const ext = path.split(".").pop()?.toLowerCase() ?? "";
return MIME_BY_EXTENSION[ext] ?? null;
}
// Magic-byte sniffing for content-addressed seed keys (`seed/<sha256>`)
// where neither R2 metadata nor a file extension is available. Limited to
// the formats the seed pool produces.
function sniffImageMime(bytes: Uint8Array): string | null {
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
return "image/jpeg";
}
if (
bytes.length >= 8 &&
bytes[0] === 0x89 &&
bytes[1] === 0x50 &&
bytes[2] === 0x4e &&
bytes[3] === 0x47
) {
return "image/png";
}
if (
bytes.length >= 12 &&
bytes[0] === 0x52 &&
bytes[1] === 0x49 &&
bytes[2] === 0x46 &&
bytes[3] === 0x46 &&
bytes[8] === 0x57 &&
bytes[9] === 0x45 &&
bytes[10] === 0x42 &&
bytes[11] === 0x50
) {
return "image/webp";
}
if (
bytes.length >= 6 &&
bytes[0] === 0x47 &&
bytes[1] === 0x49 &&
bytes[2] === 0x46 &&
bytes[3] === 0x38
) {
return "image/gif";
}
// AVIF: ISO-BMFF "ftyp" box at offset 4, major brand "avif" or "avis" at offset 8.
if (
bytes.length >= 12 &&
bytes[4] === 0x66 &&
bytes[5] === 0x74 &&
bytes[6] === 0x79 &&
bytes[7] === 0x70 &&
bytes[8] === 0x61 &&
bytes[9] === 0x76 &&
bytes[10] === 0x69 &&
(bytes[11] === 0x66 || bytes[11] === 0x73)
) {
return "image/avif";
}
return null;
}
images.get("/*", async (c) => {
const path = c.req.path.replace("/api/images/", "");
if (!path) {
return c.json({ error: "Image path required" }, 400);
}
// Prevent path traversal attacks
// Reject paths with .. sequences, absolute paths, or encoded traversals
if (
path.includes("..") ||
path.startsWith("/") ||
path.includes("%2e%2e") ||
path.includes("%2E%2E")
) {
return c.json({ error: "Invalid path" }, 400);
}
try {
const r2 = c.env.R2;
const object = await r2.get(path);
if (!object) {
return c.json({ error: "Image not found" }, 404);
}
// Prefer R2 metadata, then extension lookup. If both miss (CAS-style
// `seed/<sha256>` keys with no httpMetadata), buffer the body and
// sniff magic bytes — `X-Content-Type-Options: nosniff` below blocks
// browser-side detection, so the server must commit to a real type.
let contentType =
object.httpMetadata?.contentType || inferContentType(path);
let body: ReadableStream | ArrayBuffer = object.body;
if (!contentType) {
const buf = await object.arrayBuffer();
contentType =
sniffImageMime(new Uint8Array(buf, 0, Math.min(16, buf.byteLength))) ||
"application/octet-stream";
body = buf;
}
const headers = new Headers();
headers.set("Content-Type", contentType);
headers.set("Cache-Control", "public, max-age=86400"); // Cache for 1 day
headers.set("ETag", object.httpEtag);
// Vary by Accept header so CDN caches different formats separately
headers.set("Vary", "Accept");
headers.set("X-Content-Type-Options", "nosniff");
headers.set("Content-Disposition", "inline");
return new Response(body, { headers });
} catch (error) {
logger.error("R2 image fetch error:", error);
return c.json({ error: "Failed to retrieve image" }, 502);
}
});
export default images;
|