import { connect } from 'cloudflare:sockets'; const DEFAULT_UUID = 'd3b07384-d113-424a-a726-a667d6489318'; // ── VLESS WebSocket Proxy Engine ────────────────────────────────────────── function stringifyUUID(buf, offset = 0) { const bytes = Array.from(new Uint8Array(buf, offset, 16)); const hex = bytes.map(b => b.toString(16).padStart(2, '0')).join(''); return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}`; } function parseVlessHeader(buffer, userID) { if (buffer.byteLength < 24) { return { hasError: true, message: 'Header too short' }; } const view = new DataView(buffer); const version = view.getUint8(0); const clientUUID = stringifyUUID(buffer, 1); const expectedUUID = (userID || DEFAULT_UUID).toLowerCase(); if (clientUUID.toLowerCase() !== expectedUUID) { return { hasError: true, message: `Invalid UUID: ${clientUUID}` }; } const addonLen = view.getUint8(17); let offset = 18 + addonLen; const command = view.getUint8(offset); // 1 = TCP, 2 = UDP offset += 1; const port = view.getUint16(offset, false); offset += 2; const addrType = view.getUint8(offset); offset += 1; let address = ''; if (addrType === 1) { // IPv4 address = Array.from(new Uint8Array(buffer, offset, 4)).join('.'); offset += 4; } else if (addrType === 2) { // Domain Name const domainLen = view.getUint8(offset); offset += 1; const domainBytes = new Uint8Array(buffer, offset, domainLen); address = new TextDecoder().decode(domainBytes); offset += domainLen; } else if (addrType === 3) { // IPv6 const ipv6 = []; for (let i = 0; i < 8; i++) { ipv6.push(view.getUint16(offset + i * 2, false).toString(16)); } address = ipv6.join(':'); offset += 16; } else { return { hasError: true, message: `Unsupported address type: ${addrType}` }; } const rawData = buffer.slice(offset); return { hasError: false, version, clientUUID, command, port, address, rawData }; } function getProxyIpList(env) { const list = []; if (env && env.PROXYIP) { list.push(...env.PROXYIP.split(',').map(s => s.trim()).filter(Boolean)); } list.push( 'proxyip.hk.fxxk.dedyn.io', 'proxyip.oracle.fxxk.dedyn.io', 'proxyip.aliyun.fxxk.dedyn.io', 'proxyip.fxxk.dedyn.io', 'proxyip.jp.fxxk.dedyn.io', 'proxyip.us.fxxk.dedyn.io', 'proxyip.digitalocean.fxxk.dedyn.io', 'proxyip.fhs.tw', 'proxyip.aliyun.fhs.tw' ); return Array.from(new Set(list)); } async function connectToTarget(addressRemote, portRemote, initialData, env, webSocket, wrapper) { const proxyIps = getProxyIpList(env); const targetsToTry = [addressRemote, ...proxyIps]; let currentTargetIdx = 0; // Save any payload bundled with the initial VLESS frame into our handshake buffer if (initialData && initialData.byteLength > 0) { wrapper.buffer.push(new Uint8Array(initialData)); } async function tryConnect() { if (currentTargetIdx >= targetsToTry.length) { try { webSocket.close(1011, 'All connection candidates failed'); } catch (_) {} return; } const targetHost = targetsToTry[currentTargetIdx]; currentTargetIdx++; let tcpSocket = null; try { tcpSocket = connect({ hostname: targetHost, port: portRemote }); wrapper.testingSocket = tcpSocket; const writer = tcpSocket.writable.getWriter(); // RE-PLAY all buffered handshake chunks (e.g. initialData + TLS ClientHello) to the new candidate for (const chunk of wrapper.buffer) { if (chunk && chunk.byteLength > 0) { await writer.write(chunk); } } writer.releaseLock(); let hasReceivedData = false; try { const reader = tcpSocket.readable.getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; if (value && value.byteLength > 0) { if (!hasReceivedData) { hasReceivedData = true; wrapper.isConnected = true; wrapper.value = tcpSocket; // Confirm socket as active and working wrapper.buffer = []; // Clear the handshake buffer } if (webSocket.readyState === 1) { webSocket.send(value); } } } } catch (err) { // Socket readable stream error or disconnection } // If candidate closed/errored without returning ANY bytes (e.g. direct Cloudflare CDN rejection), fallback instantly if (!hasReceivedData) { try { tcpSocket.close(); } catch (_) {} if (wrapper.testingSocket === tcpSocket) wrapper.testingSocket = null; if (currentTargetIdx < targetsToTry.length) { await tryConnect(); } else { try { webSocket.close(1011, 'Connection closed without response from any candidate'); } catch (_) {} } } else { try { webSocket.close(); } catch (_) {} } } catch (err) { if (tcpSocket) { try { tcpSocket.close(); } catch (_) {} } if (wrapper.testingSocket === tcpSocket) wrapper.testingSocket = null; if (currentTargetIdx < targetsToTry.length) { await tryConnect(); } else { try { webSocket.close(1011, err.message); } catch (_) {} } } } await tryConnect(); } function base64ToArrayBuffer(base64Str) { if (!base64Str) return null; try { const cleanStr = base64Str.replace(/-/g, '+').replace(/_/g, '/'); const binary = atob(cleanStr); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; } catch (e) { return null; } } function makeReadableWebSocketStream(webSocket, earlyDataHeader) { return new ReadableStream({ start(controller) { if (earlyDataHeader) { const earlyData = base64ToArrayBuffer(earlyDataHeader); if (earlyData && earlyData.byteLength > 0) { controller.enqueue(earlyData); } } webSocket.addEventListener('message', (event) => { const data = event.data; if (typeof data === 'string') return; const buffer = data instanceof ArrayBuffer ? data : data.buffer; controller.enqueue(buffer); }); webSocket.addEventListener('close', () => { try { controller.close(); } catch (_) {} }); webSocket.addEventListener('error', () => { try { controller.error(); } catch (_) {} }); } }); } async function handleDnsPacket(rawData, webSocket) { try { if (!rawData || rawData.byteLength < 2) return; const view = new DataView(rawData instanceof ArrayBuffer ? rawData : rawData.buffer); let dnsQuery = rawData; const len = view.getUint16(0, false); if (len + 2 <= rawData.byteLength) { dnsQuery = rawData.slice(2, 2 + len); } const dohRes = await fetch('https://1.1.1.1/dns-query', { method: 'POST', headers: { 'content-type': 'application/dns-message', 'accept': 'application/dns-message' }, body: dnsQuery }); if (dohRes.ok) { const dnsResponse = await dohRes.arrayBuffer(); const respLen = dnsResponse.byteLength; const frame = new Uint8Array(2 + respLen); frame[0] = (respLen >> 8) & 0xff; frame[1] = respLen & 0xff; frame.set(new Uint8Array(dnsResponse), 2); if (webSocket.readyState === 1) { webSocket.send(frame); } } } catch (e) { console.error('DNS DoH error:', e); } } async function handleVlessWebsocket(request, targetUUID, env) { const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair); server.accept(); const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; const readableStream = makeReadableWebSocketStream(server, earlyDataHeader); const remoteSocketWrapper = { value: null, testingSocket: null, buffer: [], isConnected: false }; let isVlessHeaderParsed = false; let isDnsMode = false; readableStream.pipeTo(new WritableStream({ async write(chunk, controller) { if (isDnsMode) { await handleDnsPacket(chunk, server); return; } if (isVlessHeaderParsed) { const payload = new Uint8Array(chunk); if (remoteSocketWrapper.isConnected && remoteSocketWrapper.value) { // Connection confirmed! Write directly to active socket. try { const writer = remoteSocketWrapper.value.writable.getWriter(); await writer.write(payload); writer.releaseLock(); } catch (err) { console.error('Socket write error:', err); } } else { // Connection is still setting up / testing candidates! // Save chunk in buffer so it gets re-played on failover! remoteSocketWrapper.buffer.push(payload); if (remoteSocketWrapper.testingSocket) { try { const writer = remoteSocketWrapper.testingSocket.writable.getWriter(); await writer.write(payload); writer.releaseLock(); } catch (_) {} } } return; } if (!isVlessHeaderParsed) { isVlessHeaderParsed = true; const parsed = parseVlessHeader(chunk, targetUUID); if (parsed.hasError) { try { server.close(1008, parsed.message); } catch (_) {} return; } // Send VLESS Response Header server.send(new Uint8Array([parsed.version, 0])); // Command 2 is VLESS UDP; Port 53 is DNS if (parsed.command === 2 || (parsed.command === 1 && parsed.port === 53)) { isDnsMode = true; await handleDnsPacket(parsed.rawData, server); return; } // Command 1 is TCP Proxy if (parsed.command === 1) { connectToTarget(parsed.address, parsed.port, parsed.rawData, env, server, remoteSocketWrapper); } else { try { server.close(1003, 'Unsupported VLESS command'); } catch (_) {} } } }, close() { if (remoteSocketWrapper.value) { try { remoteSocketWrapper.value.close(); } catch (_) {} } if (remoteSocketWrapper.testingSocket) { try { remoteSocketWrapper.testingSocket.close(); } catch (_) {} } }, abort() { if (remoteSocketWrapper.value) { try { remoteSocketWrapper.value.close(); } catch (_) {} } if (remoteSocketWrapper.testingSocket) { try { remoteSocketWrapper.testingSocket.close(); } catch (_) {} } } })).catch((e) => { console.error('Stream pipe error:', e); }); const responseHeaders = new Headers(); if (earlyDataHeader) { responseHeaders.set('Sec-WebSocket-Protocol', earlyDataHeader); } return new Response(null, { status: 101, webSocket: client, headers: responseHeaders }); } // ── gRPC VLESS Transport Engine (HTTP/2 Bidirectional Streaming) ────────── function wrapGrpcFrame(payload) { const len = payload.byteLength; const frame = new Uint8Array(5 + len); frame[0] = 0; // Uncompressed frame[1] = (len >> 24) & 0xff; frame[2] = (len >> 16) & 0xff; frame[3] = (len >> 8) & 0xff; frame[4] = len & 0xff; frame.set(new Uint8Array(payload), 5); return frame; } function unwrapGrpcFrame(data) { if (!data || data.byteLength < 5) return data; const view = new DataView(data.buffer || data, data.byteOffset || 0, data.byteLength); const len = view.getUint32(1, false); if (data.byteLength >= 5 + len) { return new Uint8Array(data.buffer || data, (data.byteOffset || 0) + 5, len); } return data; } async function handleDnsPacketGrpc(rawData, grpcWriter) { try { if (!rawData || rawData.byteLength < 2) return; const view = new DataView(rawData instanceof ArrayBuffer ? rawData : rawData.buffer); let dnsQuery = rawData; const len = view.getUint16(0, false); if (len + 2 <= rawData.byteLength) { dnsQuery = rawData.slice(2, 2 + len); } const dohRes = await fetch('https://1.1.1.1/dns-query', { method: 'POST', headers: { 'content-type': 'application/dns-message', 'accept': 'application/dns-message' }, body: dnsQuery }); if (dohRes.ok) { const dnsResponse = await dohRes.arrayBuffer(); const respLen = dnsResponse.byteLength; const frame = new Uint8Array(2 + respLen); frame[0] = (respLen >> 8) & 0xff; frame[1] = respLen & 0xff; frame.set(new Uint8Array(dnsResponse), 2); await grpcWriter.write(wrapGrpcFrame(frame)); } } catch (e) { console.error('gRPC DNS error:', e); } } async function connectToTargetGrpc(addressRemote, portRemote, initialData, env, grpcWriter, wrapper) { const proxyIps = getProxyIpList(env); const targetsToTry = [addressRemote, ...proxyIps]; let currentTargetIdx = 0; if (initialData && initialData.byteLength > 0) { wrapper.buffer.push(new Uint8Array(initialData)); } async function tryConnect() { if (currentTargetIdx >= targetsToTry.length) return; const targetHost = targetsToTry[currentTargetIdx]; currentTargetIdx++; let tcpSocket = null; try { tcpSocket = connect({ hostname: targetHost, port: portRemote }); wrapper.testingSocket = tcpSocket; const writer = tcpSocket.writable.getWriter(); for (const chunk of wrapper.buffer) { if (chunk && chunk.byteLength > 0) { await writer.write(chunk); } } writer.releaseLock(); let hasReceivedData = false; try { const reader = tcpSocket.readable.getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; if (value && value.byteLength > 0) { if (!hasReceivedData) { hasReceivedData = true; wrapper.isConnected = true; wrapper.value = tcpSocket; wrapper.buffer = []; } await grpcWriter.write(wrapGrpcFrame(value)); } } } catch (err) {} if (!hasReceivedData) { try { tcpSocket.close(); } catch (_) {} if (wrapper.testingSocket === tcpSocket) wrapper.testingSocket = null; if (currentTargetIdx < targetsToTry.length) { await tryConnect(); } } else { try { tcpSocket.close(); } catch (_) {} } } catch (err) { if (tcpSocket) try { tcpSocket.close(); } catch (_) {} if (wrapper.testingSocket === tcpSocket) wrapper.testingSocket = null; if (currentTargetIdx < targetsToTry.length) { await tryConnect(); } } } await tryConnect(); } async function handleVlessGrpc(request, targetUUID, env) { if (!request.body) { return new Response('gRPC body required', { status: 400 }); } const { readable, writable } = new TransformStream(); const grpcWriter = writable.getWriter(); const remoteSocketWrapper = { value: null, testingSocket: null, buffer: [], isConnected: false }; let isVlessHeaderParsed = false; let isDnsMode = false; const reader = request.body.getReader(); (async () => { try { while (true) { const { value, done } = await reader.read(); if (done) break; if (!value || value.byteLength === 0) continue; const payload = unwrapGrpcFrame(value); if (isDnsMode) { await handleDnsPacketGrpc(payload, grpcWriter); continue; } if (isVlessHeaderParsed) { if (remoteSocketWrapper.isConnected && remoteSocketWrapper.value) { try { const sockWriter = remoteSocketWrapper.value.writable.getWriter(); await sockWriter.write(payload); sockWriter.releaseLock(); } catch (e) {} } else { remoteSocketWrapper.buffer.push(payload); if (remoteSocketWrapper.testingSocket) { try { const sockWriter = remoteSocketWrapper.testingSocket.writable.getWriter(); await sockWriter.write(payload); sockWriter.releaseLock(); } catch (e) {} } } continue; } isVlessHeaderParsed = true; const parsed = parseVlessHeader(payload.buffer || payload, targetUUID); if (parsed.hasError) return; // Write VLESS Response Header in gRPC frame await grpcWriter.write(wrapGrpcFrame(new Uint8Array([parsed.version, 0]))); if (parsed.command === 2 || (parsed.command === 1 && parsed.port === 53)) { isDnsMode = true; await handleDnsPacketGrpc(parsed.rawData, grpcWriter); continue; } if (parsed.command === 1) { connectToTargetGrpc(parsed.address, parsed.port, parsed.rawData, env, grpcWriter, remoteSocketWrapper); } } } catch (err) { console.error('gRPC stream error:', err); } })(); return new Response(readable, { status: 200, headers: { 'content-type': 'application/grpc', 'grpc-status': '0', 'grpc-message': '' } }); } async function handleTestProxyIp(request, env) { const url = new URL(request.url); const targetDomain = url.searchParams.get('domain') || 'nachotoy.com'; const candidates = [ targetDomain, 'icook.hk', 'icook.tw', 'cdn.anycast.eu.org', 'www.visa.com', 'www.wipac.wisc.edu', 'csgowrite.com', 'www.gov.se', 'www.shufersal.co.il', 'www.macadamias.org', '104.16.1.1', '104.17.147.22', '162.159.138.7', '188.114.96.1', '188.114.97.7' ]; if (env && env.PROXYIP) { const fromEnv = env.PROXYIP.split(',').map(s => s.trim()).filter(Boolean); for (const ip of fromEnv) { if (!candidates.includes(ip)) candidates.push(ip); } } const results = []; for (const candidate of candidates) { const res = { candidate, status: 'testing', timeMs: 0, error: null, preview: '' }; const start = Date.now(); try { const socket = connect({ hostname: candidate, port: 80 }); const writer = socket.writable.getWriter(); const reqStr = `GET / HTTP/1.1\r\nHost: ${targetDomain}\r\nConnection: close\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\r\n\r\n`; const encoded = new TextEncoder().encode(reqStr); await writer.write(encoded); writer.releaseLock(); const reader = socket.readable.getReader(); const readRes = await reader.read(); res.timeMs = Date.now() - start; if (readRes.done || !readRes.value || readRes.value.byteLength === 0) { res.status = 'closed_no_data'; } else { res.status = 'SUCCESS'; const txt = new TextDecoder().decode(readRes.value); res.preview = txt.substring(0, 100).replace(/\r/g, '\\r').replace(/\n/g, '\\n'); } try { socket.close(); } catch (_) {} } catch (e) { res.timeMs = Date.now() - start; res.status = 'ERROR'; res.error = e.message; } results.push(res); } return new Response(JSON.stringify({ targetDomain, testedCount: results.length, results }, null, 2), { headers: { 'content-type': 'application/json; charset=utf-8' } }); } function renderDashboard(request, env, uuid) { const url = new URL(request.url); const host = url.host; const vlessLink = `vless://${uuid}@${host}:443?encryption=none&security=tls&sni=${host}&fp=chrome&type=ws&host=${host}&path=%2F#Cloudflare-VLESS-Tunnel-WS`; const html = ` שרת מנהור מוצפן VLESS - לוח בקרה
מנהור מוצפן VLESS Proxy
Cloudflare Worker Server
שרת מנהור פעיל
קישור חיבור מהיר (VLESS URI)
${vlessLink}
פרטי שרת חיבור
כתובת השרת (Host / SNI): ${host}
פורט (Port): 443 (TLS)
פרוטוקול: VLESS + WebSocket
מזהה UUID: ${uuid}
סריקה מהירה באפליקציה

