···11+// the service wants signed urls and the secret is server side, so this points
22+// at the route that signs
33+export const avatarUrl = (did: string, tiny = false): string =>
44+ `/avatar/${encodeURIComponent(did)}${tiny ? "?size=tiny" : ""}`;
···11+import { createHmac } from "node:crypto";
22+import { error } from "@sveltejs/kit";
33+import { getConfig } from "$lib/server/config";
44+import type { RequestHandler } from "./$types";
55+66+// half a day, the same as what the service puts on the image
77+const MAX_AGE = 43200;
88+99+const DID = /^did:[a-z]+:[a-zA-Z0-9._:%-]{1,256}$/;
1010+1111+// the service ignores anything else, so there is no point passing it on
1212+const PASSED_THROUGH = ["size", "format"];
1313+1414+export const GET: RequestHandler = ({ params, url }) => {
1515+ const did = params.did;
1616+ if (!DID.test(did)) error(400, "Not a did");
1717+1818+ const { avatarUrl, avatarSecret } = getConfig();
1919+ if (!avatarSecret) error(404, "Avatars are not configured");
2020+2121+ const query = new URLSearchParams();
2222+ for (const key of PASSED_THROUGH) {
2323+ const value = url.searchParams.get(key);
2424+ if (value) query.set(key, value);
2525+ }
2626+2727+ const signature = createHmac("sha256", avatarSecret).update(did).digest("hex");
2828+ const target = `${avatarUrl}/${signature}/${did}${query.size ? `?${query}` : ""}`;
2929+ return new Response(null, {
3030+ status: 302,
3131+ headers: { location: target, "cache-control": `public, max-age=${MAX_AGE}` }
3232+ });
3333+};
···11+import { createHmac } from "node:crypto";
22+import { error } from "@sveltejs/kit";
33+import { getConfig } from "$lib/server/config";
44+import type { RequestHandler } from "./$types";
55+66+// the signed url only changes when the secret does, so it can cache for a day
77+const MAX_AGE = 86400;
88+99+const HEX = /^(?:[0-9a-f]{2})+$/;
1010+1111+export const GET: RequestHandler = ({ params }) => {
1212+ const hex = params.hex.toLowerCase();
1313+ if (!HEX.test(hex)) error(400, "Not a camo url");
1414+1515+ const target = Buffer.from(hex, "hex").toString("utf8");
1616+ let parsed: URL;
1717+ try {
1818+ parsed = new URL(target);
1919+ } catch {
2020+ error(400, "Not a camo url");
2121+ }
2222+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2323+ error(400, "Not a camo url");
2424+ }
2525+2626+ // redirecting unsigned would make this an open redirect, so no secret means
2727+ // no images at all
2828+ const { camoUrl, camoSecret } = getConfig();
2929+ if (!camoSecret) error(404, "Camo is not configured");
3030+3131+ const signature = createHmac("sha256", camoSecret).update(target).digest("hex");
3232+ return new Response(null, {
3333+ status: 302,
3434+ headers: {
3535+ location: `${camoUrl}/${signature}/${hex}`,
3636+ "cache-control": `public, max-age=${MAX_AGE}`
3737+ }
3838+ });
3939+};