diff --git a/_worker.js b/_worker.js index 8396d96f4..2f405eef0 100644 --- a/_worker.js +++ b/_worker.js @@ -3,11 +3,11 @@ // @ts-ignore // https://github.com/bia-pain-bache/BPB-Worker-Panel -import { connect } from "cloudflare:sockets"; +import { connect } from 'cloudflare:sockets'; // How to generate your own UUID: // https://www.uuidgenerator.net/ -let userID = "89b3cbba-e6ac-485a-9481-976a0415eab9"; +let userID = '89b3cbba-e6ac-485a-9481-976a0415eab9'; // https://www.nslookup.io/domains/cdn.xn--b6gac.eu.org/dns-records/ // https://www.nslookup.io/domains/cdn-all.xn--b6gac.eu.org/dns-records/ @@ -15,10 +15,10 @@ const proxyIPs= ['cdn.xn--b6gac.eu.org', 'cdn-all.xn--b6gac.eu.org', 'edgetunnel let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; -let dohURL = "https://cloudflare-dns.com/dns-query"; +let dohURL = 'https://cloudflare-dns.com/dns-query'; if (!isValidUUID(userID)) { - throw new Error("uuid is not valid"); + throw new Error('uuid is not valid'); } export default { @@ -34,28 +34,28 @@ export default { userID = env.UUID || userID; proxyIP = env.PROXYIP || proxyIP; dohURL = env.DNS_RESOLVER_URL || dohURL; - const upgradeHeader = request.headers.get("Upgrade"); + const upgradeHeader = request.headers.get('Upgrade'); - if (!upgradeHeader || upgradeHeader !== "websocket") { + if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); const searchParams = new URLSearchParams(url.search); - const host = request.headers.get("Host"); - const client = searchParams.get("app"); + const host = request.headers.get('Host'); + const client = searchParams.get('app'); switch (url.pathname) { - case "/cf": + case '/cf': return new Response(JSON.stringify(request.cf, null, 4), { status: 200, headers: { - "Content-Type": "application/json;charset=utf-8", + 'Content-Type': 'application/json;charset=utf-8', }, }); - case "/connect": // for test connect to cf socket + case '/connect': // for test connect to cf socket - const [hostname, port] = ["cloudflare.com", "80"]; + const [hostname, port] = ['cloudflare.com', '80']; console.log(`Connecting to ${hostname}:${port}...`); try { @@ -69,7 +69,7 @@ export default { try { await writer.write( new TextEncoder().encode( - "GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n" + 'GET / HTTP/1.1\r\nHost: ' + hostname + '\r\n\r\n' ) ); } catch (writeError) { @@ -104,31 +104,30 @@ export default { case `/sub/${userID}`: - const {singboxSub, xraySub} = await getVLESSConfig(env); - const vlessConfigs = client === "singbox" ? singboxSub : xraySub; + const normalConfigs = await getVLESSConfig(env, client); - return new Response(vlessConfigs, { + return new Response(normalConfigs, { status: 200, headers: { - "Content-Type": "text/plain;charset=utf-8" + 'Content-Type': 'text/plain;charset=utf-8' }, }); case `/fragsub/${userID}`: - const fragConfigs = await getFragVLESSConfig(env); - const fragmentSub = fragConfigs.map(config => config.fragConf); + let fragConfigs = await getFragVLESSConfig(env, 'v2ray'); + fragConfigs = fragConfigs.map(config => config.config); - return new Response(`${JSON.stringify(fragmentSub, null, 4)}`, { + return new Response(`${JSON.stringify(fragConfigs, null, 4)}`, { status: 200, headers: { - "Content-Type": "text/plain;charset=utf-8" + 'Content-Type': 'text/plain;charset=utf-8' }, }); - case `/panel`: + case '/panel': - if (request.method === "POST") { + if (request.method === 'POST') { if (!(await verifyJWTToken(request, env))) { return new Response('Unauthorized', { status: 401, @@ -139,24 +138,12 @@ export default { } const formData = await request.formData(); - await updateDataset( - env, - host, - formData.get("remoteDNS"), - formData.get("localDNS"), - formData.get("fragmentLengthMin"), - formData.get("fragmentLengthMax"), - formData.get("fragmentIntervalMin"), - formData.get("fragmentIntervalMax"), - formData.get("block-ads"), - formData.get("bypass-iran"), - formData.get("cleanIPs") - ); + await updateDataset(env, host, formData); return new Response('Success', { status: 200, headers: { - "Content-Type": "text/plain;charset=utf-8" + 'Content-Type': 'text/plain;charset=utf-8' }, }); } @@ -165,61 +152,51 @@ export default { return Response.redirect(`${url.origin}/login`, 302); } - const hostValue = await env.bpb.get("host"); + const hostValue = await env.bpb.get('host'); if (!hostValue) { - await updateDataset( - env, - host, - "https://94.140.14.14/dns-query", - "1.1.1.1", - "100", - "200", - "5", - "10", - "false", - "false", - "" - ); + await updateDataset(env, host); } - if (hostValue !== host) await env.bpb.put("host", host); - const fragConfs = await getFragVLESSConfig(env); + if (hostValue !== host) await env.bpb.put('host', host); + const fragConfs = await getFragVLESSConfig(env, 'nekoray'); const htmlPage = await renderPage(env, fragConfs); return new Response(htmlPage, { status: 200, headers: { - "Content-Type": "text/html", - "Access-Control-Allow-Origin": url.origin, - "Access-Control-Allow-Methods": "GET, POST", - "Access-Control-Allow-Headers": "Content-Type, Authorization", - "X-Content-Type-Options": "nosniff", - "X-Frame-Options": "DENY", - "Referrer-Policy": "strict-origin-when-cross-origin" + 'Content-Type': 'text/html', + 'Access-Control-Allow-Origin': url.origin, + 'Access-Control-Allow-Methods': 'GET, POST', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Referrer-Policy': 'strict-origin-when-cross-origin' } }); - case `/login`: + case '/login': + + if (await verifyJWTToken(request, env)) { + return Response.redirect(`${url.origin}/panel`, 302); + } + + let secretKey = await env.bpb.get('secretKey'); + let pwd = await env.bpb.get('pwd'); - let secretKey = await env.bpb.get("secretKey"); - let pwd = await env.bpb.get("pwd"); if (!pwd) { - await env.bpb.put("pwd", 'admin'); + await env.bpb.put('pwd', 'admin'); } if (!secretKey) { secretKey = generateSecretKey(); - await env.bpb.put("secretKey", secretKey); + await env.bpb.put('secretKey', secretKey); } - if (await verifyJWTToken(request, env)) { - return Response.redirect(`${url.origin}/panel`, 302); - } if (request.method === 'POST') { const password = await request.text(); - const savedPass = await env.bpb.get("pwd"); + const savedPass = await env.bpb.get('pwd'); if (password === savedPass) { const jwtToken = generateJWTToken(secretKey, password); const cookieHeader = `jwtToken=${jwtToken}; HttpOnly; Secure; Max-Age=${7 * 24 * 60 * 60}; Path=/; SameSite=Strict`; @@ -241,17 +218,17 @@ export default { return new Response(loginPage, { status: 200, headers: { - "Content-Type": "text/html", - "Access-Control-Allow-Origin": url.origin, - "Access-Control-Allow-Methods": "GET, POST", - "Access-Control-Allow-Headers": "Content-Type, Authorization", - "X-Content-Type-Options": "nosniff", - "X-Frame-Options": "DENY", - "Referrer-Policy": "strict-origin-when-cross-origin" + 'Content-Type': 'text/html', + 'Access-Control-Allow-Origin': url.origin, + 'Access-Control-Allow-Methods': 'GET, POST', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Referrer-Policy': 'strict-origin-when-cross-origin' }, }); - case `/logout`: + case '/logout': return new Response('Success', { status: 200, @@ -261,7 +238,7 @@ export default { }, }); - case `/panel/password`: + case '/panel/password': if (!(await verifyJWTToken(request, env))) { return new Response('Unauthorized!', { @@ -273,7 +250,7 @@ export default { } const newPwd = await request.text(); - const oldPwd = await env.bpb.get("pwd"); + const oldPwd = await env.bpb.get('pwd'); if (newPwd === oldPwd) { return new Response('Please enter a new Password!', { @@ -284,7 +261,7 @@ export default { }); } - await env.bpb.put("pwd", newPwd); + await env.bpb.put('pwd', newPwd); return new Response('Success', { status: 200, @@ -297,8 +274,8 @@ export default { default: // return new Response('Not found', { status: 404 }); // For any other path, reverse proxy to 'www.fmprc.gov.cn' and return the original response - url.hostname = "www.speedtest.net"; - url.protocol = "https:"; + url.hostname = 'www.speedtest.net'; + url.protocol = 'https:'; request = new Request(url, request); return await fetch(request); } @@ -870,15 +847,15 @@ async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { * @returns {string} */ -const getVLESSConfig = async (env) => { - let vlessWsTls = ""; - const cleanIPs = await env.bpb.get("cleanIPs"); - const hostName = await env.bpb.get("host"); +const getVLESSConfig = async (env, client) => { + let vlessWsTls = ''; + const cleanIPs = await env.bpb.get('cleanIPs'); + const hostName = await env.bpb.get('host'); const resolved = await resolveDNS(hostName); const Addresses = [ hostName, - "www.speedtest.net", - ...(cleanIPs === "" ? [] : cleanIPs.split(",")), + 'www.speedtest.net', + ...(cleanIPs ? cleanIPs.split(',') : []), ...resolved.ipv4, ...resolved.ipv6.map((ip) => `[${ip}]`), ]; @@ -893,13 +870,59 @@ const getVLESSConfig = async (env) => { }#${encodeURIComponent(`💦 BPB - ${addr}`)}\n`; }); - const singboxSub = btoa(vlessWsTls); - const xraySub = btoa(vlessWsTls.replaceAll("http/1.1", "h2,http/1.1")); + const subscription = client === 'singbox' ? btoa(vlessWsTls) : btoa(vlessWsTls.replaceAll('http/1.1', 'h2,http/1.1')); + + return subscription; +} - return {singboxSub, xraySub}; +const extractVlessParams = async (vlessConfig) => { + + const url = new URL(vlessConfig.replace('vless', 'http')); + const uuid = url.username; + const hostName = url.hostname; + const port = url.port; + const params = new URLSearchParams(url.search); + const security = params.get('security') || ''; + const encryption = params.get('encryption') || ''; + const host = params.get('host') || ''; + const sni = params.get('sni') || ''; + const path = params.get('path') || ''; + const alpn = params.get('alpn') || ''; + const fp = params.get('fp') || ''; + const type = params.get('type') || ''; + const mode = params.get('mode') || ''; + const authority = params.get('authority') || ''; + const serviceName = params.get('serviceName') || ''; + const flow = params.get('flow') || ''; + const pbk = params.get('pbk') || ''; + const sid = params.get('sid') || ''; + const spx = params.get('spx') || ''; + const headerType = params.get('headerType') || ''; + + return JSON.stringify({ + uuid: uuid, + hostName: hostName, + port: port, + path: path, + security: security, + encryption: encryption, + alpn: alpn, + host: host, + fp: fp, + type: type, + sni: sni, + mode: mode, + authority: authority, + serviceName: serviceName, + flow: flow, + pbk: pbk, + sid: sid, + spx: spx, + headerType: headerType + }); } -const getFragVLESSConfig = async (env) => { +const getFragVLESSConfig = async (env, client) => { let Configs = []; let outbounds = []; const { @@ -912,427 +935,418 @@ const getFragVLESSConfig = async (env) => { intervalMax, blockAds, bypassIran, - cleanIPs + cleanIPs, + outProxy, + outProxyParams } = await getDataset(env); - const fragConfigTemp = { - remarks: "", - dns: { - hosts: { - "geosite:category-ads-all": "127.0.0.1", - "geosite:category-ads-ir": "127.0.0.1", - "domain:googleapis.cn": "googleapis.com", - }, - servers: [ - remoteDNS, - { - address: localDNS, - domains: ["geosite:private", "geosite:category-ir", "domain:.ir"], - expectIPs: ["geoip:cn"], - port: 53, - }, - ], - }, - fakedns: [ - { - ipPool: "198.18.0.0/15", - poolSize: 10000, - }, - ], - inbounds: [ - { - port: 10808, - protocol: "socks", - settings: { - auth: "noauth", - udp: true, - userLevel: 8, - }, - sniffing: { - destOverride: ["http", "tls", "fakedns"], - enabled: true, - }, - tag: "socks", - }, - { - port: 10809, - protocol: "http", - settings: { - userLevel: 8, - }, - tag: "http", - }, - ], - log: { - loglevel: "warning", - }, - outbounds: [ - { - tag: "fragment", - protocol: "freedom", - settings: { - domainStrategy: "AsIs", - fragment: { - packets: "tlshello", - length: `${lengthMin}-${lengthMax}`, - interval: `${intervalMin}-${intervalMax}`, - }, - }, - streamSettings: { - sockopt: { - tcpKeepAliveIdle: 100, - }, - }, - }, - { - protocol: "freedom", - settings: { - domainStrategy: "UseIP", - }, - tag: "direct", - }, - { - protocol: "blackhole", - settings: { - response: { - type: "http", - }, - }, - tag: "block", - }, - ], - policy: { - levels: { - 8: { - connIdle: 300, - downlinkOnly: 1, - handshake: 4, - uplinkOnly: 1, - }, - }, - system: { - statsOutboundUplink: true, - statsOutboundDownlink: true, - }, - }, - routing: { - domainStrategy: "IPIfNonMatch", - rules: [ - { - ip: [localDNS], - outboundTag: "direct", - port: "53", - type: "field", - }, - { - domain: ["geosite:private", "geosite:category-ir", "domain:.ir"], - outboundTag: "direct", - type: "field", - }, - { - ip: ["geoip:private", "geoip:ir"], - outboundTag: "direct", - type: "field", - }, - { - domain: ["geosite:category-ads-all", "geosite:category-ads-ir"], - outboundTag: "block", - type: "field", - }, - { - balancerTag: "all", - type: "field", - network: "tcp,udp", - } - ], - balancers: [ - { - tag: "all", - selector: ["proxy"], - strategy: { - type: "leastPing", - }, - }, - ], - }, - observatory: { - probeInterval: "5m", - probeURL: "https://api.github.com/_private/browser/stats", - subjectSelector: ["proxy"], - EnableConcurrency: true, - }, - stats: {}, - }; - - const fragConfigNekorayTemp = { - dns: { - disableFallback: true, - servers: [ - { - address: remoteDNS, - domains: [], - queryStrategy: "", - }, - { - address: localDNS, - domains: ["domain:.ir", "geosite:private", "geosite:category-ir"], - queryStrategy: "", - }, - ], - tag: "dns", - }, - inbounds: [ - { - listen: "127.0.0.1", - port: 2080, - protocol: "socks", - settings: { udp: true }, - sniffing: { - destOverride: ["http", "tls", "quic"], - enabled: true, - metadataOnly: false, - routeOnly: true, - }, - tag: "socks-in", - }, - { - listen: "127.0.0.1", - port: 2081, - protocol: "http", - sniffing: { - destOverride: ["http", "tls", "quic"], - enabled: true, - metadataOnly: false, - routeOnly: true, - }, - tag: "http-in", - }, - ], - log: { loglevel: "warning" }, - outbounds: [ - { - protocol: "freedom", - settings: { - domainStrategy: "AsIs", - fragment: { - length: `${lengthMin}-${lengthMax}`, - interval: `${intervalMin}-${intervalMax}`, - packets: "tlshello", - }, - }, - streamSettings: { - sockopt: { - tcpKeepAliveIdle: 100, - }, - }, - tag: "fragment", - }, - { domainStrategy: "", protocol: "freedom", tag: "bypass" }, - { protocol: "blackhole", tag: "block" }, - { - protocol: "dns", - proxySettings: { tag: "proxy", transportLayer: true }, - settings: { - address: localDNS, - network: "tcp", - port: 53, - userLevel: 1, - }, - tag: "dns-out", - }, - ], - policy: { - levels: { 1: { connIdle: 30 } }, - system: { statsOutboundDownlink: true, statsOutboundUplink: true }, - }, - routing: { - domainStrategy: "IPIfNonMatch", - rules: [ - { ip: [localDNS], outboundTag: "bypass", type: "field" }, - { - inboundTag: ["socks-in", "http-in"], - outboundTag: "dns-out", - port: "53", - type: "field", - }, - { - domain: ["geosite:category-ads-all", "geosite:category-ads-ir"], - outboundTag: "block", - type: "field", - }, - { - ip: ["geoip:ir", "geoip:private"], - outboundTag: "bypass", - type: "field", - }, - { - domain: ["geosite:private", "geosite:category-ir", "domain:.ir"], - outboundTag: "bypass", - type: "field", - }, - { - balancerTag: "all", - type: "field", - network: "tcp,udp", - } - ], - balancers: [ - { - tag: "all", - selector: ["proxy"], - strategy: { - type: "leastPing", - }, - }, - ], - }, - observatory: { - probeInterval: "5m", - probeURL: "https://api.github.com/_private/browser/stats", - subjectSelector: ["proxy"], - EnableConcurrency: true, - }, - stats: {}, - }; - + let proxyOutbound; const resolved = await resolveDNS(host); const Addresses = [ host, "www.speedtest.net", - ...(cleanIPs === "" ? [] : cleanIPs.split(",")), + ...(cleanIPs ? cleanIPs.split(",") : []), ...resolved.ipv4, ...resolved.ipv6.map((ip) => `[${ip}]`), ]; - Addresses.forEach((addr, index) => { - - let proxyOutbound = { - protocol: "vless", - settings: { - vnext: [ - { - address: addr, - port: 443, - users: [ - { - encryption: "none", - flow: "", - id: userID, - level: 8, - security: "auto", - }, - ], - }, - ], - }, - streamSettings: { - network: "ws", - security: "tls", - tlsSettings: { - allowInsecure: false, - alpn: ["h2", "http/1.1"], - fingerprint: "chrome", - publicKey: "", - serverName: randomUpperCase(host), - shortId: "", - show: false, - spiderX: "", - }, - wsSettings: { - headers: { - Host: randomUpperCase(host), - }, - path: `/${getRandomPath(16)}?ed=2048`, - }, - sockopt: { - dialerProxy: "fragment", - }, - }, - tag: "proxy", - }; - - let fragConfig = clone(fragConfigTemp); - fragConfig.remarks = `💦 BPB Frag - ${addr}`; - fragConfig.outbounds = [{ ...proxyOutbound}, ...fragConfig.outbounds]; - delete fragConfig.observatory; - delete fragConfig.routing.balancers; - fragConfig.routing.rules.pop(); + if (outProxy) { + const proxyParams = JSON.parse(outProxyParams); + proxyOutbound = structuredClone(vlessOutbound); + + proxyOutbound.settings.vnext[0].address = proxyParams.hostName; + proxyOutbound.settings.vnext[0].port = +proxyParams.port; + proxyOutbound.settings.vnext[0].users[0].id = proxyParams.uuid; + proxyOutbound.settings.vnext[0].users[0].flow = proxyParams.flow; + proxyOutbound.streamSettings.security = proxyParams.security; + proxyOutbound.streamSettings.network = proxyParams.type; + proxyOutbound.tag = "out"; + proxyOutbound.streamSettings.sockopt.dialerProxy = "proxy"; + + switch (proxyParams.security) { + + case 'tls': + + proxyOutbound.streamSettings.tlsSettings.serverName = proxyParams.sni; + proxyOutbound.streamSettings.tlsSettings.fingerprint = proxyParams.fp; + proxyOutbound.streamSettings.tlsSettings.alpn = proxyParams.alpn === '' ? [] : proxyParams.alpn.split(','); + delete proxyOutbound.streamSettings.realitySettings; + break; + + case 'reality': + + proxyOutbound.streamSettings.realitySettings.publicKey = proxyParams.pbk; + proxyOutbound.streamSettings.realitySettings.shortId = proxyParams.sid; + proxyOutbound.streamSettings.realitySettings.serverName = proxyParams.sni; + proxyOutbound.streamSettings.realitySettings.fingerprint = proxyParams.fp; + proxyOutbound.streamSettings.realitySettings.spiderX = proxyParams.spx; + delete proxyOutbound.mux; + delete proxyOutbound.streamSettings.tlsSettings; + delete proxyOutbound.streamSettings.tcpSettings; + break; + + default: + + delete proxyOutbound.streamSettings.tlsSettings; + delete proxyOutbound.streamSettings.realitySettings; + break; + } + + switch (proxyParams.type) { + + case 'tcp': - if (!bypassIran) { - fragConfig.dns.servers[1].domains = ["geosite:private"]; - fragConfig.routing.rules[1].domain = ["geosite:private"]; - fragConfig.routing.rules[2].ip = ["geoip:private"]; + if (proxyParams.headerType === 'http') { + proxyOutbound.streamSettings.tcpSettings.header.request.headers.Host = proxyParams.host?.split(','); + proxyOutbound.streamSettings.tcpSettings.header.request.path = proxyParams.path?.split(','); + } + + if (!proxyParams.headerType) { + proxyOutbound.streamSettings.tcpSettings.header.type = 'none'; + delete proxyOutbound.streamSettings.tcpSettings.header.request; + delete proxyOutbound.streamSettings.tcpSettings.header.response; + } + + delete proxyOutbound.streamSettings.grpcSettings; + delete proxyOutbound.streamSettings.wsSettings; + break; + + case 'ws': + + proxyOutbound.streamSettings.wsSettings.headers.Host = proxyParams.host; + proxyOutbound.streamSettings.wsSettings.path = proxyParams.path; + delete proxyOutbound.streamSettings.grpcSettings; + delete proxyOutbound.streamSettings.tcpSettings; + break; + + case 'grpc': + + proxyOutbound.streamSettings.grpcSettings.authority = proxyParams.authority; + proxyOutbound.streamSettings.grpcSettings.serviceName = proxyParams.serviceName; + proxyOutbound.streamSettings.grpcSettings.multiMode = proxyParams.mode === 'multi'; + delete proxyOutbound.mux; + delete proxyOutbound.streamSettings.tcpSettings; + delete proxyOutbound.streamSettings.wsSettings; + break; } + } - if (!blockAds) { - fragConfig.dns.hosts = {"domain:googleapis.cn": "googleapis.com"}; + Addresses.forEach((addr, index) => { + + let proxyOut; + let outbound = structuredClone(vlessOutbound); + delete outbound.mux; + delete outbound.streamSettings.grpcSettings; + delete outbound.streamSettings.realitySettings; + delete outbound.streamSettings.tcpSettings; + outbound.settings.vnext[0].address = addr; + outbound.settings.vnext[0].port = 443; + outbound.settings.vnext[0].users[0].id = userID; + outbound.streamSettings.tlsSettings.serverName = randomUpperCase(host); + outbound.streamSettings.wsSettings.headers.Host = randomUpperCase(host); + outbound.streamSettings.wsSettings.path = `/${getRandomPath(16)}?ed=2048`; + + if (client === 'v2ray') { + let fragConfig = structuredClone(fragConfigTemp); + fragConfig.remarks = `💦 BPB Frag - ${addr}`; + fragConfig.dns.servers[0] = remoteDNS; + fragConfig.dns.servers[1].address = localDNS; + fragConfig.outbounds[2].settings.fragment.length = `${lengthMin}-${lengthMax}`; + fragConfig.outbounds[2].settings.fragment.interval = `${intervalMin}-${intervalMax}`; + fragConfig.routing.rules[0].ip = [localDNS]; + + if (proxyOutbound) { + proxyOut = structuredClone(proxyOutbound); + fragConfig.outbounds = [{...proxyOutbound}, { ...outbound}, ...fragConfig.outbounds]; + } else { + fragConfig.outbounds = [{ ...outbound}, ...fragConfig.outbounds]; + } + + delete fragConfig.observatory; + delete fragConfig.routing.balancers; + fragConfig.routing.rules.pop(); + + if (!bypassIran) { + fragConfig.dns.servers[1].domains = ["geosite:private"]; + fragConfig.routing.rules[1].domain = ["geosite:private"]; + fragConfig.routing.rules[2].ip = ["geoip:private"]; + } + + if (!blockAds) { + fragConfig.dns.hosts = {"domain:googleapis.cn": "googleapis.com"}; + fragConfig.routing.rules.pop(); + } + + Configs.push({ + address: addr, + config: fragConfig + }); + + } else { + + let fragConfig = structuredClone(fragConfigNekorayTemp); + fragConfig.dns.servers[0].address = remoteDNS; + fragConfig.dns.servers[1].address = localDNS; + fragConfig.outbounds[2].settings.fragment.length = `${lengthMin}-${lengthMax}`; + fragConfig.outbounds[2].settings.fragment.interval = `${intervalMin}-${intervalMax}`; + fragConfig.routing.rules[0].ip = [localDNS]; + + if (proxyOutbound) { + proxyOut = structuredClone(proxyOutbound); + fragConfig.outbounds = [{...proxyOutbound}, { ...outbound}, ...fragConfig.outbounds]; + } else { + fragConfig.outbounds = [{ ...outbound}, ...fragConfig.outbounds]; + } + + delete fragConfig.observatory; + delete fragConfig.routing.balancers; fragConfig.routing.rules.pop(); + + if (!bypassIran) { + fragConfig.dns.servers[1].domains = ["geosite:private"]; + fragConfig.routing.rules[3].ip = ["geoip:private"]; + fragConfig.routing.rules[4].domain = ["geosite:private"]; + } + + if (!blockAds) { + fragConfig.routing.rules.pop(); + } + + Configs.push({ + address: addr, + config: fragConfig + }); + } + outbound.tag = `proxy_${index + 1}`; + + if (proxyOutbound) { + proxyOut.tag = `out_${index + 1}`; + proxyOut.streamSettings.sockopt.dialerProxy = `proxy_${index + 1}`; + outbounds.push({...proxyOut}, {...outbound}); + } else { + outbounds.push({...outbound}); + } - let fragConfigNekoray = clone(fragConfigNekorayTemp); - fragConfigNekoray.outbounds = [{ ...proxyOutbound}, ...fragConfigNekoray.outbounds]; - delete fragConfigNekoray.observatory; - delete fragConfigNekoray.routing.balancers; - fragConfigNekoray.routing.rules.pop(); + }); + if (client === 'v2ray') { + let bestPing = structuredClone(fragConfigTemp); + bestPing.remarks = '💦 BPB Frag - Best Ping 💥'; + bestPing.dns.servers[0] = remoteDNS; + bestPing.dns.servers[1].address = localDNS; + bestPing.outbounds[2].settings.fragment.length = `${lengthMin}-${lengthMax}`; + bestPing.outbounds[2].settings.fragment.interval = `${intervalMin}-${intervalMax}`; + bestPing.routing.rules[0].ip = [localDNS]; + bestPing.outbounds = [...outbounds, ...bestPing.outbounds]; + if (!bypassIran) { - fragConfigNekoray.dns.servers[1].domains = ["geosite:private"]; - fragConfigNekoray.routing.rules[3].ip = ["geoip:private"]; - fragConfigNekoray.routing.rules[4].domain = ["geosite:private"]; + bestPing.dns.servers[1].domains = ["geosite:private"]; + bestPing.routing.rules[1].domain = ["geosite:private"]; + bestPing.routing.rules[2].ip = ["geoip:private"]; } - + if (!blockAds) { - fragConfigNekoray.routing.rules.pop(); + bestPing.dns.hosts = {"domain:googleapis.cn": "googleapis.com"}; + bestPing.routing.rules.splice(3,1); + } + + if (proxyOutbound) { + bestPing.observatory.subjectSelector = ["out"]; + bestPing.routing.balancers[0].selector = ["out"]; } - proxyOutbound.tag += `_${index + 1}`; - outbounds.push({...proxyOutbound}); + Configs.push({ + address: "Best-Ping", + config: bestPing, + }); + + } else { - const config = { - address: addr, - fragConf: fragConfig, - fragConfNeko: fragConfigNekoray, - }; + let bestPing = structuredClone(fragConfigNekorayTemp); + bestPing.dns.servers[0].address = remoteDNS; + bestPing.dns.servers[1].address = localDNS; + bestPing.outbounds[2].settings.fragment.length = `${lengthMin}-${lengthMax}`; + bestPing.outbounds[2].settings.fragment.interval = `${intervalMin}-${intervalMax}`; + bestPing.routing.rules[0].ip = [localDNS]; + bestPing.outbounds = [ ...outbounds, ...bestPing.outbounds]; + + if (!bypassIran) { + bestPing.dns.servers[1].domains = ["geosite:private"]; + bestPing.routing.rules[3].ip = ["geoip:private"]; + bestPing.routing.rules[4].domain = ["geosite:private"]; + } + + if (!blockAds) { + bestPing.routing.rules.splice(2,1); + } + + if (proxyOutbound) { + bestPing.observatory.subjectSelector = ["out"]; + bestPing.routing.balancers[0].selector = ["out"]; + } + + Configs.push({ + address: "Best-Ping", + config: bestPing, + }); + } + + return Configs; +} + +const updateDataset = async (env, host, Settings) => { + + const vlessConfig = Settings?.get('outProxy'); + const initData = { + host: host, + remoteDNS: Settings?.get('remoteDNS') || 'https://94.140.14.14/dns-query', + localDNS: Settings?.get('localDNS') || '8.8.8.8', + lengthMin: Settings?.get('fragmentLengthMin') || '100', + lengthMax: Settings?.get('fragmentLengthMax') || '200', + intervalMin: Settings?.get('fragmentIntervalMin') || '5', + intervalMax: Settings?.get('fragmentIntervalMax') || '10', + blockAds: Settings?.get('block-ads') || 'false', + bypassIran: Settings?.get('bypass-iran') || 'false', + cleanIPs: Settings?.get('cleanIPs')?.replaceAll(' ', '') || '', + outProxy: vlessConfig || '', + outProxyParams: vlessConfig ? await extractVlessParams(vlessConfig) : '' + }; + + try { + for (const [key, value] of Object.entries(initData)) { + await env.bpb.put(key, value); + } + } catch (error) { + throw new error(error); + } +} + +const getDataset = async (env) => { + const data = await env.bpb.list(); + const keys = [...data.keys].map(({ name }) => name); + let values; + + try { + values = await Promise.all( + keys.map((key) => env.bpb.get(key)) + ); + } catch (error) { + throw new error(error); + } + + const dataset = { + ...keys.reduce((acc, key, i) => { + const value = values[i]; + if (key === 'blockAds' || key === 'bypassIran') { + acc[key] = value === 'true'; + } else { + acc[key] = value; + } + return acc; + }, {}), + }; + + return dataset; +} + +const randomUpperCase = (str) => { + let result = ''; + for (let i = 0; i < str.length; i++) { + result += Math.random() < 0.5 ? str[i].toUpperCase() : str[i]; + } + return result; +} + +const getRandomPath = (length) => { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +const resolveDNS = async (domain) => { + const dohURLv4 = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=A`; + const dohURLv6 = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=AAAA`; + + try { + const [ipv4Response, ipv6Response] = await Promise.all([ + fetch(dohURLv4, { headers: { accept: 'application/dns-json' } }), + fetch(dohURLv6, { headers: { accept: 'application/dns-json' } }), + ]); + + const ipv4Addresses = await ipv4Response.json(); + const ipv6Addresses = await ipv6Response.json(); + + const ipv4 = ipv4Addresses.Answer + ? ipv4Addresses.Answer.map((record) => record.data) + : []; + const ipv6 = ipv6Addresses.Answer + ? ipv6Addresses.Answer.map((record) => record.data) + : []; + + return { ipv4, ipv6 }; + } catch (error) { + console.error('Error resolving DNS:', error); + } +} + +const generateJWTToken = (password, secretKey) => { + const header = { + alg: 'HS256', + typ: 'JWT' + }; + + const payload = { + exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), + data: { password } + }; + const encodedHeader = btoa(JSON.stringify(header)); + const encodedPayload = btoa(JSON.stringify(payload)); + const signature = btoa(crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${encodedHeader}.${encodedPayload}.${secretKey}`))); + + return `Bearer ${encodedHeader}.${encodedPayload}.${signature}`; +} + +const generateSecretKey = () => { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); +} + +const verifyJWTToken = async (request, env) => { + const secretKey = await env.bpb.get('secretKey'); + if (!secretKey) return false; + + try { + const cookie = request.headers.get('Cookie'); + const cookieMatch = cookie ? cookie.match(/(^|;\s*)jwtToken=([^;]*)/) : null; + const token = cookieMatch ? cookieMatch.pop() : null; - Configs.push({...config}); - }); + if (!token) return false; - let bestPingConfig = clone(fragConfigTemp); - bestPingConfig.remarks = '💦 BPB Frag - Best Ping 💥'; - bestPingConfig.outbounds = [...outbounds, ...bestPingConfig.outbounds]; + const tokenWithoutBearer = token.startsWith('Bearer ') ? token.slice(7) : token; + const [encodedHeader, encodedPayload, signature] = tokenWithoutBearer.split('.'); + const payload = JSON.parse(atob(encodedPayload)); - if (!bypassIran) { - bestPingConfig.dns.servers[1].domains = ["geosite:private"]; - bestPingConfig.routing.rules[1].domain = ["geosite:private"]; - bestPingConfig.routing.rules[2].ip = ["geoip:private"]; - } + const expectedSignature = btoa(crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(`${encodedHeader}.${encodedPayload}.${secretKey}`) + )); - if (!blockAds) { - bestPingConfig.dns.hosts = {"domain:googleapis.cn": "googleapis.com"}; - bestPingConfig.routing.rules.splice(3,1); - } + if (signature !== expectedSignature) return false; - let bestPingNeko = clone(fragConfigNekorayTemp); - bestPingNeko.outbounds = [ ...outbounds, ...bestPingNeko.outbounds]; - - if (!bypassIran) { - bestPingNeko.dns.servers[1].domains = ["geosite:private"]; - bestPingNeko.routing.rules[3].ip = ["geoip:private"]; - bestPingNeko.routing.rules[4].domain = ["geosite:private"]; - } + const now = Math.floor(Date.now() / 1000); + if (payload.exp < now) return false; - if (!blockAds) { - bestPingNeko.routing.rules.splice(2,1); + return true; + } catch (error) { + return false; } - - Configs.push({ - address: "Best-Ping", - fragConf: bestPingConfig, - fragConfNeko: bestPingNeko, - }); - - return Configs; } const renderPage = async (env, fragConfigs) => { @@ -1346,7 +1360,8 @@ const renderPage = async (env, fragConfigs) => { intervalMax, blockAds, bypassIran, - cleanIPs + cleanIPs, + outProxy } = await getDataset(env); const genCustomConfRow = async (configs) => { @@ -1361,7 +1376,7 @@ const renderPage = async (env, fragConfigs) => { }