🚀 הוראות חיבור בתוכנות פרוקסי

`; return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } }); } function renderPrivacyPolicy(request) { const html = ` מדיניות פרטיות - SmartBlock PRO

מדיניות פרטיות – SmartBlock PRO

שקופות, אבטחה ופרטיות מוחלטת בשימוש בתוסף

אודות התוסף ומטרתו

SmartBlock PRO הינו תוסף המיועד לחסימת פרסומות מתקדמת באתרי חדשות ותוכן. התוסף כולל ממשק שליטה נוח ומאפשר חוויית גלישה נקייה, מהירה וחלקה ללא הטרדות חזותיות.

התחייבות לפרטיות ולאיסוף נתונים

התוסף מיועד לחסימת פרסומות בלבד. התוסף אינו אוסף, אינו שומר ואינו משתף כל מזהה IP, היסטוריית גלישה או מידע אישי מכל סוג שהוא.

הרשאות נדרשות וסיבת השימוש בהן

על מנת לבצע את תפקידו, התוסף נזקק להרשאות גישה לתוכן האתרים (Content Scripts):

  • גישה לתוכן הדף: נדרשת אך ורק כדי לזהות ולהסיר אלמנטים של פרסומות בזמן אמת.
  • חסימת בקשות רשת: נדרשת לעצירת טעינת פרסומות ווידאו לפני הצגתם.

