From 6a7b113e29a9bd4916fb1727633b26f1cbaf5314 Mon Sep 17 00:00:00 2001 From: anson Date: Mon, 28 Oct 2024 22:16:17 +0800 Subject: [PATCH] Initial commit --- .github/workflows/zip_flows.yml | 29 + .gitignore | 137 ++ LICENSE | 201 +++ README.md | 212 +++ _worker.js | 2133 +++++++++++++++++++++++++++++++ _worker.js.zip | Bin 0 -> 20671 bytes wrangler.toml | 13 + 7 files changed, 2725 insertions(+) create mode 100644 .github/workflows/zip_flows.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 _worker.js create mode 100644 _worker.js.zip create mode 100644 wrangler.toml diff --git a/.github/workflows/zip_flows.yml b/.github/workflows/zip_flows.yml new file mode 100644 index 0000000..d9999e0 --- /dev/null +++ b/.github/workflows/zip_flows.yml @@ -0,0 +1,29 @@ +name: zip_flows # 工作流程的名称 + +on: + workflow_dispatch: # 手动触发 + push: + paths: + - '_worker.js' # 当 _worker.js 文件发生变动时触发 + +permissions: + contents: write + +jobs: + package-and-commit: + runs-on: ubuntu-latest # 运行环境,这里使用最新版本的 Ubuntu + steps: + - name: Checkout Repository # 检出代码 + uses: actions/checkout@v2 + + - name: Zip the worker file # 将 _worker.js 文件打包成 worker.js.zip + run: zip _worker.js.zip _worker.js + + - name: Commit and push the packaged file # 提交并推送打包后的文件 + uses: EndBug/add-and-commit@v7 + with: + add: '_worker.js.zip' + message: 'Automatically package and commit _worker.js.zip' + author_name: github-actions[bot] + author_email: actions[bot]@users.noreply.github.com + token: ${{ secrets.GH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caa19dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,137 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov +.DS_Store + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +.DS_Store +.vscode +package-lock.json +yarn.lock + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0990bfb --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# [am-cf-trojan](https://github.com/amclubs/am-cf-trojan) +这是基于CF平台的脚本,部署Trojan 配置信息转换为订阅内容。可以方便地将 Trojan 节点配置信息转换到 Clash 或 Singbox 或Quantumult X等工具中。 + +# +▶️ **新人[YouTube](https://youtube.com/@AM_CLUB)** 需要您的支持,请务必帮我**点赞**、**关注**、**打开小铃铛**,***十分感谢!!!*** ✅ +
🎁 不要只是下载或Fork。请 **follow** 我的GitHub、给我所有项目一个 **Star** 星星(拜托了)!你的支持是我不断前进的动力! 💖 +
✅**解锁更多技术请点击进入 YouTube频道[【@AM_CLUB】](https://youtube.com/@AM_CLUB) 、[【个人博客】](https://am.809098.xyz)** 、TG群[【AM科技 | 分享交流群】](https://t.me/AM_CLUBS) 、获取免费节点[【进群发送关键字: 订阅】](https://t.me/AM_CLUBS) + +# Cloudflare Workers 和 Pages 生成Trojan节点,实现订阅连接可以一键订阅节点 +- Trojan免费节点部署视频教程:[点击进入观看](https://youtu.be/uh27CVVi6HA) +- VLESS免费节点部署视频教程:[点击进入观看](https://youtu.be/dPH63nITA0M) +- 优选IP和优选反代IP视频教程:[点击进入观看](https://youtu.be/pKrlfRRB0gU) +- 聚合节点订阅视频教程:[点击进入观看](https://youtu.be/YBO2hf96150) + +## Workers 部署方法 [视频教程](https://www.youtube.com/watch?v=uh27CVVi6HA&t=31s) + +1. 部署 CF Worker: + + - 在 CF Worker 控制台中创建一个新的 Worker。 + - 将 [_worker.js](https://github.com/amclubs/am-cf-trojan/blob/main/_worker.js) 的内容粘贴到 Worker 编辑器中。 + +2. 添加优选线路: + + - 给 `ipLocal` 按格式添加优选域名/优选IP,若不带端口号 TLS默认端口为443,#号后为备注别名,例如: + + ```js + let ipLocal = [ + 'visa.cn:443#youtube.com/@AM_CLUB 订阅频道获取更多教程', + 'icook.hk#t.me/AM_CLUBS 加入交流群解锁更多优选节点', + 'time.is:443#github.com/amclubs GitHub仓库查看更多项目' + ]; + ``` + + - 或 给 `ipUrlTxt`或 `ipUrlCsv` 添加 **优选IP/域名** 地址,例如: + + ```js + let ipUrlTxt = [ + 'https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt', + ]; + ``` + ```js + let ipUrlCsv = [ + 'https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.csv', + ]; + ``` + +3. 访问订阅内容: + + - 访问 `https://[YOUR-WORKERS-URL]/[PASSWORD]` 即可获取订阅内容。 + - 例如 `https://vless.google.workers.dev/auto` 就是你的通用自适应订阅地址。 + - 例如 `https://vless.google.workers.dev/auto?sub` Base64订阅格式,适用PassWall,SSR+等。 + - 例如 `https://vless.google.workers.dev/auto?clash` Clash订阅格式,适用OpenClash等。 + - 例如 `https://vless.google.workers.dev/auto?singbox` singbox订阅格式,适用singbox等。 + +4. 给 workers绑定 自定义域: + + - 在 workers控制台的 `触发器`选项卡,下方点击 `添加自定义域`。 + - 填入你已转入 CF 域名解析服务的次级域名,例如:`vless.google.com`后 点击`添加自定义域`,等待证书生效即可。 + +## Pages 上传 部署方法 [视频教程](https://www.youtube.com/watch?v=uh27CVVi6HA&t=336s) + +1. 部署 CF Pages: + - 下载 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-trojan/main/_worker.js.zip) 文件,并点上 Star !!! + - 在 CF Pages 控制台中选择 `上传资产`后,为你的项目取名后点击 `创建项目`,然后上传你下载好的 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-trojan/main/_worker.js.zip) 文件后点击 `部署站点`。 + - 部署完成后点击 `继续处理站点` 后,选择 `设置` > `环境变量` > **制作**为生产环境定义变量 > `添加变量`。 + 变量名称填写**PASSWORD**,值则为你的密码,后点击 `保存`即可。 + - 返回 `部署` 选项卡,在右下角点击 `创建新部署` 后,重新上传 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-trojan/main/_worker.js.zip) 文件后点击 `保存并部署` 即可。 + +2. 添加优选线路: + + - 添加变量 `ipLocal` 本地静态的优选线路,若不带端口号 TLS默认端口为443,#号后为备注别名,例如: + + ``` + visa.cn:443#youtube.com/@AM_CLUB 订阅频道获取更多教程 + icook.hk#t.me/AM_CLUBS 加入交流群解锁更多优选节点 + time.is:443#github.com/amclubs GitHub仓库查看更多项目 + time.is#你可以只放域名 如下 + www.visa.com.sg + skk.moe#也可以放域名带端口 如下 + www.wto.org:8443 + www.csgo.com:2087#节点名放在井号之后即可 + icook.hk#若不带端口号默认端口为443 + 104.17.152.41#IP也可以 + [2606:4700:e7:25:4b9:f8f8:9bfb:774a]#IPv6也OK + ``` +- 或添加变量 `ipUrlTxt` 添加 **优选IP/域名** 地址,例如: + ``` + https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt + ``` + +- 或添加变量 `ipUrlCsv` 添加 **优选IP/域名** 地址,例如: + ``` + https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.csv + ``` + +3. 访问订阅内容: + - 访问 `https://[YOUR-PAGES-URL]/[PASSWORD]` 即可获取订阅内容。 + - 例如 `https://am-cf-trojan.pages.dev/auto` 就是你的通用自适应订阅地址。 + - 例如 `https://am-cf-trojan.pages.dev/auto?sub` Base64订阅格式,适用PassWall,SSR+等。 + - 例如 `https://am-cf-trojan.pages.dev/auto?clash` Clash订阅格式,适用OpenClash等。 + - 例如 `https://am-cf-trojan.pages.dev/auto?sb` singbox订阅格式,适用singbox等。 + - 例如 `https://am-cf-trojan.pages.dev/auto?surge` surge订阅格式,适用surge 4/5。 + +4. 给 Pages绑定 CNAME自定义域: + - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。 + - 填入你的自定义次级域名,注意不要使用你的根域名,例如: + 您分配到的域名是 `cftest.dynv6.net`,则添加自定义域填入 `trojan.cftest.dynv6.net`即可; + - 按照 CF 的要求将返回你的域名DNS服务商,添加 该自定义域 `trojan`的 CNAME记录 `am-cf-trojan.pages.dev` 后,点击 `激活域`即可。 + +## Pages GitHub 部署方法 [视频教程](https://www.youtube.com/watch?v=uh27CVVi6HA&t=511s) + +1. 部署 CF Pages: + - 在 Github 上先 Fork 本项目,并点上 Star !!! + - 在 CF Pages 控制台中选择 `连接到 Git`后,选中 `am-cf-trojan`项目后点击 `开始设置`。 + - 在 `设置构建和部署`页面下方,选择 `环境变量(高级)`后并 `添加变量`, + 变量名称填写**PASSWORD**,值则为你的密码,后点击 `保存并部署`即可。 + +2. 添加优选线路: + + - 添加变量 `ipLocal` 本地静态的优选线路,若不带端口号 TLS默认端口为443,#号后为备注别名,例如: + + ``` + visa.cn:443#youtube.com/@AM_CLUB 订阅频道获取更多教程 + icook.hk#t.me/AM_CLUBS 加入交流群解锁更多优选节点 + time.is:443#github.com/amclubs GitHub仓库查看更多项目 + time.is#你可以只放域名 如下 + www.visa.com.sg + skk.moe#也可以放域名带端口 如下 + www.wto.org:8443 + www.csgo.com:2087#节点名放在井号之后即可 + icook.hk#若不带端口号默认端口为443 + 104.17.152.41#IP也可以 + [2606:4700:e7:25:4b9:f8f8:9bfb:774a]#IPv6也OK + ``` +- 或添加变量 `ipUrlTxt` 添加 **优选IP/域名** 地址,例如: + ``` + https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt + ``` + +- 或添加变量 `ipUrlCsv` 添加 **优选IP/域名** 地址,例如: + ``` + https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.csv + ``` + +3. 访问订阅内容: + - 访问 `https://[YOUR-PAGES-URL]/[PASSWORD]` 即可获取订阅内容。 + - 例如 `https://am-cf-trojan.pages.dev/auto` 就是你的通用自适应订阅地址。 + - 例如 `https://am-cf-trojan.pages.dev/auto?sub` Base64订阅格式,适用PassWall,SSR+等。 + - 例如 `https://am-cf-trojan.pages.dev/auto?clash` Clash订阅格式,适用OpenClash等。 + - 例如 `https://am-cf-trojan.pages.dev/auto?sb` singbox订阅格式,适用singbox等。 + - 例如 `https://am-cf-trojan.pages.dev/auto?surge` surge订阅格式,适用surge 4/5。 + +4. 给 Pages绑定 CNAME自定义域: + - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。 + - 填入你的自定义次级域名,注意不要使用你的根域名,例如: + 您分配到的域名是 `cftest.dynv6.net`,则添加自定义域填入 `trojan.cftest.dynv6.net`即可; + - 按照 CF 的要求将返回你的域名DNS服务商,添加 该自定义域 `trojan`的 CNAME记录 `am-cf-trojan.pages.dev` 后,点击 `激活域`即可。 + + +## 变量说明 +| 变量名 | 示例 | 必填 | 备注 | YT | +|-----|-----|-----|-----|-----| +| PASSWORD | auto |✅| 节点的密码,可以取任意值 | | +| PROXYIP | cdn-b100.xn--b6gac.eu.org |❌| 访问CloudFlare的CDN代理节点(支持多ProxyIP, ProxyIP之间使用`,`或 换行 作间隔),支持端口设置默认443 如: cdn-b100.xn--b6gac.eu.org:8443 | [Video](https://youtu.be/pKrlfRRB0gU) | +| SOCKS5 | user:password@127.0.0.1:1080 |❌| 优先作为访问CFCDN站点的SOCKS5代理 | [Video](https://youtu.be/Bw82BH_ecC4) | +| DNS_RESOLVER_URL | https://cloudflare-dns.com/dns-query |❌| DNS解析获取作用,小白勿用 | | +| IP_LOCAL | `icook.hk:2053#官方优选域名` |❌| 本地优选域名/优选IP(支持多元素之间`,`或 换行 作间隔) | | +| IP_URL_TXT | [https://raw.github.../ipv4.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt) |❌| 优选ipv4、ipv6、域名、API地址(支持多个之间`,`或 换行 作间隔) |[Video](https://youtu.be/dzxezRV1v-o) | +| IP_URL_CSV | [https://raw.github.../ipv4.csv](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.csv) |❌| 优选ipv4/6的IP测速结果(支持多元素, 元素之间使用`,`作间隔) |[Video](https://youtu.be/vX3U3FuuTT8)| +| NO_TLS | true/false |❌| 默认false,是否开启TLS系列端口,只有workers部署才可以使非用TLS系列端口 | | +| SL | 5 |❌| `CSV`文件里的测速结果满足速度下限 || +| 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等 订阅转换配置文件 || +| SUB_CONVERTER | https://url.v1.mk |❌| clash、singbox等 订阅转换后端的api地址 || +| SUB_NAME | @AM_CLUB |❌ | 订阅名称 || +| CF_EMAIL | test@gmail.com |❌| CF账户邮箱(要和`CF_KEY`同时填才生效, 订阅信息将显示请求使用量, 小白别用) || +| CF_KEY | c6a944b5c9c18c235288bced8b85e |❌| CF账户Global API Key(要和`CF_EMAIL`同时填才生效, 订阅信息将显示请求使用量, 小白别用) || +| TG_TOKEN | 6823456:XXXXXXX0qExVUhHDAbXXXqWXgBA |❌| 发送TG通知的机器人token || +| TG_ID | 6946912345 |❌ | 接收TG通知的账户数字ID || + + +## 订阅工具 [点击进入视频教程](https://youtu.be/xGOL57cmvaw) [点进进入karing视频教程](https://youtu.be/M3vLLBWfuFg) +- [(安卓)v2rayNG](https://github.com/2dust/v2rayNG/releases) [(安卓)singbox](https://github.com/SagerNet/sing-box/releases) [(苹果)singbox](https://github.com/SagerNet/sing-box/releases) [(苹果)Hiddify](https://github.com/hiddify/hiddify-next/releases) +- [(win)v2rayN](https://github.com/2dust/v2rayN/releases) [(win)singbox](https://github.com/SagerNet/sing-box/releases) [(win)clashvergerev](https://github.com/clash-verge-rev/clash-verge-rev/releases) [(win)Hiddify](https://github.com/hiddify/hiddify-next/releases) [(win)clashnyanpasu](https://github.com/LibNyanpasu/clash-nyanpasu/releases) [(mac)clashnyanpasu](https://github.com/LibNyanpasu/clash-nyanpasu/releases) +- [(mac)v2rayU](https://github.com/yanue/V2rayU/releases) [(mac)singbox](https://github.com/SagerNet/sing-box/releases) [(mac)clashvergerev](https://github.com/clash-verge-rev/clash-verge-rev/releases) [(mac)Hiddify](https://github.com/hiddify/hiddify-next/releases) +- [(安卓、苹果、win、mac)karing](https://karing.app/download) + +## 已适配自适应订阅内容 + - [v2rayN](https://github.com/2dust/v2rayN) + - [v2rayU](https://github.com/yanue/V2rayU/releases) + - [sing-box](https://github.com/SagerNet/sing-box) + - clash.meta([clash-verge-rev +](https://github.com/clash-verge-rev/clash-verge-rev),[Clash Nyanpasu](https://github.com/keiko233/clash-nyanpasu),ClashX Meta、openclash) + - Quantumult X + - 小火箭 + - surge + - [karing](https://karing.app/download) + + + # +
[点击展开] 赞赏支持 ~🧧 + *我非常感谢您的赞赏和支持,它们将极大地激励我继续创新,持续产生有价值的工作。* + + - **USDT-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD` + +
+ + # + 免责声明: + - 1、该项目设计和开发仅供学习、研究和安全测试目的。请于下载后 24 小时内删除, 不得用作任何商业用途, 文字、数据及图片均有所属版权, 如转载须注明来源。 + - 2、使用本程序必循遵守部署服务器所在地区的法律、所在国家和用户所在国家的法律法规。对任何人或团体使用该项目时产生的任何后果由使用者承担。 + - 3、作者不对使用该项目可能引起的任何直接或间接损害负责。作者保留随时更新免责声明的权利,且不另行通知。 + \ No newline at end of file diff --git a/_worker.js b/_worker.js new file mode 100644 index 0000000..ac5b2b6 --- /dev/null +++ b/_worker.js @@ -0,0 +1,2133 @@ +/** + * YouTube Channel: https://youtube.com/@AM_CLUB + * GitHub Repository: https://github.com/amclubs + * Telegram Group: https://t.me/AM_CLUBS + * Personal Blog: https://am.809098.xyz + */ + +// @ts-ignore +import { connect } from 'cloudflare:sockets'; + +// Generate your own UUID using the following command in PowerShell: +// Powershell -NoExit -Command "[guid]::NewGuid()" +let userID = 'auto'; + +// Proxy IPs to choose from +let proxyIPs = [ + 'cdn.xn--b6gac.eu.org', + 'cdn-all.xn--b6gac.eu.org', + 'workers.cloudflare.cyou' +]; +// Randomly select a proxy IP from the list +let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; +let proxyPort = 443; + +// Setting the socks5 will ignore proxyIP +// Example: user:pass@host:port or host:port +let socks5 = ''; +let socks5Enable = false; +let parsedSocks5 = {}; + +// https://cloudflare-dns.com/dns-query or https://dns.google/dns-query +// DNS-over-HTTPS URL +let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; + +// Preferred address API interface +let ipUrlTxt = [ + 'https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.txt', + // 'https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv6.txt' +]; +let ipUrlCsv = [ + // 'https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/ipv4.csv' +]; +// Preferred addresses with optional TLS subscription +let ipLocal = [ + 'visa.cn:443#youtube.com/@AM_CLUB 订阅频道获取更多教程', + 'icook.hk#t.me/AM_CLUBS 加入交流群解锁更多优选节点', + 'time.is:443#github.com/amclubs GitHub仓库查看更多项目' +]; +let noTLS = 'false'; +let sl = 5; + +// let tagName = 'amclubs'; +let subUpdateTime = 6; // Subscription update time in hours +let timestamp = 4102329600000; // Timestamp for the end date (2099-12-31) +let total = 99 * 1125899906842624; // PB (perhaps referring to bandwidth or total entries) +let download = Math.floor(Math.random() * 1099511627776); +let upload = download; + +// Regex pattern to match IP addresses and ports +// let regex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[.*\]):?(\d+)?#?(.*)?$/; + +// Network protocol type +let network = 'ws'; // WebSocket + +// Fake UUID and hostname for configuration generation +let fakeUserID; +let fakeHostName; + +// Subscription and conversion details +let subProtocol = 'https'; +let subConverter = 'url.v1.mk'; // Subscription conversion backend using Sheep's function +let subConfig = "https://raw.githubusercontent.com/amclubs/ACL4SSR/main/Clash/config/ACL4SSR_Online_Full_MultiMode.ini"; // Subscription profile +let fileName = 'youtube.com/@am_club'; +let isBase64 = true; + +let botToken = ''; +let chatID = ''; + +let pwd; + +// if (!isValidUUID(userID)) { +// throw new Error('uuid is invalid'); +// } + +export default { + /** + * @param {import("@cloudflare/workers-types").Request} request + * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env + * @param {import("@cloudflare/workers-types").ExecutionContext} ctx + * @returns {Promise} + */ + async fetch(request, env, ctx) { + try { + // Destructure environment variables for clarity + const { + PASSWORD, + PROXYIP, + SOCKS5, + DNS_RESOLVER_URL, + IP_LOCAL, + IP_URL_TXT, + IP_URL_CSV, + NO_TLS, + SL, + SUB_CONFIG, + SUB_CONVERTER, + SUB_NAME, + CF_EMAIL, + CF_KEY, + CF_ID = 0, + TG_TOKEN, + TG_ID, + //兼容 + ADDRESSESAPI, + } = env; + + userID = (PASSWORD || userID).toLowerCase(); + pwd = sha256.sha224(userID); + + if (PROXYIP) { + proxyIPs = await addIpText(PROXYIP); + proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; + const [ip, port] = proxyIP.split(':'); + proxyIP = ip; + proxyPort = port || proxyPort; + } + + const url = new URL(request.url); + socks5 = SOCKS5 || url.searchParams.get('socks5') || socks5; + parsedSocks5 = await parseSocks5FromUrl(socks5, url); + if (parsedSocks5) { + socks5Enable = true; + } + + dohURL = (DNS_RESOLVER_URL || dohURL); + + if (IP_LOCAL) { + ipLocal = await addIpText(IP_LOCAL); + } + //兼容旧的,如果有IP_URL_TXT新的则不用旧的 + if (ADDRESSESAPI) { + ipUrlTxt = await addIpText(ADDRESSESAPI); + } + if (IP_URL_TXT) { + ipUrlTxt = await addIpText(IP_URL_TXT); + } + if (IP_URL_CSV) { + ipUrlCsv = await addIpText(IP_URL_CSV); + } + + noTLS = (NO_TLS || noTLS); + sl = (SL || sl); + subConfig = (SUB_CONFIG || subConfig); + subConverter = (SUB_CONVERTER || subConverter); + fileName = (SUB_NAME || subConverter); + botToken = (TG_TOKEN || botToken); + chatID = (TG_ID || chatID); + + // Unified protocol for handling subconverters + const [subProtocol, subConverterWithoutProtocol] = (subConverter.startsWith("http://") || subConverter.startsWith("https://")) + ? subConverter.split("://") + : [undefined, subConverter]; + subConverter = subConverterWithoutProtocol; + + // console.log(`proxyIPs: ${proxyIPs} \n proxyIP: ${proxyIP} \n ipLocal: ${ipLocal} \n ipUrlTxt: ${ipUrlTxt} `); + + //const uuid = url.searchParams.get('uuid')?.toLowerCase() || 'null'; + const ua = request.headers.get('User-Agent') || 'null'; + const userAgent = ua.toLowerCase(); + const host = request.headers.get('Host'); + const upgradeHeader = request.headers.get('Upgrade'); + // Calculate expiry and upload/download limits + const expire = Math.floor(timestamp / 1000); + + // If WebSocket upgrade, handle WebSocket request + if (upgradeHeader === 'websocket') { + return await trojanOverWSHandler(request); + } + + fakeUserID = await getFakeUserID(userID); + fakeHostName = fakeUserID.slice(6, 9) + "." + fakeUserID.slice(13, 19); + console.log(`userID: ${userID}`); + console.log(`fakeUserID: ${fakeUserID}`); + // Handle routes based on the path + switch (url.pathname.toLowerCase()) { + case '/': { + // Serve the nginx disguise page + return new Response(await nginx(), { + headers: { + 'Content-Type': 'text/html; charset=UTF-8', + 'referer': 'https://www.google.com/search?q=am.809098.xyz', + }, + }); + } + + case `/${fakeUserID}`: { + // Disguise UUID node generation + const fakeConfig = await getTROJANConfig(userID, host, 'CF-FAKE-UA', url); + return new Response(fakeConfig, { status: 200 }); + } + + case `/${userID}`: { + // Handle real UUID requests and get node info + await sendMessage( + `#获取订阅 ${fileName}`, + request.headers.get('CF-Connecting-IP'), + `UA: ${userAgent}\n域名: ${url.hostname}\n入口: ${url.pathname + url.search}` + ); + + const trojanConfig = await getTROJANConfig(userID, host, userAgent, url); + const isMozilla = userAgent.includes('mozilla'); + + const config = await getCFConfig(CF_EMAIL, CF_KEY, CF_ID); + if (CF_EMAIL && CF_KEY) { + ({ upload, download, total } = config); + } + + // Prepare common headers + const commonHeaders = { + "Content-Type": isMozilla ? "text/html;charset=utf-8" : "text/plain;charset=utf-8", + "Profile-Update-Interval": `${subUpdateTime}`, + "Subscription-Userinfo": `upload=${upload}; download=${download}; total=${total}; expire=${expire}`, + }; + + // Add download headers if not a Mozilla browser + if (!isMozilla) { + commonHeaders["Content-Disposition"] = `attachment; filename=${fileName}; filename*=utf-8''${encodeURIComponent(fileName)}`; + } + + return new Response(trojanConfig, { + status: 200, + headers: commonHeaders, + }); + } + + default: { + // Serve the default nginx disguise page + return new Response(await nginx(), { + headers: { + 'Content-Type': 'text/html; charset=UTF-8', + 'referer': 'https://www.google.com/search?q=am.809098.xyz', + }, + }); + } + } + } catch (err) { + // Log error for debugging purposes + console.error('Error processing request:', err); + return new Response(`Error: ${err.message}`, { status: 500 }); + } + }, +}; + + +/** ---------------------Tools------------------------------ */ + +export async function hashHex_f(string) { + const encoder = new TextEncoder(); + const data = encoder.encode(string); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); + return hashHex; +} + +/** + * Checks if a given string is a valid UUID. + * Note: This is not a real UUID validation. + * @param {string} uuid The string to validate as a UUID. + * @returns {boolean} True if the string is a valid UUID, false otherwise. + */ +function isValidUUID(uuid) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +const byteToHex = []; +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 256).toString(16).slice(1)); +} + +function unsafeStringify(arr, offset = 0) { + return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); +} + +function stringify(arr, offset = 0) { + const uuid = unsafeStringify(arr, offset); + if (!isValidUUID(uuid)) { + throw TypeError("Stringified UUID is invalid"); + } + return uuid; +} + +async function getFakeUserID(userID) { + const date = new Date().toISOString().split('T')[0]; + const rawString = `${userID}-${date}`; + + const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(rawString)); + const hashArray = Array.from(new Uint8Array(hashBuffer)).map(b => ('00' + b.toString(16)).slice(-2)).join(''); + + return `${hashArray.substring(0, 8)}-${hashArray.substring(8, 12)}-${hashArray.substring(12, 16)}-${hashArray.substring(16, 20)}-${hashArray.substring(20, 32)}`; +} + +function revertFakeInfo(content, userID, hostName) { + //console.log(`revertFakeInfo-->: isBase64 ${isBase64} \n content: ${content}`); + if (isBase64) { + content = atob(content);//Base64 decrypt + } + content = content.replace(new RegExp(fakeUserID, 'g'), userID).replace(new RegExp(fakeHostName, 'g'), hostName); + if (isBase64) { + content = btoa(content);//Base64 encryption + } + return content; +} + +/** + * Decodes a base64 string into an ArrayBuffer. + * @param {string} base64Str The base64 string to decode. + * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error. + */ +function base64ToArrayBuffer(base64Str) { + if (!base64Str) { + return { earlyData: null, error: null }; + } + try { + // go use modified Base64 for URL rfc4648 which js atob not support + base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); + const decode = atob(base64Str); + const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); + return { earlyData: arryBuffer.buffer, error: null }; + } catch (error) { + return { earlyData: null, error }; + } +} + +async function addIpText(envAdd) { + var addText = envAdd.replace(/[ |"'\r\n]+/g, ',').replace(/,+/g, ','); + //console.log(addText); + if (addText.charAt(0) == ',') { + addText = addText.slice(1); + } + if (addText.charAt(addText.length - 1) == ',') { + addText = addText.slice(0, addText.length - 1); + } + const add = addText.split(','); + // console.log(add); + return add; +} + +function socks5Parser(socks5) { + let [latter, former] = socks5.split("@").reverse(); + let username, password, hostname, port; + + if (former) { + const formers = former.split(":"); + if (formers.length !== 2) { + throw new Error('Invalid SOCKS address format: authentication must be in the "username:password" format'); + } + [username, password] = formers; + } + + const latters = latter.split(":"); + port = Number(latters.pop()); + if (isNaN(port)) { + throw new Error('Invalid SOCKS address format: port must be a number'); + } + + hostname = latters.join(":"); + const isIPv6 = hostname.includes(":") && !/^\[.*\]$/.test(hostname); + if (isIPv6) { + throw new Error('Invalid SOCKS address format: IPv6 addresses must be enclosed in brackets, e.g., [2001:db8::1]'); + } + + //console.log(`socks5Parser-->: username ${username} \n password: ${password} \n hostname: ${hostname} \n port: ${port}`); + return { username, password, hostname, port }; +} + +async function parseSocks5FromUrl(socks5, url) { + if (/\/socks5?=/.test(url.pathname)) { + socks5 = url.pathname.split('5=')[1]; + } else if (/\/socks[5]?:\/\//.test(url.pathname)) { + socks5 = url.pathname.split('://')[1].split('#')[0]; + } + + const authIdx = socks5.indexOf('@'); + if (authIdx !== -1) { + let userPassword = socks5.substring(0, authIdx); + const base64Regex = /^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i; + if (base64Regex.test(userPassword) && !userPassword.includes(':')) { + userPassword = atob(userPassword); + } + socks5 = `${userPassword}@${socks5.substring(authIdx + 1)}`; + } + + if (socks5) { + try { + return socks5Parser(socks5); + } catch (err) { + console.log(err.toString()); + return null; + } + } + return null; +} + +// sha256 Hash Algorithm in pure JavaScript +/** + * [js-sha256] + */ +(function () { + 'use strict'; + + var ERROR = 'input is invalid type'; + var WINDOW = typeof window === 'object'; + var root = WINDOW ? window : {}; + if (root.JS_SHA256_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var EXTRA = [-2147483648, 8388608, 32768, 128]; + var SHIFT = [24, 16, 8, 0]; + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; + + var blocks = []; + + if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { + ArrayBuffer.isView = function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + }; + } + + var createOutputMethod = function (outputType, is224) { + return function (message) { + return new Sha256(is224, true).update(message)[outputType](); + }; + }; + + var createMethod = function (is224) { + var method = createOutputMethod('hex', is224); + if (NODE_JS) { + method = nodeWrap(method, is224); + } + method.create = function () { + return new Sha256(is224); + }; + method.update = function (message) { + return method.create().update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createOutputMethod(type, is224); + } + return method; + }; + + var nodeWrap = function (method, is224) { + var crypto = require('node:crypto') + var Buffer = require('node:buffer').Buffer; + var algorithm = is224 ? 'sha224' : 'sha256'; + var bufferFrom; + if (Buffer.from && !root.JS_SHA256_NO_BUFFER_FROM) { + bufferFrom = Buffer.from; + } else { + bufferFrom = function (message) { + return new Buffer(message); + }; + } + var nodeMethod = function (message) { + if (typeof message === 'string') { + return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); + } else { + if (message === null || message === undefined) { + throw new Error(ERROR); + } else if (message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } + } + if (Array.isArray(message) || ArrayBuffer.isView(message) || + message.constructor === Buffer) { + return crypto.createHash(algorithm).update(bufferFrom(message)).digest('hex'); + } else { + return method(message); + } + }; + return nodeMethod; + }; + + var createHmacOutputMethod = function (outputType, is224) { + return function (key, message) { + return new HmacSha256(key, is224, true).update(message)[outputType](); + }; + }; + + var createHmacMethod = function (is224) { + var method = createHmacOutputMethod('hex', is224); + method.create = function (key) { + return new HmacSha256(key, is224); + }; + method.update = function (key, message) { + return method.create(key).update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createHmacOutputMethod(type, is224); + } + return method; + }; + + function Sha256(is224, sharedMemory) { + if (sharedMemory) { + blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + this.blocks = blocks; + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + if (is224) { + this.h0 = 0xc1059ed8; + this.h1 = 0x367cd507; + this.h2 = 0x3070dd17; + this.h3 = 0xf70e5939; + this.h4 = 0xffc00b31; + this.h5 = 0x68581511; + this.h6 = 0x64f98fa7; + this.h7 = 0xbefa4fa4; + } else { // 256 + this.h0 = 0x6a09e667; + this.h1 = 0xbb67ae85; + this.h2 = 0x3c6ef372; + this.h3 = 0xa54ff53a; + this.h4 = 0x510e527f; + this.h5 = 0x9b05688c; + this.h6 = 0x1f83d9ab; + this.h7 = 0x5be0cd19; + } + + this.block = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + this.is224 = is224; + } + + Sha256.prototype.update = function (message) { + if (this.finalized) { + return; + } + var notString, type = typeof message; + if (type !== 'string') { + if (type === 'object') { + if (message === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } else if (!Array.isArray(message)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + notString = true; + } + var code, index = 0, i, length = message.length, blocks = this.blocks; + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = this.block; + this.block = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + + if (notString) { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >>> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >>> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >>> 2] |= (0xc0 | (code >>> 6)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >>> 2] |= (0xe0 | (code >>> 12)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >>> 2] |= (0xf0 | (code >>> 18)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += this.bytes / 4294967296 << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; + + Sha256.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >>> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + blocks[14] = this.hBytes << 3 | this.bytes >>> 29; + blocks[15] = this.bytes << 3; + this.hash(); + }; + + Sha256.prototype.hash = function () { + var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, + h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; + + for (j = 16; j < 64; ++j) { + // rightrotate + t1 = blocks[j - 15]; + s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); + t1 = blocks[j - 2]; + s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); + blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; + } + + bc = b & c; + for (j = 0; j < 64; j += 4) { + if (this.first) { + if (this.is224) { + ab = 300032; + t1 = blocks[0] - 1413257819; + h = t1 - 150054599 << 0; + d = t1 + 24177077 << 0; + } else { + ab = 704751109; + t1 = blocks[0] - 210244248; + h = t1 - 1521486534 << 0; + d = t1 + 143694565 << 0; + } + this.first = false; + } else { + s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + ab = a & b; + maj = ab ^ (a & c) ^ bc; + ch = (e & f) ^ (~e & g); + t1 = h + s1 + ch + K[j] + blocks[j]; + t2 = s0 + maj; + h = d + t1 << 0; + d = t1 + t2 << 0; + } + s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); + s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); + da = d & a; + maj = da ^ (d & b) ^ ab; + ch = (h & e) ^ (~h & f); + t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; + t2 = s0 + maj; + g = c + t1 << 0; + c = t1 + t2 << 0; + s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); + s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); + cd = c & d; + maj = cd ^ (c & a) ^ da; + ch = (g & h) ^ (~g & e); + t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; + t2 = s0 + maj; + f = b + t1 << 0; + b = t1 + t2 << 0; + s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); + s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); + bc = b & c; + maj = bc ^ (b & d) ^ cd; + ch = (f & g) ^ (~f & h); + t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; + t2 = s0 + maj; + e = a + t1 << 0; + a = t1 + t2 << 0; + this.chromeBugWorkAround = true; + } + + this.h0 = this.h0 + a << 0; + this.h1 = this.h1 + b << 0; + this.h2 = this.h2 + c << 0; + this.h3 = this.h3 + d << 0; + this.h4 = this.h4 + e << 0; + this.h5 = this.h5 + f << 0; + this.h6 = this.h6 + g << 0; + this.h7 = this.h7 + h << 0; + }; + + Sha256.prototype.hex = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var hex = HEX_CHARS[(h0 >>> 28) & 0x0F] + HEX_CHARS[(h0 >>> 24) & 0x0F] + + HEX_CHARS[(h0 >>> 20) & 0x0F] + HEX_CHARS[(h0 >>> 16) & 0x0F] + + HEX_CHARS[(h0 >>> 12) & 0x0F] + HEX_CHARS[(h0 >>> 8) & 0x0F] + + HEX_CHARS[(h0 >>> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + + HEX_CHARS[(h1 >>> 28) & 0x0F] + HEX_CHARS[(h1 >>> 24) & 0x0F] + + HEX_CHARS[(h1 >>> 20) & 0x0F] + HEX_CHARS[(h1 >>> 16) & 0x0F] + + HEX_CHARS[(h1 >>> 12) & 0x0F] + HEX_CHARS[(h1 >>> 8) & 0x0F] + + HEX_CHARS[(h1 >>> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + + HEX_CHARS[(h2 >>> 28) & 0x0F] + HEX_CHARS[(h2 >>> 24) & 0x0F] + + HEX_CHARS[(h2 >>> 20) & 0x0F] + HEX_CHARS[(h2 >>> 16) & 0x0F] + + HEX_CHARS[(h2 >>> 12) & 0x0F] + HEX_CHARS[(h2 >>> 8) & 0x0F] + + HEX_CHARS[(h2 >>> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + + HEX_CHARS[(h3 >>> 28) & 0x0F] + HEX_CHARS[(h3 >>> 24) & 0x0F] + + HEX_CHARS[(h3 >>> 20) & 0x0F] + HEX_CHARS[(h3 >>> 16) & 0x0F] + + HEX_CHARS[(h3 >>> 12) & 0x0F] + HEX_CHARS[(h3 >>> 8) & 0x0F] + + HEX_CHARS[(h3 >>> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + + HEX_CHARS[(h4 >>> 28) & 0x0F] + HEX_CHARS[(h4 >>> 24) & 0x0F] + + HEX_CHARS[(h4 >>> 20) & 0x0F] + HEX_CHARS[(h4 >>> 16) & 0x0F] + + HEX_CHARS[(h4 >>> 12) & 0x0F] + HEX_CHARS[(h4 >>> 8) & 0x0F] + + HEX_CHARS[(h4 >>> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + + HEX_CHARS[(h5 >>> 28) & 0x0F] + HEX_CHARS[(h5 >>> 24) & 0x0F] + + HEX_CHARS[(h5 >>> 20) & 0x0F] + HEX_CHARS[(h5 >>> 16) & 0x0F] + + HEX_CHARS[(h5 >>> 12) & 0x0F] + HEX_CHARS[(h5 >>> 8) & 0x0F] + + HEX_CHARS[(h5 >>> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + + HEX_CHARS[(h6 >>> 28) & 0x0F] + HEX_CHARS[(h6 >>> 24) & 0x0F] + + HEX_CHARS[(h6 >>> 20) & 0x0F] + HEX_CHARS[(h6 >>> 16) & 0x0F] + + HEX_CHARS[(h6 >>> 12) & 0x0F] + HEX_CHARS[(h6 >>> 8) & 0x0F] + + HEX_CHARS[(h6 >>> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; + if (!this.is224) { + hex += HEX_CHARS[(h7 >>> 28) & 0x0F] + HEX_CHARS[(h7 >>> 24) & 0x0F] + + HEX_CHARS[(h7 >>> 20) & 0x0F] + HEX_CHARS[(h7 >>> 16) & 0x0F] + + HEX_CHARS[(h7 >>> 12) & 0x0F] + HEX_CHARS[(h7 >>> 8) & 0x0F] + + HEX_CHARS[(h7 >>> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; + } + return hex; + }; + + Sha256.prototype.toString = Sha256.prototype.hex; + + Sha256.prototype.digest = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var arr = [ + (h0 >>> 24) & 0xFF, (h0 >>> 16) & 0xFF, (h0 >>> 8) & 0xFF, h0 & 0xFF, + (h1 >>> 24) & 0xFF, (h1 >>> 16) & 0xFF, (h1 >>> 8) & 0xFF, h1 & 0xFF, + (h2 >>> 24) & 0xFF, (h2 >>> 16) & 0xFF, (h2 >>> 8) & 0xFF, h2 & 0xFF, + (h3 >>> 24) & 0xFF, (h3 >>> 16) & 0xFF, (h3 >>> 8) & 0xFF, h3 & 0xFF, + (h4 >>> 24) & 0xFF, (h4 >>> 16) & 0xFF, (h4 >>> 8) & 0xFF, h4 & 0xFF, + (h5 >>> 24) & 0xFF, (h5 >>> 16) & 0xFF, (h5 >>> 8) & 0xFF, h5 & 0xFF, + (h6 >>> 24) & 0xFF, (h6 >>> 16) & 0xFF, (h6 >>> 8) & 0xFF, h6 & 0xFF + ]; + if (!this.is224) { + arr.push((h7 >>> 24) & 0xFF, (h7 >>> 16) & 0xFF, (h7 >>> 8) & 0xFF, h7 & 0xFF); + } + return arr; + }; + + Sha256.prototype.array = Sha256.prototype.digest; + + Sha256.prototype.arrayBuffer = function () { + this.finalize(); + + var buffer = new ArrayBuffer(this.is224 ? 28 : 32); + var dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + dataView.setUint32(20, this.h5); + dataView.setUint32(24, this.h6); + if (!this.is224) { + dataView.setUint32(28, this.h7); + } + return buffer; + }; + + function HmacSha256(key, is224, sharedMemory) { + var i, type = typeof key; + if (type === 'string') { + var bytes = [], length = key.length, index = 0, code; + for (i = 0; i < length; ++i) { + code = key.charCodeAt(i); + if (code < 0x80) { + bytes[index++] = code; + } else if (code < 0x800) { + bytes[index++] = (0xc0 | (code >>> 6)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else if (code < 0xd800 || code >= 0xe000) { + bytes[index++] = (0xe0 | (code >>> 12)); + bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); + bytes[index++] = (0xf0 | (code >>> 18)); + bytes[index++] = (0x80 | ((code >>> 12) & 0x3f)); + bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } + } + key = bytes; + } else { + if (type === 'object') { + if (key === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { + key = new Uint8Array(key); + } else if (!Array.isArray(key)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + } + + if (key.length > 64) { + key = (new Sha256(is224, true)).update(key).array(); + } + + var oKeyPad = [], iKeyPad = []; + for (i = 0; i < 64; ++i) { + var b = key[i] || 0; + oKeyPad[i] = 0x5c ^ b; + iKeyPad[i] = 0x36 ^ b; + } + + Sha256.call(this, is224, sharedMemory); + + this.update(iKeyPad); + this.oKeyPad = oKeyPad; + this.inner = true; + this.sharedMemory = sharedMemory; + } + HmacSha256.prototype = new Sha256(); + + HmacSha256.prototype.finalize = function () { + Sha256.prototype.finalize.call(this); + if (this.inner) { + this.inner = false; + var innerHash = this.array(); + Sha256.call(this, this.is224, this.sharedMemory); + this.update(this.oKeyPad); + this.update(innerHash); + Sha256.prototype.finalize.call(this); + } + }; + + var exports = createMethod(); + exports.sha256 = exports; + exports.sha224 = createMethod(true); + exports.sha256.hmac = createHmacMethod(); + exports.sha224.hmac = createHmacMethod(true); + + if (COMMON_JS) { + module.exports = exports; + } else { + root.sha256 = exports.sha256; + root.sha224 = exports.sha224; + if (AMD) { + define(function () { + return exports; + }); + } + } +})(); + +/** ---------------------Get data------------------------------ */ + +let subParams = ['sub', 'base64', 'b64', 'clash', 'singbox', 'sb']; +/** + * @param {string} userID + * @param {string | null} host + * @param {string} userAgent + * @param {string} _url + * @returns {Promise} + */ +async function getTROJANConfig(userID, host, userAgent, _url) { + // console.log(`------------getTROJANConfig------------------`); + // console.log(`userID: ${userID} \n host: ${host} \n userAgent: ${userAgent} \n _url: ${_url}`); + + userAgent = userAgent.toLowerCase(); + let port = 443; + if (host.includes('.workers.dev')) { + port = 80; + } + const [v2ray, clash] = getConfigLink(userID, host, host, port, host); + + if (userAgent.includes('mozilla') && !subParams.some(param => _url.searchParams.has(param))) { + return getHtmlResponse(socks5Enable, userID, host, v2ray, clash); + } + + // Get node information + fakeHostName = getFakeHostName(host); + const ipUrlTxtAndCsv = await getIpUrlTxtAndCsv(noTLS); + + // console.log(`txt: ${ipUrlTxtAndCsv.txt} \n csv: ${ipUrlTxtAndCsv.csv}`); + let content = await getSubscribeNode(userAgent, _url, host, fakeHostName, fakeUserID, noTLS, ipUrlTxtAndCsv.txt, ipUrlTxtAndCsv.csv); + + return _url.pathname === `/${fakeUserID}` ? content : revertFakeInfo(content, userID, host); + +} + +function getHtmlResponse(socks5Enable, userID, host, v2ray, clash) { + const subRemark = `IP_URL_TXT/IP_URL_CSV/IP_LOCAL`; + let proxyIPRemark = `PROXYIP: ${proxyIP}`; + + if (socks5Enable) { + proxyIPRemark = `socks5: ${parsedSocks5.hostname}:${parsedSocks5.port}`; + } + + let remark = `您的订阅节点由设置变量 ${subRemark} 提供, 当前使用反代是${proxyIPRemark}`; + + if (!proxyIP && !socks5Enable) { + remark = `您的订阅节点由设置变量 ${subRemark} 提供, 当前没设置反代, 推荐您设置PROXYIP变量或SOCKS5变量或订阅连接带proxyIP`; + } + + return getConfigHtml(userID, host, remark, v2ray, clash); +} + +function getFakeHostName(host) { + if (host.includes(".pages.dev")) { + return `${fakeHostName}.pages.dev`; + } else if (host.includes(".workers.dev") || host.includes("notls") || noTLS === 'true') { + return `${fakeHostName}.workers.dev`; + } + return `${fakeHostName}.xyz`; +} + +async function getIpUrlTxtAndCsv(noTLS) { + if (noTLS === 'true') { + return { + txt: await getIpUrlTxt(ipUrlTxt), + csv: await getIpUrlCsv('FALSE') + }; + } + return { + txt: await getIpUrlTxt(ipUrlTxt), + csv: await getIpUrlCsv('TRUE') + }; +} + +async function getIpUrlTxt(ipUrlTxts) { + if (!ipUrlTxts || ipUrlTxts.length === 0) { + return []; + } + + let ipTxt = ""; + + // Create an AbortController object to control the cancellation of fetch requests + const controller = new AbortController(); + + // Set a timeout to trigger the cancellation of all requests after 2 seconds + const timeout = setTimeout(() => { + controller.abort(); // Cancel all requests + }, 2000); + + try { + // Use Promise.allSettled to wait for all API requests to complete, regardless of success or failure + // Iterate over the api array and send a fetch request to each API URL + const responses = await Promise.allSettled(ipUrlTxts.map(apiUrl => fetch(apiUrl, { + method: 'GET', + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;', + 'User-Agent': 'amclubs/am-cf-tunnel' + }, + signal: controller.signal // Attach the AbortController's signal to the fetch request to allow cancellation when needed + }).then(response => response.ok ? response.text() : Promise.reject()))); + + // Iterate through all the responses + for (const response of responses) { + // Check if the request was fulfilled successfully + if (response.status === 'fulfilled') { + // Get the response content + const content = await response.value; + ipTxt += content + '\n'; + } + } + } catch (error) { + console.error(error); + } finally { + // Clear the timeout regardless of success or failure + clearTimeout(timeout); + } + + // Process the result using addIpText function + const newIpTxt = await addIpText(ipTxt); + // console.log(`ipUrlTxts: ${ipUrlTxts} \n ipTxt: ${ipTxt} \n newIpTxt: ${newIpTxt} `); + + // Return the processed result + return newIpTxt; +} + +async function getIpUrlCsv(tls) { + // Check if the CSV URLs are valid + if (!ipUrlCsv || ipUrlCsv.length === 0) { + return []; + } + + const newAddressesCsv = []; + + // Fetch and process all CSVs concurrently + const fetchCsvPromises = ipUrlCsv.map(async (csvUrl) => { + try { + const response = await fetch(csvUrl); + + // Ensure the response is successful + if (!response.ok) { + console.error('Error fetching CSV:', response.status, response.statusText); + return; + } + + // Parse the CSV content and split it into lines + const text = await response.text(); + const lines = text.includes('\r\n') ? text.split('\r\n') : text.split('\n'); + + // Ensure we have a non-empty CSV + if (lines.length < 2) { + console.error('CSV file is empty or has no data rows'); + return; + } + + // Extract the header and get required field indexes + const header = lines[0].trim().split(','); + const tlsIndex = header.indexOf('TLS'); + const ipAddressIndex = 0; // Assuming the first column is IP address + const portIndex = 1; // Assuming the second column is port + const dataCenterIndex = tlsIndex + 1; // Data center assumed to be right after TLS + const speedIndex = header.length - 1; // Last column for speed + + // If the required fields are missing, skip this CSV + if (tlsIndex === -1) { + console.error('CSV file missing required TLS field'); + return; + } + + // Process the data rows + for (let i = 1; i < lines.length; i++) { + const columns = lines[i].trim().split(','); + + // Skip empty or malformed rows + if (columns.length < header.length) { + continue; + } + + // Check if TLS matches and speed is greater than sl + const tlsValue = columns[tlsIndex].toUpperCase(); + const speedValue = parseFloat(columns[speedIndex]); + + if (tlsValue === tls && speedValue > sl) { + const ipAddress = columns[ipAddressIndex]; + const port = columns[portIndex]; + const dataCenter = columns[dataCenterIndex]; + newAddressesCsv.push(`${ipAddress}:${port}#${dataCenter}`); + } + } + } catch (error) { + console.error('Error processing CSV URL:', csvUrl, error); + } + }); + + // Wait for all CSVs to be processed + await Promise.all(fetchCsvPromises); + + return newAddressesCsv; +} + +const protocolTypeBase64 = 'dHJvamFu'; +/** + * Get node configuration information + * @param {*} uuid + * @param {*} host + * @param {*} address + * @param {*} port + * @param {*} remarks + * @returns + */ +function getConfigLink(uuid, host, address, port, remarks) { + const protocolType = atob(protocolTypeBase64); + + const encryption = 'none'; + let path = '/?ed=2560'; + const fingerprint = 'randomized'; + let tls = ['tls', true]; + if (host.includes('.workers.dev') || host.includes('pages.dev')) { + path = `/${host}${path}`; + remarks += ' 请通过绑定自定义域名订阅!'; + } + + const v2ray = getV2rayLink({ protocolType, host, uuid, address, port, remarks, encryption, path, fingerprint, tls }); + const clash = getClashLink(protocolType, host, address, port, uuid, path, tls, fingerprint); + + return [v2ray, clash]; +} + +/** + * Get vless information + * @param {*} param0 + * @returns + */ +function getV2rayLink({ protocolType, host, uuid, address, port, remarks, encryption, path, fingerprint, tls }) { + let sniAndFp = `&sni=${host}&fp=${fingerprint}`; + if (portSet_http.has(parseInt(port))) { + tls = ['', false]; + sniAndFp = ''; + } + + const v2rayLink = `${protocolType}://${uuid}@${address}:${port}?encryption=${encryption}&security=${tls[0]}&type=${network}&host=${host}&path=${encodeURIComponent(path)}${sniAndFp}#${encodeURIComponent(remarks)}`; + return v2rayLink; +} + +/** + * getClashLink + * @param {*} protocolType + * @param {*} host + * @param {*} address + * @param {*} port + * @param {*} uuid + * @param {*} path + * @param {*} tls + * @param {*} fingerprint + * @returns + */ +function getClashLink(protocolType, host, address, port, uuid, path, tls, fingerprint) { + return `- {type: ${protocolType}, 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}}}}`; + + // return ` + // - type: ${protocolType} + // name: ${host} + // server: ${address} + // port: ${port} + // uuid: ${uuid} + // network: ${network} + // tls: ${tls[1]} + // udp: false + // sni: ${host} + // client-fingerprint: ${fingerprint} + // ws-opts: + // path: "${path}" + // headers: + // host: ${host} + // `; +} + +/** + * Generate home page + * @param {*} userID + * @param {*} hostName + * @param {*} remark + * @param {*} v2ray + * @param {*} clash + * @returns + */ +function getConfigHtml(userID, host, remark, v2ray, clash) { + // HTML Head with CSS and FontAwesome library + const htmlHead = ` + + am-cf-tunnel(AM科技) + + + + `; + + // Prepare header string with left alignment + const header = ` +

+ Telegram交流群 技术大佬~在线交流
+ t.me/AM_CLUBS +

+ GitHub项目地址 点击Star!Star!Star!
+ https://github.com/amclubs/am-cf-trojan +

+ YouTube频道,订阅频道,更多技术分享
+ https://youtube.com/@AM_CLUB +

+ `; + + // Prepare the output string + const httpAddr = `https://${host}/${userID}`; + const output = ` +################################################################ +订阅地址, 支持 Base64、clash-meta、sing-box、Quantumult X、小火箭、surge 等订阅格式, ${remark} +--------------------------------------------------------------- +通用订阅地址: +${httpAddr}?sub + +Base64订阅地址: +${httpAddr}?base64 + +clash订阅地址: +${httpAddr}?clash + +singbox订阅地址: +${httpAddr}?singbox +--------------------------------------------------------------- +################################################################ +v2ray +--------------------------------------------------------------- +${v2ray} +--------------------------------------------------------------- +################################################################ +clash-meta +--------------------------------------------------------------- +${clash} +--------------------------------------------------------------- +################################################################ + `; + + // Final HTML + const html = ` + +${htmlHead} + + ${header} +
${output}
+ + + + `; + + return html; +} + + +let portSet_http = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]); +let portSet_https = new Set([443, 8443, 2053, 2096, 2087, 2083]); +/** + * + * @param {*} host + * @param {*} uuid + * @param {*} noTLS + * @param {*} ipUrlTxt + * @param {*} ipUrlCsv + * @returns + */ +async function getSubscribeNode(userAgent, _url, host, fakeHostName, fakeUserID, noTLS, ipUrlTxt, ipUrlCsv) { + // Use Set object to remove duplicates + const uniqueIpTxt = [...new Set([...ipLocal, ...ipUrlTxt, ...ipUrlCsv])]; + let responseBody = splitNodeData(uniqueIpTxt, noTLS, fakeHostName, fakeUserID, userAgent); + // console.log(`getSubscribeNode---> responseBody: ${responseBody} `); + + if (!userAgent.includes(('CF-FAKE-UA').toLowerCase())) { + + let url = `https://${host}/${fakeUserID}`; + + if (isClashCondition(userAgent, _url)) { + isBase64 = false; + url = createSubConverterUrl('clash', url, subConfig, subConverter, subProtocol); + } else if (isSingboxCondition(userAgent, _url)) { + isBase64 = false; + url = createSubConverterUrl('singbox', url, subConfig, subConverter, subProtocol); + } else { + return responseBody; + } + const response = await fetch(url, { + headers: { + 'User-Agent': `${userAgent} am-cf-tunnel/amclubs` + } + }); + responseBody = await response.text(); + //console.log(`getSubscribeNode---> url: ${url} `); + } + + return responseBody; +} + +function createSubConverterUrl(target, url, subConfig, subConverter, subProtocol) { + 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`; +} + +function isClashCondition(userAgent, _url) { + return (userAgent.includes('clash') && !userAgent.includes('nekobox')) || (_url.searchParams.has('clash') && !userAgent.includes('subConverter')); +} + +function isSingboxCondition(userAgent, _url) { + return userAgent.includes('sing-box') || userAgent.includes('singbox') || ((_url.searchParams.has('singbox') || _url.searchParams.has('sb')) && !userAgent.includes('subConverter')); +} + +/** + * + * @param {*} uniqueIpTxt + * @param {*} noTLS + * @param {*} host + * @param {*} uuid + * @returns + */ +function splitNodeData(uniqueIpTxt, noTLS, host, uuid, userAgent) { + // Regex to match IPv4 and IPv6 + const regex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[.*\]):?(\d+)?#?(.*)?$/; + + // Region codes mapped to corresponding emojis + const regionMap = { + 'SG': '🇸🇬 SG', + 'HK': '🇭🇰 HK', + 'KR': '🇰🇷 KR', + 'JP': '🇯🇵 JP', + 'GB': '🇬🇧 GB', + 'US': '🇺🇸 US', + 'TW': '🇼🇸 TW', + 'CF': '📶 CF' + }; + + const responseBody = uniqueIpTxt.map(ipTxt => { + let address = ipTxt; + let port = "443"; + let remarks = ""; + + const match = address.match(regex); + if (match) { + address = match[1]; + port = match[2] || port; + remarks = match[3] || host; + } else { + let ip, newPort, extra; + + if (ipTxt.includes(':') && ipTxt.includes('#')) { + [ip, newPort, extra] = ipTxt.split(/[:#]/); + } else if (ipTxt.includes(':')) { + [ip, newPort] = ipTxt.split(':'); + } else if (ipTxt.includes('#')) { + [ip, extra] = ipTxt.split('#'); + } else { + ip = ipTxt; + } + + address = ip; + port = newPort || port; + remarks = extra || host; + + // console.log(`splitNodeData---> ip: ${ip} \n extra: ${extra} \n port: ${port}`); + } + + // Replace region code with corresponding emoji + remarks = regionMap[remarks] || remarks; + + // Check if TLS is disabled and if the port is in the allowed set + if (noTLS !== 'true' && portSet_http.has(parseInt(port))) { + return null; // Skip this iteration + } + + const [v2ray, clash] = getConfigLink(uuid, host, address, port, remarks); + return v2ray; + }).filter(Boolean).join('\n'); + + let base64Response = responseBody; + return btoa(base64Response); +} + +/** ---------------------Get CF data------------------------------ */ + +async function getCFConfig(email, key, accountIndex) { + try { + const now = new Date(); + const today = new Date(now); + today.setHours(0, 0, 0, 0); + + // Calculate default value + const ud = Math.floor(((now - today.getTime()) / 86400000) * 24 * 1099511627776 / 2); + let upload = ud; + let download = ud; + let total = 24 * 1099511627776; + + if (email && key) { + const accountId = await getAccountId(email, key); + if (accountId) { + // Calculate start and end time + now.setUTCHours(0, 0, 0, 0); + const startDate = now.toISOString(); + const endDate = new Date().toISOString(); + + // Get summary data + const [pagesSumResult, workersSumResult] = await getCFSum(accountId, accountIndex, email, key, startDate, endDate); + upload = pagesSumResult; + download = workersSumResult; + total = 102400; + } + } + + return { upload, download, total }; + } catch (error) { + console.error('Error in getCFConfig:', error); + return { upload: 0, download: 0, total: 0 }; + } +} + +/** + * + * @param {*} email + * @param {*} key + * @returns + */ +async function getAccountId(email, key) { + try { + const url = 'https://api.cloudflare.com/client/v4/accounts'; + const headers = { + 'X-AUTH-EMAIL': email, + 'X-AUTH-KEY': key + }; + + const response = await fetch(url, { headers }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + //console.error('getAccountId-->', data); + + return data?.result?.[0]?.id || false; + } catch (error) { + console.error('Error fetching account ID:', error); + return false; + } +} + +/** + * + * @param {*} accountId + * @param {*} accountIndex + * @param {*} email + * @param {*} key + * @param {*} startDate + * @param {*} endDate + * @returns + */ +async function getCFSum(accountId, accountIndex, email, key, startDate, endDate) { + try { + const [startDateISO, endDateISO] = [new Date(startDate), new Date(endDate)].map(d => d.toISOString()); + + const query = JSON.stringify({ + query: `query getBillingMetrics($accountId: String!, $filter: AccountWorkersInvocationsAdaptiveFilter_InputObject) { + viewer { + accounts(filter: {accountTag: $accountId}) { + pagesFunctionsInvocationsAdaptiveGroups(limit: 1000, filter: $filter) { + sum { + requests + } + } + workersInvocationsAdaptive(limit: 10000, filter: $filter) { + sum { + requests + } + } + } + } + }`, + variables: { + accountId, + filter: { datetime_geq: startDateISO, datetime_leq: endDateISO } + }, + }); + + const headers = { + 'Content-Type': 'application/json', + 'X-AUTH-EMAIL': email, + 'X-AUTH-KEY': key + }; + + const response = await fetch('https://api.cloudflare.com/client/v4/graphql', { + method: 'POST', + headers, + body: query + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const res = await response.json(); + const accounts = res?.data?.viewer?.accounts?.[accountIndex]; + + if (!accounts) { + throw new Error('找不到账户数据'); + } + + const getSumRequests = (data) => data?.reduce((total, item) => total + (item?.sum?.requests || 0), 0) || 0; + + const pagesSum = getSumRequests(accounts.pagesFunctionsInvocationsAdaptiveGroups); + const workersSum = getSumRequests(accounts.workersInvocationsAdaptive); + + return [pagesSum, workersSum]; + + } catch (error) { + console.error('Error fetching billing metrics:', error); + return [0, 0]; + } +} + +const API_URL = 'http://ip-api.com/json/'; +const TELEGRAM_API_URL = 'https://api.telegram.org/bot'; +/** + * Send message to Telegram channel + * @param {string} type + * @param {string} ip I + * @param {string} [add_data=""] + */ +async function sendMessage(type, ip, add_data = "") { + if (botToken && chatID) { + try { + const ipResponse = await fetch(`${API_URL}${ip}?lang=zh-CN`); + let msg = `${type}\nIP: ${ip}\n${add_data}`; + + if (ipResponse.ok) { + const ipInfo = await ipResponse.json(); + msg = `${type}\nIP: ${ip}\n国家: ${ipInfo.country}\n城市: ${ipInfo.city}\n组织: ${ipInfo.org}\nASN: ${ipInfo.as}\n${add_data}`; + } else { + console.error(`Failed to fetch IP info. Status: ${ipResponse.status}`); + } + + const telegramUrl = `${TELEGRAM_API_URL}${botToken}/sendMessage`; + const params = new URLSearchParams({ + chat_id: chatID, + parse_mode: 'HTML', + text: msg + }); + + await fetch(`${telegramUrl}?${params.toString()}`, { + method: 'GET', + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml', + 'Accept-Encoding': 'gzip, deflate, br', + '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' + } + }); + + } catch (error) { + console.error('Error sending message:', error); + } + } else { + console.warn('botToken or chatID is missing.'); + } +} + + +/** -------------------processing logic-------------------------------- */ +/** + * Handles TROJAN over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the TROJAN header. + * @param {import("@cloudflare/workers-types").Request} request The incoming request object. + * @returns {Promise} A Promise that resolves to a WebSocket response object. + */ +async function trojanOverWSHandler(request) { + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + webSocket.accept(); + + let address = ""; + let portWithRandomLog = ""; + const remoteSocketWrapper = { value: null }; + let udpStreamWrite = null; + + // Logging function + const log = (info, event = "") => { + console.log(`[${address}:${portWithRandomLog}] ${info}`, event); + }; + + // Get early data WebSocket protocol header + const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; + const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); + + // Handle WebSocket data + const handleStreamData = async (chunk) => { + if (udpStreamWrite) { + return udpStreamWrite(chunk); + } + + if (remoteSocketWrapper.value) { + const writer = remoteSocketWrapper.value.writable.getWriter(); + await writer.write(chunk); + writer.releaseLock(); + return; + } + + // Parse Trojan protocol header + const { hasError, message, portRemote = 443, addressRemote = "", rawClientData, addressType } = await parseTrojanHeader(chunk, userID); + address = addressRemote; + portWithRandomLog = `${portRemote}--${Math.random()} tcp`; + + if (hasError) { + throw new Error(message); + } + + // Handle TCP outbound connection + handleTCPOutBound(remoteSocketWrapper, addressRemote, portRemote, rawClientData, webSocket, null, log, addressType); + }; + + // WebSocket stream pipe + readableWebSocketStream.pipeTo( + new WritableStream({ + write: handleStreamData, + close: () => log("readableWebSocketStream is closed"), + abort: (reason) => log("readableWebSocketStream is aborted", JSON.stringify(reason)), + }) + ).catch((err) => { + log("readableWebSocketStream pipeTo error", err); + }); + + return new Response(null, { + status: 101, + // @ts-ignore + webSocket: client + }); +} + + + + +/** + * Handles outbound TCP connections. + * + * @param {any} remoteSocket + * @param {string} addressRemote The remote address to connect to. + * @param {number} portRemote The remote port to connect to. + * @param {Uint8Array} rawClientData The raw client data to write. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. + * @param {Uint8Array} trojanResponseHeader The TROJAN response header. + * @param {function} log The logging function. + * @returns {Promise} The remote socket. + */ +async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, trojanResponseHeader, log, addressType,) { + + /** + * Connects to a given address and port and writes data to the socket. + * @param {string} address The address to connect to. + * @param {number} port The port to connect to. + * @returns {Promise} A Promise that resolves to the connected socket. + */ + async function connectAndWrite(address, port, socks = false) { + /** @type {import("@cloudflare/workers-types").Socket} */ + const tcpSocket = socks ? await socks5Connect(addressType, address, port, log) + : connect({ + hostname: address, + port: port, + }); + remoteSocket.value = tcpSocket; + console.log(`connectAndWrite-${socks} connected to ${address}:${port}`); + const writer = tcpSocket.writable.getWriter(); + await writer.write(rawClientData); + writer.releaseLock(); + return tcpSocket; + } + + /** + * Retries connecting to the remote address and port if the Cloudflare socket has no incoming data. + * @returns {Promise} A Promise that resolves when the retry is complete. + */ + async function retry() { + const tcpSocket = socks5Enable ? await connectAndWrite(addressRemote, portRemote, true) : await connectAndWrite(proxyIP || addressRemote, proxyPort || portRemote); + + console.log(`retry-${socks5Enable} connected to ${addressRemote}:${portRemote}`); + tcpSocket.closed.catch(error => { + console.log('retry tcpSocket closed error', error); + }).finally(() => { + safeCloseWebSocket(webSocket); + }) + remoteSocketToWS(tcpSocket, webSocket, trojanResponseHeader, null, log); + } + + const tcpSocket = await connectAndWrite(addressRemote, portRemote); + + // when remoteSocket is ready, pass to websocket + // remote--> ws + remoteSocketToWS(tcpSocket, webSocket, trojanResponseHeader, retry, log); +} + +/** + * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket. + * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from. + * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT. + * @param {(info: string)=> void} log The logging function. + * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket. + */ +function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { + let readableStreamCancel = false; + const stream = new ReadableStream({ + start(controller) { + webSocketServer.addEventListener('message', (event) => { + const message = event.data; + controller.enqueue(message); + }); + + webSocketServer.addEventListener('close', () => { + safeCloseWebSocket(webSocketServer); + controller.close(); + }); + + webSocketServer.addEventListener('error', (err) => { + log('webSocketServer has error'); + controller.error(err); + }); + const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); + if (error) { + controller.error(error); + } else if (earlyData) { + controller.enqueue(earlyData); + } + }, + + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + + cancel(reason) { + log(`ReadableStream was canceled, due to ${reason}`) + readableStreamCancel = true; + safeCloseWebSocket(webSocketServer); + } + }); + + return stream; +} + +// https://xtls.github.io/development/protocols/trojan.html + +/** + * Processes the TROJAN header buffer and returns an object with the relevant information. + * @param {ArrayBuffer} trojanBuffer The TROJAN header buffer to process. + * @param {string} userID The user ID to validate against the UUID in the TROJAN header. + * @returns {{ + * hasError: boolean, + * message?: string, + * addressRemote?: string, + * addressType?: number, + * portRemote?: number, + * rawDataIndex?: number, + * trojanVersion?: Uint8Array, + * isUDP?: boolean + * }} An object with the relevant information extracted from the TROJAN header buffer. + */ +async function parseTrojanHeader(buffer, userID) { + if (buffer.byteLength < 56) { + return { + hasError: true, + message: "invalid data" + }; + } + let crLfIndex = 56; + if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) { + return { + hasError: true, + message: "invalid header format (missing CR LF)" + }; + } + const password = new TextDecoder().decode(buffer.slice(0, crLfIndex)); + if (password !== pwd) { + return { + hasError: true, + message: "invalid password" + }; + } + + const socks5DataBuffer = buffer.slice(crLfIndex + 2); + if (socks5DataBuffer.byteLength < 6) { + return { + hasError: true, + message: "invalid SOCKS5 request data" + }; + } + + const view = new DataView(socks5DataBuffer); + const cmd = view.getUint8(0); + if (cmd !== 1) { + return { + hasError: true, + message: "unsupported command, only TCP (CONNECT) is allowed" + }; + } + + const addressType = view.getUint8(1); + // 0x01: IPv4 address + // 0x03: Domain name + // 0x04: IPv6 address + let addressLength = 0; + let addressIndex = 2; + let address = ""; + switch (addressType) { + case 1: + addressLength = 4; + address = new Uint8Array( + socks5DataBuffer.slice(addressIndex, addressIndex + addressLength) + ).join("."); + break; + case 3: + addressLength = new Uint8Array( + socks5DataBuffer.slice(addressIndex, addressIndex + 1) + )[0]; + addressIndex += 1; + address = new TextDecoder().decode( + socks5DataBuffer.slice(addressIndex, addressIndex + addressLength) + ); + break; + case 4: + addressLength = 16; + const dataView = new DataView(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); + const ipv6 = []; + for (let i = 0; i < 8; i++) { + ipv6.push(dataView.getUint16(i * 2).toString(16)); + } + address = ipv6.join(":"); + break; + default: + return { + hasError: true, + message: `invalid addressType is ${addressType}` + }; + } + + if (!address) { + return { + hasError: true, + message: `address is empty, addressType is ${addressType}` + }; + } + + const portIndex = addressIndex + addressLength; + const portBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2); + const portRemote = new DataView(portBuffer).getUint16(0); + return { + hasError: false, + addressRemote: address, + portRemote, + rawClientData: socks5DataBuffer.slice(portIndex + 4), + addressType: addressType + }; +} + +/** + * Converts a remote socket to a WebSocket connection. + * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to. + * @param {ArrayBuffer | null} trojanResponseHeader The TROJAN response header. + * @param {(() => Promise) | null} retry The function to retry the connection if it fails. + * @param {(info: string) => void} log The logging function. + * @returns {Promise} A Promise that resolves when the conversion is complete. + */ +async function remoteSocketToWS(remoteSocket, webSocket, trojanResponseHeader, retry, log) { + // remote--> ws + let remoteChunkCount = 0; + let chunks = []; + /** @type {ArrayBuffer | null} */ + let trojanHeader = trojanResponseHeader; + let hasIncomingData = false; // check if remoteSocket has incoming data + await remoteSocket.readable + .pipeTo( + new WritableStream({ + start() { + }, + /** + * + * @param {Uint8Array} chunk + * @param {*} controller + */ + async write(chunk, controller) { + hasIncomingData = true; + remoteChunkCount++; + if (webSocket.readyState !== WS_READY_STATE_OPEN) { + controller.error( + 'webSocket.readyState is not open, maybe close' + ); + } + if (trojanHeader) { + webSocket.send(await new Blob([trojanHeader, chunk]).arrayBuffer()); + trojanHeader = null; + } else { + // console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`); + // seems no need rate limit this, CF seems fix this??.. + // if (remoteChunkCount > 20000) { + // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M + // await delay(1); + // } + webSocket.send(chunk); + } + }, + close() { + log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); + // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. + }, + abort(reason) { + console.error(`remoteConnection!.readable abort`, reason); + }, + }) + ) + .catch((error) => { + console.error( + `remoteSocketToWS has exception `, + error.stack || error + ); + safeCloseWebSocket(webSocket); + }); + + // seems is cf connect socket have error, + // 1. Socket.closed will have error + // 2. Socket.readable will be close without any data coming + if (hasIncomingData === false && retry) { + log(`retry`) + retry(); + } +} + +const WS_READY_STATE_OPEN = 1; +const WS_READY_STATE_CLOSING = 2; +/** + * Closes a WebSocket connection safely without throwing exceptions. + * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close. + */ +function safeCloseWebSocket(socket) { + try { + if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { + socket.close(); + } + } catch (error) { + console.error('safeCloseWebSocket error', error); + } +} + +async function socks5Connect(ipType, remoteIp, remotePort, log) { + const { username, password, hostname, port } = parsedSocks5; + const socket = connect({ hostname, port }); + const writer = socket.writable.getWriter(); + const reader = socket.readable.getReader(); + const encoder = new TextEncoder(); + + const sendSocksGreeting = async () => { + const greeting = new Uint8Array([5, 2, 0, 2]); + await writer.write(greeting); + console.log('SOCKS5 greeting sent'); + }; + + const handleAuthResponse = async () => { + const res = (await reader.read()).value; + if (res[1] === 0x02) { + console.log("SOCKS5 server requires authentication"); + if (!username || !password) { + console.log("Please provide username and password"); + throw new Error("Authentication required"); + } + const authRequest = new Uint8Array([ + 1, username.length, ...encoder.encode(username), + password.length, ...encoder.encode(password) + ]); + await writer.write(authRequest); + const authResponse = (await reader.read()).value; + if (authResponse[0] !== 0x01 || authResponse[1] !== 0x00) { + console.log("SOCKS5 server authentication failed"); + throw new Error("Authentication failed"); + } + } + }; + + const sendSocksRequest = async () => { + let DSTADDR; + switch (ipType) { + case 1: + DSTADDR = new Uint8Array([1, ...remoteIp.split('.').map(Number)]); + break; + case 2: + DSTADDR = new Uint8Array([3, remoteIp.length, ...encoder.encode(remoteIp)]); + break; + case 3: + DSTADDR = new Uint8Array([4, ...remoteIp.split(':').flatMap(x => [ + parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16) + ])]); + break; + default: + console.log(`Invalid address type: ${ipType}`); + throw new Error("Invalid address type"); + } + const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, remotePort >> 8, remotePort & 0xff]); + await writer.write(socksRequest); + console.log('SOCKS5 request sent'); + + const response = (await reader.read()).value; + if (response[1] !== 0x00) { + console.log("SOCKS5 connection failed"); + throw new Error("Connection failed"); + } + console.log("SOCKS5 connection established"); + }; + + try { + await sendSocksGreeting(); + await handleAuthResponse(); + await sendSocksRequest(); + } catch (error) { + console.log(error.message); + return null; // Return null on failure + } finally { + writer.releaseLock(); + reader.releaseLock(); + } + return socket; +} + + +/** -------------------Home page-------------------------------- */ +async function nginx() { + const text = ` + + + + Welcome to nginx! + + + +

Welcome to nginx!

+

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

+ +

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

+ +

Thank you for using nginx.

+ + + ` + return text; +} diff --git a/_worker.js.zip b/_worker.js.zip new file mode 100644 index 0000000000000000000000000000000000000000..1cc7d98d2d0869f281cb8a2d0693d12f64ecdac3 GIT binary patch literal 20671 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 literal 0 HcmV?d00001 diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..09b50c8 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,13 @@ +name = "am-trojan" +type = "javascript" +compatibility_date = "2024-07-28" + +[build] +upload = { "dir" = "./", "format" = "service-worker" } + +[site] +bucket = "./public" +entry-point = "workers-site" + +[env.production] +compatibility_flags = ["nodejs_compat"]