forked from tangled.org/core
this repo has no description
at opengraph 3.0 kB view raw
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};