const COOKIE = "__Host-relay-session"; const DAY = 24 * 60 * 60; const REMEMBER = 30 * DAY; const bytes = new TextEncoder(); function isLocal(request: Request): boolean { const host = new URL(request.url).hostname; return host === "localhost" || host === "127.0.0.1" || host === "[::1]"; } async function key(secret: string): Promise { return crypto.subtle.importKey("raw", bytes.encode(secret), {name: "HMAC", hash: "SHA-256"}, false, ["sign", "verify"]); } function hex(buffer: ArrayBuffer): string { return Array.from(new Uint8Array(buffer), value => value.toString(16).padStart(2, "0")).join(""); } export async function authenticated(request: Request, secret?: string): Promise { if (isLocal(request)) return true; if (!secret || secret.length < 24) return false; const cookie = request.headers.get("cookie")?.split(";").map(s => s.trim()).find(s => s.startsWith(COOKIE + "=")); if (!cookie) return false; const token = cookie.slice(COOKIE.length + 1); const match = /^(\d{10})\.([a-f0-9]{64})$/.exec(token); if (!match) return false; const expiry = Number(match[1]); const now = Math.floor(Date.now() / 1000); if (expiry < now || expiry > now + REMEMBER + 60) return false; const signature = Uint8Array.from(match[2].match(/../g)!, pair => parseInt(pair, 16)); return crypto.subtle.verify("HMAC", await key(secret), signature, bytes.encode(match[1])); } export async function unlock(request: Request, secret?: string): Promise { if (!secret || secret.length < 24) return Response.json({error: "Set a PROXY_PASSWORD secret of at least 24 characters before using this online."}, {status: 503}); const reader = request.body?.getReader(); const chunks: Uint8Array[] = []; let length = 0; if (reader) { while (true) { const {done,value} = await reader.read(); if (done) break; length += value.byteLength; if (length > 1024) { await reader.cancel(); return new Response("Too large", {status:413}); } chunks.push(value); } } const body = new Uint8Array(length); let offset = 0; for (const chunk of chunks) { body.set(chunk,offset); offset += chunk.byteLength; } const raw = new TextDecoder().decode(body); const form = new URLSearchParams(raw); const supplied = form.get("password") ?? ""; const actualHash = await crypto.subtle.digest("SHA-256", bytes.encode(secret)); const suppliedHash = await crypto.subtle.digest("SHA-256", bytes.encode(supplied)); if (!crypto.subtle.timingSafeEqual(actualHash, suppliedHash)) { return Response.json({error: "Incorrect proxy passphrase."}, {status: 401, headers: {"cache-control": "no-store"}}); } const duration = form.get("remember") === "1" ? REMEMBER : DAY; const expiry = String(Math.floor(Date.now() / 1000) + duration); const signature = hex(await crypto.subtle.sign("HMAC", await key(secret), bytes.encode(expiry))); return new Response(null, {status: 204, headers: { "set-cookie": COOKIE + "=" + expiry + "." + signature + "; Path=/; Secure; HttpOnly; "+(new URL(request.url).searchParams.get("embedded")==="1"?"SameSite=None; Partitioned; ":"SameSite=Strict; ")+"Max-Age=" + duration, "cache-control": "no-store", }}); }