├── .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 |
Key:
1928 |
1929 |
1930 |
Value:
1931 |
1932 |
1933 |
Save
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 |
--------------------------------------------------------------------------------