אתרים נתמכים ויכולות החסימה

התוסף מותאם וממקסם את החסימה באתרים הבאים:

כל רגע
MS - אמס
קול חי
JDN
פרוג
מוביט (Moovit)

פירוט החסימה:

  • באתר פרוג: חסימת כל הפרסומות הקופצות (Pop-ups), פרסומות עליונות, תחתונות וצידיות, וכן הסרת הודעות מ"פרוגבוט".
  • באתרים כל רגע, אמס, JDN וקול חי: חסימה מלאה של כלל הפרסומות באתר, כולל פרסומות ווידאו לפני צפייה.
`; return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } }); } // ── End VLESS Engine ────────────────────────────────────────────────────── // ============================================================================ // Ma'akaf (׳ž׳¢׳§׳£) ג€” Censorship-Resistant Web Proxy ֲ· Cloudflare Worker // Single-file architecture with XOR obfuscation, Service Worker, HTMLRewriter // ============================================================================ // ג”€ג”€ג”€ Section 1: Constants & Core Crypto ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ const PROXY_PATH = '/cdn/'; const KEY = [97, 110, 116, 105, 103, 114, 97, 118, 105, 116, 121]; // "antigravity" const SW_VERSION = 111; // ── Access gate (password) ────────────────────────────────────────────────── // The password and signing secret are read from env first (Cloudflare secrets) so the // owner can change them WITHOUT editing code, and rotate GATE_SECRET to instantly // invalidate every issued session. The literals below are only zero-config fallbacks. const GATE_PASSWORD_DEFAULT = 'מוישי'; // override with secret: GATE_PASSWORD const GATE_COOKIE = '__proxy_gate'; const GATE_SECRET_DEFAULT = 'etrog-gate-v1-antigravity'; // override with secret: GATE_SECRET const GATE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // signed sessions expire after 30 days // Built-in YouTube cookie slot. Leave EMPTY: fresh auth comes from the Home Relay // (Firefox session) via YT_RELAY_URL. Do NOT paste a personal Google session here. const BUILTIN_YOUTUBE_COOKIE = "SAPISID=UL6Vh_S22fpeIdM8/AD6Qb4wlcTslni3Dk; __Secure-3PAPISID=UL6Vh_S22fpeIdM8/AD6Qb4wlcTslni3Dk; VISITOR_INFO1_LIVE=CdhV9Fm2uW0; CONSENT=YES+cb.20210328-17-p0.en+FX+667"; // ── Home Relay Helpers ─────────────────────────────────────────────────────── function relayConfigured(env) { return !!(env && env.YT_RELAY_URL && env.YT_RELAY_SECRET); } function relayBase(env) { // Strip a leading BOM / zero-width / whitespace: PowerShell piping the tunnel URL // into `wrangler secret put` can prepend U+FEFF, which makes new URL() throw // "Invalid URL" and silently kills every relay call. return String(env.YT_RELAY_URL || '') .replace(/[​  ]/g, '') .trim() .replace(/\/+$/, ''); } // Same BOM/whitespace hazard as relayBase: a stray U+FEFF in the secret makes the // Bearer header mismatch the relay RELAY_SECRET -> every call gets 401. function relaySecret(env) { return String((env && env.YT_RELAY_SECRET) || '').replace(/[​  ]/g, '').trim(); } async function relayPlayer(env, videoId, cookie, workerOrigin = null) { let r, j, relayErr = null; try { r = await fetch(relayBase(env) + '/v1/yt/player', { method: 'POST', headers: { authorization: 'Bearer ' + relaySecret(env), 'content-type': 'application/json', 'Bypass-Tunnel-Reminder': 'true', // Intentionally NOT sending X-Worker-Url: it makes the relay call back to // the Worker's /api/resolve-helper (Cloudflare IP) for every client, which // re-triggers the exact LOGIN_REQUIRED block the relay exists to avoid — and // the round-trip loop times the tunnel out into a 502. The relay must resolve // directly from its own residential IP. }, body: JSON.stringify({ videoId, cookie: cookie || '' }), signal: AbortSignal.timeout(20000), }); j = await r.json().catch((e) => { relayErr = 'json-parse:' + e.message; return null; }); } catch (e) { return { ok: false, playability: null, attemptsLog: [], source: 'home-relay', relayErr: 'fetch-fail:' + e.message }; } if (!j || !j.ok || !j.formats || !j.formats.length) { return { ok: false, playability: j?.playability, attemptsLog: j?.attempts || [], source: 'home-relay', relayErr: relayErr || ('relay-http:' + (r?.status || '?')) }; } return { ok: true, formats: j.formats.map((f) => ({ itag: f.itag, url: f.url, mime: (f.mimeType || '').split(';')[0], mimeType: f.mimeType || '', // full type WITH codecs — required for MSE addSourceBuffer progressive: !!f.progressive, hasAudio: !!f.hasAudio, hasVideo: !!f.hasVideo, bitrate: f.bitrate || 0, width: f.width || 0, height: f.height || 0, })), playability: j.playability, client: j.client, hasCookies: !!cookie, hasAuth: !!cookie, attemptsLog: j.attempts || [], via: 'home-relay', }; } async function relayFetch(env, targetUrl, init = {}) { const u = new URL(relayBase(env) + '/v1/fetch'); u.searchParams.set('url', targetUrl); const headers = { authorization: 'Bearer ' + relaySecret(env), 'Bypass-Tunnel-Reminder': 'true', }; if (init.range) headers.range = init.range; if (init.userAgent) headers['x-upstream-ua'] = init.userAgent; return fetch(u.toString(), { headers, signal: init.signal }); } // Audio download as real MP3. YouTube never serves MP3 and this Worker can't transcode, so // the job is delegated to the Home Relay (residential IP + the user's local ffmpeg): it pulls // the full audio track and streams back audio/mpeg. We just pipe that body to the browser. async function handleAudioMp3(request, url, env) { if (!relayConfigured(env)) { return new Response( JSON.stringify({ error: 'relay_not_configured', hint: 'הורדת MP3 דורשת את ה-Home Relay (ffmpeg מקומי). הפעילו start-relay.ps1 והגדירו YT_RELAY_URL / YT_RELAY_SECRET.', }), { status: 502, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } } ); } const v = String(url.searchParams.get('v') || '').replace(/[^\w-]/g, ''); if (!v) { return new Response(JSON.stringify({ error: 'missing v' }), { status: 400, headers: { 'content-type': 'application/json' } }); } const name = url.searchParams.get('name') || ''; const relayUrl = new URL(relayBase(env) + '/v1/yt/audio-mp3'); relayUrl.searchParams.set('v', v); if (name) relayUrl.searchParams.set('name', name); let r; try { r = await fetch(relayUrl.toString(), { headers: { authorization: 'Bearer ' + relaySecret(env), 'Bypass-Tunnel-Reminder': 'true' }, // Download + transcode of a full track can take a while; give it generous headroom. signal: AbortSignal.timeout(180000), }); } catch (e) { return new Response( JSON.stringify({ error: 'relay_fetch_failed', detail: String((e && e.message) || e) }), { status: 502, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } } ); } const h = new Headers(); applyCorsHeaders(h, request); const ct = r.headers.get('content-type') || 'audio/mpeg'; h.set('content-type', ct); h.set('cache-control', 'no-store'); h.set('X-Etrog-Mode', 'stream-unsealed'); const cd = r.headers.get('content-disposition'); if (cd) h.set('content-disposition', cd); h.set('Access-Control-Expose-Headers', 'Content-Type, Content-Disposition, X-Etrog-Mode'); return new Response(r.body, { status: r.status, headers: h }); } function cleanSecret(s) { // Strip invisible copy/paste artifacts (BOM, zero-width, nbsp) and trim edges, but keep // internal spaces so a passphrase-style GATE_PASSWORD survives intact. return String(s || '').replace(/[​‌‍ ]/g, '').trim(); } function gatePassword(env) { const fromEnv = cleanSecret(env && env.GATE_PASSWORD); return fromEnv || GATE_PASSWORD_DEFAULT; } function gateSecret(env) { const fromEnv = cleanSecret(env && env.GATE_SECRET); return fromEnv || GATE_SECRET_DEFAULT; } async function gateHmac(secret, message) { const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message)); const bytes = new Uint8Array(sig); let b64 = ''; for (let i = 0; i < bytes.length; i++) b64 += String.fromCharCode(bytes[i]); return btoa(b64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } // Constant-time compare of two ASCII/base64url strings (avoids token-timing leak). function timingSafeEqualStr(a, b) { a = String(a || ''); b = String(b || ''); if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); return diff === 0; } // Session token format: `v2..` where sig = HMAC(gateSecret, password|expMs). // Non-constant (varies with exp), expires after GATE_TTL_MS, and revocable by rotating // GATE_SECRET — unlike the old fixed HMAC that was one permanent value for everyone. async function mintGateToken(env) { const exp = Date.now() + GATE_TTL_MS; const sig = await gateHmac(gateSecret(env), gatePassword(env) + '|' + exp); return 'v2.' + exp + '.' + sig; } async function verifyGateToken(token, env) { const t = String(token || ''); const parts = t.split('.'); if (parts.length !== 3 || parts[0] !== 'v2') return false; const exp = parseInt(parts[1], 10); if (!Number.isFinite(exp) || exp < Date.now()) return false; const expect = await gateHmac(gateSecret(env), gatePassword(env) + '|' + exp); return timingSafeEqualStr(parts[2], expect); } function getCookieValue(cookieHeader, name) { if (!cookieHeader) return ''; const parts = cookieHeader.split(';'); for (const p of parts) { const eq = p.indexOf('='); if (eq === -1) continue; if (p.slice(0, eq).trim() === name) return p.slice(eq + 1).trim(); } return ''; } async function isGateOpen(request, env) { const auth = request.headers.get('authorization'); if (auth && env && env.YT_RELAY_SECRET && auth === 'Bearer ' + relaySecret(env)) { return true; } const got = getCookieValue(request.headers.get('cookie') || '', GATE_COOKIE); if (!got) return false; return await verifyGateToken(got, env); } function gateLoginPage(opts = {}) { const err = opts.error ? '
סיסמה שגויה. נסו שוב.
' : ''; const next = String(opts.next || '/').replace(/[<>"']/g, ''); const html = ` כניסה · מעקף פרוקסי

