This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / camo / src / index.js
3.1 kB 103 lines
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 signature before we lookup cache, if we do the other way 26 // then any random signature for the same url will be let through after 27 // a single successful request 28 const key = await crypto.subtle.importKey( 29 "raw", 30 new TextEncoder().encode(env.CAMO_SHARED_SECRET), 31 { name: "HMAC", hash: "SHA-256" }, 32 false, 33 ["sign", "verify"], 34 ); 35 36 const computedSigBuffer = await crypto.subtle.sign("HMAC", key, urlBytes); 37 const computedSig = Array.from(new Uint8Array(computedSigBuffer)) 38 .map((b) => b.toString(16).padStart(2, "0")) 39 .join(""); 40 41 console.log({ 42 level: "debug", 43 message: "camo target: " + targetUrl, 44 computedSignature: computedSig, 45 providedSignature: signatureHex, 46 targetUrl: targetUrl, 47 }); 48 49 const sigBytes = Uint8Array.from( 50 signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)), 51 ); 52 const valid = await crypto.subtle.verify("HMAC", key, sigBytes, urlBytes); 53 54 if (!valid) { 55 return new Response("Invalid signature", { status: 403 }); 56 } 57 58 // check if we have an entry in the cache with the target url 59 let cacheKey = new Request(targetUrl); 60 let response = await cache.match(cacheKey); 61 if (response) { 62 return response; 63 } 64 65 let parsedUrl; 66 try { 67 parsedUrl = new URL(targetUrl); 68 if (!["https:", "http:"].includes(parsedUrl.protocol)) { 69 return new Response("Only HTTP(S) allowed", { status: 400 }); 70 } 71 } catch { 72 return new Response("Malformed URL", { status: 400 }); 73 } 74 75 // fetch from the parsed URL 76 const res = await fetch(parsedUrl.toString(), { 77 headers: { "User-Agent": "Tangled Camo v0.1.0" }, 78 }); 79 80 const allowedMimeTypes = require("./mimetypes.json"); 81 82 const contentType = 83 res.headers.get("Content-Type") || "application/octet-stream"; 84 85 if (!allowedMimeTypes.includes(contentType.split(";")[0].trim())) { 86 return new Response("Unsupported media type", { status: 415 }); 87 } 88 89 const headers = new Headers(); 90 headers.set("Content-Type", contentType); 91 headers.set("Cache-Control", "public, max-age=86400, immutable"); 92 93 // serve and cache it with cf 94 response = new Response(await res.arrayBuffer(), { 95 status: res.status, 96 headers, 97 }); 98 99 await cache.put(cacheKey, response.clone()); 100 101 return response; 102 }, 103};