From a9c777023625d963754b4c51d90a5c89c4f0b918 Mon Sep 17 00:00:00 2001 From: amclubs Date: Sun, 6 Oct 2024 12:44:44 +0800 Subject: [PATCH] Initial commit --- .github/workflows/zip_flows.yml | 29 + .gitignore | 23 + README.md | 148 ++ _worker.js | 1335 ++++++++++++++++++ _worker.js.zip | Bin 0 -> 14949 bytes ipv4.csv | 2245 +++++++++++++++++++++++++++++++ ipv4.txt | 61 + ipv4HK.txt | 13 + ipv4US.txt | 13 + 9 files changed, 3867 insertions(+) create mode 100644 .github/workflows/zip_flows.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 _worker.js create mode 100644 _worker.js.zip create mode 100644 ipv4.csv create mode 100644 ipv4.txt create mode 100644 ipv4HK.txt create mode 100644 ipv4US.txt diff --git a/.github/workflows/zip_flows.yml b/.github/workflows/zip_flows.yml new file mode 100644 index 0000000..d9999e0 --- /dev/null +++ b/.github/workflows/zip_flows.yml @@ -0,0 +1,29 @@ +name: zip_flows # 工作流程的名称 + +on: + workflow_dispatch: # 手动触发 + push: + paths: + - '_worker.js' # 当 _worker.js 文件发生变动时触发 + +permissions: + contents: write + +jobs: + package-and-commit: + runs-on: ubuntu-latest # 运行环境,这里使用最新版本的 Ubuntu + steps: + - name: Checkout Repository # 检出代码 + uses: actions/checkout@v2 + + - name: Zip the worker file # 将 _worker.js 文件打包成 worker.js.zip + run: zip _worker.js.zip _worker.js + + - name: Commit and push the packaged file # 提交并推送打包后的文件 + uses: EndBug/add-and-commit@v7 + with: + add: '_worker.js.zip' + message: 'Automatically package and commit _worker.js.zip' + author_name: github-actions[bot] + author_email: actions[bot]@users.noreply.github.com + token: ${{ secrets.GH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..403adbc --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules +/dist + + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..a169b8f --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +# [am-tunnel](https://github.com/ansoncloud8/am-cf-tunnel) +▶️ **新人[YouTube](https://youtube.com/@AM_CLUB)** 需要您的支持,请务必帮我**点赞**、**关注**、**打开小铃铛**,***十分感谢!!!*** ✅ +
🎁 不要只是下载或Fork。请 **follow** 我的GitHub、给我所有项目一个 **Star** 星星(拜托了)!你的支持是我不断前进的动力! 💖 +
✅**解锁更多技术请访问[【个人博客】](https://am.809098.xyz)** +# +## 保留旧版本,新版本迁移到这个库 [am-cf-tunnel](https://github.com/ansoncloud8/am-cf-tunnel) + +# Cloudflare Workers 和 Pages 生成节点订阅 + +这是一个基于 Cloudflare Workers 和 Pages平台的脚本,在原版的基础上修改了显示 VLESS 配置信息转换为订阅内容。使用该脚本,你可以方便地将 VLESS、trojan 配置信息使用在线配置转换到 Clash、 Singbox 、Quantumult X等工具中。 + +- 基础部署视频教程:[小白教程](https://www.youtube.com/watch?v=f9hDJCqAEGA) 小白必看 一步到胃 最佳推荐!!! +- 快速部署视频教程:[详细教程](https://www.youtube.com/watch?v=XS0EgqckUKo) ***最佳推荐!!!*** +- 进阶使用视频教程:[进阶教程](https://www.youtube.com/watch?v=JDAQYD6bvEM) 折腾自己的优选IP +- 高级使用视频教程:[高级教程](https://www.youtube.com/watch?v=lQ2Evd_xPRY) 成为折腾的高手 + +- 官网教程:[AM科技](https://am.809098.xyz) YouTube频道:[@AM_CLUB](https://youtube.com/@AM_CLUB) Telegram交流群:[@AM_CLUBS](https://t.me/AM_CLUBS) 免费订阅:[进群发关键字: 订阅](https://t.me/AM_CLUBS) + +## 订阅工具 +- [(安卓)v2rayNG](https://github.com/2dust/v2rayNG/releases) [(安卓)singbox](https://github.com/SagerNet/sing-box/releases) [(苹果)singbox](https://github.com/SagerNet/sing-box/releases) [(苹果)Hiddify](https://github.com/hiddify/hiddify-next/releases) +- [(win)v2rayN](https://github.com/2dust/v2rayN/releases) [(win)singbox](https://github.com/SagerNet/sing-box/releases) [(win)clashvergerev](https://github.com/clash-verge-rev/clash-verge-rev/releases) [(win)Hiddify](https://github.com/hiddify/hiddify-next/releases) [(win)clashnyanpasu](https://github.com/LibNyanpasu/clash-nyanpasu/releases) [(mac)clashnyanpasu](https://github.com/LibNyanpasu/clash-nyanpasu/releases) +- [(mac)v2rayU](https://github.com/yanue/V2rayU/releases) [(mac)singbox](https://github.com/SagerNet/sing-box/releases) [(mac)clashvergerev](https://github.com/clash-verge-rev/clash-verge-rev/releases) [(mac)Hiddify](https://github.com/hiddify/hiddify-next/releases) + +# 免责声明 + +本免责声明适用于 GitHub 上的 “am-tunnel” 项目(以下简称“该项目”),项目链接为:https://github.com/ansoncloud8/am-tunnel + +### 用途 +该项目被设计和开发仅供学习、研究和安全测试目的。它旨在为安全研究者、学术界人士和技术爱好者提供一个了解和实践网络通信技术的工具。 + +### 合法性 +使用者在下载和使用该项目时,必须遵守当地法律和规定。使用者有责任确保他们的行为符合其所在地区的法律、规章以及其他适用的规定。 + +### 免责 +1. 作为该项目的作者,我(以下简称“作者”)强调该项目应仅用于合法、道德和教育目的。 +2. 作者不鼓励、不支持也不促进任何形式的非法使用该项目。如果发现该项目被用于非法或不道德的活动,作者将强烈谴责这种行为。 +3. 作者对任何人或团体使用该项目进行的任何非法活动不承担责任。使用者使用该项目时产生的任何后果由使用者本人承担。 +4. 作者不对使用该项目可能引起的任何直接或间接损害负责。 +5. 通过使用该项目,使用者表示理解并同意本免责声明的所有条款。如果使用者不同意这些条款,应立即停止使用该项目。 + +作者保留随时更新本免责声明的权利,且不另行通知。最新的免责声明版本将会在该项目的 GitHub 页面上发布。 + +## 风险提示 +- 通过提交虚假的节点配置给订阅服务,避免节点配置信息泄露。 + + +## Workers 部署方法 [视频教程](https://www.youtube.com/watch?v=f9hDJCqAEGA) +1. 部署 Cloudflare Worker: + - 在 Cloudflare Worker 控制台中创建一个新的 Worker。 + - 将 [worker.js](https://github.com/ansoncloud8/am-tunnel/blob/dev/_worker.js) 的内容粘贴到 Worker 编辑器中。 + - 将第 6 行 `userID` 修改成你自己的 **UUID** 。 +2. 访问订阅内容: + - 访问 `https://[YOUR-WORKERS-URL]/[UUID]` 即可获取订阅内容。 + - 例如 `https://vless.google.workers.dev/90cd4a77-141a-43c9-991b-08263cfe9c10` 就是你的通用自适应订阅地址。 + - 例如 `https://vless.google.workers.dev/sub/90cd4a77-141a-43c9-991b-08263cfe9c10` Base64订阅格式,适用PassWall,SSR+等。 + - 例如 `https://vless.google.workers.dev/sub/90cd4a77-141a-43c9-991b-08263cfe9c10?format=clash` Clash订阅格式,适用OpenClash等。 + - 例如 `https://vless.google.workers.dev/sub/bestip/90cd4a77-141a-43c9-991b-08263cfe9c10?format=singbox&uuid=68ecf7d9-5eb3-31ee-fe78-134a3d519356` singbox订阅格式,适用singbox等。 + - 例如 `https://vless.google.workers.dev/sub/bestip/90cd4a77-141a-43c9-991b-08263cfe9c10?format=qx&uuid=68ecf7d9-5eb3-31ee-fe78-134a3d519356` Quantumult X订阅格式,适用Quantumult X工具。 +3. 给 workers绑定 自定义域: + - 在 workers控制台的 `触发器`选项卡,下方点击 `添加自定义域`。 + - 填入你已转入 CloudFlare 域名解析服务的次级域名,例如:`vless.google.com`后 点击`添加自定义域`,等待证书生效即可。 + + + +## Pages 上传 部署方法 **最佳推荐!!!** [视频教程](https://www.youtube.com/watch?v=8oZvklBkMj4) +1. 部署 Cloudflare Pages: + - 下载 [_worker.js.zip](https://raw.githubusercontent.com/ansoncloud8/am-tunnel/dev/_worker.js.zip) 文件,并点上 Star !!! + - 在 Cloudflare Pages 控制台中选择 `上传资产`后,为你的项目取名后点击 `创建项目`,然后上传你下载好的 [_worker.js.zip](https://raw.githubusercontent.com/ansoncloud8/am-tunnel/dev/_worker.js.zip) 文件后点击 `部署站点`。 + - 部署完成后点击 `继续处理站点` 后,选择 `设置` > `环境变量` > **制作**为生产环境定义变量 > `添加变量`。 + 变量名称填写**UUID**,值则为你的UUID,后点击 `保存`即可。 + - 返回 `部署` 选项卡,在右下角点击 `创建新部署` 后,重新上传 [_worker.js.zip](https://raw.githubusercontent.com/ansoncloud8/am-tunnel/dev/_worker.js.zip) 文件后点击 `保存并部署` 即可。 +2. 访问订阅内容: + - 访问 `https://[YOUR-PAGES-URL]/[YOUR-UUID]` 即可获取订阅内容。 + - 例如 `https://vless.google.pages.dev/90cd4a77-141a-43c9-991b-08263cfe9c10` 就是你的通用自适应订阅地址。 + - 例如 `https://vless.google.pages.dev/sub/90cd4a77-141a-43c9-991b-08263cfe9c10` Base64订阅格式,适用PassWall,SSR+等。 + - 例如 `https://vless.google.pages.dev/sub/90cd4a77-141a-43c9-991b-08263cfe9c10?format=clash` Clash订阅格式,适用OpenClash等。 + - 例如 `https://vless.google.pages.dev/sub/bestip/90cd4a77-141a-43c9-991b-08263cfe9c10?format=singbox&uuid=68ecf7d9-5eb3-31ee-fe78-134a3d519356` singbox订阅格式,适用singbox等。 + - 例如 `https://vless.google.pages.dev/sub/bestip/90cd4a77-141a-43c9-991b-08263cfe9c10?format=qx&uuid=68ecf7d9-5eb3-31ee-fe78-134a3d519356` Quantumult X订阅格式,适用Quantumult X工具。 + + +3. 给 Pages绑定 CNAME自定义域:[视频教程](https://www.youtube.com/watch?v=8oZvklBkMj4) + - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。 + - 填入你的自定义次级域名,注意不要使用你的根域名,例如: + 您分配到的域名是 `google.com`,则添加自定义域填入 `vless.google.com`即可; + - 按照 Cloudflare 的要求将返回你的域名DNS服务商,添加 该自定义域 `vless`的 CNAME记录 `vless.google.pages.dev` 后,点击 `激活域`即可。 + + + +## Pages GitHub 部署方法 +1. 部署 Cloudflare Pages: + - 在 Github 上先 Fork 本项目,并点上 Star !!! + - 在 Cloudflare Pages 控制台中选择 `连接到 Git`后,选中 `am-tunnel`项目后点击 `开始设置`。 + - 在 `设置构建和部署`页面下方,选择 `环境变量(高级)`后并 `添加变量` + 变量名称填写**UUID**,值则为你的UUID,后点击 `保存并部署`即可。 + +2. 访问订阅内容: + - 访问 `https://[YOUR-PAGES-URL]/[YOUR-UUID]` 即可获取订阅内容。 + - 例如 `https://vless.google.pages.dev/90cd4a77-141a-43c9-991b-08263cfe9c10` 就是你的通用自适应订阅地址。 + - 例如 `https://vless.google.pages.dev/sub/90cd4a77-141a-43c9-991b-08263cfe9c10` Base64订阅格式,适用PassWall,SSR+等。 + - 例如 `https://vless.google.pages.dev/sub/90cd4a77-141a-43c9-991b-08263cfe9c10?format=clash` Clash订阅格式,适用OpenClash等。 + - 例如 `https://vless.google.pages.dev/sub/bestip/90cd4a77-141a-43c9-991b-08263cfe9c10?format=singbox&uuid=68ecf7d9-5eb3-31ee-fe78-134a3d519356` singbox订阅格式,适用singbox等。 + - 例如 `https://vless.google.pages.dev/sub/bestip/90cd4a77-141a-43c9-991b-08263cfe9c10?format=qx&uuid=68ecf7d9-5eb3-31ee-fe78-134a3d519356` Quantumult X订阅格式,适用Quantumult X工具。 + +3. 给 Pages绑定 CNAME自定义域:[视频教程](https://www.youtube.com/watch?v=8oZvklBkMj4) + - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。 + - 填入你的自定义次级域名,注意不要使用你的根域名,例如: + 您分配到的域名是 `google.com`,则添加自定义域填入 `vless.google.com`即可; + - 按照 Cloudflare 的要求将返回你的域名DNS服务商,添加 该自定义域 `vless`的 CNAME记录 `vless.google.pages.dev` 后,点击 `激活域`即可。 + + + + +## 变量说明 +| 变量名 | 示例 | 必填 | 备注 | YT | | +|--------|---------|-|-----|-----|--------| +| UUID | 90cd4a77-141a-43c9-991b-08263cfe9c10 |√| Powershell -NoExit -Command "[guid]::NewGuid()"| [Video](https://www.youtube.com/watch?v=8oZvklBkMj4) | | +| PROXYIP | cdn.xn--b6gac.eu.org |×| 备选作为访问CloudFlareCDN站点的代理节点(支持多ProxyIP, ProxyIP之间使用`,`或 换行 作间隔) | [Video](https://youtu.be/CiSdaNOCyOk) | | +| ADDRESSESAPI | https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt |×| 备选作为优选IP的自己库) | [Video](https://youtu.be/9WoMNrxV0HE) | | +| SUBCONFIG | [https://raw.github.../ACL4SSR_Online_Full_MultiMode.ini](https://raw.githubusercontent.com/amclubs/ACL4SSR/main/Clash/config/ACL4SSR_Online_Full_MultiMode.ini) | clash、singbox等 订阅转换配置文件 | + + +## Star 星星走起 +[![Stargazers over time](https://starchart.cc/ansoncloud8/am-tunnel.svg?variant=adaptive)](https://starchart.cc/ansoncloud8/am-tunnel) + +## 已适配自适应订阅内容 + - [v2rayN](https://github.com/2dust/v2rayN) + - [v2rayU](https://github.com/yanue/V2rayU/releases) + - [sing-box](https://github.com/SagerNet/sing-box) + - clash.meta([clash-verge-rev +](https://github.com/clash-verge-rev/clash-verge-rev),[Clash Nyanpasu](https://github.com/keiko233/clash-nyanpasu),~[clash-verge](https://github.com/zzzgydi/clash-verge/tree/main)~,ClashX Meta、openclash) + - Quantumult X + - 小火箭 + - surge + + # +▶️ **新人[YouTube](https://youtube.com/@AM_CLUB)** 需要您的支持,请务必帮我**点赞**、**关注**、**打开小铃铛**,***十分感谢!!!*** ✅ +
🎁 不要只是下载或Fork。请 **follow** 我的GitHub、给我所有项目一个 **Star** 星星(拜托了)!你的支持是我不断前进的动力! 💖 + + # +
[点击展开] 赞赏支持 ~🧧 +*我非常感谢您的赞赏和支持,它们将极大地激励我继续创新,持续产生有价值的工作。* + +- **USDT-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD` + +
+ +# 感谢 +[3Kmfi6HP](https://github.com/3Kmfi6HP/EDtunnel)、[ACL4SSR](https://github.com/ACL4SSR/ACL4SSR/tree/master/Clash/config) diff --git a/_worker.js b/_worker.js new file mode 100644 index 0000000..a17852b --- /dev/null +++ b/_worker.js @@ -0,0 +1,1335 @@ +/** +*- Telegram交流群:https://t.me/AM_CLUBS +*- YouTube频道:https://youtube.com/@AM_CLUB +*- VLESS订阅地址:https://worker.amcloud.filegear-sg.me/866853eb-5293-4f09-bf00-e13eb237c655 +*- Github仓库地址:https://github.com/ansoncloud8 +**/ + +// @ts-ignore +import { connect } from 'cloudflare:sockets'; + +// How to generate your own UUID: +// [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" +let userID = '866853eb-5293-4f09-bf00-e13eb237c655'; + +const proxyIPs = ['cdn.xn--b6gac.eu.org', 'cdn-all.xn--b6gac.eu.org', 'workers.cloudflare.cyou']; + +// if you want to use ipv6 or single proxyIP, please add comment at this line and remove comment at the next line +let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; +// use single proxyIP instead of random +// let proxyIP = 'cdn.xn--b6gac.eu.org'; +// ipv6 proxyIP example remove comment to use +// let proxyIP = "[2a01:4f8:c2c:123f:64:5:6810:c55a]" + +let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query + +// 设置优选地址api接口 +let addressesapi = [ + 'https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt', //可参考内容格式 自行搭建。 + //'https://raw.githubusercontent.com/ansoncloud8/am-tunnel/dev/ipv6.txt', //IPv6优选内容格式 自行搭建。 +]; + +// 设置优选地址,不带端口号默认443,TLS订阅生成 +let addresses = [ + 'icook.hk:443#t.me/AM_CLUBS',//官方优选域名 + //'cloudflare.cfgo.cc:443#关注YouTube频道@AM_CLUB', + 'visa.com:443#youtube.com/@AM_CLUB' +]; + +let autoaddress = [ + 'icook.hk:443', + 'cloudflare.cfgo.cc:443', + 'visa.com:443' +]; + +let FileName = 'ansoncloud8.github.io'; +let tagName = 'youtube.com/@am_club' +let SUBUpdateTime = 6; +let total = 99;//PB +//let timestamp = now; +let timestamp = 4102329600000;//2099-12-31 +const regex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[.*\]):?(\d+)?#?(.*)?$/; + +// 虚假uuid和hostname,用于发送给配置生成服务 +let fakeUserID = generateUUID(); +let fakeHostName = generateRandomString(); + +let sub = 'worker.amcloud.filegear-sg.me';// 内置优选订阅生成器,可自行搭建 +let subconverter = 'url.v1.mk';// clash订阅转换后端,目前使用肥羊的订阅转换功能。自带虚假uuid和host订阅。 +let subconfig = "https://raw.githubusercontent.com/ansoncloud8/ACL4SSR/main/Clash/config/ACL4SSR_Online_Full_MultiMode.ini"; //订阅配置文件 + + +if (!isValidUUID(userID)) { + throw new Error('uuid is invalid'); +} + +export default { + /** + * @param {import("@cloudflare/workers-types").Request} request + * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env + * @param {import("@cloudflare/workers-types").ExecutionContext} ctx + * @returns {Promise} + */ + async fetch(request, env, ctx) { + // uuid_validator(request); + try { + let expire = Math.floor(timestamp / 1000); + let UD = Math.floor(((timestamp - Date.now()) / timestamp * 99 * 1099511627776 * 1024) / 2); + const url = new URL(request.url); + const uuid = url.searchParams.get('uuid') ? url.searchParams.get('uuid').toLowerCase() : "null"; + + sub = env.SUB || sub; + userID = env.UUID || userID; + proxyIP = env.PROXYIP || proxyIP; + dohURL = env.DNS_RESOLVER_URL || dohURL; + subconfig = env.SUBCONFIG || subconfig; + let userID_Path = userID; + if (userID.includes(',')) { + userID_Path = userID.split(',')[0]; + } + + if (env.ADDRESSESAPI){ + addressesapi = []; + addressesapi = await ADD(env.ADDRESSESAPI); + } + + const upgradeHeader = request.headers.get('Upgrade'); + if (!upgradeHeader || upgradeHeader !== 'websocket') { + switch (url.pathname) { + case `/cf`: { + return new Response(JSON.stringify(request.cf, null, 4), { + status: 200, + headers: { + "Content-Type": "application/json;charset=utf-8", + }, + }); + } + case `/${userID_Path}`: { + const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); + return new Response(`${vlessConfig}`, { + status: 200, + headers: { + "Content-Type": "text/html; charset=utf-8", + } + }); + }; + case `/sub/${userID_Path}`: { + const url = new URL(request.url); + const searchParams = url.searchParams; + const format = searchParams.get('format') ? searchParams.get('format').toLowerCase() : null; + const dq = searchParams.get('dq') ? searchParams.get('dq') : null; + const btoa_not = searchParams.get('btoa') ? searchParams.get('btoa').toLowerCase() : null; + const vlessSubConfig = createVLESSSub(userID, request.headers.get('Host'), format, dq); + + // Construct and return response object + if (format === 'qx') { + return new Response(vlessSubConfig, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } else if (btoa_not === 'btoa') { + return new Response(vlessSubConfig, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } else { + return new Response(btoa(vlessSubConfig), { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } + }; + case `/bestip/${uuid}`: { + const newAddressesapi = await getAddressesapi(addressesapi); + // const vlessSubConfig = createVlessBestIpSub(userID, request.headers.get('Host'), newAddressesapi,'format'); + //拿随机 + fakeUserID = uuid; + fakeHostName = url.searchParams.get('host') ? url.searchParams.get('host').toLowerCase() : request.headers.get('Host'); + const format = url.searchParams.get('format') ? url.searchParams.get('format').toLowerCase() : "null"; + const vlessSubConfig = createVlessBestIpSub(fakeUserID, fakeHostName, newAddressesapi, format); + const btoa_not = url.searchParams.get('btoa') ? url.searchParams.get('btoa').toLowerCase() : null; + + if (btoa_not === 'btoa') { + return new Response(vlessSubConfig, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + }else { + const base64Response = btoa(vlessSubConfig); // 重新进行 Base64 编码 + const response = new Response(base64Response, { + headers: { + // "Content-Disposition": `attachment; filename*=utf-8''${encodeURIComponent(FileName)}; filename=${FileName}`, + "content-type": "text/plain; charset=utf-8", + "Profile-Update-Interval": `${SUBUpdateTime}`, + "Subscription-Userinfo": `upload=${UD}; download=${UD}; total=${total}; expire=${expire}`, + }, + }); + return response; + } + }; + case `/sub/bestip/${userID_Path}`: { + const tls = true; + // 如果是使用默认域名,则改成一个workers的域名,订阅器会加上代理 + const hostName = request.headers.get('Host'); + const userAgentHeader = request.headers.get('User-Agent'); + const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null"; + const format = url.searchParams.get('format') ? url.searchParams.get('format').toLowerCase() : "null"; + if (hostName.includes(".workers.dev")) { + fakeHostName = `${fakeHostName}.${generateRandomString()}${generateRandomNumber()}.workers.dev`; + } else if (hostName.includes(".pages.dev")) { + fakeHostName = `${fakeHostName}.${generateRandomString()}${generateRandomNumber()}.pages.dev`; + } else if (hostName.includes("worker") || hostName.includes("notls") || tls == false) { + fakeHostName = `notls.${fakeHostName}${generateRandomNumber()}.net`; + } else { + fakeHostName = `${fakeHostName}.${generateRandomNumber()}.xyz` + } + let content = ""; + let suburl = ""; + let isBase64 = false; + if ((userAgent.includes('clash') && !userAgent.includes('nekobox')) || format === 'clash') { + suburl = `https://${subconverter}/sub?target=clash&url=https%3A%2F%2F${hostName}/bestip/${fakeUserID}%3Fhost%3D${fakeHostName}%26uuid%3D${fakeUserID}&insert=false&config=${encodeURIComponent(subconfig)}%26proxyip%3D${proxyIP}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`; + } else if ((userAgent.includes('sing-box') || userAgent.includes('singbox')) || format === 'singbox') { + suburl = `https://${subconverter}/sub?target=singbox&url=https%3A%2F%2F${hostName}/bestip/${fakeUserID}%3Fhost%3D${fakeHostName}%26uuid%3D${fakeUserID}&insert=false&config=${encodeURIComponent(subconfig)}%26proxyip%3D${proxyIP}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`; + } else if (format === 'qx') { + const newAddressesapi = await getAddressesapi(addressesapi); + const vlessSubConfig = createVlessBestIpSub(userID, request.headers.get('Host'), newAddressesapi, format); + return new Response(vlessSubConfig, { + headers: { + // "Content-Disposition": `attachment; filename*=utf-8''${encodeURIComponent(FileName)}; filename=${FileName}`, + "content-type": "text/plain; charset=utf-8", + "Profile-Update-Interval": `${SUBUpdateTime}`, + "Subscription-Userinfo": `upload=${UD}; download=${UD}; total=${total}; expire=${expire}`, + }, + }); + } else { + suburl = `https://${sub}/bestip/${fakeUserID}?host=${fakeHostName}&uuid=${fakeUserID}&proxyip=${proxyIP}`; + isBase64 = true; + } + try { + const response = await fetch(suburl, { + headers: { + 'User-Agent': 'ansoncloud8.github.io' + } + }); + content = await response.text(); + const result = revertFakeInfo(content, userID_Path, hostName, isBase64); + + // content.replace(new RegExp(fakeUserID, 'g'), userID) + return new Response(result, { + headers: { + // "Content-Disposition": `attachment; filename*=utf-8''${encodeURIComponent(FileName)}; filename=${FileName}`, + "content-type": "text/plain; charset=utf-8", + "Profile-Update-Interval": `${SUBUpdateTime}`, + "Subscription-Userinfo": `upload=${UD}; download=${UD}; total=${total}; expire=${expire}`, + }, + }); + } catch (error) { + console.error('Error fetching content:', error); + return `Error fetching content: ${error.message}`; + } + }; + default: + // return new Response('Not found', { status: 404 }); + //首页改成一个nginx伪装页 + return new Response(await nginx(), { + headers: { + 'Content-Type': 'text/html; charset=UTF-8', + 'referer': 'https://www.google.com/search?q=ansoncloud8.github.io', + }, + }); + + // For any other path, reverse proxy to 'ramdom website' and return the original response, caching it in the process + // const randomHostname = cn_hostnames[Math.floor(Math.random() * cn_hostnames.length)]; + // const newHeaders = new Headers(request.headers); + // newHeaders.set('cf-connecting-ip', '1.2.3.4'); + // newHeaders.set('x-forwarded-for', '1.2.3.4'); + // newHeaders.set('x-real-ip', '1.2.3.4'); + // newHeaders.set('referer', 'https://www.google.com/search?q=ansoncloud8.github.io'); + // // Use fetch to proxy the request to 15 different domains + // const proxyUrl = 'https://' + randomHostname + url.pathname + url.search; + // let modifiedRequest = new Request(proxyUrl, { + // method: request.method, + // headers: newHeaders, + // body: request.body, + // redirect: 'manual', + // }); + // const proxyResponse = await fetch(modifiedRequest, { redirect: 'manual' }); + // // Check for 302 or 301 redirect status and return an error response + // if ([301, 302].includes(proxyResponse.status)) { + // return new Response(`Redirects to ${randomHostname} are not allowed.`, { + // status: 403, + // statusText: 'Forbidden', + // }); + // } + // // Return the response from the proxy server + // return proxyResponse; + } + } else { + return await vlessOverWSHandler(request); + } + } catch (err) { + /** @type {Error} */ let e = err; + return new Response(e.toString()); + } + }, +}; + +export async function uuid_validator(request) { + const hostname = request.headers.get('Host'); + const currentDate = new Date(); + + const subdomain = hostname.split('.')[0]; + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, '0'); + const day = String(currentDate.getDate()).padStart(2, '0'); + + const formattedDate = `${year}-${month}-${day}`; + + // const daliy_sub = formattedDate + subdomain + const hashHex = await hashHex_f(subdomain); + // subdomain string contains timestamps utc and uuid string TODO. + console.log(hashHex, subdomain, formattedDate); +} + +export async function hashHex_f(string) { + const encoder = new TextEncoder(); + const data = encoder.encode(string); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); + return hashHex; +} + +/** + * Handles VLESS over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the VLESS header. + * @param {import("@cloudflare/workers-types").Request} request The incoming request object. + * @returns {Promise} A Promise that resolves to a WebSocket response object. + */ +async function vlessOverWSHandler(request) { + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + webSocket.accept(); + + let address = ''; + let portWithRandomLog = ''; + let currentDate = new Date(); + const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { + console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || ''); + }; + const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; + + const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); + + /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ + let remoteSocketWapper = { + value: null, + }; + let udpStreamWrite = null; + let isDns = false; + + // ws --> remote + readableWebSocketStream.pipeTo(new WritableStream({ + async write(chunk, controller) { + if (isDns && udpStreamWrite) { + return udpStreamWrite(chunk); + } + if (remoteSocketWapper.value) { + const writer = remoteSocketWapper.value.writable.getWriter() + await writer.write(chunk); + writer.releaseLock(); + return; + } + + const { + hasError, + message, + portRemote = 443, + addressRemote = '', + rawDataIndex, + vlessVersion = new Uint8Array([0, 0]), + isUDP, + } = processVlessHeader(chunk, userID); + address = addressRemote; + portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `; + if (hasError) { + // controller.error(message); + throw new Error(message); // cf seems has bug, controller.error will not end stream + } + + // If UDP and not DNS port, close it + if (isUDP && portRemote !== 53) { + throw new Error('UDP proxy only enabled for DNS which is port 53'); + // cf seems has bug, controller.error will not end stream + } + + if (isUDP && portRemote === 53) { + isDns = true; + } + + // ["version", "附加信息长度 N"] + const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); + const rawClientData = chunk.slice(rawDataIndex); + + // TODO: support udp here when cf runtime has udp support + if (isDns) { + const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); + udpStreamWrite = write; + udpStreamWrite(rawClientData); + return; + } + handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); + }, + close() { + log(`readableWebSocketStream is close`); + }, + abort(reason) { + log(`readableWebSocketStream is abort`, JSON.stringify(reason)); + }, + })).catch((err) => { + log('readableWebSocketStream pipeTo error', err); + }); + + return new Response(null, { + status: 101, + webSocket: client, + }); +} + +/** + * Handles outbound TCP connections. + * + * @param {any} remoteSocket + * @param {string} addressRemote The remote address to connect to. + * @param {number} portRemote The remote port to connect to. + * @param {Uint8Array} rawClientData The raw client data to write. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. + * @param {Uint8Array} vlessResponseHeader The VLESS response header. + * @param {function} log The logging function. + * @returns {Promise} The remote socket. + */ +async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { + + /** + * Connects to a given address and port and writes data to the socket. + * @param {string} address The address to connect to. + * @param {number} port The port to connect to. + * @returns {Promise} A Promise that resolves to the connected socket. + */ + async function connectAndWrite(address, port) { + /** @type {import("@cloudflare/workers-types").Socket} */ + const tcpSocket = connect({ + hostname: address, + port: port, + }); + remoteSocket.value = tcpSocket; + log(`connected to ${address}:${port}`); + const writer = tcpSocket.writable.getWriter(); + await writer.write(rawClientData); // first write, nomal is tls client hello + writer.releaseLock(); + return tcpSocket; + } + + /** + * Retries connecting to the remote address and port if the Cloudflare socket has no incoming data. + * @returns {Promise} A Promise that resolves when the retry is complete. + */ + async function retry() { + const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) + tcpSocket.closed.catch(error => { + console.log('retry tcpSocket closed error', error); + }).finally(() => { + safeCloseWebSocket(webSocket); + }) + remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); + } + + const tcpSocket = await connectAndWrite(addressRemote, portRemote); + + // when remoteSocket is ready, pass to websocket + // remote--> ws + remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); +} + +/** + * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket. + * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from. + * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT. + * @param {(info: string)=> void} log The logging function. + * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket. + */ +function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { + let readableStreamCancel = false; + const stream = new ReadableStream({ + start(controller) { + webSocketServer.addEventListener('message', (event) => { + const message = event.data; + controller.enqueue(message); + }); + + webSocketServer.addEventListener('close', () => { + safeCloseWebSocket(webSocketServer); + controller.close(); + }); + + webSocketServer.addEventListener('error', (err) => { + log('webSocketServer has error'); + controller.error(err); + }); + const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); + if (error) { + controller.error(error); + } else if (earlyData) { + controller.enqueue(earlyData); + } + }, + + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + + cancel(reason) { + log(`ReadableStream was canceled, due to ${reason}`) + readableStreamCancel = true; + safeCloseWebSocket(webSocketServer); + } + }); + + return stream; +} + +// https://xtls.github.io/development/protocols/vless.html +// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw + +/** + * Processes the VLESS header buffer and returns an object with the relevant information. + * @param {ArrayBuffer} vlessBuffer The VLESS header buffer to process. + * @param {string} userID The user ID to validate against the UUID in the VLESS header. + * @returns {{ + * hasError: boolean, + * message?: string, + * addressRemote?: string, + * addressType?: number, + * portRemote?: number, + * rawDataIndex?: number, + * vlessVersion?: Uint8Array, + * isUDP?: boolean + * }} An object with the relevant information extracted from the VLESS header buffer. + */ +function processVlessHeader(vlessBuffer, userID) { + if (vlessBuffer.byteLength < 24) { + return { + hasError: true, + message: 'invalid data', + }; + } + + const version = new Uint8Array(vlessBuffer.slice(0, 1)); + let isValidUser = false; + let isUDP = false; + const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17)); + const slicedBufferString = stringify(slicedBuffer); + // check if userID is valid uuid or uuids split by , and contains userID in it otherwise return error message to console + const uuids = userID.includes(',') ? userID.split(",") : [userID]; + // uuid_validator(hostName, slicedBufferString); + + + // isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()); + isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim(); + + console.log(`userID: ${slicedBufferString}`); + + if (!isValidUser) { + return { + hasError: true, + message: 'invalid user', + }; + } + + const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; + //skip opt for now + + const command = new Uint8Array( + vlessBuffer.slice(18 + optLength, 18 + optLength + 1) + )[0]; + + // 0x01 TCP + // 0x02 UDP + // 0x03 MUX + if (command === 1) { + isUDP = false; + } else if (command === 2) { + isUDP = true; + } else { + return { + hasError: true, + message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, + }; + } + const portIndex = 18 + optLength + 1; + const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); + // port is big-Endian in raw data etc 80 == 0x005d + const portRemote = new DataView(portBuffer).getUint16(0); + + let addressIndex = portIndex + 2; + const addressBuffer = new Uint8Array( + vlessBuffer.slice(addressIndex, addressIndex + 1) + ); + + // 1--> ipv4 addressLength =4 + // 2--> domain name addressLength=addressBuffer[1] + // 3--> ipv6 addressLength =16 + const addressType = addressBuffer[0]; + let addressLength = 0; + let addressValueIndex = addressIndex + 1; + let addressValue = ''; + switch (addressType) { + case 1: + addressLength = 4; + addressValue = new Uint8Array( + vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) + ).join('.'); + break; + case 2: + addressLength = new Uint8Array( + vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) + )[0]; + addressValueIndex += 1; + addressValue = new TextDecoder().decode( + vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) + ); + break; + case 3: + addressLength = 16; + const dataView = new DataView( + vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) + ); + // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + const ipv6 = []; + for (let i = 0; i < 8; i++) { + ipv6.push(dataView.getUint16(i * 2).toString(16)); + } + addressValue = ipv6.join(':'); + // seems no need add [] for ipv6 + break; + default: + return { + hasError: true, + message: `invild addressType is ${addressType}`, + }; + } + if (!addressValue) { + return { + hasError: true, + message: `addressValue is empty, addressType is ${addressType}`, + }; + } + + return { + hasError: false, + addressRemote: addressValue, + addressType, + portRemote, + rawDataIndex: addressValueIndex + addressLength, + vlessVersion: version, + isUDP, + }; +} + + +/** + * Converts a remote socket to a WebSocket connection. + * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to. + * @param {ArrayBuffer | null} vlessResponseHeader The VLESS response header. + * @param {(() => Promise) | null} retry The function to retry the connection if it fails. + * @param {(info: string) => void} log The logging function. + * @returns {Promise} A Promise that resolves when the conversion is complete. + */ +async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { + // remote--> ws + let remoteChunkCount = 0; + let chunks = []; + /** @type {ArrayBuffer | null} */ + let vlessHeader = vlessResponseHeader; + let hasIncomingData = false; // check if remoteSocket has incoming data + await remoteSocket.readable + .pipeTo( + new WritableStream({ + start() { + }, + /** + * + * @param {Uint8Array} chunk + * @param {*} controller + */ + async write(chunk, controller) { + hasIncomingData = true; + remoteChunkCount++; + if (webSocket.readyState !== WS_READY_STATE_OPEN) { + controller.error( + 'webSocket.readyState is not open, maybe close' + ); + } + if (vlessHeader) { + webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); + vlessHeader = null; + } else { + // console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`); + // seems no need rate limit this, CF seems fix this??.. + // if (remoteChunkCount > 20000) { + // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M + // await delay(1); + // } + webSocket.send(chunk); + } + }, + close() { + log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); + // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. + }, + abort(reason) { + console.error(`remoteConnection!.readable abort`, reason); + }, + }) + ) + .catch((error) => { + console.error( + `remoteSocketToWS has exception `, + error.stack || error + ); + safeCloseWebSocket(webSocket); + }); + + // seems is cf connect socket have error, + // 1. Socket.closed will have error + // 2. Socket.readable will be close without any data coming + if (hasIncomingData === false && retry) { + log(`retry`) + retry(); + } +} + +/** + * Decodes a base64 string into an ArrayBuffer. + * @param {string} base64Str The base64 string to decode. + * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error. + */ +function base64ToArrayBuffer(base64Str) { + if (!base64Str) { + return { earlyData: null, error: null }; + } + try { + // go use modified Base64 for URL rfc4648 which js atob not support + base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); + const decode = atob(base64Str); + const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); + return { earlyData: arryBuffer.buffer, error: null }; + } catch (error) { + return { earlyData: null, error }; + } +} + +/** + * Checks if a given string is a valid UUID. + * Note: This is not a real UUID validation. + * @param {string} uuid The string to validate as a UUID. + * @returns {boolean} True if the string is a valid UUID, false otherwise. + */ +function isValidUUID(uuid) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +const WS_READY_STATE_OPEN = 1; +const WS_READY_STATE_CLOSING = 2; +/** + * Closes a WebSocket connection safely without throwing exceptions. + * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close. + */ +function safeCloseWebSocket(socket) { + try { + if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { + socket.close(); + } + } catch (error) { + console.error('safeCloseWebSocket error', error); + } +} + +const byteToHex = []; + +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 256).toString(16).slice(1)); +} + +function unsafeStringify(arr, offset = 0) { + return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); +} + +function stringify(arr, offset = 0) { + const uuid = unsafeStringify(arr, offset); + if (!isValidUUID(uuid)) { + throw TypeError("Stringified UUID is invalid"); + } + return uuid; +} + + +/** + * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over. + * @param {ArrayBuffer} vlessResponseHeader The VLESS response header. + * @param {(string) => void} log The logging function. + * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream. + */ +async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { + + let isVlessHeaderSent = false; + const transformStream = new TransformStream({ + start(controller) { + + }, + transform(chunk, controller) { + // udp message 2 byte is the the length of udp data + // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message + for (let index = 0; index < chunk.byteLength;) { + const lengthBuffer = chunk.slice(index, index + 2); + const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); + const udpData = new Uint8Array( + chunk.slice(index + 2, index + 2 + udpPakcetLength) + ); + index = index + 2 + udpPakcetLength; + controller.enqueue(udpData); + } + }, + flush(controller) { + } + }); + + // only handle dns udp for now + transformStream.readable.pipeTo(new WritableStream({ + async write(chunk) { + const resp = await fetch(dohURL, // dns server url + { + method: 'POST', + headers: { + 'content-type': 'application/dns-message', + }, + body: chunk, + }) + const dnsQueryResult = await resp.arrayBuffer(); + const udpSize = dnsQueryResult.byteLength; + // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); + const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); + if (webSocket.readyState === WS_READY_STATE_OPEN) { + log(`doh success and dns message length is ${udpSize}`); + if (isVlessHeaderSent) { + webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); + } else { + webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); + isVlessHeaderSent = true; + } + } + } + })).catch((error) => { + log('dns udp has error' + error) + }); + + const writer = transformStream.writable.getWriter(); + + return { + /** + * + * @param {Uint8Array} chunk + */ + write(chunk) { + writer.write(chunk); + } + }; +} + +async function ADD(envadd) { + var addtext = envadd.replace(/[ |"'\r\n]+/g, ',').replace(/,+/g, ','); // 将空格、双引号、单引号和换行符替换为逗号 + //console.log(addtext); + if (addtext.charAt(0) == ',') addtext = addtext.slice(1); + if (addtext.charAt(addtext.length - 1) == ',') addtext = addtext.slice(0, addtext.length - 1); + const add = addtext.split(','); + // console.log(add); + return add; +} + +async function getAddressesapi(api) { + if (!api || api.length === 0) { + return []; + } + + let newapi = ""; + + // 创建一个AbortController对象,用于控制fetch请求的取消 + const controller = new AbortController(); + + const timeout = setTimeout(() => { + controller.abort(); // 取消所有请求 + }, 2000); // 2秒后触发 + + try { + // 使用Promise.allSettled等待所有API请求完成,无论成功或失败 + // 对api数组进行遍历,对每个API地址发起fetch请求 + const responses = await Promise.allSettled(api.map(apiUrl => fetch(apiUrl, { + method: 'get', + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;', + 'User-Agent': 'ansoncloud8.github.io' + }, + signal: controller.signal // 将AbortController的信号量添加到fetch请求中,以便于需要时可以取消请求 + }).then(response => response.ok ? response.text() : Promise.reject()))); + + // 遍历所有响应 + for (const response of responses) { + // 检查响应状态是否为'fulfilled',即请求成功完成 + if (response.status === 'fulfilled') { + // 获取响应的内容 + const content = await response.value; + newapi += content + '\n'; + } + } + } catch (error) { + console.error(error); + } finally { + // 无论成功或失败,最后都清除设置的超时定时器 + clearTimeout(timeout); + } + + const newAddressesapi = await ADD(newapi); + + // 返回处理后的结果 + return newAddressesapi; +} + + +function generateRandomString() { + let minLength = 2; + let maxLength = 3; + let length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; + let characters = 'abcdefghijklmnopqrstuvwxyz'; + let result = ''; + for (let i = 0; i < length; i++) { + result += characters[Math.floor(Math.random() * characters.length)]; + } + return result; +} + +function generateUUID() { + let uuid = ''; + for (let i = 0; i < 32; i++) { + let num = Math.floor(Math.random() * 16); + if (num < 10) { + uuid += num; + } else { + uuid += String.fromCharCode(num + 55); + } + } + return uuid.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5').toLowerCase(); +} + +function generateRandomNumber() { + let minNum = 100000; + let maxNum = 999999; + return Math.floor(Math.random() * (maxNum - minNum + 1)) + minNum; +} + +function revertFakeInfo(content, userID, hostName, isBase64) { + if (isBase64) content = atob(content);//Base64解码 + content = content.replace(new RegExp(fakeUserID, 'g'), userID).replace(new RegExp(fakeHostName, 'g'), hostName); + if (isBase64) content = btoa(content);//Base64编码 + return content; +} + +/** + * + * @param {string} userID - single or comma separated userIDs + * @param {string | null} hostName + * @returns {string} + */ +function getVLESSConfig(userIDs, hostName) { + const commonUrlPart = `:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`; + const hashSeparator = "################################################################"; + + // Split the userIDs into an array + const userIDArray = userIDs.split(","); + + // Prepare output string for each userID + const output = userIDArray.map((userID) => { + const vlessMain = 'vless://' + userID + '@' + hostName + commonUrlPart; + const vlessSec = 'vless://' + userID + '@' + proxyIP + commonUrlPart; + return `################################################################ +telegram 交流群 技术大佬~在线交流! +t.me/AM_CLUBS +--------------------------------------------------------------- +github 项目地址 点击Star!Star!Star!!! +https://github.com/ansoncloud8/am-tunnel +--------------------------------------------------------------- +订阅YouTube频道,更多技术分享 +https://youtube.com/@AM_CLUB +################################################################ + +v2ray default ip +--------------------------------------------------------------- +${vlessMain} + +--------------------------------------------------------------- +v2ray with bestip +--------------------------------------------------------------- +${vlessSec} + +--------------------------------------------------------------- +clash-meta +--------------------------------------------------------------- +- type: vless + name: ${hostName} + server: ${hostName} + port: 443 + uuid: ${userID} + network: ws + tls: true + udp: false + sni: ${hostName} + client-fingerprint: chrome + ws-opts: + path: "/?ed=2048" + headers: + host: ${hostName} +--------------------------------------------------------------- +################################################################ +`; + }).join('\n'); + const sublink = `https://${hostName}/sub/${userIDArray[0]}` + const subbestip = `https://${hostName}/bestip/${userIDArray[0]}?uuid=${userIDArray[0]}`; + const singboxlink = `https://${hostName}/sub/bestip/${userIDArray[0]}?format=singbox&uuid=${fakeUserID}`; + const quantumultxlink = `https://${hostName}/sub/bestip/${userIDArray[0]}?format=qx&uuid=${fakeUserID}`; + const clashlink = `https://${hostName}/sub/bestip/${userIDArray[0]}?format=clash&uuid=${fakeUserID}`; + // Prepare header string + + // Prepare header string + const header = ` +

图片描述 +Welcome! This function generates configuration for VLESS protocol. If you found this useful, please check our GitHub project for more: +欢迎!这是生成 VLESS 协议的配置。如果您发现这个项目很好用,请查看我们的 GitHub 项目给我一个star: +am-tunnel + +VLESS节点订阅连接(v2rayU、v2rayN等工具)自动生成 +VLESS节点订阅连接(v2rayU、v2rayN等工具)优选IP +Clash节点订阅连接(clash-verge-rev、openclash等工具)自动生成 +Clash节点订阅连接(clash-verge-rev、openclash等工具)优选IP +(sin-box工具)节点订阅连接优选IP +(Quantumult X工具)节点订阅连接优选IP +nekobox节点订阅连接优选IP +

`; + // HTML Head with CSS and FontAwesome library + const htmlHead = ` + + tunnel: VLESS configuration + + + + + + + `; + + // Join output with newlines, wrap inside and + return ` + + ${htmlHead} + +
${header}
+
${output}
+ + + `; +} + +let portSet_http = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]); +let portSet_https = new Set([443, 8443, 2053, 2096, 2087, 2083]); + +function createVLESSSub(userID_Path, hostName, format, dq) { + const userIDArray = userID_Path.includes(',') ? userID_Path.split(',') : [userID_Path]; + const commonUrlPart_http = `?encryption=none&security=none&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`; + const commonUrlPart_https = `?encryption=none&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`; + //trojan + const trojan_http = `?alpn=http%2F1.1&security=none&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`; + const trojan_https = `?alpn=http%2F1.1&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`; +//const trojanLink = `trojan://${密码}@${address}:${port}?security=tls&sni=${sni}&alpn=http%2F1.1&fp=randomized&type=ws&host=${伪装域名}&path=${encodeURIComponent(最终路径)}#${encodeURIComponent(addressid + 节点备注)}`; + + const output = userIDArray.flatMap((userID) => { + if (format === 'qx') { + var host = hostName.split('.').slice(1).join('.'); + const httpsConfigurations = Array.from(portSet_https).flatMap((port) => { + if(dq){ + let tag = '📶 CF_' + port; + //🇸🇬 SG:新加坡 🇭🇰 HK:香港 🇰🇷 KR:韩国 🇯🇵 JP:日本 🇬🇧 GB:英国 🇺🇸 US:美国 🇼🇸 TW:台湾 + if (dq === 'SG') { + tag = '🇸🇬 SG_' + port; + } else if (dq === 'HK') { + tag = '🇭🇰 HK_' + port; + } else if (dq === 'KR') { + tag = '🇰🇷 KR_' + port; + } else if (dq === 'JP') { + tag = '🇯🇵 JP_' + port; + } else if (dq === 'GB') { + tag = '🇬🇧 GB_' + port; + } else if (dq === 'US') { + tag = '🇺🇸 US_' + port; + } else if (dq === 'TW') { + tag = '🇼🇸 TW_' + port; + } + const sgHttps = 'vless=' + dq.toLowerCase() + '.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=' + tag; + return [sgHttps]; + } + const sgHttps = 'vless=sg.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=🇸🇬 SG_' + port; + const hkHttps = 'vless=hk.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=🇭🇰 HK_' + port; + const krHttps = 'vless=kr.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=🇰🇷 KR_' + port; + const jpHttps = 'vless=jp.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=🇯🇵 JP_' + port; + const usHttps = 'vless=us.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=🇺🇸 US_' + port; + const twHttps = 'vless=tw.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=🇼🇸 TW_' + port; + const cfHttps = 'vless=cf.' + port + '.' + host + ':' + port + ',method=none,password=' + userID + ',obfs=wss,obfs-uri=/?ed=2048,obfs-host=' + hostName + ',tls-verification=true,tls-host=' + hostName + ',fast-open=false,udp-relay=false,tag=📶 CF_' + port; + return [sgHttps, hkHttps, krHttps, jpHttps, usHttps, twHttps, cfHttps]; + }); + + return [...httpsConfigurations]; + } else if (format === 'trojan') { + const httpConfigurations = Array.from(portSet_http).flatMap((port) => { + if (!hostName.includes('pages.dev')) { + const urlPart = tagName + ` (${hostName}-HTTP-${port})`; + const vlessMainHttp = 'trojan://' + userID + '@' + hostName + ':' + port + trojan_http + urlPart; + return autoaddress.flatMap((proxyIP) => { + const vlessSecHttp = 'trojan://' + userID + '@' + proxyIP + ':' + port + trojan_https + urlPart + '-' + proxyIP; + return [vlessMainHttp, vlessSecHttp]; + }); + } + return []; + }); + + const httpsConfigurations = Array.from(portSet_https).flatMap((port) => { + const urlPart = tagName + ` (${hostName}-HTTPS-${port})`; + const vlessMainHttps = 'trojan://' + userID + '@' + hostName + ':' + port + trojan_http + urlPart; + return autoaddress.flatMap((proxyIP) => { + const vlessSecHttps = 'trojan://' + userID + '@' + proxyIP + ':' + port + trojan_https + urlPart + '-' + proxyIP; + return [vlessMainHttps, vlessSecHttps]; + }); + }); + + return [...httpConfigurations, ...httpsConfigurations]; + + }else { + const httpConfigurations = Array.from(portSet_http).flatMap((port) => { + if (!hostName.includes('pages.dev')) { + const urlPart = tagName + ` (${hostName}-HTTP-${port})`; + const vlessMainHttp = 'vless://' + userID + '@' + hostName + ':' + port + commonUrlPart_http + urlPart; + return autoaddress.flatMap((proxyIP) => { + const vlessSecHttp = 'vless://' + userID + '@' + proxyIP + ':' + port + commonUrlPart_http + urlPart + '-' + proxyIP; + return [vlessMainHttp, vlessSecHttp]; + }); + } + return []; + }); + + const httpsConfigurations = Array.from(portSet_https).flatMap((port) => { + const urlPart = tagName + ` (${hostName}-HTTPS-${port})`; + const vlessMainHttps = 'vless://' + userID + '@' + hostName + ':' + port + commonUrlPart_https + urlPart; + return autoaddress.flatMap((proxyIP) => { + const vlessSecHttps = 'vless://' + userID + '@' + proxyIP + ':' + port + commonUrlPart_https + urlPart + '-' + proxyIP; + return [vlessMainHttps, vlessSecHttps]; + }); + }); + + return [...httpConfigurations, ...httpsConfigurations]; + } + }); + + return output.join('\n'); +} + +function createVlessBestIpSub(userID_Path, hostName, newAddressesapi, format) { + + addresses = addresses.concat(newAddressesapi); + // 使用Set对象去重 + const uniqueAddresses = [...new Set(addresses)]; + + const responseBody = uniqueAddresses.map((address, i) => { + let port = "443"; + let addressid = address; + let dq = tagName; + + const match = addressid.match(regex); + if (!match) { + if (address.includes(':') && address.includes('#')) { + const parts = address.split(':'); + address = parts[0]; + const subParts = parts[1].split('#'); + port = subParts[0]; + addressid = subParts[1]; + } else if (address.includes(':')) { + const parts = address.split(':'); + address = parts[0]; + port = parts[1]; + } else if (address.includes('#')) { + const parts = address.split('#'); + address = parts[0]; + addressid = parts[1]; + } + + if (addressid.includes(':')) { + addressid = addressid.split(':')[0]; + } + } else { + address = match[1]; + port = match[2] || port; + addressid = match[3] || address; + } + dq = addressid + '_' + i; + //🇸🇬 SG:新加坡 🇭🇰 HK:香港 🇰🇷 KR:韩国 🇯🇵 JP:日本 🇬🇧 GB:英国 🇺🇸 US:美国 🇼🇸 TW:台湾 + if (addressid.includes('AM')) { + addressid = addressid; + dq = addressid; + } else if (addressid === 'SG') { + addressid = '🇸🇬 SG_' + i; + } else if (addressid === 'HK') { + addressid = '🇭🇰 HK_' + i; + } else if (addressid === 'KR') { + addressid = '🇰🇷 KR_' + i; + } else if (addressid === 'JP') { + addressid = '🇯🇵 JP_' + i; + } else if (addressid === 'GB') { + addressid = '🇬🇧 GB_' + i; + } else if (addressid === 'US') { + addressid = '🇺🇸 US_' + i; + } else if (addressid === 'TW') { + addressid = '🇼🇸 TW_' + i; + } else if (addressid === 'CF') { + addressid = '📶 ' + addressid + '_' + i; + } else { + addressid = '📶 ' + addressid + '_' + i; + dq = tagName+ '_' + i; + } + + let vlessLink = `vless://${userID_Path}@${address}:${port}?encryption=none&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=&path=%2F%3Fed%3D2048#${dq}`; + if (port === '80' || port === '8080' || port === '8880' || port === '2052' || port === '2086' || port === '2095' || port === '2082' ) { + vlessLink = `vless://${userID_Path}@${address}:${port}?encryption=none&security=&fp=random&type=ws&host=${hostName}&path=&path=%2F%3Fed%3D2048#${dq}`; + } + + if (format === 'qx') { + //80, 8080, 8880, 2052, 2086, 2095, 2082 + //443, 8443, 2053, 2096, 2087, 2083 + if (port === '80' || port === '8080' || port === '8880' || port === '2052' || port === '2086' || port === '2095' || port === '2082' ) { + vlessLink = `vless=${address}:${port},method=none,password=${userID_Path},obfs=ws,obfs-uri=/?ed=2048,obfs-host=${hostName},fast-open=false,udp-relay=false,tag=${addressid}`; + }else{ + vlessLink = `vless=${address}:${port},method=none,password=${userID_Path},obfs=wss,obfs-uri=/?ed=2048,obfs-host=${hostName},tls-verification=true,tls-host=${hostName},fast-open=false,udp-relay=false,tag=${addressid}`; + } + } + //trojan + if (format === 'trojan') { + if (port === '80' || port === '8080' || port === '8880' || port === '2052' || port === '2086' || port === '2095' || port === '2082' ) { + vlessLink = `trojan://${userID_Path}@${address}:${port}?alpn=http%2F1.1&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=&path=%2F%3Fed%3D2048#${dq}`; + }else{ + vlessLink = `trojan://${userID_Path}@${address}:${port}?alpn=http%2F1.1&security=&fp=random&type=ws&host=${hostName}&path=&path=%2F%3Fed%3D2048#${dq}`; + } + } + + + return vlessLink; + }).join('\n'); + return responseBody; +} + + +async function nginx() { + const text = ` + + + + Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and + working. Further configuration is required.

+ +

For online documentation and support please refer to + nginx.org.
+ Commercial support is available at + nginx.com.

+ +

Thank you for using nginx.

+ + + ` + return text; +} diff --git a/_worker.js.zip b/_worker.js.zip new file mode 100644 index 0000000000000000000000000000000000000000..536d3cc3488a7df817382f050bddfa74796e4e17 GIT binary patch literal 14949 zcmZ|0V~j3L6Rtb9ZQHii*tTu0@iVq<+gxMYwr$(q@0auEWG6eFN>_4s|Er`^UH7FZ z0}6%)1Ox;HBu*}_CEDif)X5D5#4!N`1OtQzWZ>@LWMk&UVC}4?3JnBGh7Mx!zi{<{ z1p)>;2L%EG^ZW1am9CdFu3GA^&IcN1Tm)I!@9zW6%2L}h zeTPnJx40TKDCi(cqLyZ)>Dnu@5 zire>%IQ6Fuxwa({-62z@_fu)3r{M>diU^!@Smh zMqjJ9Epljz?GbzhK0_rt86meEo%XS+YV=i50C2nf^66*$ zyBRZinn=uEEdA4X`(kPJKWvIvssDJl=nNR|a@+DM;+? zpRJ*O91Vfh-Zhami&> z*D+3^Cm0`QTH(qscFL#fT>wmhdXgGV&4h?c^&H+r>##)|;SV^jNQfuplRETMv@ymR zc^*4U6&@PpYeLaLaZ^IAbQv))tsbr3lBeN|=`WH(CALGjS>#yC<}sxaJ|Df6-E+3P zL(CA(?M+<<)!nbXd)Tz*T~atyM{7*c0%spH&B$Z_9s;b(3Dh9rO>3Qx)6JG3t*l$( zq(PV?w01MdVrBRiYc*A+^W!_j*s)gNKwS)~l+p|Oi!0BOC!44>kZQPj*#zVx`NZDZlU*($}ZBMqhlwum?;u+tFfj^9;T!&NA} zc@~COWz>mG#yIGy&ui6#w%u+*FUzv7`$oz+_>}P&^Jp)MJpY=yRO@ErG0LM+&c?uD zK1V=be+XsTZ97(8(rPH8oWJe7_mn zQNnJ3$7$~dBH`CQp3q5y zeRJ=})tR8si^4|h@9ELL*MCKiPe>S4&`b*=9k=toclXTu7aZm~qvO!FYWbQ(_FWCG zhZUy$=*KJU)|X!WYcJ>}N5}Seo`^vMPi=Mm>7~k#=WFqlc`R)$SIMvje{oTfH=2tY z)O1ZgBOQJhxsU>CqKzDOzFBeX5rr&TtVVqPmh=S~Lsbq^80p-*hGg7W*t%KnsR)i# zTd%iGpA%?wRaTsb`h=&)ynpravW1U$E=xE0pKy>sj|61SxM??`=7x znkTuFNfXK5s9KBsf)5>w<=H$=7jw)eKWE2KG)hy>8vIBLRbDZnyo3R)3?*?0W33@TW_2I6|!IkXWidzuOblq zxbx(Mie~vtJ>@RbEgGsHqW@u#cv+`8Uk|Vm2Qv2pfBILmEnj7>cZoE(=d zH#_3xRnzmc_EHfxYX@)e3Tl46>s4}kFVPogKIDptvCwYV&n_36$;-_%fq0ncDkDgM_1bnU=0Y$Kp4f)og^OilPetz0rU z+DA|tRgY0&AnZaNj|_y)5v5c&J51e(yup_dq^Jg5)Ubyf0woR)T-@&`4wOzIE>0nl z=W!4r_2T1#2Q9(vhoFvR4dt>YZir3Y0;6CedZxW7S>=gz>FJ8?if9J>+`en(bR@iM zwCrAM2+aq3OtfSv#$qT!1P9dAM}ns%R6;|<4mc-=6K(NS1U|&_#71}sv*oKUix=V| zSO^CeM?+JTTs)7GoPsV5CIbe60+^>^Fw*nU7as%#0NSXsCJl*H6{xVp8Gd1q5hBL& z^*M@6J&qu7qZCxTJIp?!ZB5>IOcteO%&DfPw(!DxczR)s5)Qq2WatEH9_v?@@^}5u zw{728$WjJ+k%LwXY}n4ssA926WO~aRAO{*Qv#F9`TfQ{C32I2qj&W>vey^|0AOm?Y<+PvIqq+zJVqL>31?@YU^kzCkA zqED0k|G*%R$8C+2DE@>))wxk0>=GR#HV#$5W1f;>)3D}dQG>SqZe#g>raf;V!%t*a8s z*}>mPytTIYf;1DAV^sQ1XOeWIEtJGnqze;&och0b6Y40tdnbl)2zuGkVqC_!Ib$!$udVnBK^9f_31C$|q<7omCN-YAbT zPrdKmTk$C%-``DhHw0bD!gKy}gk@{ZkZleRF{-29rc(@O+!y z&DrYodHVsqCtEq$=O`lqO?Y8w7piIq@mg{4ZiD6fxGI3`qiBrtxAhCmMK^V+UtA|% zN<+Ey?@VDVo_!wLH_ZU0yW!lzjC&^p`Td$EmBNz@hA^5XisYLDOZ zIeK{O>-ROEbwrXS9!t>e^|U|DCBb=w4q34#tt3Wvjg0}XTlzlp?^A5hZFN_-bfN(P!+kH?KkjTHye%aQ}7aC}lhM&D$t`lqSpM2>| z)V$Ajedg|F3}LWl@HUgB<&u z@b&tNxciw%hi9q1%Uei+l_Yvr%`e~nWk$NEmoM_sHG}U@yag`1FN8=a< zos0DNn?8iongu1CNY6h$fe8>(94CCo%SK2Jk?S=n2Y}s$yYxrVGOE+|_GeiQ7sJ0I zjDR=~7TmWauEt{@qp$k&2wh?~eAj+kB$9+dY_~Bu2Hz7n0G-uV>8AYn`YKG$!n(iqk+JR2TnJMd9m?xSJ(g()5rO8I4N3(ILh zf3|oZzez@}=|vj;{rIB$o8|hyV^T^@yoX1M$q>oJqss`00gYCGIWPb!T{OYA(C|nC zM{4SKSeBXf*?!(HaODCL{YkL0{-XNY&k+Ax=Kak%EiQ+S3-{G&$%S{i{|vSlBT$AxWFC#?tLTfRsaS2giv z&%fetL4%cjFm2wec(m}166m*$w8Z{W8G68biZJ9(SE>BNklCGh(t}W;(jm-Tgd|Xp z>fGunOGJbfTh8upL=`uvd3A<8{CRD>_DkNbXg=R>US@<#G%k=JL3}uPSd7kX!25o+ zofP+jca3-*X?07$_i^x+q+vw1y(tAqXMV7KVq)u8W1W9QkG>5~qq?+7>*E&5GS$^C zWpGX4YqMUrjr`QQQU|%6Vd&z@fx{ekC|^+i?+{F8w*+1<%(iI`IFAUZ!dB)Wt{wK- zSH3yr8^!FC6@_PEock-@Dj2w>IuIvJ8$2f+)##CEpB!Wn1T8F-@phlt-+A!I?r!S3 z?x*fY%#x_B&nRa|mHXN?)KSp`ot7fUu@KZj>bX~8_JXv>0}SewB6Iv(nb`88i*%mc zb5=V}#AR_6XL#$qGL_jt>rtL>ng!lu!b^PQojb-p7(^+yj(@%>6Ev`dv9d^mbccl; zJE}vQj5!!)k1JZa2*DHajX9|TGKCI)!x{{BRH$xl?2zhg6~wj0V5dG;I_k9;!30y% z%jE{H(RWjsej37oL_8uP;^H!~^_cP_4hetKaQw4)gw#q)*Wsrpf`e*w%RpJ~)e~(3 zV`GSgTv_Aphgz-GDjiJ7Wky0Jpg&nrWKn@I;T5J&pzwmt``~Cl;L~e}$}Wv`&{4IH z@dksCY6FAar)Y&C5VuVo1?{gf=5?j-gmfC|%B0|3Y0dwshEJJDuO71ZNzFkCsg1M2 zSxd0xMpl-gt1U@GDg2QeVF-Yw1KABFgAu}%McItJ&SSNRHd!uTVqm4b#7^~JZeE`J zXBRb62U>qVFG`cFg@r?+V016^qQzmGKzwt5MkO?ILUE2$$t^*^SdVT9EaWa3^YD_^ zHn8;1l@8N$TTPgGz(nqoZ8S4m*P#sx$OB(0nAF!46Y`k$T|A-)G+O55p(l#ww$pKe zKO8Xh9Zy*sv3imllwjLt4ZlFSlZ&i#o9>7%LCl&>PRi|%Zi2225afj!?aX5y2&2W4JR_`k$$2ax|j@;cma?d@$n+f%Vp(_9^PhN3Q zztZ3PG-ks~{^jiBQ)TW28?>exI=nMZm%!LvgW@ah9>>Dc_?jOGK}tB32=1 zu)eZ43Ue0|HH8)ZicDLpbwr6i1pJWUvPaa_Z)(^I9s7}2Rg~*9a4O~IT5O z+gc%Z?@0yv4aVbTT)7owe*~IrtiwHuA}<8es?h1r2WN&Bg^l`Iir@*MImM)^uOCxm zrxVbPB8ttWY6s`nX4DF^>6N*1gbHECPLPZMFBVFKJISIvkOStb&VNtslo}xLhzq#b zO&JLvXbH0cc{=H)q2Xaf=_8q{qZf#E|aD?f0@pCrZf*wfCfM=ZKDb0;R(vOxbUb*F@Ne+CuHv>%CBt{^X^R^XywUy z)^I&SyfKPq#g(JlB%<2EdBsCqViMAz*d&{5Hk_whqL?cj;(`ByGz_r35kg4CCA2mu7S!jBqEK}*03qOq!$vpd7y+F z%mw!o7D8Pm9t>_~u8!Zoop(e>B7#6a&^Qki`T?w6c9z4WhGe-04m)jIe`!AwWkW zI@~>za^%Ro!#5=W)x&FlZG}M6U*GayEc~jKy4)FvY}{Wkfd-t zDq^ z&b2P@9!j${Ki#~>CAD>-Q2NhJhW-{H_7IZzOEOc9XsRX=$M)wF*pzmbTs&05Q6~T- zN)#>5J>!$yt99PF7idE}w&W)4|Q zYwapmj5ZQ7;hiqhQvpGweHpnbI&1|^dygOQ&gkYRXfv!&w`NXc9mc3W?lb|%o-KJh ziIiVVexSTLe--*8EVxvH1BqT;@euD#r(atu_Q}%34R6sO!@NdiYUrle&*OI}pvDj@ zR1hNj7z0yJZ8-RV?~Y=Hph28afGNo{AY>lMNv2|_c7kngkXMC&D`{HlVeDx4I;ZF1 zW>@7PcE~!ujMNL(4~7b!V$9q&$fRwJh%@HR%y5k~TfYxOM^@i{>2lz_>^QI*O<~-* zsZh0=#HNXa(O}_WT5l=FgFhPD;2{Uau^s&;6Mx;!#fcu_6d&BZkGy_h1R5ILO`>*? zrq;G2u?7TPPOgy&a<7{mnEI`E#&Mw=*6IC&x}i#as8@rEiwuj)+nv;I8#pJRQ6#{B zurZ*XY`~D&h<6_V5h~1BhIYR@xviNq5YpEPn$Cs))hkJ4N#TiCgeujx$v?!c8KyBo zC?#yLh5F|oCXC#8Lo4wDO&yz^R%aGh!0|1Fbh((G5(dhw4lBE$_a%LtNEb-1H(Dc#jXJAJ{ z&DFJvd^3H03cLEIw!`&u<#4B?&bXQ%t}S<5qokr0RY7fzNK-Fnm@SInZ%rhBl-g6z zO@w-bw&BCfYF<{<`3uL+Y5HFo=F%cH5Zpyurh+9a9EllGcu2(KnC0LjS=WMQRAdfH zLnFRG#?A!0=g6_zj^hZN#v^QSR?RBDqXhqJywciedVJ`X9wgi#Vs#zQc?zIhSJ723 zs%-Dr4TL*q@o)~$ix6G3?`_GDagjeU$OGmKwaI&H5;VizLpPo=7KyV{v!6htic%%U zGlxtDfAJkM-)VDDWU|^mg7^#c@=cfHSM1U_bql#oFs0vIL~lW|ly#@anGw_s_CZW@ zk|Hju)o9n%2F=uQ9Fhm3LyQ~6WnqSykUyS;fMu2cl_C_*-2OXknS15LNICUbqcQfy zFWT5L1!jZXdqqpMkoH-X*x!J)>$LlHg_x)?G9j~E6h@0)3A>hsquL1zQCHh;7BgW;LKZPmjT??=!t8>Gb&#FuJBG zBN!A^1%<7mgq)^5%n(PF?1Zr0{&tSMUYcZ}Cumi3xN8V-uNB}@w_G%6sfZ4`N@ih3 zuUR;Uu)nx2rT=ptEUmgK*ACyAtR33i-W9HuyAU^4EHAaTfMfTn$^sO`Og~=<&As)J z7OB|+Wk+R`hFSk{jd!mf4pjV#jk_|Y3nQ9MbUpdQXcQeUZg)=-#bt9@RvqSdUu1h-K)7p&yn(N`$w z>LE}w6OjZ_lD#B@nSLkkD8WE(H zk4Ix1cr2=Xs!pUUv*`&x&k%{i~T3L z8Qyzvq+xDNe=yQ+#X^O>6n)N<bSkOejMj4Tx~Y%~>yRDTt@+Y}&uj|wW3=wBl#`b1%+ z9}*CqH(w(WFcoM8$1zL&++hnLP|i01|C)*C-X9iH2S$tQSzLx;VtVSNn<!g;bPlxCW5^cW@?4)VW<{-Vu)U01(%RRHc7xb<|(6c`aFj z>JSQdw$2u^P$tj{Z{R-^g&)otyrFn+6(fGoNEU^-Q7RPvP1~`b>6Zpj@ zd(QzjO;@3QS&8F^cFF{jK~ zl)tQ2$5-Cw-VPMK?%v)5gYl_x@(rQ?)ye^O4agfSJ;na)S~^FK$%`f*=ZNZVH2f$q zGB@UHJ2Xx$V?aHrB;Wmm(l+o5LiuulJ`s;e?C~M&(C@!C2e_#-tR#bAX1E{|ph34v zvt1>SR%1JyK1>#4NfwAiB-x`@3X<%nXWYz&G@P2{AqphL!r=MDRit`N7Llyek}ih< ztUP2I0UU^y@Lg2O-g{jc(XUz_fGd-vDBox{0{(b2NSaBUR?uau)kjln?z_h%j8;j^ zvsLY9b&~Rzxap7F$R;p^`C)wR)?tfLb4m+D9z8|ve#1Qo@i{5EmC<OYpcU9zy}uZGF6NhZk<4lh zK=0RW)>Uk>`9_CTTZFS7kN|SZkNLztBQ3(jQqFUVA(SR2h4P;F7*CbFW(JnRLL>Oj za^>#+2UYp_HcoiQU{!60=G93{9v034KjCj^S5cx5Fj&3~=C979QMT{x=tXinci;{z z;)(u#9NZ*A$HAAcgmHiN)rMl7P3Lsl=e|QfE@nVV_V1WPJD zz*0Hqm9{XQR+U)RbO?X<`?a>#u+9L{cd0g`G$Jc|OoS%veX^Nl zTlZ4>53~i@F3eEIWFNebB0?>FK)9ohM+-?FyTTyZ^a-gET~=*sIgbp2K}0-Kqrw;p zx z;yC~;DT5ROs?>J9HgGJH{c{eQ5PH!@J+@F6O<ixxg;F>}pt zuSCLx>|U{o65a-21I!>HB0s-?rWrc{OzYNzzCAwP&hX}FW( z%WW}@D~k1MIQvL*+`g2jz<4aoQP!Wpa*LGHLyQR*TL%>-jXs16%P}TRWLJc_j^Gh+Sq!6)PTAOw8 zH7BGEifL_Jq*5!=sHBVOChUZ>^AF+_71_@-u@&FK)@j*Yn{xfaep3XHpn@m_coaV$ z-U?AX`p~qGqU=-^Yo+lgx}_!H6=(^7R7-_G#r?Wb`nJ`ZNhVNPxpHHwC`GG9FT0Zmra&6+E`(@P zBeHT;1YiJ6Dq>a&=cF2vG~w{!^y9W|)%jf~2WPZ-BF#LG{r_69YAo1#;}$aP2yPJ) z2fzRws_Q)z?4SK22^^~hZulDUtuA~z_#CBK_o)eN=B`8Djdrwt{~WC9QK}bGE$0qbVhW&^`b!cZ<$pu>#QKPAZ;AeW$uxWVv0N> zI&(-%aou{@v`3353Nf)_k!FVC-^}6LVfWk)UZ5L79Q)H5pw?uKq=d&0^n%ico2~^+ zt-w*LpI-BKuWJuuSfqU(T@kd0MYPCdy;3&2Eu^DkxR>&4`*Et%gFbGzy}od0&DbLz zqaQ$~Y{GZ5IVc9)5|N&zhso1;Q`L^2qR&HPMJf&(=F-RL==yp=1IrrLjU=5=ZD z@FLfihg4u>X?)Sk3i2XgB#oUFh9?}gh@qhxJ6u;1+0AF2h#K3H^!e!MSwmo(Z_ZRN zIY^%Sffux4@Z`2MibPV1nNcs+^fg=m7GT40F4Z{c`n?uvoeJkewWb0^mp)@wvbPM2#;dV; z7#AGgXBfPNyri&5`bhb{7}^Ycd`LqESmTjl4nYK$~!kr=#=K+bI*H@AcUz8ZEONnFWCl9 zt73~m9OO}ju&)(Ili<|_HG7jdo<4%nZoms08nR;2inD4-r$Rs}BCl{XH#bYeD)2+O zUTk{GC)sko?Fh22Ysq&)xt;viNuNqzsMTmi5j#a?UZ(GCRRV`X5f0i_ ze`qkbw}rcr^c!dVutJrFsrBpm5H>lFtVKBMFmm?{E3l8KHS~}LwJplk^3`xS#*=GRHJxE_^p4j9gh%T z#rEO-_`WfKWo3X;DojzZD>t75vfI=0vbxzl>-+J%^?sAW*(un)+U*4EXW$v<=l=U- ze!GBqnb+I?GM(04<-Y&gO=!O>_$uH%P_wApj6zEOZ$e^WHr^h>Cv&WQUPLcNsjT*{ z@eR^8b@RqC-!_j z{hm5~tadGd1+|)Sh9L%f54zwcq-vYDmD~9gcqwYu+J2?`{qVy*?-EwkE3NlJ|1Fnm z;$vE;`&NGhm#Fru{q-As)=yC_<b;`cZ_=+=~;Cyq8W)R%T#^Af+bLIHCuIEZ0z*Y18EGfa(r66WdrK6u}L z-*#%)56I-=Wp?0m3FkG~Ff-OXMW-#c`)XeyntNaXOuAay0y158 z+n2h(?L}cWevFD?7nb8aA%k}eW}XGQXV6y}e0O=>dtUpeZ*&X>axrU~s?Z@n3}O$K z+Mjy2z;m7XkW=F4o=#JbydU&kN#&+Kj&+0(*9quUJ3=>+nq`6-^i-x;AH~~sqS0N> znUQ^ZFmhM`C!rsl3ZoRJhE>`L1vap&Tcf9j@Vmctd0v8Dp1*@O-uzrU>R;bgTx*;H z1Ac;EI69RZ>MGX+PBQnSbqLbcao%yCWuni|owsgvz6;$i6B{~p`!(Z)-3@N1IXjeu zSpWuE#p+!qj*EY^vZ{A`i7JbbMcGd?nhoktoqylu?GBA4KX%eJn5}@s{UoOL%Xcji zQKLx2sS+HPXi_KK%FTO`%27(rr%sM+aZ0~T$>4wawZJJO z?QJbVjP)jD1+OaNuyPGfk9!k!=GCm*AqshNS?9xW0JjUmZABBg+1?Y-GsY zOp&|CQo75Nx!)k{5TnU6{^l@`c=Jgok3B43d5S-5`aI=PX4^91)+;Obvz_eEza$b? z?2kW)JRZ>+jd~f`>RFy9#obr9WXe?gJ!5t}Ownq1`agXH!FXZr{rwx#kjON6SB$&) zai^-$D1|e5E1v!luh~)D_mreIML3fHMI>${1qXLXV(({nEm7+DLJMPt>iej0=F?F- zZ>Mk=?XhG-6pPoGxptZK9uaZ>?G}?515zzqqu=P>?p6VBk+dvab#dclBj4YKVXjf4 zv{wOM*7d-!#Eh1@t@pqKQ-@XB!?0Jva;UK8K`+a|%KRR#w0D%8;pY-d9h8ARbVj=Q zr7%A2jMl!KNE{=Ca$O=6rdShF7Dof9q_Ymzm$Dqq3@?hgxEJp%fh%h(SjS+}s4WZf zYg6n8n~{b^AGIjXOP&do;L3AMCNNe1%bnt1CKwv`;oHLeD=Gg@JiT554^%;hL+rSO z4zY14(b1GP?*WJI+t?BhXdUNT{^!FO!HV>bdg!oTi=u~z~YGRFB zjk!i^44OgzYnZ23k6zQQ-~G$?<1Lw|#_xRhOG5Ev$m-4Z4`IwKjVuY3BosddHR}3H zxhR#VmL#UZ)|eplYKW6i$VP(k>CyJRuMvSLRwr*2j~N4(yieX?Gu@y(Kt zhxHn&i*1TNTAfa*K9O9_#l@WGdMaECXpcVroQ;#I(XJ^2!BlD=){Ovz*znmd0728! z#?&7NF%HJXKnF#>WToW~IXFuYA1O9+U=laBjzE`5x7!^(B>+SotXFAo;I_+Xe>HPk znAMO#uNnMA@jwxlPRD-C5(-SuInZ(4f*D#OQ>=5{{~HH&GY6y}agwof+_UKTq~2#w z4Zxham;x1bGn0okKxZ0Chf`g>J=N6&SKoiKN8yj+ok(GykNWwSk% zbsoWpKTGE#kCOsmhpQxL8&DNlW4z6U0eP7_kU8ZLwj=V6t$V$xz3jZ0u(=3nQL|}I zGxXbZb7q8dh)A7v+l+Uo2<0$=B4F_Xf+a{G&P&!U@$Od;FU1|GgG0r!cJD$k|Tt6^MGiPb^L7dPVb(-3#)w_ZdH&svdi+;hMriRfsxj z`j+Ps!n_?EVSJUa9)K~XW2J#w_@{lz^hvaP8%=?yrc4OOzr&PY!cykBG$+i?g)6h$ zp+~dV>)k*w$DsGq=r$G0W-8|Ju-1_a`T+Gy7P_WaL^5?dOd$`j{EI&-@o>9?*WE_`NQzv6DRT|e4)?g z`FmLwhovNwZM}i+FvNkPStz{oip8%~t=+shkNs`MV+Sl$wjgS>zzL6`fUd)y=Fjp) z#-}on{LSSv9p&cMe^rIaCMy6-G}0-3HZkkYOu`SVHbhEO-fr9tz1V;>-SFDq=(Yb!y{W|q9v(M%o4PnKZ-gV3fQy0`M3l9*_hI#?sLl7@3<*|?NvHQy z>h|FoYaD#`$Mc5on=7*NRA;rbZYQ#?_oww{xDJ3o^gU$m(X|!LIeoxCQ|l!mzwkj$Z+ zV5fb|rGuIDxVY>j4zCpV?oE*smJi2=J{PFg*KoabudGtVR$H#-{ZFkpx}rFWF%EfY z#@HC!-)!s?Yry!856HMnW|WW?AJ4o}84~?w$E;Eb4V`F8m>@+_wT9GNyfMN>+?R4p zFFGf9U_bVf{vWfWDyIKFy|HMT3!Hl?ak=TKv9H5P2FR}Z5M5sXpeG}dy{cA|mvuw} zbHswPB*XeAdB9tk!9*D%G!0V(2I-YY2B5~Nvb+g?RO-jX#VtAy(iDAK=;xD-r4mGC zDU&y|<r*|>_|5mEBRL9LwqG_QjVYZz zPE+9iC)WmF1=fJqCC9*71F;;KWSWut5V!zMc0b(5pl*6AWg5`(pP8%1Bz}nW*onKk zDx3ypFq{%?#!+w~E^@Ty#45U{ew^~Cp1ku)lVl|y7eUXS={#^KQsRUqImTIHi6|!6 zaCC_P8XK`V1SwUO>EYDp_#pTS!7p_PCMY{=O6xFrD$uJ|@M`>eS_>5g>cy6az6|wi zD?v6A^mq>Z)$dx*&H+~p>F4Sz(vD;~qHPUerQI3>YDWYOkFJ!YOpM09`j*JW7<=fv z@j#3s8BS7wQ7lcAPepW3gH8@LkD>kGklm7ZreJ99Zn1ka`+nMb7-sHTYWrrL%Ew%o*8p2L`&Iv5=a6t#cd;KvHvG(dM-)svsgpTM1R zJ~m;TGV-IBV=ZyIZ6RxL4%(B!-6@gSY z2+o8ynu^q45#0k*-q3?21&V=dEZZ?~A!RYL+@JdliqamdBaH|1_T?OzXzbKZCx4v= zcs|gyzab%8Ed;t56c4Ai%MN|EfV)s8L?La5Upblfp)PIL9RGV`U^L!tFR%n&V-WW&kz(#8h8E#!`{7D*I?7TW( z&h5s{uihMfcf1q`{hqG&@kG^)(KYqEtHo6nLR4J5&F3{yG673CJwNyPjaZpSGS~x0D7qel1b))pfmQ z@h`AnZtMT~Vg!HpqrN40)7xNs!)zZ=DC~cAKQ9<%w*q!=uLjVR-@ZP-ymO%Atqxn*r8#P$tE&NJuJO_!TM@m4?o9s-PJd1 zQN60qnT2;jXx)eH8BByCbt%&{LKyQ)>F*n7NmIPLrqVbKCk!n}e@c`5p@){L{?DgS zt-7Is0)3$-Y21GGUu^XxO1{a?z3nCpnXmmX*Bt+Q2`_=NjC)NR5gb>WEke(gm~LRH zBQ*r?kW*JBk%tt=RxSZERP;JpbaR;SG{mVDigeCYZqAaCuJl4}pYQahzRObWuR}T5-@vcp>Ql{D=08_ryZYTu=Bupk zE8g0`l@voRTs5U(&qiVDjI0(*jL0G{eNY+}$7nOgcGw>PKVj`n0 z&X$}_INh5{ad@sqtyQBJmdHySI-ZciI_FU*iWBNV!*rqqu`8A^Z(D3Ozf}lcNDl_M z{4P4(>^Q~fF}|YD;kdujbne<~vo+<&fuHG!0~eRaJf%<;gLvx{!OrM;H)ng0z`}#l zOgEQqxye~&!wC9k`NkGb*E5Hwt@gC25*2Or*x9V*iFJ@o&E+Vb=Fbxh*8sk_(77&| zKeiQ$f32f7aQ&Ok_uLXuDIs`gC)m0(tF1*_9sfq_qwW|62C(wZeb5~~ky40c1=@HfCNv#ARVW(TuSH5ytBOOtQzi#1ovH9|F6RKb z1&=J^V@r(uQd7x!d`->m#A(E&MQ)%NlUWAsQf1x^IxVspV$DCYMXpV?`neH8waI6o z#JGl^J-^TnjRAd9cK$z2Gh*pfU4&di zkOHh~I1tP1!d=K+&6f+eXy2|>LA+vidkmB( zym+EKqL_1kkZFls{hBrf>3Y-n%du?>w`%0rCX%OcUJ2;}12cS5T#LxpK=HPB6O)#o z-hyRbX(|>v<8YuQBQvKU*$!f#chK_^#*&Jgd`x(Ri?Wy5+)w%$0j0*6m1jOZUfIG4xh-lm=t zudAAT=9*OwG(3o#lzYi+Ica`KwB0cm;U`_+ejs3VZaWk|Y_Dhg~USKwdw$X*%;t}m1 zr>FyaOUX!@NZHGHXxwuF=DAs>)ClC