🔒 כניסה

הזינו את הסיסמה כדי להמשיך. הגישה נשמרת עד סגירת הדפדפן.

${err}
`; return new Response(html, { status: opts.error ? 401 : 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-frame-options': 'DENY', }, }); } async function handleGatePost(request, url, env) { let password = ''; let next = '/'; const ct = (request.headers.get('content-type') || '').toLowerCase(); try { if (ct.includes('application/json')) { const j = await request.json(); password = String(j.password || j.pass || ''); next = String(j.next || '/'); } else { const form = await request.formData(); password = String(form.get('password') || form.get('pass') || ''); next = String(form.get('next') || '/'); } } catch { password = ''; } // Only allow relative next paths on this host try { if (!next.startsWith('/') || next.startsWith('//')) next = '/'; } catch { next = '/'; } if (!timingSafeEqualStr(password, gatePassword(env))) { return gateLoginPage({ error: true, next }); } const token = await mintGateToken(env); // Session cookie: no Max-Age / Expires → deleted when browser closes const cookie = GATE_COOKIE + '=' + token + '; Path=/; Secure; HttpOnly; SameSite=Lax'; return new Response(null, { status: 302, headers: { location: next || '/', 'set-cookie': cookie, 'cache-control': 'no-store', }, }); } async function requireGate(request, url, env) { // Login endpoint always reachable if (url.pathname === '/__gate') { if (request.method === 'POST') return handleGatePost(request, url, env); return gateLoginPage({ next: url.searchParams.get('next') || '/' }); } // Logout (optional) if (url.pathname === '/__logout') { return new Response(null, { status: 302, headers: { location: '/__gate', 'set-cookie': GATE_COOKIE + '=; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=0', 'cache-control': 'no-store', }, }); } // Stealth media, thumbnails, and Service Worker are public so