|
| 1 | +(async function () { |
| 2 | + const BASE = "https://h4f.me/url?ref="; |
| 3 | + const SELF_HOST = location.hostname; |
| 4 | + const SKIP_SCHEMES = ["mailto:", "tel:", "sms:", "whatsapp:", "tg:", "skype:", "javascript:"]; |
| 5 | + const RECEIVER_PUBLIC_KEY = "TOKEN_HERE"; |
| 6 | + |
| 7 | + function b64uToBuf(s) { |
| 8 | + s = s.replace(/-/g, "+").replace(/_/g, "/"); |
| 9 | + s = s.padEnd(Math.ceil(s.length / 4) * 4, "="); |
| 10 | + return Uint8Array.from(atob(s), c => c.charCodeAt(0)); |
| 11 | + } |
| 12 | + |
| 13 | + function bufToB64u(b) { |
| 14 | + return btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); |
| 15 | + } |
| 16 | + |
| 17 | + async function encrypt(message) { |
| 18 | + const receiverKeyRaw = b64uToBuf(RECEIVER_PUBLIC_KEY); |
| 19 | + const eph = await crypto.subtle.generateKey({ name: "X25519" }, true, ["deriveBits"]); |
| 20 | + const receiverKey = await crypto.subtle.importKey("raw", receiverKeyRaw, { name: "X25519" }, false, []); |
| 21 | + const shared = await crypto.subtle.deriveBits({ name: "X25519", public: receiverKey }, eph.privateKey, 256); |
| 22 | + const keyMaterial = await crypto.subtle.digest("SHA-256", shared); |
| 23 | + const aesKey = await crypto.subtle.importKey("raw", keyMaterial, { name: "AES-GCM" }, false, ["encrypt"]); |
| 24 | + const iv = crypto.getRandomValues(new Uint8Array(12)); |
| 25 | + const encrypted = new Uint8Array(await crypto.subtle.encrypt( |
| 26 | + { name: "AES-GCM", iv }, |
| 27 | + aesKey, |
| 28 | + new TextEncoder().encode(message) |
| 29 | + )); |
| 30 | + const ephPubRaw = new Uint8Array(await crypto.subtle.exportKey("raw", eph.publicKey)); |
| 31 | + const payload = new Uint8Array(ephPubRaw.length + iv.length + encrypted.length); |
| 32 | + payload.set(ephPubRaw, 0); |
| 33 | + payload.set(iv, 32); |
| 34 | + payload.set(encrypted, 44); |
| 35 | + return bufToB64u(payload); |
| 36 | + } |
| 37 | + |
| 38 | + function isInternal(href) { |
| 39 | + if (href.startsWith("/")) return true; |
| 40 | + try { |
| 41 | + const u = new URL(href, location.origin); |
| 42 | + return u.hostname === SELF_HOST; |
| 43 | + } catch { |
| 44 | + return true; |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + async function rewrite() { |
| 49 | + const links = [...document.querySelectorAll("a[href]")]; |
| 50 | + for (const a of links) { |
| 51 | + const href = a.getAttribute("href"); |
| 52 | + if (!href) continue; |
| 53 | + if (href.startsWith("#")) continue; |
| 54 | + if (SKIP_SCHEMES.some(s => href.toLowerCase().startsWith(s))) continue; |
| 55 | + if (href.startsWith(BASE)) continue; |
| 56 | + if (isInternal(href)) continue; |
| 57 | + const encrypted = await encrypt(href); |
| 58 | + a.setAttribute("href", BASE + encrypted); |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + if (document.readyState === "loading") { |
| 63 | + document.addEventListener("DOMContentLoaded", rewrite); |
| 64 | + } else { |
| 65 | + rewrite(); |
| 66 | + } |
| 67 | +})(); |
0 commit comments