forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
fork
Configure Feed
Select the types of activity you want to include in your feed.
1export default {
2 async fetch(request, env) {
3 const url = new URL(request.url);
4
5 if (url.pathname === "/" || url.pathname === "") {
6 return new Response(
7 "This is Tangled's Camo service. It proxies images served from knots via Cloudflare.",
8 );
9 }
10
11 const cache = caches.default;
12
13 const pathParts = url.pathname.slice(1).split("/");
14 if (pathParts.length < 2) {
15 return new Response("Bad URL", { status: 400 });
16 }
17
18 const [signatureHex, ...hexUrlParts] = pathParts;
19 const hexUrl = hexUrlParts.join("");
20 const urlBytes = Uint8Array.from(
21 hexUrl.match(/.{2}/g).map((b) => parseInt(b, 16)),
22 );
23 const targetUrl = new TextDecoder().decode(urlBytes);
24
25 // check if we have an entry in the cache with the target url
26 let cacheKey = new Request(targetUrl);
27 let response = await cache.match(cacheKey);
28 if (response) {
29 return response;
30 }
31
32 // else compute the signature
33 const key = await crypto.subtle.importKey(
34 "raw",
35 new TextEncoder().encode(env.CAMO_SHARED_SECRET),
36 { name: "HMAC", hash: "SHA-256" },
37 false,
38 ["sign", "verify"],
39 );
40
41 const computedSigBuffer = await crypto.subtle.sign("HMAC", key, urlBytes);
42 const computedSig = Array.from(new Uint8Array(computedSigBuffer))
43 .map((b) => b.toString(16).padStart(2, "0"))
44 .join("");
45
46 console.log({
47 level: "debug",
48 message: "camo target: " + targetUrl,
49 computedSignature: computedSig,
50 providedSignature: signatureHex,
51 targetUrl: targetUrl,
52 });
53
54 const sigBytes = Uint8Array.from(
55 signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)),
56 );
57 const valid = await crypto.subtle.verify("HMAC", key, sigBytes, urlBytes);
58
59 if (!valid) {
60 return new Response("Invalid signature", { status: 403 });
61 }
62
63 let parsedUrl;
64 try {
65 parsedUrl = new URL(targetUrl);
66 if (!["https:", "http:"].includes(parsedUrl.protocol)) {
67 return new Response("Only HTTP(S) allowed", { status: 400 });
68 }
69 } catch {
70 return new Response("Malformed URL", { status: 400 });
71 }
72
73 // fetch from the parsed URL
74 const res = await fetch(parsedUrl.toString(), {
75 headers: { "User-Agent": "Tangled Camo v0.1.0" },
76 });
77
78 const allowedMimeTypes = require("./mimetypes.json");
79
80 const contentType =
81 res.headers.get("Content-Type") || "application/octet-stream";
82
83 if (!allowedMimeTypes.includes(contentType.split(";")[0].trim())) {
84 return new Response("Unsupported media type", { status: 415 });
85 }
86
87 const headers = new Headers();
88 headers.set("Content-Type", contentType);
89 headers.set("Cache-Control", "public, max-age=86400, immutable");
90
91 // serve and cache it with cf
92 response = new Response(await res.arrayBuffer(), {
93 status: res.status,
94 headers,
95 });
96
97 await cache.put(cacheKey, response.clone());
98
99 return response;
100 },
101};