├── .github └── workflows │ └── zip_flows.yml ├── .gitignore ├── LICENSE ├── README.md ├── _worker.js ├── _worker.js.zip ├── _worker.src.js ├── ipHK.txt ├── ipJP.txt ├── ipUS.txt ├── ipUrl.txt ├── ipv4.csv ├── ipv4.txt └── proxyip.txt /.github/workflows/zip_flows.yml: -------------------------------------------------------------------------------- 1 | name: zip_flows # 工作流程的名称 2 | 3 | on: 4 | workflow_dispatch: # 手动触发 5 | push: 6 | paths: 7 | - '_worker.js' # 当 _worker.js 文件发生变动时触发 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | package-and-commit: 14 | runs-on: ubuntu-latest # 运行环境,这里使用最新版本的 Ubuntu 15 | steps: 16 | - name: Checkout Repository # 检出代码 17 | uses: actions/checkout@v2 18 | 19 | - name: Zip the worker file # 将 _worker.js 文件打包成 worker.js.zip 20 | run: zip _worker.js.zip _worker.js 21 | 22 | - name: Commit and push the packaged file # 提交并推送打包后的文件 23 | uses: EndBug/add-and-commit@v7 24 | with: 25 | add: '_worker.js.zip' 26 | message: 'Automatically package and commit _worker.js.zip' 27 | author_name: github-actions[bot] 28 | author_email: actions[bot]@users.noreply.github.com 29 | token: ${{ secrets.GH_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [am-cf-tunnel](https://github.com/amclubs/am-cf-tunnel) 2 | 这是一个基于 Cloudflare Workers 和 Pages平台的脚本,在原版的基础上修改了显示 VLESS 配置信息转换为订阅内容。使用该脚本,你可以方便地将 VLESS 配置信息使用在线配置转换到 Clash、 Singbox 、Quantumult X等工具中订阅使用。Cloudflare Workers 和 Pages 生成VLESS节点,实现订阅连接可以一键订阅节点。[最新视频教程](https://youtu.be/emEBm8Gw2wI) 3 | 4 | # 5 | ▶️ **新人[YouTube](https://youtube.com/@am_clubs?sub_confirmation=1)** 需要您的支持,请务必帮我**点赞**、**关注**、**打开小铃铛**,***十分感谢!!!*** ✅ 6 |
🎁请 **follow** 我的[GitHub](https://github.com/amclubs)、给我所有项目一个 **Star** 星星(拜托了)!你的支持是我不断前进的动力! 💖 7 |
✅**解锁更多技能** [加入TG群【am_clubs】](https://t.me/am_clubs)、[YouTube频道【@am_clubs】](https://youtube.com/@am_clubs?sub_confirmation=1)、[【博客(国内)】](https://amclubss.com)、[【博客(国际)】](https://amclubs.blogspot.com) 8 |
✅点击观看教程[CLoudflare免费节点](https://www.youtube.com/playlist?list=PLGVQi7TjHKXbrY0Pk8gm3T7m8MZ-InquF) | [VPS搭建节点](https://www.youtube.com/playlist?list=PLGVQi7TjHKXaVlrHP9Du61CaEThYCQaiY) | [获取免费域名](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR) | [免费VPN](https://www.youtube.com/playlist?list=PLGVQi7TjHKXY7V2JF-ShRSVwGANlZULdk) | [IPTV源](https://www.youtube.com/playlist?list=PLGVQi7TjHKXbkozDYVsDRJhbnNaEOC76w) | [Mac和Win工具](https://www.youtube.com/playlist?list=PLGVQi7TjHKXYBWu65yP8E08HxAu9LbCWm) | [AI分享](https://www.youtube.com/playlist?list=PLGVQi7TjHKXaodkM-mS-2Nwggwc5wRjqY) 9 | 10 | # 推荐视频教程 11 | - [Error 1101 和 522 报错解决方案教程](https://youtu.be/4fcyJjstFdg) | [优选IP和优选反代IP视频教程](https://youtu.be/pKrlfRRB0gU) | [解决常见订阅测试-1问题教程](https://youtu.be/kYQxV1G-ePw) 12 | - [VLESS免费节点部署教程](https://youtu.be/dPH63nITA0M) | [Trojan免费节点部署教程](https://youtu.be/uh27CVVi6HA) | [从入门到精通免费部署教程](https://youtu.be/ag12Rpc9KP4) | [聚合节点订阅教程](https://youtu.be/YBO2hf96150) 13 | - [GitHub私有库存储优选IP文教程](https://youtu.be/vX3U3FuuTT8) | [CF免费KV存储优选IP文件教程](https://youtu.be/dzxezRV1v-o) [获取免费域名教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR) | [获取CF自家域名无限节点](https://youtu.be/novrPiMsK70) 14 | - [🔥amclubs-cfnat自动优先IP视频教程(Win桌面版)](https://youtu.be/-a6NJ6vPSu4) | [🔥Linux & openwrt软路由版](https://youtu.be/ZC6fxZwPaiM) | [🔥Mac版](https://youtu.be/gf6gncc2yEE) | [🔥安卓(Android)手机版](https://youtu.be/7yamDM38MFw) | [🔥docker版](https://youtu.be/gRnNwoeUQKU) 15 | 16 | ### CF端口类型: 17 | ~~~ 18 | HTTP:80,8080,8880,2052,2082,2086,2095 19 | HTTPS:443,2053,2083,2087,2096,8443 20 | ~~~ 21 | 22 | ## 一、Workers 部署方法 [视频教程](https://www.youtube.com/watch?v=wgeM9XvZ5RA&t=195s) 23 |
24 | 点击展开/收起 25 | 26 | 1. 部署 Cloudflare Worker: 27 | - 在 Cloudflare Worker 控制台中创建一个新的 Worker。 28 | - 将 [_worker.js](https://github.com/amclubs/am-cf-tunnel/blob/main/_worker.js) 的内容粘贴到 Worker 编辑器中。 29 | 2. 给 workers绑定 自定义域: [免费域名申请教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR) 30 | - 在 workers控制台的 `设置` 选项卡 -> 点击 `域和路由` -> 右方点击 -> `添加` -> 选择 `自定义域`。 31 | - 填入你已转入 CloudFlare 域名 (amclubss.com) 解析服务的次级域名,例如:`vless.amclubss.com`后 点击 `添加域`,等待证书生效即可。 32 | 3. 给UUID设置KV存储桶(可选项,推荐设置): 33 | - 在 CloudFlare主页的左边菜单的 ` 存储和数据库` 选项卡 -> 展开选择点击 `KV` -> 右方点击 -> `创建` -> 填入 `命名空间名称`(此名称自己命名) 后 -> 点击 `添加`。(此步已有可忽略) 34 | - 在 workers控制台的 `设置` 选项卡 -> 点击 `绑定` -> 右方点击 -> `添加` -> 选择 `KV 命名空间` -> 变量名称 填入 `amclubs`(此名称固定不能变) -> KV 命名空间 选择 在上面创建的 `命名空间名称`后 -> 右下方点击 `部署`。 35 | 4. 访问订阅内容: 36 | - 访问 `https://[YOUR-WORKERS-URL]/[UUID]` 即可获取订阅内容(默认UUID是:d0298536-d670-4045-bbb1-ddd5ea68683e)。 37 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?sub` 就是你的通用自适应订阅地址(Quantumult X、Clash、singbox、小火箭、v2rayN、v2rayU、surge、PassWall、SSR+、Karing等)。 38 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?base64` Base64订阅格式,适用PassWall,SSR+等。 39 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?clash` Clash订阅格式,适用OpenClash等。 40 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?singbox` singbox订阅格式,适用singbox等。 41 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?sub&IP_URL=https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipUrl.txt` 自动义变量等参数。 42 | 5. 修改默认UUID变量,使用KV存储桶(可选项,推荐修改,防止别人用你节点): 43 | - 访问 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e/ui` 即可进入修改UUID页面 44 | - 在UUID页面UUID项 -> 填入 `新的UUID` 后,[在线获取UUID](https://1024tools.com/uuid) -> 点击 `Save`。 45 | - 保存成功后,原UUID已作废不能访问,用新UUID访问 `https://vless.amclubss.com/新的UUID` 即可获取订阅内容。 46 | 47 |
48 | 49 | ## 二、Pages 上传 部署方法 **最佳推荐!!!** [视频教程](https://www.youtube.com/watch?v=wgeM9XvZ5RA&t=1203s) 50 |
51 | 点击展开/收起 52 | 53 | 1. 部署 Cloudflare Pages: 54 | - 下载 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/_worker.js.zip) 文件,并点上 Star !!! 55 | - 在 Cloudflare Pages 控制台中选择 `上传资产`后,为你的项目取名后点击 `创建项目`,然后上传你下载好的 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/_worker.js.zip) 文件后点击 `部署站点`。 56 | 2. 给 Pages绑定 CNAME自定义域:[无域名绑定Cloudflare部署视频教程]->[免费域名教程1](https://youtu.be/wHJ6TJiCF0s) [免费域名教程2](https://youtu.be/yEF1YoLVmig) [免费域名教程3](https://www.youtube.com/watch?v=XS0EgqckUKo&t=320s) 57 | - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。 58 | - 填入你的自定义次级域名,注意不要使用你的根域名,例如: 59 | 您分配到的域名是 `amclubss.com`,则添加自定义域填入 `vless.amclubss.com`即可,点击 `激活域`即可。 60 | 3. 给UUID设置KV存储桶(可选项,推荐设置): 61 | - 在 CloudFlare主页的左边菜单的 ` 存储和数据库` 选项卡 -> 展开选择点击 `KV` -> 右方点击 -> `创建` -> 填入 `命名空间名称`(此名称自己命名) 后 -> 点击 `添加`。(此步已有可忽略) 62 | - 在 workers控制台的 `设置` 选项卡 -> 点击 `绑定` -> 右方点击 -> `添加` -> 选择 `KV 命名空间` -> 变量名称 填入 `amclubs`(此名称固定不能变) -> KV 命名空间 选择 在上面创建的 `命名空间名称`后 -> 右下方点击 `部署`。 63 | - 在 `设置` 选项卡,在右上角点击 `创建部署` 后,重新上传 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/_worker.js.zip) 文件后点击 `保存并部署` 即可。 64 | 4. 访问订阅内容: 65 | - 访问 `https://[YOUR-WORKERS-URL]/[UUID]` 即可获取订阅内容(默认UUID是:d0298536-d670-4045-bbb1-ddd5ea68683e)。 66 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?sub` 就是你的通用自适应订阅地址(Quantumult X、Clash、singbox、小火箭、v2rayN、v2rayU、surge、PassWall、SSR+、Karing等)。 67 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?base64` Base64订阅格式,适用PassWall,SSR+等。 68 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?clash` Clash订阅格式,适用OpenClash等。 69 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?singbox` singbox订阅格式,适用singbox等。 70 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?sub&IP_URL=https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipUrl.txt` 自动义变量等参数。 71 | 5. 修改默认UUID变量,使用KV存储桶(可选项,推荐修改,防止别人用你节点): 72 | - 访问 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e/ui` 即可进入修改UUID页面 73 | - 在UUID页面UUID项 -> 填入 `新的UUID` 后,[在线获取UUID](https://1024tools.com/uuid) -> 点击 `Save`。 74 | - 保存成功后,原UUID已作废不能访问,用新UUID访问 `https://vless.amclubss.com/新的UUID` 即可获取订阅内容。 75 | 76 |
77 | 78 | ## 三、Pages GitHub 部署方法 [视频教程](https://www.youtube.com/watch?v=dPH63nITA0M&t=654s) 79 |
80 | 点击展开/收起 81 | 82 | 1. 部署 Cloudflare Pages: 83 | - 在 Github 上先 Fork 本项目,并点上 Star !!! 84 | - 在 Cloudflare Pages 控制台中选择 `连接到 Git`后,选中 `am-cf-tunnel`项目后点击 `开始设置`。 85 | - 在 `设置构建和部署`页面下方,后点击 `保存并部署`即可。 86 | 2. 给 Pages绑定 CNAME自定义域:[无域名绑定Cloudflare部署视频教程]->[免费域名教程1](https://youtu.be/wHJ6TJiCF0s) [免费域名教程2](https://youtu.be/yEF1YoLVmig) [免费域名教程3](https://www.youtube.com/watch?v=XS0EgqckUKo&t=320s) 87 | - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。 88 | - 填入你的自定义次级域名,注意不要使用你的根域名,例如: 89 | 您分配到的域名是 `amclubss.com`,则添加自定义域填入 `vless.amclubss.com`即可,点击 `激活域`即可。 90 | 3. 给UUID设置KV存储桶(可选项,推荐设置): 91 | - 在 CloudFlare主页的左边菜单的 ` 存储和数据库` 选项卡 -> 展开选择点击 `KV` -> 右方点击 -> `创建` -> 填入 `命名空间名称`(此名称自己命名) 后 -> 点击 `添加`。(此步已有可忽略) 92 | - 在 workers控制台的 `设置` 选项卡 -> 点击 `绑定` -> 右方点击 -> `添加` -> 选择 `KV 命名空间` -> 变量名称 填入 `amclubs`(此名称固定不能变) -> KV 命名空间 选择 在上面创建的 `命名空间名称`后 -> 右下方点击 `部署`。 93 | - 在 `设置` 选项卡,在右上角点击 `创建部署` 后,重新选择 `部署` 即可。 94 | 4. 访问订阅内容: 95 | - 访问 `https://[YOUR-WORKERS-URL]/[UUID]` 即可获取订阅内容(默认UUID是:d0298536-d670-4045-bbb1-ddd5ea68683e)。 96 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?sub` 就是你的通用自适应订阅地址(Quantumult X、Clash、singbox、小火箭、v2rayN、v2rayU、surge、PassWall、SSR+、Karing等)。 97 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?base64` Base64订阅格式,适用PassWall,SSR+等。 98 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?clash` Clash订阅格式,适用OpenClash等。 99 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?singbox` singbox订阅格式,适用singbox等。 100 | - 例如 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e?sub&IP_URL=https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipUrl.txt` 自动义变量等参数。 101 | 5. 修改默认UUID变量,使用KV存储桶(可选项,推荐修改,防止别人用你节点): 102 | - 访问 `https://vless.amclubss.com/d0298536-d670-4045-bbb1-ddd5ea68683e/ui` 即可进入修改UUID页面 103 | - 在UUID页面UUID项 -> 填入 `新的UUID` 后,[在线获取UUID](https://1024tools.com/uuid) -> 点击 `Save`。 104 | - 保存成功后,原UUID已作废不能访问,用新UUID访问 `https://vless.amclubss.com/新的UUID` 即可获取订阅内容。 105 | 106 |
107 | 108 | ## 四、变量说明 [视频教程](https://www.youtube.com/watch?v=ag12Rpc9KP4&t=739s) 109 | | 变量名 | 示例 | 必填 | 备注 | YT | 110 | |-----|-----|-----|-----|-----| 111 | | UUID | d0298536-d670-4045-bbb1-ddd5ea68683e(默认) |✅| 支持Cloudflare的KV存储桶设置 [在线获取UUID](https://1024tools.com/uuid) 如果是Trojan节点的变量是:PASSWORD | | 112 | | PROT_TYPE | 默认空 |❌| 默认空,就是生成vless和trojan节点,vless(只生成vless节点),trojan(只生成trojan节点) | [教程](https://www.youtube.com/watch?v=emEBm8Gw2wI&t=922s) | 113 | | IP_URL | [https://raw.github.../ipUrl.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipUrl.txt) |❌| (推荐)优选(ipv4、ipv6、域名、API)地址(支持多个之间`,`或 换行 作间隔),支持文件连接后里带PROXYIP参数,可以实现不同区域优先IP使用不同的PROXYIP固定区域,解决IP乱跳问题 | [教程](https://www.youtube.com/watch?v=4fcyJjstFdg&t=349s)| 114 | | IP_URL_TXT | [https://raw.github.../ipv4.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt) |❌| (不推荐)优选ipv4、ipv6、域名、API地址(支持多个之间`,`或 换行 作间隔) |[教程](https://youtu.be/dzxezRV1v-o) [教程](https://youtu.be/vX3U3FuuTT8)| 115 | | IP_URL_CSV | [https://raw.github.../ipv4.csv](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.csv) |❌| (不推荐)优选ipv4/6的IP测速结果(支持多元素, 元素之间使用`,`作间隔) |[教程](https://youtu.be/vX3U3FuuTT8)| 116 | | IP_LOCAL | `icook.hk:2053#官方优选域名` |❌| (不推荐)本地优选域名/优选IP(支持多元素之间`,`或 换行 作间隔) | | 117 | | PROXYIP | proxyip.amclubs.kozow.com

[https://raw.github.../proxyip.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/proxyip.txt) |❌| 访问CloudFlare的CDN代理节点(支持多PROXYIP, PROXYIP之间使用`,`或 换行 作间隔),支持端口设置默认443 如: proxyip.amclubs.kozow.com:2053 ,支持远程txt或csv文件| [教程](https://youtu.be/pKrlfRRB0gU) | 118 | | SOCKS5 | user:password@127.0.0.1:1080 |❌| 优先作为访问CFCDN站点的SOCKS5代理 | [教程](https://youtu.be/Bw82BH_ecC4) | 119 | | DNS_RESOLVER_URL | https://cloudflare-dns.com/dns-query |❌| DNS解析获取作用,小白勿用 | | 120 | | NO_TLS | true/false |❌| 默认false,是否开启TLS系列端口,只有workers部署才可以使非用TLS系列端口 | | 121 | | SL | 5 |❌| `CSV`文件里的测速结果满足速度下限 || 122 | | SUB_CONFIG | [https://raw.github.../ACL4SSR_Online_Mini.ini](https://raw.githubusercontent.com/amclubs/ACL4SSR/main/Clash/config/ACL4SSR_Online_Full_MultiMode.ini) |❌| clash、singbox等 订阅转换配置文件 || 123 | | SUB_CONVERTER | url.v1.mk |❌| clash、singbox等 订阅转换后端的api地址 || 124 | | SUB_NAME | AM科技 |❌ | 订阅名称 || 125 | | CF_EMAIL | test@gmail.com |❌| CF账户邮箱(要和`CF_KEY`同时填才生效, 订阅信息将显示请求使用量, 小白别用) || 126 | | CF_KEY | c6a944b5c9c18c235288bced8b85e |❌| CF账户Global API Key(要和`CF_EMAIL`同时填才生效, 订阅信息将显示请求使用量, 小白别用) || 127 | | TG_TOKEN | 6823456:XXXXXXX0qExVUhHDAbXXXqWXgBA |❌| 发送TG通知的机器人token || 128 | | TG_ID | 6946912345 |❌ | 接收TG通知的账户数字ID || 129 | 130 | 131 | ## 五、已适配订阅工具 [点击进入视频教程](https://youtu.be/xGOL57cmvaw) [点进进入karing视频教程](https://youtu.be/M3vLLBWfuFg) 132 | - Mac(苹果电脑) 133 | - [v2rayU](https://github.com/yanue/V2rayU/releases) | [clash-verge-rev](https://github.com/clash-verge-rev/clash-verge-rev/releases) | [Quantumult X](https://apps.apple.com/us/app/quantumult-x/id1443988620) | [小火箭](https://apps.apple.com/us/app/shadowrocket/id932747118) | [surge](https://apps.apple.com/us/app/surge-5/id1442620678) | [karing](https://karing.app/download) | [sing-box](https://github.com/SagerNet/sing-box/releases) | [Clash Nyanpasu](https://github.com/keiko233/clash-nyanpasu/releases) | [openclash](https://github.com/vernesong/OpenClash/releases) | [Hiddify](https://github.com/hiddify/hiddify-next/releases) 134 | 135 | - Win(win系统电脑) 136 | - [v2rayN](https://github.com/2dust/v2rayN/releases) | [clash-verge-rev](https://github.com/clash-verge-rev/clash-verge-rev/releases) | [sing-box](https://github.com/SagerNet/sing-box/releases) | [Clash Nyanpasu](https://github.com/keiko233/clash-nyanpasu/releases) | [openclash](https://github.com/vernesong/OpenClash/releases) | [karing](https://karing.app/download) | [Hiddify](https://github.com/hiddify/hiddify-next/releases) 137 | 138 | - IOS(苹果手机) 139 | - [clash-verge-rev](https://github.com/clash-verge-rev/clash-verge-rev/releases) | [Quantumult X](https://apps.apple.com/us/app/quantumult-x/id1443988620) | [小火箭](https://apps.apple.com/us/app/shadowrocket/id932747118) | [surge](https://apps.apple.com/us/app/surge-5/id1442620678) | [sing-box](https://github.com/SagerNet/sing-box/releases) | [Clash Nyanpasu](https://github.com/keiko233/clash-nyanpasu/releases) | [karing](https://karing.app/download) | [Hiddify](https://github.com/hiddify/hiddify-next/releases) 140 | 141 | - Android(安卓手机) 142 | - [v2rayNG](https://github.com/2dust/v2rayNG/releases) | [clash-verge-rev](https://github.com/clash-verge-rev/clash-verge-rev/releases) | [sing-box](https://github.com/SagerNet/sing-box/releases) | [Clash Nyanpasu](https://github.com/keiko233/clash-nyanpasu/releases) | [karing](https://karing.app/download) | [Hiddify](https://github.com/hiddify/hiddify-next/releases) 143 | 144 | - 软路由 145 | - [openclash(clash.meta)](https://github.com/vernesong/OpenClash/releases) 146 | 147 | # 感谢 148 | [3Kmfi6HP](https://github.com/3Kmfi6HP/EDtunnel)、[ACL4SSR](https://github.com/ACL4SSR/ACL4SSR/tree/master/Clash/config) 149 | 150 | # 151 |
152 |
[点击展开] 赞赏支持 ~🧧 153 | *我非常感谢您的赞赏和支持,它们将极大地激励我继续创新,持续产生有价值的工作。* 154 | 155 | - **USDT-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD` 156 | - **TRX-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD` 157 | 158 |
159 |
160 | TRC10/TRC20扫码支付 161 |
162 |
163 |
164 | 165 | # 166 | 免责声明: 167 | - 1、该项目设计和开发仅供学习、研究和安全测试目的。请于下载后 24 小时内删除, 不得用作任何商业用途, 文字、数据及图片均有所属版权, 如转载须注明来源。 168 | - 2、使用本程序必循遵守部署服务器所在地区的法律、所在国家和用户所在国家的法律法规。对任何人或团体使用该项目时产生的任何后果由使用者承担。 169 | - 3、作者不对使用该项目可能引起的任何直接或间接损害负责。作者保留随时更新免责声明的权利,且不另行通知。 170 | 171 | -------------------------------------------------------------------------------- /_worker.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-cf-tunnel/c5822aaa547a3389251288218e7306c5dd495c89/_worker.js.zip -------------------------------------------------------------------------------- /_worker.src.js: -------------------------------------------------------------------------------- 1 | /** 2 | * YouTube Channel: https://youtube.com/@am_clubs 3 | * Telegram Group: https://t.me/am_clubs 4 | * GitHub Repository: https://github.com/amclubs 5 | * Personal Blog: https://amclubs.blogspot.com 6 | * Personal Blog: https://amclubss.com 7 | */ 8 | 9 | // @ts-ignore 10 | import { connect } from 'cloudflare:sockets'; 11 | 12 | // Generate your own UUID using the following command in PowerShell: 13 | // Powershell -NoExit -Command "[guid]::NewGuid()" 14 | let userID = 'd0298536-d670-4045-bbb1-ddd5ea68683e'; 15 | let kvUUID; 16 | 17 | // Proxy IPs to choose from 18 | let proxyIPs = [ 19 | 'proxyip.amclubs.camdvr.org', 20 | 'proxyip.amclubs.kozow.com' 21 | ]; 22 | // Randomly select a proxy IP from the list 23 | let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; 24 | let proxyPort = 443; 25 | let proxyIpTxt = atob('aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2FtY2x1YnMvYW0tY2YtdHVubmVsL21haW4vcHJveHlpcC50eHQ='); 26 | 27 | // Setting the socks5 will ignore proxyIP 28 | // Example: user:pass@host:port or host:port 29 | let socks5 = ''; 30 | let socks5Enable = false; 31 | let parsedSocks5 = {}; 32 | 33 | // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query 34 | // DNS-over-HTTPS URL 35 | let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; 36 | 37 | // Preferred address API interface 38 | const defaultIpUrlTxt = atob('aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2FtY2x1YnMvYW0tY2YtdHVubmVsL21haW4vaXB2NC50eHQ='); 39 | let randomNum = 25; 40 | let ipUrl = [ 41 | 42 | ]; 43 | let ipUrlTxt = [ 44 | defaultIpUrlTxt 45 | ]; 46 | let ipUrlCsv = [ 47 | // atob('aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2FtY2x1YnMvYW0tY2YtdHVubmVsL21haW4vaXB2NC5jc3Y=') 48 | ]; 49 | // Preferred addresses with optional TLS subscription 50 | let ipLocal = [ 51 | 'visa.cn:443#youtube.com/@am_clubs AM科技(订阅频道观看教程)', 52 | 'icook.hk#t.me/am_clubs TG群(加入解锁免费节点)', 53 | 'time.is#github.com/amclubs GitHub仓库(关注查看新功能)', 54 | '127.0.0.1:1234#amclubss.com (博客)cfnat' 55 | ]; 56 | let noTLS = 'false'; 57 | let sl = 5; 58 | 59 | let tagName = atob('YW1jbHVicw=='); 60 | let subUpdateTime = 6; // Subscription update time in hours 61 | let timestamp = 4102329600000; // Timestamp for the end date (2099-12-31) 62 | let total = 99 * 1125899906842624; // PB (perhaps referring to bandwidth or total entries) 63 | let download = Math.floor(Math.random() * 1099511627776); 64 | let upload = download; 65 | 66 | // Network protocol type 67 | let network = 'ws'; // WebSocket 68 | 69 | // Fake UUID and hostname for configuration generation 70 | let fakeUserID; 71 | let fakeHostName; 72 | 73 | // Subscription and conversion details 74 | let subProtocol = 'https'; 75 | let subConverter = atob('dXJsLnYxLm1r'); // Subscription conversion backend using Sheep's function 76 | let subConfig = atob('aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2FtY2x1YnMvQUNMNFNTUi9tYWluL0NsYXNoL2NvbmZpZy9BQ0w0U1NSX09ubGluZV9GdWxsX011bHRpTW9kZS5pbmk='); // Subscription profile 77 | let fileName = atob('QU0lRTclQTclOTElRTYlOEElODA='); //'AM%E7%A7%91%E6%8A%80'; 78 | let isBase64 = true; 79 | 80 | let botToken = ''; 81 | let chatID = ''; 82 | 83 | let projectName = atob('YW1jbHVicy9hbS1jZi10dW5uZWw'); 84 | let ytName = atob('aHR0cHM6Ly95b3V0dWJlLmNvbS9AYW1fY2x1YnM='); 85 | const httpPattern = /^http(s)?:\/\/.+/; 86 | 87 | const protTypeBase64 = 'ZG14bGMzTT0='; 88 | const protTypeBase64Tro = 'ZEhKdmFtRnU='; 89 | 90 | if (!isValidUUID(userID)) { 91 | throw new Error('uuid is invalid'); 92 | } 93 | 94 | export default { 95 | /** 96 | * @param {import("@cloudflare/workers-types").Request} request 97 | * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env 98 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx 99 | * @returns {Promise} 100 | */ 101 | async fetch(request, env, ctx) { 102 | try { 103 | let { 104 | UUID, 105 | PROXYIP, 106 | SOCKS5, 107 | DNS_RESOLVER_URL, 108 | IP_LOCAL, 109 | IP_URL, 110 | IP_URL_TXT, 111 | IP_URL_CSV, 112 | NO_TLS, 113 | SL, 114 | SUB_CONFIG, 115 | SUB_CONVERTER, 116 | SUB_NAME, 117 | CF_EMAIL, 118 | CF_KEY, 119 | CF_ID = 0, 120 | TG_TOKEN, 121 | TG_ID, 122 | //兼容 123 | ADDRESSESAPI, 124 | } = env; 125 | const kvCheckResponse = await checkKVNamespaceBinding(env); 126 | if (!kvCheckResponse) { 127 | kvUUID = await getKVData(env); 128 | // console.log(`kvUUID: ${kvUUID} \n `); 129 | } 130 | const url = new URL(request.url); 131 | //兼容双协议 132 | userID = (kvUUID || UUID || userID).toLowerCase(); 133 | 134 | PROXYIP = url.searchParams.get('PROXYIP') || PROXYIP; 135 | if (PROXYIP) { 136 | if (httpPattern.test(PROXYIP)) { 137 | let proxyIpTxt = await addIpText(PROXYIP); 138 | let ipUrlTxtAndCsv; 139 | if (PROXYIP.endsWith('.csv')) { 140 | ipUrlTxtAndCsv = await getIpUrlTxtAndCsv(noTLS, null, proxyIpTxt); 141 | 142 | } else { 143 | ipUrlTxtAndCsv = await getIpUrlTxtAndCsv(noTLS, proxyIpTxt, null); 144 | } 145 | const uniqueIpTxt = [...new Set([...ipUrlTxtAndCsv.txt, ...ipUrlTxtAndCsv.csv])]; 146 | proxyIP = uniqueIpTxt[Math.floor(Math.random() * uniqueIpTxt.length)]; 147 | } else { 148 | proxyIPs = await addIpText(PROXYIP); 149 | proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; 150 | } 151 | } else { 152 | let proxyIpTxts = await addIpText(proxyIpTxt); 153 | let ipUrlTxtAndCsv = await getIpUrlTxtAndCsv(noTLS, proxyIpTxts, null); 154 | let updatedIps = ipUrlTxtAndCsv.txt.map(ip => `${tagName}${download}.${ip}`); 155 | const uniqueIpTxt = [...new Set([...updatedIps, ...proxyIPs])]; 156 | proxyIP = uniqueIpTxt[Math.floor(Math.random() * uniqueIpTxt.length)]; 157 | } 158 | const [ip, port] = proxyIP.split(':'); 159 | proxyIP = ip; 160 | proxyPort = port || proxyPort; 161 | 162 | socks5 = url.searchParams.get('SOCKS5') || SOCKS5 || socks5; 163 | parsedSocks5 = await parseSocks5FromUrl(socks5, url); 164 | if (parsedSocks5) { 165 | socks5Enable = true; 166 | } 167 | 168 | dohURL = url.searchParams.get('DNS_RESOLVER_URL') || DNS_RESOLVER_URL || dohURL; 169 | 170 | IP_LOCAL = url.searchParams.get('IP_LOCAL') || IP_LOCAL; 171 | if (IP_LOCAL) { 172 | ipLocal = await addIpText(IP_LOCAL); 173 | } 174 | const newCsvUrls = []; 175 | const newTxtUrls = []; 176 | IP_URL = url.searchParams.get('IP_URL') || IP_URL; 177 | if (IP_URL) { 178 | ipUrlTxt = []; 179 | ipUrl = await addIpText(IP_URL); 180 | ipUrl = await getIpUrlTxtToArry(ipUrl); 181 | ipUrl.forEach(url => { 182 | if (getFileType(url) === 'csv') { 183 | newCsvUrls.push(url); 184 | } else { 185 | newTxtUrls.push(url); 186 | } 187 | }); 188 | } 189 | //兼容旧的,如果有IP_URL_TXT新的则不用旧的 190 | ADDRESSESAPI = url.searchParams.get('ADDRESSESAPI') || ADDRESSESAPI; 191 | IP_URL_TXT = url.searchParams.get('IP_URL_TXT') || IP_URL_TXT; 192 | IP_URL_CSV = url.searchParams.get('IP_URL_CSV') || IP_URL_CSV; 193 | if (ADDRESSESAPI) { 194 | ipUrlTxt = await addIpText(ADDRESSESAPI); 195 | } 196 | if (IP_URL_TXT) { 197 | ipUrlTxt = await addIpText(IP_URL_TXT); 198 | } 199 | if (IP_URL_CSV) { 200 | ipUrlCsv = await addIpText(IP_URL_CSV); 201 | } 202 | ipUrlCsv = [...new Set([...ipUrlCsv, ...newCsvUrls])]; 203 | ipUrlTxt = [...new Set([...ipUrlTxt, ...newTxtUrls])]; 204 | 205 | noTLS = url.searchParams.get('NO_TLS') || NO_TLS || noTLS; 206 | sl = url.searchParams.get('SL') || SL || sl; 207 | subConfig = url.searchParams.get('SUB_CONFIG') || SUB_CONFIG || subConfig; 208 | subConverter = url.searchParams.get('SUB_CONVERTER') || SUB_CONVERTER || subConverter; 209 | fileName = url.searchParams.get('SUB_NAME') || SUB_NAME || fileName; 210 | botToken = url.searchParams.get('TG_TOKEN') || TG_TOKEN || botToken; 211 | chatID = url.searchParams.get('TG_ID') || TG_ID || chatID; 212 | let protType = url.searchParams.get('PROT_TYPE'); 213 | if (protType) { 214 | protType = protType.toLowerCase(); 215 | } 216 | randomNum = url.searchParams.get('RANDOW_NUM') || randomNum; 217 | 218 | // Unified protocol for handling subconverters 219 | const [subProtocol, subConverterWithoutProtocol] = (subConverter.startsWith("http://") || subConverter.startsWith("https://")) 220 | ? subConverter.split("://") 221 | : [undefined, subConverter]; 222 | subConverter = subConverterWithoutProtocol; 223 | 224 | // console.log(`proxyIPs: ${proxyIPs} \n proxyIP: ${proxyIP} \n ipLocal: ${ipLocal} \n ipUrl: ${ipUrl} \n ipUrlTxt: ${ipUrlTxt} `); 225 | 226 | //const uuid = url.searchParams.get('uuid')?.toLowerCase() || 'null'; 227 | const ua = request.headers.get('User-Agent') || 'null'; 228 | const userAgent = ua.toLowerCase(); 229 | const host = request.headers.get('Host'); 230 | const upgradeHeader = request.headers.get('Upgrade'); 231 | const expire = Math.floor(timestamp / 1000); 232 | 233 | // If WebSocket upgrade, handle WebSocket request 234 | if (upgradeHeader === 'websocket') { 235 | if (protType === atob(atob(protTypeBase64Tro))) { 236 | return await channelOverWSHandlerTro(request); 237 | } 238 | return await channelOverWSHandler(request); 239 | } 240 | 241 | fakeUserID = await getFakeUserID(userID); 242 | fakeHostName = fakeUserID.slice(6, 9) + "." + fakeUserID.slice(13, 19); 243 | console.log(`userID: ${userID}`); 244 | console.log(`fakeUserID: ${fakeUserID}`); 245 | 246 | // Handle routes based on the path 247 | switch (url.pathname.toLowerCase()) { 248 | case '/': { 249 | return new Response(await nginx(), { 250 | headers: { 251 | 'Content-Type': 'text/html; charset=UTF-8', 252 | 'referer': 'https://www.google.com/search?q=' + fileName, 253 | }, 254 | }); 255 | } 256 | 257 | case `/${fakeUserID}`: { 258 | // Disguise UUID node generation 259 | const fakeConfig = await getchannelConfig(userID, host, 'CF-FAKE-UA', url, protType); 260 | return new Response(fakeConfig, { status: 200 }); 261 | } 262 | 263 | case `/${userID}`: { 264 | // Handle real UUID requests and get node info 265 | await sendMessage( 266 | `#获取订阅 ${fileName}`, 267 | request.headers.get('CF-Connecting-IP'), 268 | `UA: ${userAgent}\n域名: ${url.hostname}\n入口: ${url.pathname + url.search}` 269 | ); 270 | 271 | const channelConfig = await getchannelConfig(userID, host, userAgent, url, protType); 272 | const isMozilla = userAgent.includes('mozilla'); 273 | 274 | const config = await getCFConfig(CF_EMAIL, CF_KEY, CF_ID); 275 | if (CF_EMAIL && CF_KEY) { 276 | ({ upload, download, total } = config); 277 | } 278 | 279 | // Prepare common headers 280 | const commonHeaders = { 281 | "Content-Type": isMozilla ? "text/html;charset=utf-8" : "text/plain;charset=utf-8", 282 | "Profile-Update-Interval": `${subUpdateTime}`, 283 | "Subscription-Userinfo": `upload=${upload}; download=${download}; total=${total}; expire=${expire}`, 284 | }; 285 | 286 | // Add download headers if not a Mozilla browser 287 | if (!isMozilla) { 288 | commonHeaders["Content-Disposition"] = `attachment; filename=${fileName}; filename*=gbk''${fileName}`; 289 | } 290 | 291 | return new Response(channelConfig, { 292 | status: 200, 293 | headers: commonHeaders, 294 | }); 295 | } 296 | 297 | case `/${userID}/ui`: { 298 | return await showKVPage(env); 299 | } 300 | case `/${userID}/get`: { 301 | return getKVData(env); 302 | } 303 | case `/${userID}/set`: { 304 | return setKVData(request, env); 305 | } 306 | 307 | default: { 308 | // Serve the default nginx disguise page 309 | return new Response(await nginx(), { 310 | headers: { 311 | 'Content-Type': 'text/html; charset=UTF-8', 312 | 'referer': 'https://www.google.com/search?q=' + fileName, 313 | }, 314 | }); 315 | } 316 | } 317 | } catch (err) { 318 | // Log error for debugging purposes 319 | console.error('Error processing request:', err); 320 | return new Response(`Error: ${err.message}`, { status: 500 }); 321 | } 322 | }, 323 | }; 324 | 325 | 326 | /** ---------------------Tools------------------------------ */ 327 | 328 | export async function hashHex_f(string) { 329 | const encoder = new TextEncoder(); 330 | const data = encoder.encode(string); 331 | const hashBuffer = await crypto.subtle.digest('SHA-256', data); 332 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 333 | const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); 334 | return hashHex; 335 | } 336 | 337 | /** 338 | * Checks if a given string is a valid UUID. 339 | * Note: This is not a real UUID validation. 340 | * @param {string} uuid The string to validate as a UUID. 341 | * @returns {boolean} True if the string is a valid UUID, false otherwise. 342 | */ 343 | function isValidUUID(uuid) { 344 | 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; 345 | return uuidRegex.test(uuid); 346 | } 347 | 348 | const byteToHex = []; 349 | for (let i = 0; i < 256; ++i) { 350 | byteToHex.push((i + 256).toString(16).slice(1)); 351 | } 352 | 353 | function unsafeStringify(arr, offset = 0) { 354 | 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(); 355 | } 356 | 357 | function stringify(arr, offset = 0) { 358 | const uuid = unsafeStringify(arr, offset); 359 | if (!isValidUUID(uuid)) { 360 | throw TypeError("Stringified UUID is invalid"); 361 | } 362 | return uuid; 363 | } 364 | 365 | async function getFakeUserID(userID) { 366 | const date = new Date().toISOString().split('T')[0]; 367 | const rawString = `${userID}-${date}`; 368 | 369 | const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(rawString)); 370 | const hashArray = Array.from(new Uint8Array(hashBuffer)).map(b => ('00' + b.toString(16)).slice(-2)).join(''); 371 | 372 | return `${hashArray.substring(0, 8)}-${hashArray.substring(8, 12)}-${hashArray.substring(12, 16)}-${hashArray.substring(16, 20)}-${hashArray.substring(20, 32)}`; 373 | } 374 | 375 | function revertFakeInfo(content, userID, hostName) { 376 | //console.log(`revertFakeInfo-->: isBase64 ${isBase64} \n content: ${content}`); 377 | if (isBase64) { 378 | content = atob(content);//Base64 decrypt 379 | } 380 | content = content.replace(new RegExp(fakeUserID, 'g'), userID).replace(new RegExp(fakeHostName, 'g'), hostName); 381 | if (isBase64) { 382 | content = btoa(content);//Base64 encryption 383 | } 384 | return content; 385 | } 386 | 387 | /** 388 | * Decodes a base64 string into an ArrayBuffer. 389 | * @param {string} base64Str The base64 string to decode. 390 | * @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. 391 | */ 392 | function base64ToArrayBuffer(base64Str) { 393 | if (!base64Str) { 394 | return { earlyData: null, error: null }; 395 | } 396 | try { 397 | // go use modified Base64 for URL rfc4648 which js atob not support 398 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); 399 | const decode = atob(base64Str); 400 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); 401 | return { earlyData: arryBuffer.buffer, error: null }; 402 | } catch (error) { 403 | return { earlyData: null, error }; 404 | } 405 | } 406 | 407 | async function addIpText(envAdd) { 408 | var addText = envAdd.replace(/[ |"'\r\n]+/g, ',').replace(/,+/g, ','); 409 | //console.log(addText); 410 | if (addText.charAt(0) == ',') { 411 | addText = addText.slice(1); 412 | } 413 | if (addText.charAt(addText.length - 1) == ',') { 414 | addText = addText.slice(0, addText.length - 1); 415 | } 416 | const add = addText.split(','); 417 | // console.log(add); 418 | return add; 419 | } 420 | 421 | function socks5Parser(socks5) { 422 | let [latter, former] = socks5.split("@").reverse(); 423 | let username, password, hostname, port; 424 | 425 | if (former) { 426 | const formers = former.split(":"); 427 | if (formers.length !== 2) { 428 | throw new Error('Invalid SOCKS address format: authentication must be in the "username:password" format'); 429 | } 430 | [username, password] = formers; 431 | } 432 | 433 | const latters = latter.split(":"); 434 | port = Number(latters.pop()); 435 | if (isNaN(port)) { 436 | throw new Error('Invalid SOCKS address format: port must be a number'); 437 | } 438 | 439 | hostname = latters.join(":"); 440 | const isIPv6 = hostname.includes(":") && !/^\[.*\]$/.test(hostname); 441 | if (isIPv6) { 442 | throw new Error('Invalid SOCKS address format: IPv6 addresses must be enclosed in brackets, e.g., [2001:db8::1]'); 443 | } 444 | 445 | //console.log(`socks5Parser-->: username ${username} \n password: ${password} \n hostname: ${hostname} \n port: ${port}`); 446 | return { username, password, hostname, port }; 447 | } 448 | 449 | async function parseSocks5FromUrl(socks5, url) { 450 | if (/\/socks5?=/.test(url.pathname)) { 451 | socks5 = url.pathname.split('5=')[1]; 452 | } else if (/\/socks[5]?:\/\//.test(url.pathname)) { 453 | socks5 = url.pathname.split('://')[1].split('#')[0]; 454 | } 455 | 456 | const authIdx = socks5.indexOf('@'); 457 | if (authIdx !== -1) { 458 | let userPassword = socks5.substring(0, authIdx); 459 | const base64Regex = /^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i; 460 | if (base64Regex.test(userPassword) && !userPassword.includes(':')) { 461 | userPassword = atob(userPassword); 462 | } 463 | socks5 = `${userPassword}@${socks5.substring(authIdx + 1)}`; 464 | } 465 | 466 | if (socks5) { 467 | try { 468 | return socks5Parser(socks5); 469 | } catch (err) { 470 | console.log(err.toString()); 471 | return null; 472 | } 473 | } 474 | return null; 475 | } 476 | 477 | function getFileType(url) { 478 | const baseUrl = url.split('@')[0]; 479 | 480 | const extension = baseUrl.match(/\.(csv|txt)$/i); 481 | 482 | if (extension) { 483 | return extension[1].toLowerCase(); 484 | } else { 485 | return 'txt'; 486 | } 487 | } 488 | 489 | async function getFileContentType(url) { 490 | try { 491 | const response = await fetch(url); 492 | const text = await response.text(); 493 | 494 | if (text.includes(',')) { 495 | return 'csv'; 496 | } else { 497 | return 'txt'; 498 | } 499 | } catch (error) { 500 | console.error('Error fetching file:', error); 501 | return null; 502 | } 503 | } 504 | 505 | function getRandomItems(arr, count) { 506 | if (!Array.isArray(arr)) return []; 507 | 508 | const shuffled = [...arr].sort(() => 0.5 - Math.random()); 509 | return shuffled.slice(0, count); 510 | } 511 | 512 | 513 | // sha256 Hash Algorithm in pure JavaScript 514 | /** 515 | * [js-sha256] 516 | */ 517 | (function () { 518 | 'use strict'; 519 | 520 | var ERROR = 'input is invalid type'; 521 | var WINDOW = typeof window === 'object'; 522 | var root = WINDOW ? window : {}; 523 | if (root.JS_SHA256_NO_WINDOW) { 524 | WINDOW = false; 525 | } 526 | var WEB_WORKER = !WINDOW && typeof self === 'object'; 527 | var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; 528 | if (NODE_JS) { 529 | root = global; 530 | } else if (WEB_WORKER) { 531 | root = self; 532 | } 533 | var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; 534 | var AMD = typeof define === 'function' && define.amd; 535 | var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; 536 | var HEX_CHARS = '0123456789abcdef'.split(''); 537 | var EXTRA = [-2147483648, 8388608, 32768, 128]; 538 | var SHIFT = [24, 16, 8, 0]; 539 | var K = [ 540 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 541 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 542 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 543 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 544 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 545 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 546 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 547 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 548 | ]; 549 | var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; 550 | 551 | var blocks = []; 552 | 553 | if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { 554 | Array.isArray = function (obj) { 555 | return Object.prototype.toString.call(obj) === '[object Array]'; 556 | }; 557 | } 558 | 559 | if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { 560 | ArrayBuffer.isView = function (obj) { 561 | return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; 562 | }; 563 | } 564 | 565 | var createOutputMethod = function (outputType, is224) { 566 | return function (message) { 567 | return new Sha256(is224, true).update(message)[outputType](); 568 | }; 569 | }; 570 | 571 | var createMethod = function (is224) { 572 | var method = createOutputMethod('hex', is224); 573 | if (NODE_JS) { 574 | method = nodeWrap(method, is224); 575 | } 576 | method.create = function () { 577 | return new Sha256(is224); 578 | }; 579 | method.update = function (message) { 580 | return method.create().update(message); 581 | }; 582 | for (var i = 0; i < OUTPUT_TYPES.length; ++i) { 583 | var type = OUTPUT_TYPES[i]; 584 | method[type] = createOutputMethod(type, is224); 585 | } 586 | return method; 587 | }; 588 | 589 | var nodeWrap = function (method, is224) { 590 | var crypto = require('node:crypto') 591 | var Buffer = require('node:buffer').Buffer; 592 | var algorithm = is224 ? 'sha224' : 'sha256'; 593 | var bufferFrom; 594 | if (Buffer.from && !root.JS_SHA256_NO_BUFFER_FROM) { 595 | bufferFrom = Buffer.from; 596 | } else { 597 | bufferFrom = function (message) { 598 | return new Buffer(message); 599 | }; 600 | } 601 | var nodeMethod = function (message) { 602 | if (typeof message === 'string') { 603 | return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); 604 | } else { 605 | if (message === null || message === undefined) { 606 | throw new Error(ERROR); 607 | } else if (message.constructor === ArrayBuffer) { 608 | message = new Uint8Array(message); 609 | } 610 | } 611 | if (Array.isArray(message) || ArrayBuffer.isView(message) || 612 | message.constructor === Buffer) { 613 | return crypto.createHash(algorithm).update(bufferFrom(message)).digest('hex'); 614 | } else { 615 | return method(message); 616 | } 617 | }; 618 | return nodeMethod; 619 | }; 620 | 621 | var createHmacOutputMethod = function (outputType, is224) { 622 | return function (key, message) { 623 | return new HmacSha256(key, is224, true).update(message)[outputType](); 624 | }; 625 | }; 626 | 627 | var createHmacMethod = function (is224) { 628 | var method = createHmacOutputMethod('hex', is224); 629 | method.create = function (key) { 630 | return new HmacSha256(key, is224); 631 | }; 632 | method.update = function (key, message) { 633 | return method.create(key).update(message); 634 | }; 635 | for (var i = 0; i < OUTPUT_TYPES.length; ++i) { 636 | var type = OUTPUT_TYPES[i]; 637 | method[type] = createHmacOutputMethod(type, is224); 638 | } 639 | return method; 640 | }; 641 | 642 | function Sha256(is224, sharedMemory) { 643 | if (sharedMemory) { 644 | blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = 645 | blocks[4] = blocks[5] = blocks[6] = blocks[7] = 646 | blocks[8] = blocks[9] = blocks[10] = blocks[11] = 647 | blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; 648 | this.blocks = blocks; 649 | } else { 650 | this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 651 | } 652 | 653 | if (is224) { 654 | this.h0 = 0xc1059ed8; 655 | this.h1 = 0x367cd507; 656 | this.h2 = 0x3070dd17; 657 | this.h3 = 0xf70e5939; 658 | this.h4 = 0xffc00b31; 659 | this.h5 = 0x68581511; 660 | this.h6 = 0x64f98fa7; 661 | this.h7 = 0xbefa4fa4; 662 | } else { // 256 663 | this.h0 = 0x6a09e667; 664 | this.h1 = 0xbb67ae85; 665 | this.h2 = 0x3c6ef372; 666 | this.h3 = 0xa54ff53a; 667 | this.h4 = 0x510e527f; 668 | this.h5 = 0x9b05688c; 669 | this.h6 = 0x1f83d9ab; 670 | this.h7 = 0x5be0cd19; 671 | } 672 | 673 | this.block = this.start = this.bytes = this.hBytes = 0; 674 | this.finalized = this.hashed = false; 675 | this.first = true; 676 | this.is224 = is224; 677 | } 678 | 679 | Sha256.prototype.update = function (message) { 680 | if (this.finalized) { 681 | return; 682 | } 683 | var notString, type = typeof message; 684 | if (type !== 'string') { 685 | if (type === 'object') { 686 | if (message === null) { 687 | throw new Error(ERROR); 688 | } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { 689 | message = new Uint8Array(message); 690 | } else if (!Array.isArray(message)) { 691 | if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { 692 | throw new Error(ERROR); 693 | } 694 | } 695 | } else { 696 | throw new Error(ERROR); 697 | } 698 | notString = true; 699 | } 700 | var code, index = 0, i, length = message.length, blocks = this.blocks; 701 | while (index < length) { 702 | if (this.hashed) { 703 | this.hashed = false; 704 | blocks[0] = this.block; 705 | this.block = blocks[16] = blocks[1] = blocks[2] = blocks[3] = 706 | blocks[4] = blocks[5] = blocks[6] = blocks[7] = 707 | blocks[8] = blocks[9] = blocks[10] = blocks[11] = 708 | blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; 709 | } 710 | 711 | if (notString) { 712 | for (i = this.start; index < length && i < 64; ++index) { 713 | blocks[i >>> 2] |= message[index] << SHIFT[i++ & 3]; 714 | } 715 | } else { 716 | for (i = this.start; index < length && i < 64; ++index) { 717 | code = message.charCodeAt(index); 718 | if (code < 0x80) { 719 | blocks[i >>> 2] |= code << SHIFT[i++ & 3]; 720 | } else if (code < 0x800) { 721 | blocks[i >>> 2] |= (0xc0 | (code >>> 6)) << SHIFT[i++ & 3]; 722 | blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; 723 | } else if (code < 0xd800 || code >= 0xe000) { 724 | blocks[i >>> 2] |= (0xe0 | (code >>> 12)) << SHIFT[i++ & 3]; 725 | blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; 726 | blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; 727 | } else { 728 | code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); 729 | blocks[i >>> 2] |= (0xf0 | (code >>> 18)) << SHIFT[i++ & 3]; 730 | blocks[i >>> 2] |= (0x80 | ((code >>> 12) & 0x3f)) << SHIFT[i++ & 3]; 731 | blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; 732 | blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; 733 | } 734 | } 735 | } 736 | 737 | this.lastByteIndex = i; 738 | this.bytes += i - this.start; 739 | if (i >= 64) { 740 | this.block = blocks[16]; 741 | this.start = i - 64; 742 | this.hash(); 743 | this.hashed = true; 744 | } else { 745 | this.start = i; 746 | } 747 | } 748 | if (this.bytes > 4294967295) { 749 | this.hBytes += this.bytes / 4294967296 << 0; 750 | this.bytes = this.bytes % 4294967296; 751 | } 752 | return this; 753 | }; 754 | 755 | Sha256.prototype.finalize = function () { 756 | if (this.finalized) { 757 | return; 758 | } 759 | this.finalized = true; 760 | var blocks = this.blocks, i = this.lastByteIndex; 761 | blocks[16] = this.block; 762 | blocks[i >>> 2] |= EXTRA[i & 3]; 763 | this.block = blocks[16]; 764 | if (i >= 56) { 765 | if (!this.hashed) { 766 | this.hash(); 767 | } 768 | blocks[0] = this.block; 769 | blocks[16] = blocks[1] = blocks[2] = blocks[3] = 770 | blocks[4] = blocks[5] = blocks[6] = blocks[7] = 771 | blocks[8] = blocks[9] = blocks[10] = blocks[11] = 772 | blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; 773 | } 774 | blocks[14] = this.hBytes << 3 | this.bytes >>> 29; 775 | blocks[15] = this.bytes << 3; 776 | this.hash(); 777 | }; 778 | 779 | Sha256.prototype.hash = function () { 780 | var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, 781 | h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; 782 | 783 | for (j = 16; j < 64; ++j) { 784 | // rightrotate 785 | t1 = blocks[j - 15]; 786 | s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); 787 | t1 = blocks[j - 2]; 788 | s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); 789 | blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; 790 | } 791 | 792 | bc = b & c; 793 | for (j = 0; j < 64; j += 4) { 794 | if (this.first) { 795 | if (this.is224) { 796 | ab = 300032; 797 | t1 = blocks[0] - 1413257819; 798 | h = t1 - 150054599 << 0; 799 | d = t1 + 24177077 << 0; 800 | } else { 801 | ab = 704751109; 802 | t1 = blocks[0] - 210244248; 803 | h = t1 - 1521486534 << 0; 804 | d = t1 + 143694565 << 0; 805 | } 806 | this.first = false; 807 | } else { 808 | s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); 809 | s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); 810 | ab = a & b; 811 | maj = ab ^ (a & c) ^ bc; 812 | ch = (e & f) ^ (~e & g); 813 | t1 = h + s1 + ch + K[j] + blocks[j]; 814 | t2 = s0 + maj; 815 | h = d + t1 << 0; 816 | d = t1 + t2 << 0; 817 | } 818 | s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); 819 | s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); 820 | da = d & a; 821 | maj = da ^ (d & b) ^ ab; 822 | ch = (h & e) ^ (~h & f); 823 | t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; 824 | t2 = s0 + maj; 825 | g = c + t1 << 0; 826 | c = t1 + t2 << 0; 827 | s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); 828 | s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); 829 | cd = c & d; 830 | maj = cd ^ (c & a) ^ da; 831 | ch = (g & h) ^ (~g & e); 832 | t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; 833 | t2 = s0 + maj; 834 | f = b + t1 << 0; 835 | b = t1 + t2 << 0; 836 | s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); 837 | s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); 838 | bc = b & c; 839 | maj = bc ^ (b & d) ^ cd; 840 | ch = (f & g) ^ (~f & h); 841 | t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; 842 | t2 = s0 + maj; 843 | e = a + t1 << 0; 844 | a = t1 + t2 << 0; 845 | this.chromeBugWorkAround = true; 846 | } 847 | 848 | this.h0 = this.h0 + a << 0; 849 | this.h1 = this.h1 + b << 0; 850 | this.h2 = this.h2 + c << 0; 851 | this.h3 = this.h3 + d << 0; 852 | this.h4 = this.h4 + e << 0; 853 | this.h5 = this.h5 + f << 0; 854 | this.h6 = this.h6 + g << 0; 855 | this.h7 = this.h7 + h << 0; 856 | }; 857 | 858 | Sha256.prototype.hex = function () { 859 | this.finalize(); 860 | 861 | var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, 862 | h6 = this.h6, h7 = this.h7; 863 | 864 | var hex = HEX_CHARS[(h0 >>> 28) & 0x0F] + HEX_CHARS[(h0 >>> 24) & 0x0F] + 865 | HEX_CHARS[(h0 >>> 20) & 0x0F] + HEX_CHARS[(h0 >>> 16) & 0x0F] + 866 | HEX_CHARS[(h0 >>> 12) & 0x0F] + HEX_CHARS[(h0 >>> 8) & 0x0F] + 867 | HEX_CHARS[(h0 >>> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + 868 | HEX_CHARS[(h1 >>> 28) & 0x0F] + HEX_CHARS[(h1 >>> 24) & 0x0F] + 869 | HEX_CHARS[(h1 >>> 20) & 0x0F] + HEX_CHARS[(h1 >>> 16) & 0x0F] + 870 | HEX_CHARS[(h1 >>> 12) & 0x0F] + HEX_CHARS[(h1 >>> 8) & 0x0F] + 871 | HEX_CHARS[(h1 >>> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + 872 | HEX_CHARS[(h2 >>> 28) & 0x0F] + HEX_CHARS[(h2 >>> 24) & 0x0F] + 873 | HEX_CHARS[(h2 >>> 20) & 0x0F] + HEX_CHARS[(h2 >>> 16) & 0x0F] + 874 | HEX_CHARS[(h2 >>> 12) & 0x0F] + HEX_CHARS[(h2 >>> 8) & 0x0F] + 875 | HEX_CHARS[(h2 >>> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + 876 | HEX_CHARS[(h3 >>> 28) & 0x0F] + HEX_CHARS[(h3 >>> 24) & 0x0F] + 877 | HEX_CHARS[(h3 >>> 20) & 0x0F] + HEX_CHARS[(h3 >>> 16) & 0x0F] + 878 | HEX_CHARS[(h3 >>> 12) & 0x0F] + HEX_CHARS[(h3 >>> 8) & 0x0F] + 879 | HEX_CHARS[(h3 >>> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + 880 | HEX_CHARS[(h4 >>> 28) & 0x0F] + HEX_CHARS[(h4 >>> 24) & 0x0F] + 881 | HEX_CHARS[(h4 >>> 20) & 0x0F] + HEX_CHARS[(h4 >>> 16) & 0x0F] + 882 | HEX_CHARS[(h4 >>> 12) & 0x0F] + HEX_CHARS[(h4 >>> 8) & 0x0F] + 883 | HEX_CHARS[(h4 >>> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + 884 | HEX_CHARS[(h5 >>> 28) & 0x0F] + HEX_CHARS[(h5 >>> 24) & 0x0F] + 885 | HEX_CHARS[(h5 >>> 20) & 0x0F] + HEX_CHARS[(h5 >>> 16) & 0x0F] + 886 | HEX_CHARS[(h5 >>> 12) & 0x0F] + HEX_CHARS[(h5 >>> 8) & 0x0F] + 887 | HEX_CHARS[(h5 >>> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + 888 | HEX_CHARS[(h6 >>> 28) & 0x0F] + HEX_CHARS[(h6 >>> 24) & 0x0F] + 889 | HEX_CHARS[(h6 >>> 20) & 0x0F] + HEX_CHARS[(h6 >>> 16) & 0x0F] + 890 | HEX_CHARS[(h6 >>> 12) & 0x0F] + HEX_CHARS[(h6 >>> 8) & 0x0F] + 891 | HEX_CHARS[(h6 >>> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; 892 | if (!this.is224) { 893 | hex += HEX_CHARS[(h7 >>> 28) & 0x0F] + HEX_CHARS[(h7 >>> 24) & 0x0F] + 894 | HEX_CHARS[(h7 >>> 20) & 0x0F] + HEX_CHARS[(h7 >>> 16) & 0x0F] + 895 | HEX_CHARS[(h7 >>> 12) & 0x0F] + HEX_CHARS[(h7 >>> 8) & 0x0F] + 896 | HEX_CHARS[(h7 >>> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; 897 | } 898 | return hex; 899 | }; 900 | 901 | Sha256.prototype.toString = Sha256.prototype.hex; 902 | 903 | Sha256.prototype.digest = function () { 904 | this.finalize(); 905 | 906 | var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, 907 | h6 = this.h6, h7 = this.h7; 908 | 909 | var arr = [ 910 | (h0 >>> 24) & 0xFF, (h0 >>> 16) & 0xFF, (h0 >>> 8) & 0xFF, h0 & 0xFF, 911 | (h1 >>> 24) & 0xFF, (h1 >>> 16) & 0xFF, (h1 >>> 8) & 0xFF, h1 & 0xFF, 912 | (h2 >>> 24) & 0xFF, (h2 >>> 16) & 0xFF, (h2 >>> 8) & 0xFF, h2 & 0xFF, 913 | (h3 >>> 24) & 0xFF, (h3 >>> 16) & 0xFF, (h3 >>> 8) & 0xFF, h3 & 0xFF, 914 | (h4 >>> 24) & 0xFF, (h4 >>> 16) & 0xFF, (h4 >>> 8) & 0xFF, h4 & 0xFF, 915 | (h5 >>> 24) & 0xFF, (h5 >>> 16) & 0xFF, (h5 >>> 8) & 0xFF, h5 & 0xFF, 916 | (h6 >>> 24) & 0xFF, (h6 >>> 16) & 0xFF, (h6 >>> 8) & 0xFF, h6 & 0xFF 917 | ]; 918 | if (!this.is224) { 919 | arr.push((h7 >>> 24) & 0xFF, (h7 >>> 16) & 0xFF, (h7 >>> 8) & 0xFF, h7 & 0xFF); 920 | } 921 | return arr; 922 | }; 923 | 924 | Sha256.prototype.array = Sha256.prototype.digest; 925 | 926 | Sha256.prototype.arrayBuffer = function () { 927 | this.finalize(); 928 | 929 | var buffer = new ArrayBuffer(this.is224 ? 28 : 32); 930 | var dataView = new DataView(buffer); 931 | dataView.setUint32(0, this.h0); 932 | dataView.setUint32(4, this.h1); 933 | dataView.setUint32(8, this.h2); 934 | dataView.setUint32(12, this.h3); 935 | dataView.setUint32(16, this.h4); 936 | dataView.setUint32(20, this.h5); 937 | dataView.setUint32(24, this.h6); 938 | if (!this.is224) { 939 | dataView.setUint32(28, this.h7); 940 | } 941 | return buffer; 942 | }; 943 | 944 | function HmacSha256(key, is224, sharedMemory) { 945 | var i, type = typeof key; 946 | if (type === 'string') { 947 | var bytes = [], length = key.length, index = 0, code; 948 | for (i = 0; i < length; ++i) { 949 | code = key.charCodeAt(i); 950 | if (code < 0x80) { 951 | bytes[index++] = code; 952 | } else if (code < 0x800) { 953 | bytes[index++] = (0xc0 | (code >>> 6)); 954 | bytes[index++] = (0x80 | (code & 0x3f)); 955 | } else if (code < 0xd800 || code >= 0xe000) { 956 | bytes[index++] = (0xe0 | (code >>> 12)); 957 | bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); 958 | bytes[index++] = (0x80 | (code & 0x3f)); 959 | } else { 960 | code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); 961 | bytes[index++] = (0xf0 | (code >>> 18)); 962 | bytes[index++] = (0x80 | ((code >>> 12) & 0x3f)); 963 | bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); 964 | bytes[index++] = (0x80 | (code & 0x3f)); 965 | } 966 | } 967 | key = bytes; 968 | } else { 969 | if (type === 'object') { 970 | if (key === null) { 971 | throw new Error(ERROR); 972 | } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { 973 | key = new Uint8Array(key); 974 | } else if (!Array.isArray(key)) { 975 | if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { 976 | throw new Error(ERROR); 977 | } 978 | } 979 | } else { 980 | throw new Error(ERROR); 981 | } 982 | } 983 | 984 | if (key.length > 64) { 985 | key = (new Sha256(is224, true)).update(key).array(); 986 | } 987 | 988 | var oKeyPad = [], iKeyPad = []; 989 | for (i = 0; i < 64; ++i) { 990 | var b = key[i] || 0; 991 | oKeyPad[i] = 0x5c ^ b; 992 | iKeyPad[i] = 0x36 ^ b; 993 | } 994 | 995 | Sha256.call(this, is224, sharedMemory); 996 | 997 | this.update(iKeyPad); 998 | this.oKeyPad = oKeyPad; 999 | this.inner = true; 1000 | this.sharedMemory = sharedMemory; 1001 | } 1002 | HmacSha256.prototype = new Sha256(); 1003 | 1004 | HmacSha256.prototype.finalize = function () { 1005 | Sha256.prototype.finalize.call(this); 1006 | if (this.inner) { 1007 | this.inner = false; 1008 | var innerHash = this.array(); 1009 | Sha256.call(this, this.is224, this.sharedMemory); 1010 | this.update(this.oKeyPad); 1011 | this.update(innerHash); 1012 | Sha256.prototype.finalize.call(this); 1013 | } 1014 | }; 1015 | 1016 | var exports = createMethod(); 1017 | exports.sha256 = exports; 1018 | exports.sha224 = createMethod(true); 1019 | exports.sha256.hmac = createHmacMethod(); 1020 | exports.sha224.hmac = createHmacMethod(true); 1021 | 1022 | if (COMMON_JS) { 1023 | module.exports = exports; 1024 | } else { 1025 | root.sha256 = exports.sha256; 1026 | root.sha224 = exports.sha224; 1027 | if (AMD) { 1028 | define(function () { 1029 | return exports; 1030 | }); 1031 | } 1032 | } 1033 | })(); 1034 | 1035 | 1036 | /** ---------------------Get data------------------------------ */ 1037 | 1038 | let subParams = ['sub', 'base64', 'b64', 'clash', 'singbox', 'sb']; 1039 | /** 1040 | * @param {string} userID 1041 | * @param {string | null} host 1042 | * @param {string} userAgent 1043 | * @param {string} _url 1044 | * @returns {Promise} 1045 | */ 1046 | async function getchannelConfig(userID, host, userAgent, _url, protType) { 1047 | // console.log(`------------getchannelConfig------------------`); 1048 | // console.log(`userID: ${userID} \n host: ${host} \n userAgent: ${userAgent} \n _url: ${_url}`); 1049 | 1050 | userAgent = userAgent.toLowerCase(); 1051 | let port = 443; 1052 | if (host.includes('.workers.dev')) { 1053 | port = 80; 1054 | } 1055 | 1056 | if (userAgent.includes('mozilla') && !subParams.some(param => _url.searchParams.has(param))) { 1057 | if (!protType) { 1058 | protType = atob(atob(protTypeBase64)); 1059 | } 1060 | const [v2ray, clash] = getConfigLink(userID, host, host, port, host, protType); 1061 | return getHtmlResponse(socks5Enable, userID, host, v2ray, clash); 1062 | } 1063 | 1064 | // Get node information 1065 | let num = randomNum || 25; 1066 | if (protType && !randomNum) { 1067 | num = num * 2; 1068 | } 1069 | fakeHostName = getFakeHostName(host); 1070 | const ipUrlTxtAndCsv = await getIpUrlTxtAndCsv(noTLS, ipUrlTxt, ipUrlCsv, num); 1071 | 1072 | // console.log(`txt: ${ipUrlTxtAndCsv.txt} \n csv: ${ipUrlTxtAndCsv.csv}`); 1073 | let content = await getSubscribeNode(userAgent, _url, host, fakeHostName, fakeUserID, noTLS, ipUrlTxtAndCsv.txt, ipUrlTxtAndCsv.csv, protType); 1074 | 1075 | return _url.pathname === `/${fakeUserID}` ? content : revertFakeInfo(content, userID, host); 1076 | 1077 | } 1078 | 1079 | function getHtmlResponse(socks5Enable, userID, host, v2ray, clash) { 1080 | const subRemark = `IP_LOCAL/IP_URL/IP_URL_TXT/IP_URL_CSV`; 1081 | let proxyIPRemark = `PROXYIP: ${proxyIP}`; 1082 | 1083 | if (socks5Enable) { 1084 | proxyIPRemark = `socks5: ${parsedSocks5.hostname}:${parsedSocks5.port}`; 1085 | } 1086 | 1087 | let remark = `您的订阅节点由设置变量 ${subRemark} 提供, 当前使用反代是${proxyIPRemark}`; 1088 | 1089 | if (!proxyIP && !socks5Enable) { 1090 | remark = `您的订阅节点由设置变量 ${subRemark} 提供, 当前没设置反代, 推荐您设置PROXYIP变量或SOCKS5变量或订阅连接带proxyIP`; 1091 | } 1092 | 1093 | return getConfigHtml(userID, host, remark, v2ray, clash); 1094 | } 1095 | 1096 | function getFakeHostName(host) { 1097 | if (host.includes(".pages.dev")) { 1098 | return `${fakeHostName}.pages.dev`; 1099 | } else if (host.includes(".workers.dev") || host.includes("notls") || noTLS === 'true') { 1100 | return `${fakeHostName}.workers.dev`; 1101 | } 1102 | return `${fakeHostName}.xyz`; 1103 | } 1104 | 1105 | async function getIpUrlTxtAndCsv(noTLS, urlTxts, urlCsvs, num) { 1106 | if (noTLS === 'true') { 1107 | return { 1108 | txt: await getIpUrlTxt(urlTxts, num), 1109 | csv: await getIpUrlCsv(urlCsvs, 'FALSE') 1110 | }; 1111 | } 1112 | return { 1113 | txt: await getIpUrlTxt(urlTxts, num), 1114 | csv: await getIpUrlCsv(urlCsvs, 'TRUE') 1115 | }; 1116 | } 1117 | 1118 | async function getIpUrlTxt(urlTxts, num) { 1119 | if (!urlTxts || urlTxts.length === 0) { 1120 | return []; 1121 | } 1122 | 1123 | let ipTxt = ""; 1124 | const controller = new AbortController(); 1125 | const timeout = setTimeout(() => { 1126 | controller.abort(); 1127 | }, 2000); 1128 | 1129 | try { 1130 | const urlMappings = urlTxts.map(entry => { 1131 | const [url, suffix] = entry.split('@'); 1132 | return { url, suffix: suffix ? `@${suffix}` : '' }; 1133 | }); 1134 | 1135 | const responses = await Promise.allSettled( 1136 | urlMappings.map(({ url }) => 1137 | fetch(url, { 1138 | method: 'GET', 1139 | headers: { 1140 | 'Accept': 'text/html,application/xhtml+xml,application/xml;', 1141 | 'User-Agent': projectName 1142 | }, 1143 | signal: controller.signal 1144 | }).then(response => response.ok ? response.text() : Promise.reject()) 1145 | ) 1146 | ); 1147 | 1148 | for (let i = 0; i < responses.length; i++) { 1149 | const response = responses[i]; 1150 | if (response.status === 'fulfilled') { 1151 | const suffix = urlMappings[i].suffix; 1152 | const content = response.value 1153 | .split('\n') 1154 | .filter(line => line.trim() !== "") 1155 | .map(line => line + suffix) 1156 | .join('\n'); 1157 | 1158 | ipTxt += content + '\n'; 1159 | } 1160 | } 1161 | } catch (error) { 1162 | console.error(error); 1163 | } finally { 1164 | clearTimeout(timeout); 1165 | } 1166 | // console.log(`getIpUrlTxt-->ipTxt: ${ipTxt} \n `); 1167 | let newIpTxt = await addIpText(ipTxt); 1168 | 1169 | // Randomly select 50 items 1170 | const hasAcCom = urlTxts.includes(defaultIpUrlTxt); 1171 | if (hasAcCom || randomNum) { 1172 | newIpTxt = getRandomItems(newIpTxt, num); 1173 | } 1174 | 1175 | return newIpTxt; 1176 | } 1177 | 1178 | async function getIpUrlTxtToArry(urlTxts) { 1179 | if (!urlTxts || urlTxts.length === 0) { 1180 | return []; 1181 | } 1182 | 1183 | let ipTxt = ""; 1184 | 1185 | // Create an AbortController object to control the cancellation of fetch requests 1186 | const controller = new AbortController(); 1187 | 1188 | // Set a timeout to trigger the cancellation of all requests after 2 seconds 1189 | const timeout = setTimeout(() => { 1190 | controller.abort(); // Cancel all requests 1191 | }, 2000); 1192 | 1193 | try { 1194 | // Use Promise.allSettled to wait for all API requests to complete, regardless of success or failure 1195 | // Iterate over the api array and send a fetch request to each API URL 1196 | const responses = await Promise.allSettled(urlTxts.map(apiUrl => fetch(apiUrl, { 1197 | method: 'GET', 1198 | headers: { 1199 | 'Accept': 'text/html,application/xhtml+xml,application/xml;', 1200 | 'User-Agent': projectName 1201 | }, 1202 | signal: controller.signal // Attach the AbortController's signal to the fetch request to allow cancellation when needed 1203 | }).then(response => response.ok ? response.text() : Promise.reject()))); 1204 | 1205 | // Iterate through all the responses 1206 | for (const response of responses) { 1207 | // Check if the request was fulfilled successfully 1208 | if (response.status === 'fulfilled') { 1209 | // Get the response content 1210 | const content = await response.value; 1211 | ipTxt += content + '\n'; 1212 | } 1213 | } 1214 | } catch (error) { 1215 | console.error(error); 1216 | } finally { 1217 | // Clear the timeout regardless of success or failure 1218 | clearTimeout(timeout); 1219 | } 1220 | 1221 | // Process the result using addIpText function 1222 | const newIpTxt = await addIpText(ipTxt); 1223 | // console.log(`ipUrlTxts: ${ipUrlTxts} \n ipTxt: ${ipTxt} \n newIpTxt: ${newIpTxt} `); 1224 | 1225 | // Return the processed result 1226 | return newIpTxt; 1227 | } 1228 | 1229 | async function getIpUrlCsv(urlCsvs, tls) { 1230 | // Check if the CSV URLs are valid 1231 | if (!urlCsvs || urlCsvs.length === 0) { 1232 | return []; 1233 | } 1234 | 1235 | const newAddressesCsv = []; 1236 | 1237 | // Fetch and process all CSVs concurrently 1238 | const fetchCsvPromises = urlCsvs.map(async (csvUrl) => { 1239 | // Parse the URL to get the suffix (after @) 1240 | const [url, suffix] = csvUrl.split('@'); 1241 | const suffixText = suffix ? `@${suffix}` : ''; // If no @, suffixText will be an empty string 1242 | 1243 | try { 1244 | const response = await fetch(url); 1245 | 1246 | 1247 | // Ensure the response is successful 1248 | if (!response.ok) { 1249 | console.error('Error fetching CSV:', response.status, response.statusText); 1250 | return; 1251 | } 1252 | 1253 | // Parse the CSV content and split it into lines 1254 | const text = await response.text(); 1255 | const lines = text.includes('\r\n') ? text.split('\r\n') : text.split('\n'); 1256 | 1257 | // Ensure we have a non-empty CSV 1258 | if (lines.length < 2) { 1259 | console.error('CSV file is empty or has no data rows'); 1260 | return; 1261 | } 1262 | 1263 | // Extract the header and get required field indexes 1264 | const header = lines[0].trim().split(','); 1265 | const tlsIndex = header.indexOf('TLS'); 1266 | const ipAddressIndex = 0; // Assuming the first column is IP address 1267 | const portIndex = 1; // Assuming the second column is port 1268 | const dataCenterIndex = tlsIndex + 1; // Data center assumed to be right after TLS 1269 | const speedIndex = header.length - 1; // Last column for speed 1270 | 1271 | // If the required fields are missing, skip this CSV 1272 | if (tlsIndex === -1) { 1273 | console.error('CSV file missing required TLS field'); 1274 | return; 1275 | } 1276 | 1277 | // Process the data rows 1278 | for (let i = 1; i < lines.length; i++) { 1279 | const columns = lines[i].trim().split(','); 1280 | // Skip empty or malformed rows 1281 | if (columns.length < header.length) { 1282 | continue; 1283 | } 1284 | // Check if TLS matches and speed is greater than sl 1285 | const tlsValue = columns[tlsIndex].toUpperCase(); 1286 | const speedValue = parseFloat(columns[speedIndex]); 1287 | if (tlsValue === tls && speedValue > sl) { 1288 | const ipAddress = columns[ipAddressIndex]; 1289 | const port = columns[portIndex]; 1290 | const dataCenter = columns[dataCenterIndex]; 1291 | // Add suffix to the result 1292 | newAddressesCsv.push(`${ipAddress}:${port}#${dataCenter}${suffixText}`); 1293 | } 1294 | } 1295 | } catch (error) { 1296 | console.error('Error processing CSV URL:', csvUrl, error); 1297 | } 1298 | }); 1299 | 1300 | // Wait for all CSVs to be processed 1301 | await Promise.all(fetchCsvPromises); 1302 | 1303 | // console.log(`newAddressesCsv: ${newAddressesCsv} \n `); 1304 | return newAddressesCsv; 1305 | } 1306 | 1307 | /** 1308 | * Get node configuration information 1309 | * @param {*} uuid 1310 | * @param {*} host 1311 | * @param {*} address 1312 | * @param {*} port 1313 | * @param {*} remarks 1314 | * @returns 1315 | */ 1316 | function getConfigLink(uuid, host, address, port, remarks, proxyip, protType) { 1317 | const encryption = 'none'; 1318 | let path = `/?ed=2560&PROT_TYPE=${protType}`; 1319 | if (proxyip) { 1320 | path = `/?ed=2560&PROT_TYPE=${protType}&PROXYIP=${proxyip}`; 1321 | } 1322 | const fingerprint = 'randomized'; 1323 | let tls = ['tls', true]; 1324 | if (host.includes('.workers.dev') || host.includes('pages.dev')) { 1325 | path = `/${host}${path}`; 1326 | remarks += ' 请通过绑定自定义域名订阅!'; 1327 | } 1328 | 1329 | const v2ray = getV2rayLink({ protType, host, uuid, address, port, remarks, encryption, path, fingerprint, tls }); 1330 | const clash = getClashLink(protType, host, address, port, uuid, path, tls, fingerprint); 1331 | 1332 | return [v2ray, clash]; 1333 | } 1334 | 1335 | /** 1336 | * Get channel information 1337 | * @param {*} param0 1338 | * @returns 1339 | */ 1340 | function getV2rayLink({ protType, host, uuid, address, port, remarks, encryption, path, fingerprint, tls }) { 1341 | let sniAndFp = `&sni=${host}&fp=${fingerprint}`; 1342 | if (portSet_http.has(parseInt(port))) { 1343 | tls = ['', false]; 1344 | sniAndFp = ''; 1345 | } 1346 | 1347 | const v2rayLink = `${protType}://${uuid}@${address}:${port}?encryption=${encryption}&security=${tls[0]}&type=${network}&host=${host}&path=${encodeURIComponent(path)}${sniAndFp}#${encodeURIComponent(remarks)}`; 1348 | return v2rayLink; 1349 | } 1350 | 1351 | /** 1352 | * Get channel information 1353 | * @param {*} protType 1354 | * @param {*} host 1355 | * @param {*} address 1356 | * @param {*} port 1357 | * @param {*} uuid 1358 | * @param {*} path 1359 | * @param {*} tls 1360 | * @param {*} fingerprint 1361 | * @returns 1362 | */ 1363 | function getClashLink(protType, host, address, port, uuid, path, tls, fingerprint) { 1364 | return `- {type: ${protType}, name: ${host}, server: ${address}, port: ${port}, password: ${uuid}, network: ${network}, tls: ${tls[1]}, udp: false, sni: ${host}, client-fingerprint: ${fingerprint}, skip-cert-verify: true, ws-opts: {path: ${path}, headers: {Host: ${host}}}}`; 1365 | 1366 | // return ` 1367 | // - type: ${protType} 1368 | // name: ${host} 1369 | // server: ${address} 1370 | // port: ${port} 1371 | // uuid: ${uuid} 1372 | // network: ${network} 1373 | // tls: ${tls[1]} 1374 | // udp: false 1375 | // sni: ${host} 1376 | // client-fingerprint: ${fingerprint} 1377 | // ws-opts: 1378 | // path: "${path}" 1379 | // headers: 1380 | // host: ${host} 1381 | // `; 1382 | } 1383 | 1384 | /** 1385 | * Generate home page 1386 | * @param {*} userID 1387 | * @param {*} hostName 1388 | * @param {*} remark 1389 | * @param {*} v2ray 1390 | * @param {*} clash 1391 | * @returns 1392 | */ 1393 | function getConfigHtml(userID, host, remark, v2ray, clash) { 1394 | // HTML Head with CSS and FontAwesome library 1395 | const htmlHead = ` 1396 | 1397 | ${projectName}(${fileName}) 1398 | 1399 | 1438 | 1439 | `; 1440 | 1441 | // Prepare header string with left alignment 1442 | const header = ` 1443 |

1444 | Telegram交流群 点击加入,技术大佬~在线交流
1445 | https://t.me/am_clubs 1446 |

1447 | GitHub项目地址 点击进入,点下星星给个Star!Star!Star!
1448 | https://github.com/${projectName} 1449 |

1450 | YouTube频道 点击订阅频道,观看更多技术教程
1451 | ${ytName} 1452 |

1453 | `; 1454 | 1455 | // Prepare the output string 1456 | const httpAddr = `https://${host}/${userID}`; 1457 | const output = ` 1458 | ################################################################ 1459 | 订阅地址, 支持 Base64、clash-meta、sing-box、Quantumult X、小火箭、surge 等订阅格式, ${remark} 1460 | --------------------------------------------------------------- 1461 | 通用订阅地址: 1462 | ${httpAddr}?sub 1463 | 1464 | Base64订阅地址: 1465 | ${httpAddr}?base64 1466 | 1467 | clash订阅地址: 1468 | ${httpAddr}?clash 1469 | 1470 | singbox订阅地址: 1471 | ${httpAddr}?singbox 1472 | --------------------------------------------------------------- 1473 | ################################################################ 1474 | v2ray 1475 | --------------------------------------------------------------- 1476 | ${v2ray} 1477 | --------------------------------------------------------------- 1478 | ################################################################ 1479 | clash-meta 1480 | --------------------------------------------------------------- 1481 | ${clash} 1482 | --------------------------------------------------------------- 1483 | ################################################################ 1484 | `; 1485 | 1486 | // Final HTML 1487 | const html = ` 1488 | 1489 | ${htmlHead} 1490 | 1491 | ${header} 1492 |
${output}
1493 | 1504 | 1505 | 1506 | `; 1507 | 1508 | return html; 1509 | } 1510 | 1511 | let portSet_http = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]); 1512 | let portSet_https = new Set([443, 8443, 2053, 2096, 2087, 2083]); 1513 | /** 1514 | * 1515 | * @param {*} host 1516 | * @param {*} uuid 1517 | * @param {*} noTLS 1518 | * @param {*} ipUrlTxt 1519 | * @param {*} ipUrlCsv 1520 | * @returns 1521 | */ 1522 | async function getSubscribeNode(userAgent, _url, host, fakeHostName, fakeUserID, noTLS, ipUrlTxt, ipUrlCsv, protType) { 1523 | // Use Set object to remove duplicates 1524 | const uniqueIpTxt = [...new Set([...ipUrlTxt, ...ipUrlCsv])]; 1525 | let responseBody; 1526 | if (!protType) { 1527 | protType = atob(atob(protTypeBase64)); 1528 | const responseBody1 = splitNodeData(uniqueIpTxt, noTLS, fakeHostName, fakeUserID, userAgent, protType); 1529 | protType = atob(atob(protTypeBase64Tro)); 1530 | const responseBody2 = splitNodeData(uniqueIpTxt, noTLS, fakeHostName, fakeUserID, userAgent, protType); 1531 | responseBody = [responseBody1, responseBody2].join('\n'); 1532 | } else { 1533 | responseBody = splitNodeData(uniqueIpTxt, noTLS, fakeHostName, fakeUserID, userAgent, protType); 1534 | responseBody = [responseBody].join('\n'); 1535 | } 1536 | protType = atob(atob(protTypeBase64)); 1537 | const responseBodyTop = splitNodeData(ipLocal, noTLS, fakeHostName, fakeUserID, userAgent, protType); 1538 | responseBody = [responseBodyTop, responseBody].join('\n'); 1539 | responseBody = btoa(responseBody); 1540 | 1541 | // console.log(`getSubscribeNode---> responseBody: ${responseBody} `); 1542 | 1543 | if (!userAgent.includes(('CF-FAKE-UA').toLowerCase())) { 1544 | 1545 | let url = `https://${host}/${fakeUserID}`; 1546 | 1547 | if (isClashCondition(userAgent, _url)) { 1548 | isBase64 = false; 1549 | url = createSubConverterUrl('clash', url, subConfig, subConverter, subProtocol); 1550 | } else if (isSingboxCondition(userAgent, _url)) { 1551 | isBase64 = false; 1552 | url = createSubConverterUrl('singbox', url, subConfig, subConverter, subProtocol); 1553 | } else { 1554 | return responseBody; 1555 | } 1556 | const response = await fetch(url, { 1557 | headers: { 1558 | //'Content-Type': 'text/html; charset=UTF-8', 1559 | 'User-Agent': `${userAgent} ${projectName}` 1560 | } 1561 | }); 1562 | responseBody = await response.text(); 1563 | //console.log(`getSubscribeNode---> url: ${url} `); 1564 | } 1565 | 1566 | return responseBody; 1567 | } 1568 | 1569 | function createSubConverterUrl(target, url, subConfig, subConverter, subProtocol) { 1570 | return `${subProtocol}://${subConverter}/sub?target=${target}&url=${encodeURIComponent(url)}&insert=false&config=${encodeURIComponent(subConfig)}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`; 1571 | } 1572 | 1573 | function isClashCondition(userAgent, _url) { 1574 | return (userAgent.includes('clash') && !userAgent.includes('nekobox')) || (_url.searchParams.has('clash') && !userAgent.includes('subConverter')); 1575 | } 1576 | 1577 | function isSingboxCondition(userAgent, _url) { 1578 | return userAgent.includes('sing-box') || userAgent.includes('singbox') || ((_url.searchParams.has('singbox') || _url.searchParams.has('sb')) && !userAgent.includes('subConverter')); 1579 | } 1580 | 1581 | /** 1582 | * 1583 | * @param {*} uniqueIpTxt 1584 | * @param {*} noTLS 1585 | * @param {*} host 1586 | * @param {*} uuid 1587 | * @returns 1588 | */ 1589 | function splitNodeData(uniqueIpTxt, noTLS, host, uuid, userAgent, protType) { 1590 | // Regex to match IPv4 and IPv6 1591 | // const regex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[.*\]):?(\d+)?#?(.*)?$/; 1592 | const regex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[.*\]):?(\d+)?#?([^@#]*)@?(.*)?$/; 1593 | 1594 | // Region codes mapped to corresponding emojis 1595 | const regionMap = { 1596 | 'SG': '🇸🇬 SG', 1597 | 'HK': '🇭🇰 HK', 1598 | 'KR': '🇰🇷 KR', 1599 | 'JP': '🇯🇵 JP', 1600 | 'GB': '🇬🇧 GB', 1601 | 'US': '🇺🇸 US', 1602 | 'TW': '🇼🇸 TW', 1603 | 'CF': '📶 CF' 1604 | }; 1605 | 1606 | const responseBody = uniqueIpTxt.map(ipTxt => { 1607 | // console.log(`splitNodeData---> ipTxt: ${ipTxt} `); 1608 | let address = ipTxt; 1609 | let port = "443"; 1610 | let remarks = ""; 1611 | let proxyip = ""; 1612 | 1613 | const match = address.match(regex); 1614 | if (match && !ipTxt.includes('@am_clubs')) { 1615 | address = match[1]; 1616 | port = match[2] || port; 1617 | remarks = match[3] || address || host; 1618 | proxyip = match[4] || ''; 1619 | // console.log(`splitNodeData--match-> \n address: ${address} \n port: ${port} \n remarks: ${remarks} \n proxyip: ${proxyip}`); 1620 | } else { 1621 | let ip, newPort, extra; 1622 | 1623 | if (ipTxt.includes('@') && !ipTxt.includes('@am_clubs')) { 1624 | const [addressPart, proxyipPart] = ipTxt.split('@'); 1625 | ipTxt = addressPart; 1626 | proxyip = proxyipPart; 1627 | // console.log(`splitNodeData-ipTxt.includes('@')--> ipTxt: ${ipTxt} \n proxyip: ${proxyip} `); 1628 | } 1629 | if (ipTxt.includes(':') && ipTxt.includes('#')) { 1630 | [ip, newPort, extra] = ipTxt.split(/[:#]/); 1631 | } else if (ipTxt.includes(':')) { 1632 | [ip, newPort] = ipTxt.split(':'); 1633 | } else if (ipTxt.includes('#')) { 1634 | [ip, extra] = ipTxt.split('#'); 1635 | } else { 1636 | ip = ipTxt; 1637 | } 1638 | 1639 | address = ip; 1640 | port = newPort || port; 1641 | remarks = extra || address || host; 1642 | // console.log(`splitNodeData---> \n address: ${address} \n port: ${port} \n remarks: ${remarks} \n proxyip: ${proxyip}`); 1643 | } 1644 | 1645 | // Replace region code with corresponding emoji 1646 | remarks = regionMap[remarks] || remarks; 1647 | 1648 | // Check if TLS is disabled and if the port is in the allowed set 1649 | if (noTLS !== 'true' && portSet_http.has(parseInt(port))) { 1650 | return null; // Skip this iteration 1651 | } 1652 | 1653 | const [v2ray, clash] = getConfigLink(uuid, host, address, port, remarks, proxyip, protType); 1654 | return v2ray; 1655 | }).filter(Boolean).join('\n'); 1656 | 1657 | // let base64Response = responseBody; 1658 | // return btoa(base64Response); 1659 | return responseBody; 1660 | } 1661 | 1662 | /** ---------------------Get CF data------------------------------ */ 1663 | 1664 | async function getCFConfig(email, key, accountIndex) { 1665 | try { 1666 | const now = new Date(); 1667 | const today = new Date(now); 1668 | today.setHours(0, 0, 0, 0); 1669 | 1670 | // Calculate default value 1671 | const ud = Math.floor(((now - today.getTime()) / 86400000) * 24 * 1099511627776 / 2); 1672 | let upload = ud; 1673 | let download = ud; 1674 | let total = 24 * 1099511627776; 1675 | 1676 | if (email && key) { 1677 | const accountId = await getAccountId(email, key); 1678 | if (accountId) { 1679 | // Calculate start and end time 1680 | now.setUTCHours(0, 0, 0, 0); 1681 | const startDate = now.toISOString(); 1682 | const endDate = new Date().toISOString(); 1683 | 1684 | // Get summary data 1685 | const [pagesSumResult, workersSumResult] = await getCFSum(accountId, accountIndex, email, key, startDate, endDate); 1686 | upload = pagesSumResult; 1687 | download = workersSumResult; 1688 | total = 102400; 1689 | } 1690 | } 1691 | 1692 | return { upload, download, total }; 1693 | } catch (error) { 1694 | console.error('Error in getCFConfig:', error); 1695 | return { upload: 0, download: 0, total: 0 }; 1696 | } 1697 | } 1698 | 1699 | /** 1700 | * 1701 | * @param {*} email 1702 | * @param {*} key 1703 | * @returns 1704 | */ 1705 | async function getAccountId(email, key) { 1706 | try { 1707 | const url = 'https://api.cloudflare.com/client/v4/accounts'; 1708 | const headers = { 1709 | 'X-AUTH-EMAIL': email, 1710 | 'X-AUTH-KEY': key 1711 | }; 1712 | 1713 | const response = await fetch(url, { headers }); 1714 | 1715 | if (!response.ok) { 1716 | throw new Error(`HTTP error! status: ${response.status}`); 1717 | } 1718 | 1719 | const data = await response.json(); 1720 | //console.error('getAccountId-->', data); 1721 | 1722 | return data?.result?.[0]?.id || false; 1723 | } catch (error) { 1724 | console.error('Error fetching account ID:', error); 1725 | return false; 1726 | } 1727 | } 1728 | 1729 | /** 1730 | * 1731 | * @param {*} accountId 1732 | * @param {*} accountIndex 1733 | * @param {*} email 1734 | * @param {*} key 1735 | * @param {*} startDate 1736 | * @param {*} endDate 1737 | * @returns 1738 | */ 1739 | async function getCFSum(accountId, accountIndex, email, key, startDate, endDate) { 1740 | try { 1741 | const [startDateISO, endDateISO] = [new Date(startDate), new Date(endDate)].map(d => d.toISOString()); 1742 | 1743 | const query = JSON.stringify({ 1744 | query: `query getBillingMetrics($accountId: String!, $filter: AccountWorkersInvocationsAdaptiveFilter_InputObject) { 1745 | viewer { 1746 | accounts(filter: {accountTag: $accountId}) { 1747 | pagesFunctionsInvocationsAdaptiveGroups(limit: 1000, filter: $filter) { 1748 | sum { 1749 | requests 1750 | } 1751 | } 1752 | workersInvocationsAdaptive(limit: 10000, filter: $filter) { 1753 | sum { 1754 | requests 1755 | } 1756 | } 1757 | } 1758 | } 1759 | }`, 1760 | variables: { 1761 | accountId, 1762 | filter: { datetime_geq: startDateISO, datetime_leq: endDateISO } 1763 | }, 1764 | }); 1765 | 1766 | const headers = { 1767 | 'Content-Type': 'application/json', 1768 | 'X-AUTH-EMAIL': email, 1769 | 'X-AUTH-KEY': key 1770 | }; 1771 | 1772 | const response = await fetch('https://api.cloudflare.com/client/v4/graphql', { 1773 | method: 'POST', 1774 | headers, 1775 | body: query 1776 | }); 1777 | 1778 | if (!response.ok) { 1779 | throw new Error(`HTTP error! status: ${response.status}`); 1780 | } 1781 | 1782 | const res = await response.json(); 1783 | const accounts = res?.data?.viewer?.accounts?.[accountIndex]; 1784 | 1785 | if (!accounts) { 1786 | throw new Error('找不到账户数据'); 1787 | } 1788 | 1789 | const getSumRequests = (data) => data?.reduce((total, item) => total + (item?.sum?.requests || 0), 0) || 0; 1790 | 1791 | const pagesSum = getSumRequests(accounts.pagesFunctionsInvocationsAdaptiveGroups); 1792 | const workersSum = getSumRequests(accounts.workersInvocationsAdaptive); 1793 | 1794 | return [pagesSum, workersSum]; 1795 | 1796 | } catch (error) { 1797 | console.error('Error fetching billing metrics:', error); 1798 | return [0, 0]; 1799 | } 1800 | } 1801 | 1802 | const MY_KV_UUID_KEY = atob('VVVJRA=='); 1803 | async function checkKVNamespaceBinding(env) { 1804 | if (typeof env.amclubs === 'undefined') { 1805 | return new Response('Error: amclubs KV_NAMESPACE is not bound.', { 1806 | status: 400, 1807 | }) 1808 | } 1809 | } 1810 | 1811 | async function getKVData(env) { 1812 | const value = await env.amclubs.get(MY_KV_UUID_KEY); 1813 | return value ? String(value) : ''; 1814 | // return new Response(value || 'Key not found', { 1815 | // status: value ? 200 : 404 1816 | // }); 1817 | } 1818 | 1819 | async function setKVData(request, env) { 1820 | if (request.method !== 'POST') { 1821 | return new Response('Use POST method to set values', { status: 405 }); 1822 | } 1823 | 1824 | const value = await request.text(); 1825 | // console.log(`setKVData----> Received value: ${value} \n`); 1826 | 1827 | try { 1828 | await env.amclubs.put(MY_KV_UUID_KEY, value); 1829 | 1830 | // 读取存入的值,确认是否成功 1831 | const storedValue = await env.amclubs.get(MY_KV_UUID_KEY); 1832 | if (storedValue === value) { 1833 | return new Response(`${MY_KV_UUID_KEY} updated successfully`, { status: 200 }); 1834 | } else { 1835 | return new Response(`Error: Value verification failed after storage`, { status: 500 }); 1836 | } 1837 | } catch (error) { 1838 | return new Response(`Error storing value: ${error.message}`, { status: 500 }); 1839 | } 1840 | } 1841 | 1842 | async function showKVPage(env) { 1843 | const kvCheckResponse = await checkKVNamespaceBinding(env); 1844 | if (kvCheckResponse) { 1845 | return kvCheckResponse; 1846 | } 1847 | const value = await getKVData(env); 1848 | return new Response( 1849 | ` 1850 | 1851 | 1852 | 1853 | ${fileName} 1854 | 1908 | 1923 | 1924 | 1925 |
1926 |

UUID 页面

1927 | 1928 | 1929 |

1930 | 1931 | 1932 |

1933 | 1934 |
1935 |
1936 | 1937 | `, 1938 | { 1939 | headers: { 'Content-Type': 'text/html; charset=UTF-8' }, 1940 | status: 200, 1941 | } 1942 | ); 1943 | } 1944 | 1945 | 1946 | const API_URL = 'http://ip-api.com/json/'; 1947 | const TELEGRAM_API_URL = 'https://api.telegram.org/bot'; 1948 | /** 1949 | * Send message to Telegram channel 1950 | * @param {string} type 1951 | * @param {string} ip I 1952 | * @param {string} [add_data=""] 1953 | */ 1954 | async function sendMessage(type, ip, add_data = "") { 1955 | if (botToken && chatID) { 1956 | try { 1957 | const ipResponse = await fetch(`${API_URL}${ip}?lang=zh-CN`); 1958 | let msg = `${type}\nIP: ${ip}\n${add_data}`; 1959 | 1960 | if (ipResponse.ok) { 1961 | const ipInfo = await ipResponse.json(); 1962 | msg = `${type}\nIP: ${ip}\n国家: ${ipInfo.country}\n城市: ${ipInfo.city}\n组织: ${ipInfo.org}\nASN: ${ipInfo.as}\n${add_data}`; 1963 | } else { 1964 | console.error(`Failed to fetch IP info. Status: ${ipResponse.status}`); 1965 | } 1966 | 1967 | const telegramUrl = `${TELEGRAM_API_URL}${botToken}/sendMessage`; 1968 | const params = new URLSearchParams({ 1969 | chat_id: chatID, 1970 | parse_mode: 'HTML', 1971 | text: msg 1972 | }); 1973 | 1974 | await fetch(`${telegramUrl}?${params.toString()}`, { 1975 | method: 'GET', 1976 | headers: { 1977 | 'Accept': 'text/html,application/xhtml+xml,application/xml', 1978 | 'Accept-Encoding': 'gzip, deflate, br', 1979 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36' 1980 | } 1981 | }); 1982 | 1983 | } catch (error) { 1984 | console.error('Error sending message:', error); 1985 | } 1986 | } else { 1987 | console.warn('botToken or chatID is missing.'); 1988 | } 1989 | } 1990 | 1991 | 1992 | /** -------------------processing logic-------------------------------- */ 1993 | /** 1994 | * Handles channel over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the channel header. 1995 | * @param {import("@cloudflare/workers-types").Request} request The incoming request object. 1996 | * @returns {Promise} A Promise that resolves to a WebSocket response object. 1997 | */ 1998 | async function channelOverWSHandler(request) { 1999 | const webSocketPair = new WebSocketPair(); 2000 | const [client, webSocket] = Object.values(webSocketPair); 2001 | webSocket.accept(); 2002 | 2003 | let address = ''; 2004 | let portWithRandomLog = ''; 2005 | let currentDate = new Date(); 2006 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { 2007 | console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || ''); 2008 | }; 2009 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; 2010 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); 2011 | 2012 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ 2013 | let remoteSocketWapper = { 2014 | value: null, 2015 | }; 2016 | let udpStreamWrite = null; 2017 | let isDns = false; 2018 | 2019 | // ws --> remote 2020 | readableWebSocketStream.pipeTo(new WritableStream({ 2021 | async write(chunk, controller) { 2022 | if (isDns && udpStreamWrite) { 2023 | return udpStreamWrite(chunk); 2024 | } 2025 | if (remoteSocketWapper.value) { 2026 | const writer = remoteSocketWapper.value.writable.getWriter() 2027 | await writer.write(chunk); 2028 | writer.releaseLock(); 2029 | return; 2030 | } 2031 | 2032 | const { 2033 | hasError, 2034 | //message, 2035 | portRemote = 443, 2036 | addressRemote = '', 2037 | rawDataIndex, 2038 | channelVersion = new Uint8Array([0, 0]), 2039 | isUDP, 2040 | addressType, 2041 | } = processchannelHeader(chunk, userID); 2042 | address = addressRemote; 2043 | portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `; 2044 | 2045 | if (hasError) { 2046 | throw new Error(message); 2047 | } 2048 | 2049 | // If UDP and not DNS port, close it 2050 | if (isUDP && portRemote !== 53) { 2051 | throw new Error('UDP proxy only enabled for DNS which is port 53'); 2052 | } 2053 | 2054 | if (isUDP && portRemote === 53) { 2055 | isDns = true; 2056 | } 2057 | 2058 | const channelResponseHeader = new Uint8Array([channelVersion[0], 0]); 2059 | const rawClientData = chunk.slice(rawDataIndex); 2060 | 2061 | if (isDns) { 2062 | const { write } = await handleUDPOutBound(webSocket, channelResponseHeader, log); 2063 | udpStreamWrite = write; 2064 | udpStreamWrite(rawClientData); 2065 | return; 2066 | } 2067 | log(`processchannelHeader-->${addressType} Processing TCP outbound connection ${addressRemote}:${portRemote}`); 2068 | 2069 | handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, channelResponseHeader, log, addressType); 2070 | }, 2071 | close() { 2072 | log(`readableWebSocketStream is close`); 2073 | }, 2074 | abort(reason) { 2075 | log(`readableWebSocketStream is abort`, JSON.stringify(reason)); 2076 | }, 2077 | })).catch((err) => { 2078 | log('readableWebSocketStream pipeTo error', err); 2079 | }); 2080 | 2081 | return new Response(null, { 2082 | status: 101, 2083 | webSocket: client, 2084 | }); 2085 | } 2086 | 2087 | /** 2088 | * Handles channel over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the channel header. 2089 | * @param {import("@cloudflare/workers-types").Request} request The incoming request object. 2090 | * @returns {Promise} A Promise that resolves to a WebSocket response object. 2091 | */ 2092 | async function channelOverWSHandlerTro(request) { 2093 | const webSocketPair = new WebSocketPair(); 2094 | const [client, webSocket] = Object.values(webSocketPair); 2095 | webSocket.accept(); 2096 | 2097 | let address = ""; 2098 | let portWithRandomLog = ""; 2099 | const remoteSocketWrapper = { value: null }; 2100 | let udpStreamWrite = null; 2101 | 2102 | // Logging function 2103 | const log = (info, event = "") => { 2104 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event); 2105 | }; 2106 | 2107 | // Get early data WebSocket protocol header 2108 | const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; 2109 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); 2110 | 2111 | // Handle WebSocket data 2112 | const handleStreamData = async (chunk) => { 2113 | if (udpStreamWrite) { 2114 | return udpStreamWrite(chunk); 2115 | } 2116 | 2117 | if (remoteSocketWrapper.value) { 2118 | const writer = remoteSocketWrapper.value.writable.getWriter(); 2119 | await writer.write(chunk); 2120 | writer.releaseLock(); 2121 | return; 2122 | } 2123 | 2124 | // Parse channel protocol header 2125 | const { hasError, message, portRemote = 443, addressRemote = "", rawClientData, addressType } = await processchannelHeaderTro(chunk, userID); 2126 | address = addressRemote; 2127 | portWithRandomLog = `${portRemote}--${Math.random()} tcp`; 2128 | 2129 | if (hasError) { 2130 | throw new Error(message); 2131 | } 2132 | 2133 | // Handle TCP outbound connection 2134 | handleTCPOutBound(remoteSocketWrapper, addressRemote, portRemote, rawClientData, webSocket, null, log, addressType); 2135 | }; 2136 | 2137 | // WebSocket stream pipe 2138 | readableWebSocketStream.pipeTo( 2139 | new WritableStream({ 2140 | write: handleStreamData, 2141 | close: () => log("readableWebSocketStream is closed"), 2142 | abort: (reason) => log("readableWebSocketStream is aborted", JSON.stringify(reason)), 2143 | }) 2144 | ).catch((err) => { 2145 | log("readableWebSocketStream pipeTo error", err); 2146 | }); 2147 | 2148 | return new Response(null, { 2149 | status: 101, 2150 | // @ts-ignore 2151 | webSocket: client 2152 | }); 2153 | } 2154 | 2155 | /** 2156 | * Handles outbound TCP connections. 2157 | * 2158 | * @param {any} remoteSocket 2159 | * @param {string} addressRemote The remote address to connect to. 2160 | * @param {number} portRemote The remote port to connect to. 2161 | * @param {Uint8Array} rawClientData The raw client data to write. 2162 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. 2163 | * @param {Uint8Array} channelResponseHeader The channel response header. 2164 | * @param {function} log The logging function. 2165 | * @returns {Promise} The remote socket. 2166 | */ 2167 | async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, channelResponseHeader, log, addressType,) { 2168 | 2169 | /** 2170 | * Connects to a given address and port and writes data to the socket. 2171 | * @param {string} address The address to connect to. 2172 | * @param {number} port The port to connect to. 2173 | * @returns {Promise} A Promise that resolves to the connected socket. 2174 | */ 2175 | async function connectAndWrite(address, port, socks = false) { 2176 | /** @type {import("@cloudflare/workers-types").Socket} */ 2177 | const tcpSocket = socks ? await socks5Connect(addressType, address, port, log) 2178 | : connect({ 2179 | hostname: address, 2180 | port: port, 2181 | }); 2182 | remoteSocket.value = tcpSocket; 2183 | console.log(`connectAndWrite-${socks} connected to ${address}:${port}`); 2184 | const writer = tcpSocket.writable.getWriter(); 2185 | await writer.write(rawClientData); 2186 | writer.releaseLock(); 2187 | return tcpSocket; 2188 | } 2189 | 2190 | /** 2191 | * Retries connecting to the remote address and port if the Cloudflare socket has no incoming data. 2192 | * @returns {Promise} A Promise that resolves when the retry is complete. 2193 | */ 2194 | async function retry() { 2195 | const tcpSocket = socks5Enable ? await connectAndWrite(addressRemote, portRemote, true) : await connectAndWrite(proxyIP || addressRemote, proxyPort || portRemote); 2196 | 2197 | console.log(`retry-${socks5Enable} connected to ${addressRemote}:${portRemote}`); 2198 | tcpSocket.closed.catch(error => { 2199 | console.log('retry tcpSocket closed error', error); 2200 | }).finally(() => { 2201 | safeCloseWebSocket(webSocket); 2202 | }) 2203 | remoteSocketToWS(tcpSocket, webSocket, channelResponseHeader, null, log); 2204 | } 2205 | 2206 | const tcpSocket = await connectAndWrite(addressRemote, portRemote); 2207 | 2208 | // when remoteSocket is ready, pass to websocket 2209 | // remote--> ws 2210 | remoteSocketToWS(tcpSocket, webSocket, channelResponseHeader, retry, log); 2211 | } 2212 | 2213 | /** 2214 | * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket. 2215 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from. 2216 | * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT. 2217 | * @param {(info: string)=> void} log The logging function. 2218 | * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket. 2219 | */ 2220 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { 2221 | let readableStreamCancel = false; 2222 | const stream = new ReadableStream({ 2223 | start(controller) { 2224 | webSocketServer.addEventListener('message', (event) => { 2225 | const message = event.data; 2226 | controller.enqueue(message); 2227 | }); 2228 | 2229 | webSocketServer.addEventListener('close', () => { 2230 | safeCloseWebSocket(webSocketServer); 2231 | controller.close(); 2232 | }); 2233 | 2234 | webSocketServer.addEventListener('error', (err) => { 2235 | log('webSocketServer has error'); 2236 | controller.error(err); 2237 | }); 2238 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); 2239 | if (error) { 2240 | controller.error(error); 2241 | } else if (earlyData) { 2242 | controller.enqueue(earlyData); 2243 | } 2244 | }, 2245 | 2246 | pull(controller) { 2247 | // if ws can stop read if stream is full, we can implement backpressure 2248 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure 2249 | }, 2250 | 2251 | cancel(reason) { 2252 | log(`ReadableStream was canceled, due to ${reason}`) 2253 | readableStreamCancel = true; 2254 | safeCloseWebSocket(webSocketServer); 2255 | } 2256 | }); 2257 | 2258 | return stream; 2259 | } 2260 | 2261 | // https://xtls.github.io/development/protocols/channel.html 2262 | 2263 | /** 2264 | * Processes the channel header buffer and returns an object with the relevant information. 2265 | * @param {ArrayBuffer} channelBuffer The channel header buffer to process. 2266 | * @param {string} userID The user ID to validate against the UUID in the channel header. 2267 | * @returns {{ 2268 | * hasError: boolean, 2269 | * message?: string, 2270 | * addressRemote?: string, 2271 | * addressType?: number, 2272 | * portRemote?: number, 2273 | * rawDataIndex?: number, 2274 | * channelVersion?: Uint8Array, 2275 | * isUDP?: boolean 2276 | * }} An object with the relevant information extracted from the channel header buffer. 2277 | */ 2278 | function processchannelHeader(channelBuffer, userID) { 2279 | if (channelBuffer.byteLength < 24) { 2280 | return { 2281 | hasError: true, 2282 | message: 'invalid data', 2283 | }; 2284 | } 2285 | 2286 | const version = new Uint8Array(channelBuffer.slice(0, 1)); 2287 | let isValidUser = false; 2288 | let isUDP = false; 2289 | const slicedBuffer = new Uint8Array(channelBuffer.slice(1, 17)); 2290 | const slicedBufferString = stringify(slicedBuffer); 2291 | // check if userID is valid uuid or uuids split by , and contains userID in it otherwise return error message to console 2292 | const uuids = userID.includes(',') ? userID.split(",") : [userID]; 2293 | // uuid_validator(hostName, slicedBufferString); 2294 | 2295 | 2296 | // isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()); 2297 | isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim(); 2298 | 2299 | console.log(`userID: ${slicedBufferString}`); 2300 | 2301 | if (!isValidUser) { 2302 | return { 2303 | hasError: true, 2304 | message: 'invalid user', 2305 | }; 2306 | } 2307 | 2308 | const optLength = new Uint8Array(channelBuffer.slice(17, 18))[0]; 2309 | //skip opt for now 2310 | 2311 | const command = new Uint8Array( 2312 | channelBuffer.slice(18 + optLength, 18 + optLength + 1) 2313 | )[0]; 2314 | 2315 | // 0x01 TCP 2316 | // 0x02 UDP 2317 | // 0x03 MUX 2318 | if (command === 1) { 2319 | isUDP = false; 2320 | } else if (command === 2) { 2321 | isUDP = true; 2322 | } else { 2323 | return { 2324 | hasError: true, 2325 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, 2326 | }; 2327 | } 2328 | const portIndex = 18 + optLength + 1; 2329 | const portBuffer = channelBuffer.slice(portIndex, portIndex + 2); 2330 | // port is big-Endian in raw data etc 80 == 0x005d 2331 | const portRemote = new DataView(portBuffer).getUint16(0); 2332 | 2333 | let addressIndex = portIndex + 2; 2334 | const addressBuffer = new Uint8Array( 2335 | channelBuffer.slice(addressIndex, addressIndex + 1) 2336 | ); 2337 | 2338 | // 1--> ipv4 addressLength =4 2339 | // 2--> domain name addressLength=addressBuffer[1] 2340 | // 3--> ipv6 addressLength =16 2341 | const addressType = addressBuffer[0]; 2342 | let addressLength = 0; 2343 | let addressValueIndex = addressIndex + 1; 2344 | let addressValue = ''; 2345 | switch (addressType) { 2346 | case 1: 2347 | addressLength = 4; 2348 | addressValue = new Uint8Array( 2349 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 2350 | ).join('.'); 2351 | break; 2352 | case 2: 2353 | addressLength = new Uint8Array( 2354 | channelBuffer.slice(addressValueIndex, addressValueIndex + 1) 2355 | )[0]; 2356 | addressValueIndex += 1; 2357 | addressValue = new TextDecoder().decode( 2358 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 2359 | ); 2360 | break; 2361 | case 3: 2362 | addressLength = 16; 2363 | const dataView = new DataView( 2364 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 2365 | ); 2366 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 2367 | const ipv6 = []; 2368 | for (let i = 0; i < 8; i++) { 2369 | ipv6.push(dataView.getUint16(i * 2).toString(16)); 2370 | } 2371 | addressValue = ipv6.join(':'); 2372 | // seems no need add [] for ipv6 2373 | break; 2374 | default: 2375 | return { 2376 | hasError: true, 2377 | message: `invild addressType is ${addressType}`, 2378 | }; 2379 | } 2380 | if (!addressValue) { 2381 | return { 2382 | hasError: true, 2383 | message: `addressValue is empty, addressType is ${addressType}`, 2384 | }; 2385 | } 2386 | 2387 | return { 2388 | hasError: false, 2389 | addressRemote: addressValue, 2390 | portRemote, 2391 | rawDataIndex: addressValueIndex + addressLength, 2392 | channelVersion: version, 2393 | isUDP, 2394 | addressType, 2395 | }; 2396 | } 2397 | 2398 | /** 2399 | * Processes the channel header buffer and returns an object with the relevant information. 2400 | * @param {ArrayBuffer} channelBuffer The channel header buffer to process. 2401 | * @param {string} userID The user ID to validate against the UUID in the channel header. 2402 | * @returns {{ 2403 | * hasError: boolean, 2404 | * message?: string, 2405 | * addressRemote?: string, 2406 | * addressType?: number, 2407 | * portRemote?: number, 2408 | * rawDataIndex?: number, 2409 | * channelVersion?: Uint8Array, 2410 | * isUDP?: boolean 2411 | * }} An object with the relevant information extracted from the channel header buffer. 2412 | */ 2413 | async function processchannelHeaderTro(buffer, userID) { 2414 | if (buffer.byteLength < 56) { 2415 | return { 2416 | hasError: true, 2417 | message: "invalid data" 2418 | }; 2419 | } 2420 | let crLfIndex = 56; 2421 | if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) { 2422 | return { 2423 | hasError: true, 2424 | message: "invalid header format (missing CR LF)" 2425 | }; 2426 | } 2427 | const password = new TextDecoder().decode(buffer.slice(0, crLfIndex)); 2428 | if (password !== sha256.sha224(userID)) { 2429 | return { 2430 | hasError: true, 2431 | message: "invalid password" 2432 | }; 2433 | } 2434 | 2435 | const socks5DataBuffer = buffer.slice(crLfIndex + 2); 2436 | if (socks5DataBuffer.byteLength < 6) { 2437 | return { 2438 | hasError: true, 2439 | message: "invalid SOCKS5 request data" 2440 | }; 2441 | } 2442 | 2443 | const view = new DataView(socks5DataBuffer); 2444 | const cmd = view.getUint8(0); 2445 | if (cmd !== 1) { 2446 | return { 2447 | hasError: true, 2448 | message: "unsupported command, only TCP (CONNECT) is allowed" 2449 | }; 2450 | } 2451 | 2452 | const addressType = view.getUint8(1); 2453 | // 0x01: IPv4 address 2454 | // 0x03: Domain name 2455 | // 0x04: IPv6 address 2456 | let addressLength = 0; 2457 | let addressIndex = 2; 2458 | let address = ""; 2459 | switch (addressType) { 2460 | case 1: 2461 | addressLength = 4; 2462 | address = new Uint8Array( 2463 | socks5DataBuffer.slice(addressIndex, addressIndex + addressLength) 2464 | ).join("."); 2465 | break; 2466 | case 3: 2467 | addressLength = new Uint8Array( 2468 | socks5DataBuffer.slice(addressIndex, addressIndex + 1) 2469 | )[0]; 2470 | addressIndex += 1; 2471 | address = new TextDecoder().decode( 2472 | socks5DataBuffer.slice(addressIndex, addressIndex + addressLength) 2473 | ); 2474 | break; 2475 | case 4: 2476 | addressLength = 16; 2477 | const dataView = new DataView(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); 2478 | const ipv6 = []; 2479 | for (let i = 0; i < 8; i++) { 2480 | ipv6.push(dataView.getUint16(i * 2).toString(16)); 2481 | } 2482 | address = ipv6.join(":"); 2483 | break; 2484 | default: 2485 | return { 2486 | hasError: true, 2487 | message: `invalid addressType is ${addressType}` 2488 | }; 2489 | } 2490 | 2491 | if (!address) { 2492 | return { 2493 | hasError: true, 2494 | message: `address is empty, addressType is ${addressType}` 2495 | }; 2496 | } 2497 | 2498 | const portIndex = addressIndex + addressLength; 2499 | const portBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2); 2500 | const portRemote = new DataView(portBuffer).getUint16(0); 2501 | return { 2502 | hasError: false, 2503 | addressRemote: address, 2504 | portRemote, 2505 | rawClientData: socks5DataBuffer.slice(portIndex + 4), 2506 | addressType: addressType 2507 | }; 2508 | } 2509 | 2510 | /** 2511 | * Converts a remote socket to a WebSocket connection. 2512 | * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert. 2513 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to. 2514 | * @param {ArrayBuffer | null} channelResponseHeader The channel response header. 2515 | * @param {(() => Promise) | null} retry The function to retry the connection if it fails. 2516 | * @param {(info: string) => void} log The logging function. 2517 | * @returns {Promise} A Promise that resolves when the conversion is complete. 2518 | */ 2519 | async function remoteSocketToWS(remoteSocket, webSocket, channelResponseHeader, retry, log) { 2520 | // remote--> ws 2521 | let remoteChunkCount = 0; 2522 | let chunks = []; 2523 | /** @type {ArrayBuffer | null} */ 2524 | let channelHeader = channelResponseHeader; 2525 | let hasIncomingData = false; // check if remoteSocket has incoming data 2526 | await remoteSocket.readable 2527 | .pipeTo( 2528 | new WritableStream({ 2529 | start() { 2530 | }, 2531 | /** 2532 | * 2533 | * @param {Uint8Array} chunk 2534 | * @param {*} controller 2535 | */ 2536 | async write(chunk, controller) { 2537 | hasIncomingData = true; 2538 | remoteChunkCount++; 2539 | if (webSocket.readyState !== WS_READY_STATE_OPEN) { 2540 | controller.error( 2541 | 'webSocket.readyState is not open, maybe close' 2542 | ); 2543 | } 2544 | if (channelHeader) { 2545 | webSocket.send(await new Blob([channelHeader, chunk]).arrayBuffer()); 2546 | channelHeader = null; 2547 | } else { 2548 | // console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`); 2549 | // seems no need rate limit this, CF seems fix this??.. 2550 | // if (remoteChunkCount > 20000) { 2551 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M 2552 | // await delay(1); 2553 | // } 2554 | webSocket.send(chunk); 2555 | } 2556 | }, 2557 | close() { 2558 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); 2559 | // 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. 2560 | }, 2561 | abort(reason) { 2562 | console.error(`remoteConnection!.readable abort`, reason); 2563 | }, 2564 | }) 2565 | ) 2566 | .catch((error) => { 2567 | console.error( 2568 | `remoteSocketToWS has exception `, 2569 | error.stack || error 2570 | ); 2571 | safeCloseWebSocket(webSocket); 2572 | }); 2573 | 2574 | // seems is cf connect socket have error, 2575 | // 1. Socket.closed will have error 2576 | // 2. Socket.readable will be close without any data coming 2577 | if (hasIncomingData === false && retry) { 2578 | log(`retry`) 2579 | retry(); 2580 | } 2581 | } 2582 | 2583 | const WS_READY_STATE_OPEN = 1; 2584 | const WS_READY_STATE_CLOSING = 2; 2585 | /** 2586 | * Closes a WebSocket connection safely without throwing exceptions. 2587 | * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close. 2588 | */ 2589 | function safeCloseWebSocket(socket) { 2590 | try { 2591 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { 2592 | socket.close(); 2593 | } 2594 | } catch (error) { 2595 | console.error('safeCloseWebSocket error', error); 2596 | } 2597 | } 2598 | 2599 | /** 2600 | * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection. 2601 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over. 2602 | * @param {ArrayBuffer} channelResponseHeader The channel response header. 2603 | * @param {(string) => void} log The logging function. 2604 | * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream. 2605 | */ 2606 | async function handleUDPOutBound(webSocket, channelResponseHeader, log) { 2607 | 2608 | let ischannelHeaderSent = false; 2609 | const transformStream = new TransformStream({ 2610 | start(controller) { 2611 | 2612 | }, 2613 | transform(chunk, controller) { 2614 | // udp message 2 byte is the the length of udp data 2615 | // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message 2616 | for (let index = 0; index < chunk.byteLength;) { 2617 | const lengthBuffer = chunk.slice(index, index + 2); 2618 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); 2619 | const udpData = new Uint8Array( 2620 | chunk.slice(index + 2, index + 2 + udpPakcetLength) 2621 | ); 2622 | index = index + 2 + udpPakcetLength; 2623 | controller.enqueue(udpData); 2624 | } 2625 | }, 2626 | flush(controller) { 2627 | } 2628 | }); 2629 | 2630 | // only handle dns udp for now 2631 | transformStream.readable.pipeTo(new WritableStream({ 2632 | async write(chunk) { 2633 | const resp = await fetch(dohURL, // dns server url 2634 | { 2635 | method: 'POST', 2636 | headers: { 2637 | 'content-type': 'application/dns-message', 2638 | }, 2639 | body: chunk, 2640 | }) 2641 | const dnsQueryResult = await resp.arrayBuffer(); 2642 | const udpSize = dnsQueryResult.byteLength; 2643 | // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); 2644 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); 2645 | if (webSocket.readyState === WS_READY_STATE_OPEN) { 2646 | log(`doh success and dns message length is ${udpSize}`); 2647 | if (ischannelHeaderSent) { 2648 | webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 2649 | } else { 2650 | webSocket.send(await new Blob([channelResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 2651 | ischannelHeaderSent = true; 2652 | } 2653 | } 2654 | } 2655 | })).catch((error) => { 2656 | log('dns udp has error' + error) 2657 | }); 2658 | 2659 | const writer = transformStream.writable.getWriter(); 2660 | 2661 | return { 2662 | /** 2663 | * 2664 | * @param {Uint8Array} chunk 2665 | */ 2666 | write(chunk) { 2667 | writer.write(chunk); 2668 | } 2669 | }; 2670 | } 2671 | 2672 | /** 2673 | * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection. 2674 | * @param {ArrayBuffer} udpChunk - DNS query data sent from the client. 2675 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket - The WebSocket connection to send the DNS queries over. 2676 | * @param {ArrayBuffer} channelResponseHeader - The channel response header. 2677 | * @param {(string) => void} log - The logging function. 2678 | * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream. 2679 | */ 2680 | async function handleDNSQuery(udpChunk, webSocket, channelResponseHeader, log) { 2681 | try { 2682 | const dnsServer = '8.8.4.4'; 2683 | const dnsPort = 53; 2684 | let channelHeader = channelResponseHeader; 2685 | 2686 | const tcpSocket = connect({ hostname: dnsServer, port: dnsPort }); 2687 | log(`Connected to ${dnsServer}:${dnsPort}`); 2688 | 2689 | const writer = tcpSocket.writable.getWriter(); 2690 | await writer.write(udpChunk); 2691 | writer.releaseLock(); 2692 | 2693 | await tcpSocket.readable.pipeTo(new WritableStream({ 2694 | async write(chunk) { 2695 | if (webSocket.readyState === WS_READY_STATE_OPEN) { 2696 | const dataToSend = channelHeader ? await new Blob([channelHeader, chunk]).arrayBuffer() : chunk; 2697 | webSocket.send(dataToSend); 2698 | channelHeader = null; 2699 | } 2700 | }, 2701 | close() { 2702 | log(`TCP connection to DNS server (${dnsServer}) closed`); 2703 | }, 2704 | abort(reason) { 2705 | console.error(`TCP connection to DNS server (${dnsServer}) aborted`, reason); 2706 | }, 2707 | })); 2708 | } catch (error) { 2709 | console.error(`Exception in handleDNSQuery function: ${error.message}`); 2710 | } 2711 | } 2712 | 2713 | 2714 | async function socks5Connect(ipType, remoteIp, remotePort, log) { 2715 | const { username, password, hostname, port } = parsedSocks5; 2716 | const socket = connect({ hostname, port }); 2717 | const writer = socket.writable.getWriter(); 2718 | const reader = socket.readable.getReader(); 2719 | const encoder = new TextEncoder(); 2720 | 2721 | const sendSocksGreeting = async () => { 2722 | const greeting = new Uint8Array([5, 2, 0, 2]); 2723 | await writer.write(greeting); 2724 | console.log('SOCKS5 greeting sent'); 2725 | }; 2726 | 2727 | const handleAuthResponse = async () => { 2728 | const res = (await reader.read()).value; 2729 | if (res[1] === 0x02) { 2730 | console.log("SOCKS5 server requires authentication"); 2731 | if (!username || !password) { 2732 | console.log("Please provide username and password"); 2733 | throw new Error("Authentication required"); 2734 | } 2735 | const authRequest = new Uint8Array([ 2736 | 1, username.length, ...encoder.encode(username), 2737 | password.length, ...encoder.encode(password) 2738 | ]); 2739 | await writer.write(authRequest); 2740 | const authResponse = (await reader.read()).value; 2741 | if (authResponse[0] !== 0x01 || authResponse[1] !== 0x00) { 2742 | console.log("SOCKS5 server authentication failed"); 2743 | throw new Error("Authentication failed"); 2744 | } 2745 | } 2746 | }; 2747 | 2748 | const sendSocksRequest = async () => { 2749 | let DSTADDR; 2750 | switch (ipType) { 2751 | case 1: 2752 | DSTADDR = new Uint8Array([1, ...remoteIp.split('.').map(Number)]); 2753 | break; 2754 | case 2: 2755 | DSTADDR = new Uint8Array([3, remoteIp.length, ...encoder.encode(remoteIp)]); 2756 | break; 2757 | case 3: 2758 | DSTADDR = new Uint8Array([4, ...remoteIp.split(':').flatMap(x => [ 2759 | parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16) 2760 | ])]); 2761 | break; 2762 | default: 2763 | console.log(`Invalid address type: ${ipType}`); 2764 | throw new Error("Invalid address type"); 2765 | } 2766 | const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, remotePort >> 8, remotePort & 0xff]); 2767 | await writer.write(socksRequest); 2768 | console.log('SOCKS5 request sent'); 2769 | 2770 | const response = (await reader.read()).value; 2771 | if (response[1] !== 0x00) { 2772 | console.log("SOCKS5 connection failed"); 2773 | throw new Error("Connection failed"); 2774 | } 2775 | console.log("SOCKS5 connection established"); 2776 | }; 2777 | 2778 | try { 2779 | await sendSocksGreeting(); 2780 | await handleAuthResponse(); 2781 | await sendSocksRequest(); 2782 | } catch (error) { 2783 | console.log(error.message); 2784 | return null; // Return null on failure 2785 | } finally { 2786 | writer.releaseLock(); 2787 | reader.releaseLock(); 2788 | } 2789 | return socket; 2790 | } 2791 | 2792 | 2793 | /** -------------------Home page-------------------------------- */ 2794 | async function nginx() { 2795 | const text = ` 2796 | 2797 | 2798 | 2799 | 2800 | Welcome to nginx! 2801 | 2808 | 2809 | 2810 |

Welcome to nginx!

2811 |

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

2813 | 2814 |

For online documentation and support please refer to 2815 | nginx.org.
2816 | Commercial support is available at 2817 | nginx.com.

2818 | 2819 |

Thank you for using nginx.

2820 | 2821 | 2822 | ` 2823 | return text; 2824 | } -------------------------------------------------------------------------------- /ipHK.txt: -------------------------------------------------------------------------------- 1 | 127.0.0.1:1234#cfnat 2 | 104.17.142.12:443#HK 3 | 104.17.68.85:443#HK 4 | 104.17.71.31#HK 5 | 103.160.204.59:443#HK 6 | 198.62.62.4:443#HK 7 | 75.2.32.4:443#HK 8 | 104.17.142.12:443#HK 9 | 104.17.68.85:443#HK 10 | 104.17.71.31:443#HK 11 | -------------------------------------------------------------------------------- /ipJP.txt: -------------------------------------------------------------------------------- 1 | 156.238.19.8:443#JP 2 | www.4chan.org 3 | www.okcupid.com 4 | www.glassdoor.com 5 | www.udemy.com 6 | www.iakeys.com 7 | www.sean-now.com 8 | whatismyipaddress.com 9 | -------------------------------------------------------------------------------- /ipUS.txt: -------------------------------------------------------------------------------- 1 | 198.62.62.156:443#US 2 | 162.159.129.11:2053#US 3 | 162.159.129.67:8443#US 4 | cris.ns.cloudflare.com 5 | craig.ns.cloudflare.com 6 | decker.ns.cloudflare.com 7 | damien.ns.cloudflare.com 8 | dylan.ns.cloudflare.com 9 | -------------------------------------------------------------------------------- /ipUrl.txt: -------------------------------------------------------------------------------- 1 | https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipJP.txt@141.147.163.68 2 | https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipHK.txt@proxyip.amclubs.kozow.com 3 | https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.csv 4 | https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipUS.txt@proxyip.amclubs.camdvr.org 5 | 6 | 7 | -------------------------------------------------------------------------------- /ipv4.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-cf-tunnel/c5822aaa547a3389251288218e7306c5dd495c89/ipv4.csv -------------------------------------------------------------------------------- /ipv4.txt: -------------------------------------------------------------------------------- 1 | 127.0.0.1:1234#cfnat 2 | 198.62.62.156:443#US 3 | 162.159.129.11:2053#US 4 | 162.159.129.67:8443#US 5 | 104.16.134.27:2083#US 6 | 104.25.254.4:2087#US 7 | 104.17.128.1:2096#US 8 | ashton.ns.cloudflare.com 9 | abdullah.ns.cloudflare.com 10 | bowen.ns.cloudflare.com 11 | benedict.ns.cloudflare.com 12 | braden.ns.cloudflare.com 13 | [2606:4700:3036:6ed4:ffdf:f2ba:820b:ed5c]#IPV6 14 | [2606:4700:9b08:7ce1:8882:ba8c:2880:c619]#IPV6 15 | [2606:4700:9b08:dd59:154f:abf8:46b5:25bb]#IPV6 16 | cris.ns.cloudflare.com 17 | craig.ns.cloudflare.com 18 | decker.ns.cloudflare.com 19 | damien.ns.cloudflare.com 20 | dylan.ns.cloudflare.com 21 | 103.160.204.59:443#HK 22 | 198.62.62.4:443#HK 23 | 75.2.32.4:443#HK 24 | 104.17.142.12:443#HK 25 | 104.17.68.85:443#HK 26 | 104.17.71.31:443#HK 27 | time.is 28 | icook.com 29 | icook.tw 30 | ip.sb 31 | shopify.com 32 | 172.67.181.209 33 | 172.67.49.134 34 | 172.67.120.0 35 | 172.67.243.218 36 | 172.67.79.211 37 | 162.159.36.104 38 | www.digitalocean.com 39 | www.csgo.com 40 | www.whatismyip.com 41 | www.ipget.net 42 | www.hugedomains.com 43 | 103.160.204.59 44 | 198.62.62.4 45 | 75.2.32.4 46 | 104.17.142.12 47 | 104.17.68.85 48 | huxley.ns.cloudflare.com 49 | julio.ns.cloudflare.com 50 | kyree.ns.cloudflare.com 51 | lewis.ns.cloudflare.com 52 | moura.ns.cloudflare.com 53 | nikon.ns.cloudflare.com 54 | otto.ns.cloudflare.com 55 | pranab.ns.cloudflare.com 56 | rustam.ns.cloudflare.com 57 | sullivan.ns.cloudflare.com 58 | trevor.ns.cloudflare.com 59 | uriah.ns.cloudflare.com 60 | wilson.ns.cloudflare.com 61 | 162.159.133.85 62 | 172.67.106.26 63 | 172.67.110.232 64 | 172.67.95.24 65 | 172.64.201.25 66 | www.4chan.org 67 | www.okcupid.com 68 | www.glassdoor.com 69 | www.udemy.com 70 | www.iakeys.com 71 | www.sean-now.com 72 | whatismyipaddress.com 73 | www.pcmag.com 74 | www.ipchicken.com 75 | iplocation.io 76 | www.who.int 77 | www.wto.org 78 | stock.hostmonit.com 79 | www.tushencn.com 80 | www.7749tv.com 81 | manmankan.cc 82 | www.ddwhm.com 83 | ipinfo.in 84 | gamer.com.tw 85 | steamdb.info 86 | toy-people.com 87 | silkbook.com 88 | ipv4.ip.sb 89 | ip.gs 90 | dnschecker.org 91 | tasteatlas.com 92 | pixiv.net 93 | comicabc.com 94 | c.xf.free.hr 95 | yecaoyun.com 96 | 97 | 98 | -------------------------------------------------------------------------------- /proxyip.txt: -------------------------------------------------------------------------------- 1 | 1c5ee7c5-1340-46dc-81e3-5f900b498dc9.ooguy.com 2 | a01904b2-5c88-4d40-a246-e37d43aee11c.giize.com 3 | cb3c61c6-bbd2-4a54-8f6b-e2bba57998ca.accesscam.org 4 | ff1a88de-43d7-4f95-a8c6-b3ba6b2145b2.casacam.net 5 | --------------------------------------------------------------------------------