├── example
├── proxyDomain.txt
├── nat64Domain.txt
├── nat64Prefix.txt
├── ipv4.csv
├── ipJP.txt
├── ipHK.txt
├── ipUS.txt
├── proxyip.txt
├── ipUrl.txt
├── proxyip_am.txt
└── ipv4.txt
├── _worker.js.zip
├── _worker.obs.js.zip
├── .gitignore
├── .github
└── workflows
│ └── zip_flows.yml
├── ipv4.txt
├── LICENSE
├── README.md
├── _worker.obs.src.js
└── _worker.js
/example/proxyDomain.txt:
--------------------------------------------------------------------------------
1 | tiktok.com
2 | x.com
--------------------------------------------------------------------------------
/example/nat64Domain.txt:
--------------------------------------------------------------------------------
1 | chatgpt.com
2 | twitch.tv
--------------------------------------------------------------------------------
/example/nat64Prefix.txt:
--------------------------------------------------------------------------------
1 | 2602:fc59:b0:64::
2 | 2602:fc59:b0:64::
3 |
--------------------------------------------------------------------------------
/_worker.js.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amclubs/am-cf-tunnel/HEAD/_worker.js.zip
--------------------------------------------------------------------------------
/example/ipv4.csv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amclubs/am-cf-tunnel/HEAD/example/ipv4.csv
--------------------------------------------------------------------------------
/_worker.obs.js.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amclubs/am-cf-tunnel/HEAD/_worker.obs.js.zip
--------------------------------------------------------------------------------
/example/ipJP.txt:
--------------------------------------------------------------------------------
1 | 156.238.19.8:443#JP
2 | www.4chan.org
3 | www.okcupid.com
4 | www.glassdoor.com
5 | www.iakeys.com#教程样例不作维护
6 |
--------------------------------------------------------------------------------
/example/ipHK.txt:
--------------------------------------------------------------------------------
1 | 127.0.0.1:1234#cfnat
2 | 104.17.142.12:443#HK
3 | 104.17.71.31#HK
4 | c.xf.free.hr
5 | yecaoyun.com#教程样例不作维护
6 |
--------------------------------------------------------------------------------
/example/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 | www.udemy.com#教程样例不作维护
6 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/example/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
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/example/proxyip_am.txt:
--------------------------------------------------------------------------------
1 | 103.106.228.126:2053#JP
2 | 103.231.75.238:2053#TR
3 | 103.75.118.144:2053#JP
4 | 104.234.50.11:2053#US
5 | 104.234.50.12:2053#US
6 | 104.234.50.12:8443#US
7 | 104.248.145.216:443#SG
8 | 104.248.16.87:443#DE
9 | 104.248.165.18:443#GB
10 | 104.234.50.132:2053#US
11 | 104.234.50.138:2053#US
12 | 104.234.50.158:2053#US
13 | 103.82.101.179:443#FR
14 | 104.168.58.31:443#US
15 | 107.182.27.151:443#US
16 | 108.160.139.154:8443#JP
17 | 109.107.164.231:443#NL
18 | 109.120.158.198:443#NL
19 | 109.120.178.197:443#FR
20 | 109.120.178.197:8443#FR
21 | 109.120.178.231:2053#FR
22 | 109.120.179.49:2053#FR
23 | 109.107.165.225:443#NL
24 | 109.107.166.23:443#NL
25 | 109.107.166.33:443#NL
26 | 109.107.167.104:443#NL
27 | 104.224.177.224:8443#US
28 | 104.234.24.73:443#US
29 | 128.199.18.189:2053#IN
30 | 128.199.181.227:2087#SG
31 | 131.186.31.213:443#KR
32 | 131.186.38.156:8443#JP
33 | 156.154.245.83:443#US
34 | 156.230.12.71:443#SC
35 | 156.255.90.175:2053#HK
36 | 156.255.90.175:8443#HK
37 | 132.145.114.128:443#JP
38 | 128.199.22.103:8443#IN
39 | 128.199.237.143:443#SG
40 | 128.199.255.155:8443#SG
41 | 31.133.0.150:2053#PL
42 | 31.133.0.159:2053#PL
43 |
--------------------------------------------------------------------------------
/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 | otto.ns.cloudflare.com
54 | pranab.ns.cloudflare.com
55 | rustam.ns.cloudflare.com
56 | sullivan.ns.cloudflare.com
57 | trevor.ns.cloudflare.com
58 | uriah.ns.cloudflare.com
59 | wilson.ns.cloudflare.com
60 | 162.159.133.85
61 | 172.67.106.26
62 | 172.67.110.232
63 | 172.64.201.25
64 | www.4chan.org
65 | www.okcupid.com
66 | www.glassdoor.com
67 | www.udemy.com
68 | www.iakeys.com
69 | www.sean-now.com
70 | whatismyipaddress.com
71 | www.pcmag.com
72 | www.ipchicken.com
73 | iplocation.io
74 | www.who.int
75 | www.wto.org
76 | stock.hostmonit.com
77 | www.tushencn.com
78 | www.7749tv.com
79 | manmankan.cc
80 | www.ddwhm.com
81 | ipinfo.in
82 | gamer.com.tw
83 | steamdb.info
84 | toy-people.com
85 | silkbook.com
86 | ipv4.ip.sb
87 | ip.gs
88 | dnschecker.org
89 | tasteatlas.com
90 | pixiv.net
91 | comicabc.com
92 | c.xf.free.hr
93 | yecaoyun.com
94 |
95 |
96 |
--------------------------------------------------------------------------------
/example/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 | otto.ns.cloudflare.com
54 | pranab.ns.cloudflare.com
55 | rustam.ns.cloudflare.com
56 | sullivan.ns.cloudflare.com
57 | trevor.ns.cloudflare.com
58 | uriah.ns.cloudflare.com
59 | wilson.ns.cloudflare.com
60 | 162.159.133.85
61 | 172.67.106.26
62 | 172.67.110.232
63 | 172.64.201.25
64 | www.4chan.org
65 | www.okcupid.com
66 | www.glassdoor.com
67 | www.udemy.com
68 | www.iakeys.com
69 | www.sean-now.com
70 | whatismyipaddress.com
71 | www.pcmag.com
72 | www.ipchicken.com
73 | iplocation.io
74 | www.who.int
75 | www.wto.org
76 | stock.hostmonit.com
77 | www.tushencn.com
78 | www.7749tv.com
79 | manmankan.cc
80 | www.ddwhm.com
81 | ipinfo.in
82 | gamer.com.tw
83 | steamdb.info
84 | toy-people.com
85 | silkbook.com
86 | ipv4.ip.sb
87 | ip.gs
88 | dnschecker.org
89 | tasteatlas.com
90 | pixiv.net
91 | comicabc.com
92 | c.xf.free.hr
93 | yecaoyun.com
94 |
95 |
96 |
--------------------------------------------------------------------------------
/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、Trojan 配置信息转换为订阅内容。使用该脚本你可以方便地将 VLESS、Trojan 配置信息使用在线配置转换到 Clash、 Singbox 、Quantumult X等工具中订阅使用。Cloudflare Workers 和 Pages 生成VLESS、Trojan节点通过订阅器实现一键订阅节点。[最新视频教程](https://youtu.be/i-XnnP-MptY)、[🎬 YouTube](https://youtube.com/@am_clubs?sub_confirmation=1)、 [💬 Telegram](https://t.me/am_clubs)、[📂 GitHub](https://github.com/amclubs)、[🌐 Blog](https://amclubss.com)
3 |
4 | ### 🎬推荐视频教程
5 | - [Error1101和522解决教程](https://youtu.be/4fcyJjstFdg) | [优选IP和反代IP教程](https://youtu.be/pKrlfRRB0gU) | [常见-1问题教程](https://youtu.be/kYQxV1G-ePw) | [免费域名教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR) | [NAT64版教程](https://youtu.be/nx80sGpVoBM)
6 | - [VLESS免费节点部署教程](https://youtu.be/dPH63nITA0M) | [Trojan免费节点部署教程](https://youtu.be/uh27CVVi6HA) | [从入门到精通免费部署教程](https://youtu.be/ag12Rpc9KP4)| [高级固定节点区域教程](https://youtu.be/wgeM9XvZ5RA)
7 | - [GitHub私有储优选IP文教程](https://youtu.be/vX3U3FuuTT8) | [CF免费KV存储IP文件教程](https://youtu.be/dzxezRV1v-o) | [获取CF自家域名无限节点](https://youtu.be/novrPiMsK70) | [聚合节点订阅教程](https://youtu.be/YBO2hf96150)
8 | - [🔥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)
9 | - 本频道订阅器转换地址:https://sub.amclubss.com
10 |
11 | ## 📝一、前期准备资料
12 |
13 | 点击展开/收起
14 |
15 | ### 1、注册免费**cloudflare**帐号(邮箱就可以免费注册)
16 | - 注册地址:https://cloudflare.com [点击观看视频教程]
17 |
18 | ### 2、注册**免费域名** [点击观看所有免费域名视频教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR)
19 |
20 | ### 3、**订阅工具** [点击观看使用视频教程](https://youtu.be/xGOL57cmvaw)
21 | 👉 [点击加入TG群 数字套利|交流群](https://t.me/AM_CLUBS)发送关键字 **工具** 获取下载
22 |
23 | ### 4、Cloudflare标准 **端口** 知识 [点击观看优选IP视频教程](https://youtu.be/pKrlfRRB0gU)
24 | - 80系端口(HTTP):80,8080,8880,2052,2082,2086,2095
25 | - 443系端口(HTTPS):443,2053,2083,2087,2096,8443
26 | - [IP落地测试工具地址](https://ip.sb/)
27 |
28 |
29 |
30 | ##
31 | ## ⚙️ 二、Workers 部署方法 [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=165s)
32 |
33 | 点击展开/收起
34 |
35 | 1. 部署 Cloudflare Worker:
36 | - 在 CloudFlare主页的左边菜单的 `计算(Workers)` 选项卡 -> 点击 `Workers 和 Pages` -> 右上方点击 -> `创建应用程序` -> 选择 `Workers`里的 `从 Hello World! 开始` 点击 `开始使用` -> 填入 `Worker 名称`(此名称自己命名) 后 -> 右下方点击 `部署` 后-> 右上方点击 `断续处理项目`。(此步已有可忽略)。
37 | 2. 给UUID设置KV存储桶(推荐设置):
38 | - 在 CloudFlare主页的左边菜单的 `存储和数据库` 选项卡 -> 展开选择点击 `Workers KV` -> 右方点击 -> `创建实例(Create Instance)` -> 填入 `命名空间名称`(此名称自己命名) 后 -> 点击 `创建`。(此步已有可忽略)
39 | - 在 workers控制台的 `绑定` 选项卡 -> 右方点击 -> `添加绑定` -> 选择 `KV 命名空间` 右下方点击 -> `添加绑定` -> 变量名称 填入 `amclubs`(此名称固定不能变) -> KV 命名空间 选择 在上面创建的 `命名空间名称`后 -> 右下方点击 `添加绑定`。
40 | 3. 部署 Cloudflare Worker代码:
41 | - 在 workers控制台的 右上角方点击 `编辑代码(>)` 图标进入代码编辑页面。
42 | - 将 [_worker.js](https://github.com/amclubs/am-cf-tunnel/blob/main/_worker.js) 的内容粘贴到 Worker 编辑器中 右上方点击 -> `部署` 完成部署。
43 | 4. 给 workers绑定 自定义域: [免费域名申请教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR)
44 | - 在 workers控制台的 `设置` 选项卡 -> 点击 `域和路由` -> 右方点击 -> `添加` -> 选择 `自定义域`。
45 | - 填入你已转入 CloudFlare 域名 (amclubss.com) 解析服务的次级域名,例如:`vless.amclubss.com`后 点击 `添加域`,等待证书生效即可。
46 | 5. 验证部署是否成功:
47 | - 访问 `https://[YOUR-WORKERS-URL]` 即可进入登录页面,登录成功就是完成部署(默认登录密码(UUID)是:ec872d8f-72b0-4a04-b612-0327d85e18ed)。
48 | - 例如 `https://vless.amclubss.com` 然后进入登录页面 -> 输入密码 `ec872d8f-72b0-4a04-b612-0327d85e18ed` -> 点击登录 -> 成功登录。
49 | 6. 修改默认登录密码(UUID)变量,使用KV存储桶(推荐修改,防止别人用你节点):
50 | - `https://vless.amclubss.com` 然后进入登录页面 -> 输入密码 `ec872d8f-72b0-4a04-b612-0327d85e18ed` -> 点击登录 -> 成功登录。
51 | - 在登录成功页面 ID选项 -> 填入 `新的UUID` 后,[在线获取UUID](https://1024tools.com/uuid) -> 点击 `保存`。
52 | - 保存成功后,原登录密码(UUID)已作废不能访问,用新登录密码(UUID)登录访问即可。
53 | 7. 订阅连接和节点生成使用方法: [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=596s)
54 | - 进入 [am-cf-tunnel-sub](https://github.com/amclubs/am-cf-tunnel-sub) 项目 -> 根据项目教程部署和使用。(此步已有可忽略)
55 | - 本频道订阅器转换地址:https://sub.amclubss.com
56 |
57 |
58 |
59 | ##
60 | ## 📦三、Pages 上传 部署方法 **(最佳推荐!!!)** [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=1100s)
61 |
62 | 点击展开/收起
63 |
64 | 1. 部署 Cloudflare Pages:
65 | - 下载 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/_worker.js.zip) 文件,并点上 Star !!!
66 | - 在 CloudFlare主页的左边菜单的 `计算(Workers)` 选项卡 -> 点击 `Workers 和 Pages` -> 右上方点击 -> `创建应用程序` -> 选择 `Pages`里的 `拖放文件` 点击 `开始使用` -> 填入 `项目名称`(此名称自己命名)后 -> 右边点击 `创建项目` 后 -> 下方 `上传您的项目资产` 点击 `拖放或从计算机中选择` 后 -> 点击 `上传压缩文件` 然后上传你下载好的 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/_worker.js.zip) 文件后点击 `部署站点`。
67 | 2. 给UUID设置KV存储桶(推荐设置):
68 | - 在 CloudFlare主页的左边菜单的 `存储和数据库` 选项卡 -> 展开选择点击 `Workers KV` -> 右方点击 -> `创建实例(Create Instance)` -> 填入 `命名空间名称`(此名称自己命名) 后 -> 点击 `创建`。(此步已有可忽略)
69 | - 在 Pages控制台的 `设置` 选项卡 -> 点击 `绑定` -> 右方点击 -> `添加` -> 选择 `KV 命名空间` -> 变量名称 填入 `amclubs`(此名称固定不能变) -> KV 命名空间 选择 在上面创建的 `命名空间名称`后 -> 右下方点击 `保存`。
70 | - 在 `设置` 选项卡,在右上角点击 `创建部署` 后,重新上传 [_worker.js.zip](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/_worker.js.zip) 文件后点击 `保存并部署` 即可。
71 | 3. 给 Pages绑定 CNAME自定义域:[无域名绑定Cloudflare部署视频教程]->[免费域名教程1](https://youtu.be/wHJ6TJiCF0s) [免费域名教程2](https://youtu.be/yEF1YoLVmig) [免费域名教程3](https://www.youtube.com/watch?v=XS0EgqckUKo&t=320s)
72 | - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。
73 | - 填入你的自定义次级域名,注意不要使用你的根域名,例如:
74 | 您分配到的域名是 `amclubss.com`,则添加自定义域填入 `vless.amclubss.com`即可,点击 `激活域`即可。
75 | 4. 验证部署是否成功:
76 | - 访问 `https://[YOUR-WORKERS-URL]` 即可进入登录页面,登录成功就是完成部署(默认登录密码(UUID)是:ec872d8f-72b0-4a04-b612-0327d85e18ed)。
77 | - 例如 `https://vless.amclubss.com` 然后进入登录页面 -> 输入密码 `ec872d8f-72b0-4a04-b612-0327d85e18ed` -> 点击登录 -> 成功登录。
78 | 5. 修改默认登录密码(UUID)变量,使用KV存储桶(推荐修改,防止别人用你节点):
79 | - `https://vless.amclubss.com` 然后进入登录页面 -> 输入密码 `ec872d8f-72b0-4a04-b612-0327d85e18ed` -> 点击登录 -> 成功登录。
80 | - 在登录成功页面 ID选项 -> 填入 `新的UUID` 后,[在线获取UUID](https://1024tools.com/uuid) -> 点击 `保存`。
81 | - 保存成功后,原登录密码(UUID)已作废不能访问,用新登录密码(UUID)登录访问即可。
82 | 6. 订阅连接和节点生成使用方法: [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=596s)
83 | - 进入 [am-cf-tunnel-sub](https://github.com/amclubs/am-cf-tunnel-sub) 项目 -> 根据项目教程部署和使用。(此步已有可忽略)
84 | - 本频道订阅器转换地址:https://sub.amclubss.com
85 |
86 |
87 |
88 | ##
89 | ## 🧰四、Pages GitHub 部署方法 **(不推荐)** [视频教程](https://www.youtube.com/watch?v=dPH63nITA0M&t=654s)
90 |
91 | 点击展开/收起
92 |
93 | 1. 部署 Cloudflare Pages:
94 | - 在 Github 上先 Fork 本项目[am-cf-tunnel](https://github.com/amclubs/am-cf-tunnel),并点上 Star !!!
95 | - 在 CloudFlare主页的左边菜单的 `计算(Workers)` 选项卡 -> 点击 `Workers 和 Pages` -> 右上方点击 -> `创建应用程序` -> 选择 `Pages`里的 `导入现有 Git 存储库` 点击 `开始使用` -> 选择GitHub 点击`连接GitHub`根据提示授权GitHub和项目(此步已有可忽略)后 -> 选中 `am-cf-tunnel`项目后 -> 点击 `开始设置` -> 可修改`项目名称`(此名称自己命名) 后 -> 右下方点击 `保存并部署`即可。
96 | 2. 给UUID设置KV存储桶(推荐设置):
97 | - 在 CloudFlare主页的左边菜单的 `存储和数据库` 选项卡 -> 展开选择点击 `Workers KV` -> 右方点击 -> `创建实例(Create Instance)` -> 填入 `命名空间名称`(此名称自己命名) 后 -> 点击 `创建`。(此步已有可忽略)
98 | - 在 Pages控制台的 `设置` 选项卡 -> 点击 `绑定` -> 右方点击 -> `添加` -> 选择 `KV 命名空间` -> 变量名称 填入 `amclubs`(此名称固定不能变) -> KV 命名空间 选择 在上面创建的 `命名空间名称`后 -> 右下方点击 `保存`。
99 | - 在 `设置` 选项卡,点击 `部署` -> 在所有部署 找到最新一条部署记录 ,在右边点击 3个点 `...` 选择 `重试部署` 即可。
100 | 3. 给 Pages绑定 CNAME自定义域:[无域名绑定Cloudflare部署视频教程]->[免费域名教程1](https://youtu.be/wHJ6TJiCF0s) [免费域名教程2](https://youtu.be/yEF1YoLVmig) [免费域名教程3](https://www.youtube.com/watch?v=XS0EgqckUKo&t=320s)
101 | - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。
102 | - 填入你的自定义次级域名,注意不要使用你的根域名,例如:
103 | 您分配到的域名是 `amclubss.com`,则添加自定义域填入 `vless.amclubss.com`即可,点击 `激活域`即可。
104 | 4. 验证部署是否成功:
105 | - 访问 `https://[YOUR-WORKERS-URL]` 即可进入登录页面,登录成功就是完成部署(默认登录密码(UUID)是:ec872d8f-72b0-4a04-b612-0327d85e18ed)。
106 | - 例如 `https://vless.amclubss.com` 然后进入登录页面 -> 输入密码 `ec872d8f-72b0-4a04-b612-0327d85e18ed` -> 点击登录 -> 成功登录。
107 | 5. 修改默认登录密码(UUID)变量,使用KV存储桶(推荐修改,防止别人用你节点):
108 | - `https://vless.amclubss.com` 然后进入登录页面 -> 输入密码 `ec872d8f-72b0-4a04-b612-0327d85e18ed` -> 点击登录 -> 成功登录。
109 | - 在登录成功页面 ID选项 -> 填入 `新的UUID` 后,[在线获取UUID](https://1024tools.com/uuid) -> 点击 `保存`。
110 | - 保存成功后,原登录密码(UUID)已作废不能访问,用新登录密码(UUID)登录访问即可。
111 | 6. 订阅连接和节点生成使用方法: [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=596s)
112 | - 进入 [am-cf-tunnel-sub](https://github.com/amclubs/am-cf-tunnel-sub) 项目 -> 根据项目教程部署和使用。(此步已有可忽略)
113 | - 本频道订阅器转换地址:https://sub.amclubss.com
114 |
115 |
116 |
117 | ##
118 | ## 🔧五、变量说明 [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=468s)
119 | | 变量名 | 示例 | 必填 | 备注 | YT |
120 | |-----|-----|-----|-----|-----|
121 | | ID | ec872d8f-72b0-4a04-b612-0327d85e18ed(默认)|✅| 支持Cloudflare的KV存储桶设置 [在线获取UUID](https://1024tools.com/uuid) VLESS、Trojan节点共用 | |
122 | | D_URL | https://cloudflare-dns.com/dns-query |❌| DNS解析获取作用,小白勿用 | |
123 |
124 | ##
125 | ## 🧩六、节点订阅配置 [Vercel部署视频教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR) [Cloudfare部署视频教程](https://youtu.be/f8ZTvv4u3Pw)
126 |
127 |
128 | 点击展开/收起
129 |
130 | #### `①` Vercel方式部署 [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=596s)
131 | 1. Fork或克隆本仓库[am-cf-tunnel-sub](https://github.com/amclubs/am-cf-tunnel-sub)到您的 GitHub/GitLab 账户
132 | 2. 登录 [Vercel](https://vercel.com),点击"New Project" [点击观看注册视频教程]
133 | 3. 导入您的仓库,使用默认设置
134 | 4. **⚠️ 重要:在"Settings" > "Environment Variables"中添加 `UUID` 和 `HOST` 变量(必须设置)**
135 | 5. 点击"Deploy"
136 |
137 | 访问 `http://部署域名` 即可。
138 |
139 | #### `②` Cloudfare方式部署(Pages GitHub)[视频教程](https://youtu.be/f8ZTvv4u3Pw)
140 | 1. 部署 Cloudflare Pages:
141 | - 在 Github 上先 Fork 本项目[am-cf-tunnel-sub](https://github.com/amclubs/am-cf-tunnel-sub),并点上 Star !!!
142 | - 在 CloudFlare主页的左边菜单的 `计算(Workers)` 选项卡 -> 点击 `Workers 和 Pages` -> 右上方点击 -> `创建应用程序` -> 选择 `Pages`里的 `导入现有 Git 存储库` 点击 `开始使用` -> 选择GitHub 点击`连接GitHub`根据提示授权GitHub和项目(此步已有可忽略)后 -> 选中 `am-cf-tunnel-sub`项目后 -> 点击 `开始设置` -> 可修改`项目名称`(此名称自己命名) 后 -> 右下方点击 `保存并部署`即可。
143 | 2. 设置节点UUID和HOST变量:
144 | - 在 Pages控制台的 `设置` 选项卡 -> 点击 `设置` -> 左方点击 `变量和机密` -> 右方点击 `添加` -> 变量名称 填入 `UUID`(此名称固定不能变) ,值填入CF部署节点ID -> 再点击添加变量 填入 `HOST`(此名称固定不能变),值填入CF部署的自定义域名 后 -> 右下方点击 `保存`。
145 | - 在 `设置` 选项卡,点击 `部署` -> 在所有部署 找到最新一条部署记录 ,在右边点击 3个点 `...` 选择 `重试部署` 即可。
146 | 3. 给 Pages绑定 CNAME自定义域:[无域名绑定Cloudflare部署视频教程]->[免费域名教程1](https://youtu.be/wHJ6TJiCF0s) [免费域名教程2](https://youtu.be/yEF1YoLVmig) [免费域名教程3](https://www.youtube.com/watch?v=XS0EgqckUKo&t=320s)
147 | - 在 Pages控制台的 `自定义域`选项卡,下方点击 `设置自定义域`。
148 | - 填入你的自定义次级域名,注意不要使用你的根域名,例如:
149 | 您分配到的域名是 `amclubss.com`,则添加自定义域填入 `sub.amclubss.com`即可,点击 `激活域`即可。
150 | 4. 验证部署是否成功:
151 | - 访问 `https://[YOUR-WORKERS-URL]` 即可进入登录页面,登录成功就是完成部署(默认登录密码(UUID)是:ec872d8f-72b0-4a04-b612-0327d85e18ed)。
152 | - 例如 `https://sub.amclubss.com` 然后进入登录页面 -> 输入密码 `ec872d8f-72b0-4a04-b612-0327d85e18ed` -> 点击登录 -> 成功登录。
153 | 5. 修改默认登录密码(ID)变量,(强烈要求修改,防止别人用你节点):
154 | - 在 Pages控制台的 `设置` 选项卡 -> 点击 `设置` -> 左方点击 `变量和机密` -> 右方点击 `添加` -> 变量名称 填入 `ID`(此名称固定不能变) ,自己设置复杂的密码 -> 右下方点击 `保存`。
155 | - 在 `设置` 选项卡,点击 `部署` -> 在所有部署 找到最新一条部署记录 ,在右边点击 3个点 `...` 选择 `重试部署` 即可。
156 | - 保存成功后,原登录密码(ID)已作废不能访问,用新登录密码(ID)登录访问即可。
157 | 6. 本频道订阅器转换地址:https://sub.amclubss.com
158 |
159 | 7. 变量说明 [视频教程](https://www.youtube.com/watch?v=i-XnnP-MptY&t=808s)
160 |
161 | | 变量名 | 示例 | 必填 | 备注 | YT |
162 | |-----|-----|-----|-----|-----|
163 | | ID | ec872d8f-72b0-4a04-b612-0327d85e18ed(默认)|✅| 订阅器的登录密码 | |
164 | | UUID | ec872d8f-72b0-4a04-b612-0327d85e18ed |✅| Cloudflare部署节点的ID变量值[在线获取UUID](https://1024tools.com/uuid) | |
165 | | HOST | vless.amclubss.com |✅| Cloudflare部署节点的域名或自定域名 | |
166 | | IP_URL | [https://raw.github.../ipUrl.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/example/ipUrl.txt) 或 [https://raw.github.../ipv4.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/example/ipv4.txt) |❌| (推荐)优选(ipv4、ipv6、域名、API)地址(支持多个之间`,`或 换行 作间隔),支持文件连接后里带PROXYIP参数,可以实现不同区域优先IP使用不同的PROXYIP固定区域,解决IP乱跳问题 | [视频教程](https://www.youtube.com/watch?v=4fcyJjstFdg&t=349s)|
167 | | PROXYIP | proxyip.amclubs.kozow.com 或 [https://raw.github.../proxyip.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/example/proxyip.txt) |❌| 访问CloudFlare的CDN代理节点(支持多PROXYIP, PROXYIP之间使用`,`或 换行 作间隔),支持端口设置默认443 如: proxyip.amclubs.kozow.com:2053 ,支持远程txt或csv文件| [视频教程](https://youtu.be/pKrlfRRB0gU) |
168 | | SOCKS5 | user:password@127.0.0.1:1080 |❌| 优先作为访问CFCDN站点的SOCKS5代理 | [视频教程](https://youtu.be/Bw82BH_ecC4) |
169 | | NAT64 | true/false |❌| 默认false,是否开启nat做PROXYIP(反代IP),开启后优选使用NAT64再用PROXYIP | [视频教程](https://www.youtube.com/watch?v=nx80sGpVoBM&t=533s) |
170 | | NAT64_PREFIX | 2602:fc59:b0:64:: 或 [https://raw.github.../nat64Prefix.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/example/nat64Prefix.txt) |❌| 指定自定NAT64前缀,不填走CF默认的 (https://amclubss.com/public/) | [视频教程](https://www.youtube.com/watch?v=nx80sGpVoBM&t=533s)|
171 | | 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等 订阅转换配置文件 ||
172 | | SUB_CONVERTER | url.v1.mk |❌| clash、singbox等 订阅转换后端的api地址 ||
173 | | PROT_TYPE | 默认空 |❌| 默认空,就是生成vless和trojan节点,vless(只生成vless节点),trojan(只生成trojan节点) | [视频教程](https://www.youtube.com/watch?v=emEBm8Gw2wI&t=922s) |
174 | | NIP_HOST | 553558.xyz(默认) |❌| 优先IP时需要的nip服务 | |
175 | | EXTRA_IP | [https://raw.github.../ipv4.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/example/ipv4.txt) |❌| 优先IP时需要的nip服务 | |
176 | | EXTRA_IP_PROXY | [https://raw.github.../proxyip_am.txt](https://raw.githubusercontent.com/amclubs/am-cf-tunnel/main/example/proxyip_am.txt) |❌| 优先IP时需要的nip服务 | |
177 | | CF_NAMESPACE_ID | 37cf3x8xxx(Vercel方式部署才需求) |❌ | 优先IP时需要CF的KV存桶ID(存储和数据库->Workers KV->创建的命名空间ID) ||
178 | | CF_ACCOUNT_ID | 0b0e49ba2xxxx(Vercel方式部署才需求) |❌ | 优先IP时需要CF的帐号ID(计算和AI->Account Details->Account ID) ||
179 | | CF_EMAIL | xxx@gmail.com(Vercel方式部署才需求) |❌ | 优先IP时需要CF的帐号邮箱 ||
180 | | CF_API_KEY | 49ba2xxxx(Vercel方式部署才需求) |❌ | 优先IP时需要CF的API令牌(管理帐户->帐户API令牌->创建的令牌D) ||
181 |
182 | - 本频道订阅器转换地址:https://sub.amclubss.com
183 |
184 |
185 |
186 | ##
187 | ## 🛠已适配订阅工具 [点击进入视频教程](https://youtu.be/xGOL57cmvaw) [点进进入karing视频教程](https://youtu.be/M3vLLBWfuFg)
188 |
189 | 点击展开/收起
190 |
191 | - Mac(苹果电脑)
192 | - [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)
193 |
194 | - Win(win系统电脑)
195 | - [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)
196 |
197 | - IOS(苹果手机)
198 | - [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)
199 |
200 | - Android(安卓手机)
201 | - [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)
202 |
203 | - 软路由
204 | - [openclash(clash.meta)](https://github.com/vernesong/OpenClash/releases)
205 |
206 |
207 |
208 | ##
209 | ### 🙏感谢
210 | [3Kmfi6HP](https://github.com/3Kmfi6HP/EDtunnel)、[ACL4SSR](https://github.com/ACL4SSR/ACL4SSR/tree/master/Clash/config)
211 |
212 | ### 🌟推荐
213 | **【流量光】** 中转+专线高速机场 (**9.9元300G每月**) (**75元包年每月300G**) (**55元1000GB不限时**)✅畅爽晚高峰 解锁ChatGPT、全流媒体(送小火箭)
214 | 🌐官网:[https://llgjc1.com](https://llgjc1.com/#/register?code=bIUDEPTu)
215 |
216 | #
217 |
218 | ☕ [点击展开] 赞赏支持 ~🧧
219 | *我非常感谢您的赞赏和支持,它们将极大地激励我继续创新,持续产生有价值的工作。*
220 |
221 | - **USDT-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD`
222 | - **TRX-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD`
223 |
224 |
225 |

226 | TRC10/TRC20扫码支付
227 |
228 |
229 |
230 |
231 | #
232 | ⚠️免责声明:
233 | - 1、该项目设计和开发仅供学习、研究和安全测试目的。请于下载后 24 小时内删除, 不得用作任何商业用途, 文字、数据及图片均有所属版权, 如转载须注明来源。
234 | - 2、使用本程序必循遵守部署服务器所在地区的法律、所在国家和用户所在国家的法律法规。对任何人或团体使用该项目时产生的任何后果由使用者承担。
235 | - 3、作者不对使用该项目可能引起的任何直接或间接损害负责。作者保留随时更新免责声明的权利,且不另行通知。
236 |
237 |
--------------------------------------------------------------------------------
/_worker.obs.src.js:
--------------------------------------------------------------------------------
1 | /**
2 | * YouTube : https://youtube.com/@am_clubs
3 | * Telegram : https://t.me/am_clubs
4 | * GitHub : https://github.com/amclubs
5 | * BLog : https://amclubss.com
6 | */
7 |
8 | let id = atob('ZWM4NzJkOGYtNzJiMC00YTA0LWI2MTItMDMyN2Q4NWUxOGVk');
9 |
10 | let pnum = atob('NDQz');
11 | let paddrs = [
12 | atob('cHJveHlpcC5hbWNsdWJzLmNhbWR2ci5vcmc='),
13 | atob('cHJveHlpcC5hbWNsdWJzLmtvem93LmNvbQ==')
14 | ];
15 | let paddr = paddrs[Math.floor(Math.random() * paddrs.length)];
16 | let pDomain = [];
17 |
18 | let p64 = true;
19 | let p64DnUrl = atob('aHR0cHM6Ly8xLjEuMS4xL2Rucy1xdWVyeQ==');
20 | let p64Prefix = atob('MjYwMjpmYzU5OmIwOjY0Ojo=');
21 | let p64Domain = [];
22 |
23 | let s5 = '';
24 | let s5Enable = false;
25 | let parsedS5 = {};
26 |
27 | let durl = atob('aHR0cHM6Ly9za3kucmV0aGlua2Rucy5jb20vMTotUGZfX19fXzlfOEFfQU1BSWdFOGtNQUJWRERtS09IVEFLZz0=');
28 | let fname = atob('5pWw5a2X5aWX5Yip');
29 | const dataTypeTr = 'EBMbCxUX';
30 | let enableLog = false;
31 |
32 | let ytName = atob('aHR0cHM6Ly95b3V0dWJlLmNvbS9AYW1fY2x1YnM/c3ViX2NvbmZpcm1hdGlvbj0x');
33 | let tgName = atob('aHR0cHM6Ly90Lm1lL2FtX2NsdWJz');
34 | let ghName = atob('aHR0cHM6Ly9naXRodWIuY29tL2FtY2x1YnMvYW0tY2YtdHVubmVs');
35 | let bName = atob('aHR0cHM6Ly9hbWNsdWJzcy5jb20=');
36 | let pName = '5pWw5a2X5aWX5Yip';
37 |
38 | import { connect } from 'cloudflare:sockets';
39 |
40 | if (!isValidUserId(id)) {
41 | throw new Error('id is invalid');
42 | }
43 |
44 | export default {
45 | async fetch(request, env, ctx) {
46 | try {
47 | let { ID, PADDR, P64, P64PREFIX, S5, D_URL, ENABLE_LOG } = env;
48 |
49 | const kvCheckResponse = await check_kv(env);
50 | let kvData = {};
51 | if (!kvCheckResponse) {
52 | kvData = await get_kv(env) || {};
53 | log(`[fetch]--> kv_id = ${kvData.kv_id}, kv_pDomain = ${JSON.stringify(kvData.pDomain)}, kv_p64Domain = ${JSON.stringify(kvData.kv_p64Domain)}`);
54 | }
55 |
56 | const url = new URL(request.url);
57 | enableLog = url.searchParams.get('ENABLE_LOG') || ENABLE_LOG || enableLog;
58 | id = (kvData.kv_id || ID || id).toLowerCase();
59 | log(`[fetch]--> id = ${id}`);
60 |
61 | paddr = url.searchParams.get('PADDR') || PADDR || paddr;
62 | if (paddr) {
63 | const [ip, port] = paddr.split(':');
64 | paddr = ip;
65 | pnum = port || pnum;
66 | }
67 | pDomain = kvData.kv_pDomain || pDomain;
68 | log(`[fetch]--> pDomain = ${JSON.stringify(pDomain)}`);
69 |
70 | p64 = url.searchParams.get('P64') || P64 || p64;
71 | p64Prefix = url.searchParams.get('P64PREFIX') || P64PREFIX || p64Prefix;
72 | p64Domain = kvData.kv_p64Domain || p64Domain;
73 | log(`[fetch]--> p64Domain = ${JSON.stringify(p64Domain)}`);
74 |
75 | s5 = url.searchParams.get('S5') || S5 || s5;
76 | parsedS5 = await requestParserFromUrl(s5, url);
77 | if (parsedS5) {
78 | s5Enable = true;
79 | }
80 |
81 | durl = url.searchParams.get('D_URL') || D_URL || durl;
82 | let prType = url.searchParams.get(atob('UFJPVF9UWVBF'));
83 | if (prType) {
84 | prType = prType.toLowerCase();
85 | }
86 |
87 | if (request.headers.get('Upgrade') === 'websocket') {
88 | return await websvcExecutor(request);
89 | }
90 | switch (url.pathname.toLowerCase()) {
91 | case '/': {
92 | return await login(request, env);
93 | }
94 | case `/${id}/get`: {
95 | return get_kv(env);
96 | }
97 | case `/${id}/set`: {
98 | return set_kv_data(request, env);
99 | }
100 | default: {
101 | return Response.redirect(new URL('/', request.url));
102 | }
103 | }
104 | } catch (err) {
105 | console.error('Error processing request:', err);
106 | return new Response(`Error: ${err.message}`, { status: 500 });
107 | }
108 | },
109 | };
110 |
111 |
112 | /** ---------------------tools------------------------------ */
113 | function log(...args) {
114 | if (enableLog) console.log(...args);
115 | }
116 |
117 | function error(...args) {
118 | if (enableLog) console.error(...args);
119 | }
120 |
121 | function isValidUserId(uuid) {
122 | 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;
123 | return uuidRegex.test(uuid);
124 | }
125 |
126 | const byteToHex = [];
127 | for (let i = 0; i < 256; ++i) {
128 | byteToHex.push((i + 256).toString(16).slice(1));
129 | }
130 |
131 | function unsafeStringify(arr, offset = 0) {
132 | 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();
133 | }
134 |
135 | function stringify(arr, offset = 0) {
136 | const uuid = unsafeStringify(arr, offset);
137 | if (!isValidUserId(uuid)) {
138 | throw TypeError("Stringified ID is invalid");
139 | }
140 | return uuid;
141 | }
142 |
143 | function b64ToBuf(base64Str) {
144 | if (!base64Str) {
145 | return { earlyData: null, error: null };
146 | }
147 | try {
148 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
149 | const decode = atob(base64Str);
150 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
151 | return { earlyData: arryBuffer.buffer, error: null };
152 | } catch (error) {
153 | return { earlyData: null, error };
154 | }
155 | }
156 |
157 | function decodeBase64Utf8(str) {
158 | const bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0));
159 | return new TextDecoder('utf-8').decode(bytes);
160 | }
161 |
162 | function requestParser(s5) {
163 | let [latter, former] = s5.split("@").reverse();
164 | let username, password, hostname, port;
165 |
166 | if (former) {
167 | const formers = former.split(":");
168 | if (formers.length !== 2) {
169 | throw new Error('Invalid S address format: authentication must be in the "username:password" format');
170 | }
171 | [username, password] = formers;
172 | }
173 |
174 | const latters = latter.split(":");
175 | port = Number(latters.pop());
176 | if (isNaN(port)) {
177 | throw new Error('Invalid S address format: port must be a number');
178 | }
179 |
180 | hostname = latters.join(":");
181 | const isIPv6 = hostname.includes(":") && !/^\[.*\]$/.test(hostname);
182 | if (isIPv6) {
183 | throw new Error('Invalid S address format: IPv6 addresses must be enclosed in brackets, e.g., [2001:db8::1]');
184 | }
185 |
186 | return { username, password, hostname, port };
187 | }
188 |
189 | async function requestParserFromUrl(s5, url) {
190 | if (/\/s5?=/.test(url.pathname)) {
191 | s5 = url.pathname.split('5=')[1];
192 | } else if (/\/socks[5]?:\/\//.test(url.pathname)) {
193 | s5 = url.pathname.split('://')[1].split('#')[0];
194 | }
195 |
196 | const authIdx = s5.indexOf('@');
197 | if (authIdx !== -1) {
198 | let userPassword = s5.substring(0, authIdx);
199 | const base64Regex = /^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i;
200 | if (base64Regex.test(userPassword) && !userPassword.includes(':')) {
201 | userPassword = atob(userPassword);
202 | }
203 | s5 = `${userPassword}@${s5.substring(authIdx + 1)}`;
204 | }
205 |
206 | if (s5) {
207 | try {
208 | return requestParser(s5);
209 | } catch (err) {
210 | error(err.toString());
211 | return null;
212 | }
213 | }
214 | return null;
215 | }
216 |
217 | function xorEn(plain, key) {
218 | const encoder = new TextEncoder();
219 | const p = encoder.encode(plain);
220 | const k = encoder.encode(key);
221 | const out = new Uint8Array(p.length);
222 | for (let i = 0; i < p.length; i++) {
223 | out[i] = p[i] ^ k[i % k.length];
224 | }
225 | return btoa(String.fromCharCode(...out));
226 | }
227 |
228 | function xorDe(b64, key) {
229 | const data = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
230 | const encoder = new TextEncoder();
231 | const decoder = new TextDecoder();
232 | const k = encoder.encode(key);
233 | const out = new Uint8Array(data.length);
234 | for (let i = 0; i < data.length; i++) {
235 | out[i] = data[i] ^ k[i % k.length];
236 | }
237 | return decoder.decode(out);
238 | }
239 |
240 | async function getDomainToRouteX(addressRemote, portRemote, s5Enable, p64Flag = false) {
241 | let finalTargetHost = addressRemote;
242 | let finalTargetPort = portRemote;
243 | try {
244 | log(`[getDomainToRouteX]--> paddr=${paddr}, p64Prefix=${p64Prefix}, addressRemote=${addressRemote}, p64=${p64}`);
245 | log(`[getDomainToRouteX]--> pDomain=${JSON.stringify(pDomain)}, p64Domain=${JSON.stringify(p64Domain)}`);
246 |
247 | const safeMatch = (domains, target) => {
248 | try {
249 | return Array.isArray(domains) && domains.some(domain => matchesDomainPattern(target, domain));
250 | } catch (e) {
251 | log(`[error]--> matchesDomainPattern failed: ${e.message}`);
252 | return false;
253 | }
254 | };
255 |
256 | const resultDomain = safeMatch(pDomain, addressRemote);
257 | const result64Domain = safeMatch(p64Domain, addressRemote);
258 | log(`[getDomainToRouteX]--> match pDomain=${resultDomain}, match p64Domain=${result64Domain}, p64Flag=${p64Flag}`);
259 |
260 | if (s5Enable) {
261 | log(`[getDomainToRouteX]--> s5Enable=true, use remote directly`);
262 | } else if (resultDomain) {
263 | finalTargetHost = paddr;
264 | finalTargetPort = pnum || portRemote;
265 | log(`[getDomainToRouteX]--> Matched pDomain, use paddr=${finalTargetHost}, port=${finalTargetPort}`);
266 | } else if (result64Domain || (p64Flag && p64)) {
267 | try {
268 | finalTargetHost = await resolveDomainToRouteX(addressRemote);
269 | finalTargetPort = portRemote;
270 | log(`[getDomainToRouteX]--> Resolved p64Domain via resolveDomainToRouteX: ${finalTargetHost}`);
271 | } catch (err) {
272 | log(`[retry]--> resolveDomainToRouteX failed: ${err.message}`);
273 | finalTargetHost = paddr || addressRemote;
274 | finalTargetPort = pnum || portRemote;
275 | }
276 | } else if (p64Flag) {
277 | finalTargetHost = paddr || addressRemote;
278 | finalTargetPort = portRemote;
279 | log(`[getDomainToRouteX]--> fallback by p64Flag, host=${finalTargetHost}, port=${finalTargetPort}`);
280 | }
281 |
282 | log(`[getDomainToRouteX]--> Final target: ${finalTargetHost}:${finalTargetPort}`);
283 | return { finalTargetHost, finalTargetPort };
284 | } catch (err) {
285 | log(`[fatal]--> getDomainToRouteX failed: ${err.message}`);
286 | if (p64Flag) {
287 | finalTargetHost = paddr || addressRemote;
288 | finalTargetPort = portRemote;
289 | log(`[fatal-fallback]--> fallback by p64Flag, host=${finalTargetHost}, port=${finalTargetPort}`);
290 | }
291 | return { finalTargetHost, finalTargetPort };
292 | }
293 | }
294 |
295 | function matchesDomainPattern(hostname, pattern) {
296 | if (!hostname || !pattern) return false;
297 |
298 | hostname = hostname.toLowerCase();
299 | pattern = pattern.toLowerCase();
300 | const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
301 | const ipv6Regex = /^\[?([a-f0-9:]+)\]?$/i;
302 | if (ipv4Regex.test(hostname) || ipv6Regex.test(hostname)) {
303 | return false;
304 | }
305 |
306 | const hostParts = hostname.split('.');
307 | const patternParts = pattern.split('.');
308 |
309 | if (hostParts.length < patternParts.length) return false;
310 |
311 | for (let i = 1; i <= patternParts.length; i++) {
312 | if (hostParts[hostParts.length - i] !== patternParts[patternParts.length - i]) {
313 | return false;
314 | }
315 | }
316 | return true;
317 | }
318 |
319 | async function resolveDomainToRouteX(domain) {
320 | try {
321 | log(`[resolveDomainToRouteX] Starting domain resolution: ${domain}`);
322 | const response = await fetch(`${p64DnUrl}?name=${domain}&type=A`, {
323 | headers: {
324 | Accept: "application/dns-json",
325 | },
326 | });
327 | if (!response.ok) {
328 | throw new Error(`[resolveDomainToRouteX] request failed with status code: ${response.status}`);
329 | }
330 |
331 | const result = await response.json();
332 | log(`[resolveDomainToRouteX] Query result: ${JSON.stringify(result, null, 2)}`);
333 | const aRecord = result?.Answer?.find(record => record.type === 1 && record.data);
334 | if (!aRecord) {
335 | throw new Error("No valid A record found");
336 | }
337 | const ipv4 = aRecord.data;
338 | log(`[resolveDomainToRouteX] Found IPv4 address: ${ipv4}`);
339 | const ipv6 = convertToRouteX(ipv4);
340 | log(`[resolveDomainToRouteX] Converted IPv6 address: ${ipv6}`);
341 | return ipv6;
342 | } catch (err) {
343 | error(`[Error] Failed to get routeX address: ${err.message}`);
344 | throw new Error(`[resolveDomainToRouteX] resolution failed: ${err.message}`);
345 | }
346 | }
347 |
348 | function convertToRouteX(ipv4Address) {
349 | const parts = ipv4Address.trim().split('.');
350 | if (parts.length !== 4) {
351 | throw new Error('Invalid IPv4 address');
352 | }
353 | const hexParts = parts.map(part => {
354 | const num = Number(part);
355 | if (!/^\d+$/.test(part) || isNaN(num) || num < 0 || num > 255) {
356 | throw new Error(`Invalid IPv4 segment: ${part}`);
357 | }
358 | return num.toString(16).padStart(2, '0');
359 | });
360 |
361 | let withBrackets = true
362 | log(`[convertToRouteX] p64Prefix--->: ${p64Prefix}`);
363 | if (!p64Prefix || typeof p64Prefix !== 'string' || !p64Prefix.includes('::')) {
364 | throw new Error('[convertToRouteX] Invalid manual prefix; must be a valid IPv6 prefix');
365 | }
366 | const ipv6Tail = `${hexParts[0]}${hexParts[1]}:${hexParts[2]}${hexParts[3]}`.toLowerCase();
367 | const fullIPv6 = `${p64Prefix}${ipv6Tail}`;
368 | return withBrackets ? `[${fullIPv6}]` : fullIPv6;
369 | }
370 |
371 | function stringToArray(str) {
372 | if (!str) return [];
373 | return str
374 | .split(/[\n,]+/)
375 | .map(s => s.trim())
376 | .filter(Boolean);
377 | }
378 |
379 |
380 | /** ---------------------cf data------------------------------ */
381 | const MY_KV_ALL_KEY = 'KV_CONFIG';
382 | async function check_kv(env) {
383 | if (!env || !env.amclubs) {
384 | return new Response('Error: amclubs KV_NAMESPACE is not bound.', {
385 | status: 400,
386 | });
387 | }
388 | if (typeof env.amclubs === 'undefined') {
389 | return new Response('Error: amclubs KV_NAMESPACE is not bound.', {
390 | status: 400,
391 | })
392 | }
393 | return null;
394 | }
395 |
396 | async function get_kv(env) {
397 | try {
398 | const config = await env.amclubs.get(MY_KV_ALL_KEY, { type: 'json' });
399 | if (!config) {
400 | return {
401 | kv_id: '',
402 | kv_pDomain: [],
403 | kv_p64Domain: []
404 | };
405 | }
406 | return {
407 | kv_id: config.kv_id || '',
408 | kv_pDomain: Array.isArray(config.kv_pDomain) ? config.kv_pDomain : stringToArray(config.kv_pDomain),
409 | kv_p64Domain: Array.isArray(config.kv_p64Domain) ? config.kv_p64Domain : stringToArray(config.kv_p64Domain)
410 | };
411 | } catch (err) {
412 | error('[get_kv] Error reading KV:', err);
413 | return {
414 | kv_id: '',
415 | kv_pDomain: [],
416 | kv_p64Domain: []
417 | };
418 | }
419 | }
420 |
421 | async function set_kv_data(request, env) {
422 | try {
423 | const { kv_id, kv_pDomain, kv_p64Domain } = await request.json();
424 | const data = {
425 | kv_id,
426 | kv_pDomain: stringToArray(kv_pDomain),
427 | kv_p64Domain: stringToArray(kv_p64Domain)
428 | };
429 | await env.amclubs.put(MY_KV_ALL_KEY, JSON.stringify(data));
430 | return new Response('保存成功', { status: 200 });
431 | } catch (err) {
432 | return new Response('保存失败: ' + err.message, { status: 500 });
433 | }
434 | }
435 |
436 | async function show_kv_page(env) {
437 | const kvCheckResponse = await check_kv(env);
438 | if (kvCheckResponse) {
439 | return kvCheckResponse;
440 | }
441 | const { kv_id, kv_pDomain, kv_p64Domain } = await get_kv(env);
442 | log('[show_kv_page] KV数据:', { kv_id, kv_pDomain, kv_p64Domain });
443 |
444 | return new Response(
445 | renderPage({
446 | base64Title: pName,
447 | suffix: '-设置',
448 | heading: `配置设置`,
449 | bodyContent: `
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
489 | `
490 | }),
491 | { headers: { "Content-Type": "text/html; charset=UTF-8" }, status: 200 }
492 | );
493 | }
494 |
495 |
496 | /** -------------------websvc logic-------------------------------- */
497 | const WS_READY_STATE_OPEN = 1;
498 | const WS_READY_STATE_CLOSING = 2;
499 | async function websvcExecutor(request) {
500 | const webSocketPair = new WebSocketPair();
501 | const [client, webSocket] = Object.values(webSocketPair);
502 | webSocket.accept();
503 |
504 | let address = '';
505 | let portWithRandomLog = '';
506 | let currentDate = new Date();
507 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
508 | console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || '');
509 | };
510 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';
511 | const readableWebSocketStream = websvcStream(webSocket, earlyDataHeader, log);
512 |
513 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
514 | let remoteSocketWapper = {
515 | value: null,
516 | };
517 | let udpStreamWrite = null;
518 | let isDns = false;
519 |
520 | readableWebSocketStream.pipeTo(new WritableStream({
521 | async write(chunk, controller) {
522 | if (isDns && udpStreamWrite) {
523 | return udpStreamWrite(chunk);
524 | }
525 | if (remoteSocketWapper.value) {
526 | const writer = remoteSocketWapper.value.writable.getWriter()
527 | await writer.write(chunk);
528 | writer.releaseLock();
529 | return;
530 | }
531 |
532 | const {
533 | hasError,
534 | //message,
535 | portRemote = 443,
536 | addressRemote = '',
537 | rawDataIndex,
538 | channelVersion = new Uint8Array([0, 0]),
539 | isUDP,
540 | addressType,
541 | } = handleRequestHeader(chunk, id);
542 | address = addressRemote;
543 | portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `;
544 | log(`handleRequestHeader-->${addressType} Processing TCP outbound connection ${addressRemote}:${portRemote}`);
545 |
546 | if (hasError) {
547 | throw new Error(message);
548 | }
549 |
550 | if (isUDP && portRemote !== 53) {
551 | throw new Error('UDP proxy only enabled for DNS which is port 53');
552 | }
553 |
554 | if (isUDP && portRemote === 53) {
555 | isDns = true;
556 | }
557 |
558 | const channelResponseHeader = new Uint8Array([channelVersion[0], 0]);
559 | const rawClientData = chunk.slice(rawDataIndex);
560 |
561 | if (isDns) {
562 | const { write } = await handleUPOut(webSocket, channelResponseHeader, log);
563 | udpStreamWrite = write;
564 | udpStreamWrite(rawClientData);
565 | return;
566 | }
567 |
568 | handleTPOut(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, channelResponseHeader, log, addressType);
569 | },
570 | close() {
571 | log(`readableWebSocketStream is close`);
572 | },
573 | abort(reason) {
574 | log(`readableWebSocketStream is abort`, JSON.stringify(reason));
575 | },
576 | })).catch((err) => {
577 | log('readableWebSocketStream pipeTo error', err);
578 | });
579 |
580 | return new Response(null, {
581 | status: 101,
582 | webSocket: client,
583 | });
584 | }
585 |
586 |
587 |
588 | function websvcStream(pipeServer, earlyDataHeader, log) {
589 | let readableStreamCancel = false;
590 | const stream = new ReadableStream({
591 | start(controller) {
592 | pipeServer.addEventListener('message', (event) => {
593 | const message = event.data;
594 | controller.enqueue(message);
595 | });
596 |
597 | pipeServer.addEventListener('close', () => {
598 | closeDataStream(pipeServer);
599 | controller.close();
600 | });
601 |
602 | pipeServer.addEventListener('error', (err) => {
603 | log('pipeServer has error');
604 | controller.error(err);
605 | });
606 | const { earlyData, error } = b64ToBuf(earlyDataHeader);
607 | if (error) {
608 | controller.error(error);
609 | } else if (earlyData) {
610 | controller.enqueue(earlyData);
611 | }
612 | },
613 |
614 | pull(controller) {
615 | // if ws can stop read if stream is full, we can implement backpressure
616 | },
617 |
618 | cancel(reason) {
619 | log(`ReadableStream was canceled, due to ${reason}`)
620 | readableStreamCancel = true;
621 | closeDataStream(pipeServer);
622 | }
623 | });
624 |
625 | return stream;
626 | }
627 |
628 | async function handleTPOut(remoteS, addressRemote, portRemote, rawClientData, pipe, channelResponseHeader, log, addressType) {
629 |
630 | async function connectAndWrite(address, port, socks = false) {
631 | const tcpS = socks ? await serviceCall(addressType, address, port, log) : connect({ hostname: address, port: port, servername: addressRemote });
632 | remoteS.value = tcpS;
633 | log(`[connectAndWrite]--> s5:${socks} connected to ${address}:${port}`);
634 | const writer = tcpS.writable.getWriter();
635 | await writer.write(rawClientData);
636 | writer.releaseLock();
637 | return tcpS;
638 | }
639 |
640 | async function retry() {
641 | const finalTargetHost = paddr || addressRemote;
642 | const finalTargetPort = pnum || portRemote;
643 | const tcpS = s5Enable ? await connectAndWrite(finalTargetHost, finalTargetPort, true) : await connectAndWrite(finalTargetHost, finalTargetPort);
644 | log(`[retry]--> s5:${s5Enable} connected to ${finalTargetHost}:${finalTargetPort}`);
645 | tcpS.closed.catch(error => {
646 | log('[retry]--> tcpS closed error', error);
647 | }).finally(() => {
648 | closeDataStream(pipe);
649 | })
650 | transferDataStream(tcpS, pipe, channelResponseHeader, null, log);
651 | }
652 |
653 | async function nat64() {
654 | const finalTargetHost = await resolveDomainToRouteX(addressRemote);
655 | const finalTargetPort = portRemote;
656 | const tcpS = s5Enable ? await connectAndWrite(finalTargetHost, finalTargetPort, true) : await connectAndWrite(finalTargetHost, finalTargetPort);
657 | log(`[nat64]--> s5:${s5Enable} connected to ${finalTargetHost}:${finalTargetPort}`);
658 | tcpS.closed.catch(error => {
659 | log('[nat64]--> tcpS closed error', error);
660 | }).finally(() => {
661 | closeDataStream(pipe);
662 | })
663 | transferDataStream(tcpS, pipe, channelResponseHeader, null, log);
664 | }
665 |
666 | async function finalStep() {
667 | try {
668 | if (p64) {
669 | log('[finalStep] p64=true → try nat64() first, then retry() if nat64 fails');
670 | const ok = await tryOnce(nat64, 'nat64');
671 | if (!ok) await tryOnce(retry, 'retry');
672 | } else {
673 | log('[finalStep] p64=false → try retry() first, then nat64() if retry fails');
674 | const ok = await tryOnce(retry, 'retry');
675 | if (!ok) await tryOnce(nat64, 'nat64');
676 | }
677 | } catch (err) {
678 | log('[finalStep] error:', err);
679 | }
680 | }
681 |
682 | async function tryOnce(fn, tag) {
683 | try {
684 | const ok = await fn();
685 | log(`[finalStep] ${tag} finished normally`);
686 | return true;
687 | } catch (err) {
688 | log(`[finalStep] ${tag} failed:`, err);
689 | return false;
690 | }
691 | }
692 |
693 | const { finalTargetHost, finalTargetPort } = await getDomainToRouteX(addressRemote, portRemote, s5Enable, false);
694 | const tcpS = await connectAndWrite(finalTargetHost, finalTargetPort, s5Enable ? true : false);
695 | transferDataStream(tcpS, pipe, channelResponseHeader, finalStep, log);
696 | }
697 |
698 | async function transferDataStream(remoteS, pipe, channelResponseHeader, retry, log) {
699 | let remoteChunkCount = 0;
700 | let chunks = [];
701 | let channelHeader = channelResponseHeader;
702 | let hasIncomingData = false;
703 | await remoteS.readable
704 | .pipeTo(
705 | new WritableStream({
706 | start() {
707 | },
708 | async write(chunk, controller) {
709 | hasIncomingData = true;
710 | remoteChunkCount++;
711 | if (pipe.readyState !== WS_READY_STATE_OPEN) {
712 | controller.error(
713 | '[transferDataStream]--> pipe.readyState is not open, maybe close'
714 | );
715 | }
716 | if (channelHeader) {
717 | pipe.send(await new Blob([channelHeader, chunk]).arrayBuffer());
718 | channelHeader = null;
719 | } else {
720 | pipe.send(chunk);
721 | }
722 | },
723 | close() {
724 | log(`[transferDataStream]--> serviceCallion!.readable is close with hasIncomingData is ${hasIncomingData}`);
725 | },
726 | abort(reason) {
727 | console.error(`[transferDataStream]--> serviceCallion!.readable abort`, reason);
728 | },
729 | })
730 | )
731 | .catch((error) => {
732 | console.error(`[transferDataStream]--> transferDataStream has exception `, error.stack || error);
733 | closeDataStream(pipe);
734 | });
735 |
736 | if (hasIncomingData === false && typeof retry === 'function') {
737 | log(`[transferDataStream]--> no data, invoke retry flow`);
738 | await retry();
739 | }
740 | }
741 |
742 | async function handleUPOut(pipe, channelResponseHeader, log) {
743 | let ischannelHeaderSent = false;
744 | const transformStream = new TransformStream({
745 | start(controller) {
746 |
747 | },
748 | transform(chunk, controller) {
749 | for (let index = 0; index < chunk.byteLength;) {
750 | const lengthBuffer = chunk.slice(index, index + 2);
751 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
752 | const udpData = new Uint8Array(
753 | chunk.slice(index + 2, index + 2 + udpPakcetLength)
754 | );
755 | index = index + 2 + udpPakcetLength;
756 | controller.enqueue(udpData);
757 | }
758 | },
759 | flush(controller) {
760 | }
761 | });
762 |
763 | transformStream.readable.pipeTo(new WritableStream({
764 | async write(chunk) {
765 | const resp = await fetch(durl, // dns server url
766 | {
767 | method: 'POST',
768 | headers: {
769 | 'content-type': 'application/dns-message',
770 | },
771 | body: chunk,
772 | })
773 | const dnsQueryResult = await resp.arrayBuffer();
774 | const udpSize = dnsQueryResult.byteLength;
775 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
776 | if (pipe.readyState === WS_READY_STATE_OPEN) {
777 | log(`doh success and dns message length is ${udpSize}`);
778 | if (ischannelHeaderSent) {
779 | pipe.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
780 | } else {
781 | pipe.send(await new Blob([channelResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
782 | ischannelHeaderSent = true;
783 | }
784 | }
785 | }
786 | })).catch((error) => {
787 | error('dns udp has error' + error)
788 | });
789 |
790 | const writer = transformStream.writable.getWriter();
791 |
792 | return {
793 | /**
794 | *
795 | * @param {Uint8Array} chunk
796 | */
797 | write(chunk) {
798 | writer.write(chunk);
799 | }
800 | };
801 | }
802 |
803 | async function serviceCall(ipType, remoteIp, remotePort, log) {
804 | const { username, password, hostname, port } = parsedS5;
805 | const socket = connect({ hostname, port });
806 | const writer = socket.writable.getWriter();
807 | const reader = socket.readable.getReader();
808 | const encoder = new TextEncoder();
809 |
810 | const sendSocksGreeting = async () => {
811 | const greeting = new Uint8Array([5, 2, 0, 2]);
812 | await writer.write(greeting);
813 | };
814 |
815 | const handleAuthResponse = async () => {
816 | const res = (await reader.read()).value;
817 | if (res[1] === 0x02) {
818 | if (!username || !password) {
819 | throw new Error("Authentication required");
820 | }
821 | const authRequest = new Uint8Array([
822 | 1, username.length, ...encoder.encode(username),
823 | password.length, ...encoder.encode(password)
824 | ]);
825 | await writer.write(authRequest);
826 | const authResponse = (await reader.read()).value;
827 | if (authResponse[0] !== 0x01 || authResponse[1] !== 0x00) {
828 | throw new Error("Authentication failed");
829 | }
830 | }
831 | };
832 |
833 | const sendSocksRequest = async () => {
834 | let DSTADDR;
835 | switch (ipType) {
836 | case 1:
837 | DSTADDR = new Uint8Array([1, ...remoteIp.split('.').map(Number)]);
838 | break;
839 | case 2:
840 | DSTADDR = new Uint8Array([3, remoteIp.length, ...encoder.encode(remoteIp)]);
841 | break;
842 | case 3:
843 | DSTADDR = new Uint8Array([4, ...remoteIp.split(':').flatMap(x => [
844 | parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)
845 | ])]);
846 | break;
847 | default:
848 | throw new Error("Invalid address type");
849 | }
850 | const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, remotePort >> 8, remotePort & 0xff]);
851 | await writer.write(socksRequest);
852 |
853 | const response = (await reader.read()).value;
854 | if (response[1] !== 0x00) {
855 | throw new Error("Connection failed");
856 | }
857 | };
858 |
859 | try {
860 | await sendSocksGreeting();
861 | await handleAuthResponse();
862 | await sendSocksRequest();
863 | } catch (error) {
864 | error(error.message);
865 | return null;
866 | } finally {
867 | writer.releaseLock();
868 | reader.releaseLock();
869 | }
870 | return socket;
871 | }
872 |
873 | function handleRequestHeader(channelBuffer, id) {
874 | if (channelBuffer.byteLength < 24) {
875 | return {
876 | hasError: true,
877 | message: 'invalid data',
878 | };
879 | }
880 |
881 | const version = new Uint8Array(channelBuffer.slice(0, 1));
882 | let isValidUser = false;
883 | let isUDP = false;
884 | const slicedBuffer = new Uint8Array(channelBuffer.slice(1, 17));
885 | const slicedBufferString = stringify(slicedBuffer);
886 | const uuids = id.includes(',') ? id.split(",") : [id];
887 |
888 | isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim();
889 | if (!isValidUser) {
890 | return {
891 | hasError: true,
892 | message: 'invalid user',
893 | };
894 | }
895 |
896 | const optLength = new Uint8Array(channelBuffer.slice(17, 18))[0];
897 | const command = new Uint8Array(
898 | channelBuffer.slice(18 + optLength, 18 + optLength + 1)
899 | )[0];
900 |
901 | if (command === 1) {
902 | isUDP = false;
903 | } else if (command === 2) {
904 | isUDP = true;
905 | } else {
906 | return {
907 | hasError: true,
908 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
909 | };
910 | }
911 | const portIndex = 18 + optLength + 1;
912 | const portBuffer = channelBuffer.slice(portIndex, portIndex + 2);
913 | const portRemote = new DataView(portBuffer).getUint16(0);
914 |
915 | let addressIndex = portIndex + 2;
916 | const addressBuffer = new Uint8Array(
917 | channelBuffer.slice(addressIndex, addressIndex + 1)
918 | );
919 |
920 | const addressType = addressBuffer[0];
921 | let addressLength = 0;
922 | let addressValueIndex = addressIndex + 1;
923 | let addressValue = '';
924 | switch (addressType) {
925 | case 1:
926 | addressLength = 4;
927 | addressValue = new Uint8Array(
928 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
929 | ).join('.');
930 | break;
931 | case 2:
932 | addressLength = new Uint8Array(
933 | channelBuffer.slice(addressValueIndex, addressValueIndex + 1)
934 | )[0];
935 | addressValueIndex += 1;
936 | addressValue = new TextDecoder().decode(
937 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
938 | );
939 | break;
940 | case 3:
941 | addressLength = 16;
942 | const dataView = new DataView(
943 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
944 | );
945 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334
946 | const ipv6 = [];
947 | for (let i = 0; i < 8; i++) {
948 | ipv6.push(dataView.getUint16(i * 2).toString(16));
949 | }
950 | addressValue = ipv6.join(':');
951 | // seems no need add [] for ipv6
952 | break;
953 | default:
954 | return {
955 | hasError: true,
956 | message: `invild addressType is ${addressType}`,
957 | };
958 | }
959 | if (!addressValue) {
960 | return {
961 | hasError: true,
962 | message: `addressValue is empty, addressType is ${addressType}`,
963 | };
964 | }
965 |
966 | return {
967 | hasError: false,
968 | addressRemote: addressValue,
969 | portRemote,
970 | rawDataIndex: addressValueIndex + addressLength,
971 | channelVersion: version,
972 | isUDP,
973 | addressType,
974 | };
975 | }
976 |
977 | async function handleRequestHeaderTr(buffer, id) {
978 | if (buffer.byteLength < 56) {
979 | return {
980 | hasError: true,
981 | message: "invalid data"
982 | };
983 | }
984 | let crLfIndex = 56;
985 | if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) {
986 | return {
987 | hasError: true,
988 | message: "invalid header format (missing CR LF)"
989 | };
990 | }
991 | const password = new TextDecoder().decode(buffer.slice(0, crLfIndex));
992 | if (password !== sha256.sha224(id)) {
993 | return {
994 | hasError: true,
995 | message: "invalid password"
996 | };
997 | }
998 |
999 | const s5DataBuffer = buffer.slice(crLfIndex + 2);
1000 | if (s5DataBuffer.byteLength < 6) {
1001 | return {
1002 | hasError: true,
1003 | message: "invalid S5 request data"
1004 | };
1005 | }
1006 |
1007 | const view = new DataView(s5DataBuffer);
1008 | const cmd = view.getUint8(0);
1009 | if (cmd !== 1) {
1010 | return {
1011 | hasError: true,
1012 | message: "unsupported command, only TCP (CONNECT) is allowed"
1013 | };
1014 | }
1015 |
1016 | const addressType = view.getUint8(1);
1017 | let addressLength = 0;
1018 | let addressIndex = 2;
1019 | let address = "";
1020 | switch (addressType) {
1021 | case 1:
1022 | addressLength = 4;
1023 | address = new Uint8Array(
1024 | s5DataBuffer.slice(addressIndex, addressIndex + addressLength)
1025 | ).join(".");
1026 | break;
1027 | case 3:
1028 | addressLength = new Uint8Array(
1029 | s5DataBuffer.slice(addressIndex, addressIndex + 1)
1030 | )[0];
1031 | addressIndex += 1;
1032 | address = new TextDecoder().decode(
1033 | s5DataBuffer.slice(addressIndex, addressIndex + addressLength)
1034 | );
1035 | break;
1036 | case 4:
1037 | addressLength = 16;
1038 | const dataView = new DataView(s5DataBuffer.slice(addressIndex, addressIndex + addressLength));
1039 | const ipv6 = [];
1040 | for (let i = 0; i < 8; i++) {
1041 | ipv6.push(dataView.getUint16(i * 2).toString(16));
1042 | }
1043 | address = ipv6.join(":");
1044 | break;
1045 | default:
1046 | return {
1047 | hasError: true,
1048 | message: `invalid addressType is ${addressType}`
1049 | };
1050 | }
1051 |
1052 | if (!address) {
1053 | return {
1054 | hasError: true,
1055 | message: `address is empty, addressType is ${addressType}`
1056 | };
1057 | }
1058 |
1059 | const portIndex = addressIndex + addressLength;
1060 | const portBuffer = s5DataBuffer.slice(portIndex, portIndex + 2);
1061 | const portRemote = new DataView(portBuffer).getUint16(0);
1062 | return {
1063 | hasError: false,
1064 | addressRemote: address,
1065 | portRemote,
1066 | rawClientData: s5DataBuffer.slice(portIndex + 4),
1067 | addressType: addressType
1068 | };
1069 | }
1070 |
1071 | function closeDataStream(socket) {
1072 | try {
1073 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
1074 | socket.close();
1075 | }
1076 | } catch (error) {
1077 | console.error('closeDataStream error', error);
1078 | }
1079 | }
1080 |
1081 |
1082 | /** -------------------home page-------------------------------- */
1083 | async function login(request, env) {
1084 | const headers = {
1085 | "Content-Type": "text/html; charset=UTF-8",
1086 | "referer": "https://www.google.com/search?q=" + fname
1087 | };
1088 | if (request.method === "POST") {
1089 | const formData = await request.formData();
1090 | const inputPassword = formData.get("password");
1091 | if (inputPassword === id) {
1092 | return await show_kv_page(env);
1093 | } else {
1094 | return new Response(
1095 | renderPage({
1096 | base64Title: pName,
1097 | suffix: '-登录失败',
1098 | heading: '❌ 登录失败',
1099 | bodyContent: `
1100 | 密码错误,请重新尝试。
1101 | 返回登录页面
1102 | `
1103 | }),
1104 | { headers: { "Content-Type": "text/html; charset=UTF-8" }, status: 200 }
1105 | );
1106 | }
1107 | }
1108 |
1109 | return new Response(
1110 | renderPage({
1111 | base64Title: pName,
1112 | suffix: '-登录',
1113 | heading: '请输入密码登录',
1114 | bodyContent: `
1115 |
1119 | `
1120 | }),
1121 | { headers: { "Content-Type": "text/html; charset=UTF-8" }, status: 200 }
1122 | );
1123 |
1124 | }
1125 |
1126 | function renderPage({ base64Title, suffix = '', heading, bodyContent }) {
1127 | const title = decodeBase64Utf8(base64Title);
1128 | const fullTitle = title + suffix;
1129 |
1130 | return `
1131 |
1132 |
1133 |
1134 | ${fullTitle}
1135 |
1207 |
1208 |
1209 |
1210 |
${heading}
1211 | ${bodyContent}
1212 |
1222 |
1223 |
1224 | `;
1225 | }
1226 |
--------------------------------------------------------------------------------
/_worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * YouTube : https://youtube.com/@am_clubs
3 | * Telegram : https://t.me/am_clubs
4 | * GitHub : https://github.com/amclubs
5 | * BLog : https://amclubss.com
6 | */
7 |
8 | let id = atob('ZWM4NzJkOGYtNzJiMC00YTA0LWI2MTItMDMyN2Q4NWUxOGVk');
9 |
10 | let pnum = atob('NDQz');
11 | let paddrs = [
12 | atob('cHJveHlpcC5hbWNsdWJzLmNhbWR2ci5vcmc='),
13 | atob('cHJveHlpcC5hbWNsdWJzLmtvem93LmNvbQ==')
14 | ];
15 | let paddr = paddrs[Math.floor(Math.random() * paddrs.length)];
16 | let pDomain = [];
17 |
18 | let p64 = true;
19 | let p64DnUrl = atob('aHR0cHM6Ly8xLjEuMS4xL2Rucy1xdWVyeQ==');
20 | let p64Prefix = atob('MjYwMjpmYzU5OmIwOjY0Ojo=');
21 | let p64Domain = [];
22 |
23 | let s5 = '';
24 | let s5Enable = false;
25 | let parsedS5 = {};
26 |
27 | let durl = atob('aHR0cHM6Ly9za3kucmV0aGlua2Rucy5jb20vMTotUGZfX19fXzlfOEFfQU1BSWdFOGtNQUJWRERtS09IVEFLZz0=');
28 | let fname = atob('5pWw5a2X5aWX5Yip');
29 | const dataTypeTr = 'EBMbCxUX';
30 | let enableLog = false;
31 |
32 | let ytName = atob('aHR0cHM6Ly95b3V0dWJlLmNvbS9AYW1fY2x1YnM/c3ViX2NvbmZpcm1hdGlvbj0x');
33 | let tgName = atob('aHR0cHM6Ly90Lm1lL2FtX2NsdWJz');
34 | let ghName = atob('aHR0cHM6Ly9naXRodWIuY29tL2FtY2x1YnMvYW0tY2YtdHVubmVs');
35 | let bName = atob('aHR0cHM6Ly9hbWNsdWJzcy5jb20=');
36 | let pName = '5pWw5a2X5aWX5Yip';
37 |
38 | import { connect } from 'cloudflare:sockets';
39 |
40 | if (!isValidUserId(id)) {
41 | throw new Error('id is invalid');
42 | }
43 |
44 | export default {
45 | async fetch(request, env, ctx) {
46 | try {
47 | let { ID, PADDR, P64, P64PREFIX, S5, D_URL, ENABLE_LOG } = env;
48 |
49 | const kvCheckResponse = await check_kv(env);
50 | let kvData = {};
51 | if (!kvCheckResponse) {
52 | kvData = await get_kv(env) || {};
53 | log(`[fetch]--> kv_id = ${kvData.kv_id}, kv_pDomain = ${JSON.stringify(kvData.pDomain)}, kv_p64Domain = ${JSON.stringify(kvData.kv_p64Domain)}`);
54 | }
55 |
56 | const url = new URL(request.url);
57 | enableLog = url.searchParams.get('ENABLE_LOG') || ENABLE_LOG || enableLog;
58 | id = (kvData.kv_id || ID || id).toLowerCase();
59 | log(`[fetch]--> id = ${id}`);
60 |
61 | paddr = url.searchParams.get('PADDR') || PADDR || paddr;
62 | if (paddr) {
63 | const [ip, port] = paddr.split(':');
64 | paddr = ip;
65 | pnum = port || pnum;
66 | }
67 | pDomain = kvData.kv_pDomain || pDomain;
68 | log(`[fetch]--> pDomain = ${JSON.stringify(pDomain)}`);
69 |
70 | p64 = url.searchParams.get('P64') || P64 || p64;
71 | p64Prefix = url.searchParams.get('P64PREFIX') || P64PREFIX || p64Prefix;
72 | p64Domain = kvData.kv_p64Domain || p64Domain;
73 | log(`[fetch]--> p64Domain = ${JSON.stringify(p64Domain)}`);
74 |
75 | s5 = url.searchParams.get('S5') || S5 || s5;
76 | parsedS5 = await requestParserFromUrl(s5, url);
77 | if (parsedS5) {
78 | s5Enable = true;
79 | }
80 |
81 | durl = url.searchParams.get('D_URL') || D_URL || durl;
82 | let prType = url.searchParams.get(atob('UFJPVF9UWVBF'));
83 | if (prType) {
84 | prType = prType.toLowerCase();
85 | }
86 |
87 | if (request.headers.get('Upgrade') === 'websocket') {
88 | if (prType === xorDe(dataTypeTr, 'datatype')) {
89 | return await websvcExecutorTr(request);
90 | }
91 | return await websvcExecutor(request);
92 | }
93 | switch (url.pathname.toLowerCase()) {
94 | case '/': {
95 | return await login(request, env);
96 | }
97 | case `/${id}/get`: {
98 | return get_kv(env);
99 | }
100 | case `/${id}/set`: {
101 | return set_kv_data(request, env);
102 | }
103 | default: {
104 | return Response.redirect(new URL('/', request.url));
105 | }
106 | }
107 | } catch (err) {
108 | console.error('Error processing request:', err);
109 | return new Response(`Error: ${err.message}`, { status: 500 });
110 | }
111 | },
112 | };
113 |
114 |
115 | /** ---------------------tools------------------------------ */
116 | function log(...args) {
117 | if (enableLog) console.log(...args);
118 | }
119 |
120 | function error(...args) {
121 | if (enableLog) console.error(...args);
122 | }
123 |
124 | function isValidUserId(uuid) {
125 | 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;
126 | return uuidRegex.test(uuid);
127 | }
128 |
129 | const byteToHex = [];
130 | for (let i = 0; i < 256; ++i) {
131 | byteToHex.push((i + 256).toString(16).slice(1));
132 | }
133 |
134 | function unsafeStringify(arr, offset = 0) {
135 | 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();
136 | }
137 |
138 | function stringify(arr, offset = 0) {
139 | const uuid = unsafeStringify(arr, offset);
140 | if (!isValidUserId(uuid)) {
141 | throw TypeError("Stringified ID is invalid");
142 | }
143 | return uuid;
144 | }
145 |
146 | function b64ToBuf(base64Str) {
147 | if (!base64Str) {
148 | return { earlyData: null, error: null };
149 | }
150 | try {
151 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
152 | const decode = atob(base64Str);
153 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
154 | return { earlyData: arryBuffer.buffer, error: null };
155 | } catch (error) {
156 | return { earlyData: null, error };
157 | }
158 | }
159 |
160 | function decodeBase64Utf8(str) {
161 | const bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0));
162 | return new TextDecoder('utf-8').decode(bytes);
163 | }
164 |
165 | function requestParser(s5) {
166 | let [latter, former] = s5.split("@").reverse();
167 | let username, password, hostname, port;
168 |
169 | if (former) {
170 | const formers = former.split(":");
171 | if (formers.length !== 2) {
172 | throw new Error('Invalid S address format: authentication must be in the "username:password" format');
173 | }
174 | [username, password] = formers;
175 | }
176 |
177 | const latters = latter.split(":");
178 | port = Number(latters.pop());
179 | if (isNaN(port)) {
180 | throw new Error('Invalid S address format: port must be a number');
181 | }
182 |
183 | hostname = latters.join(":");
184 | const isIPv6 = hostname.includes(":") && !/^\[.*\]$/.test(hostname);
185 | if (isIPv6) {
186 | throw new Error('Invalid S address format: IPv6 addresses must be enclosed in brackets, e.g., [2001:db8::1]');
187 | }
188 |
189 | return { username, password, hostname, port };
190 | }
191 |
192 | async function requestParserFromUrl(s5, url) {
193 | if (/\/s5?=/.test(url.pathname)) {
194 | s5 = url.pathname.split('5=')[1];
195 | } else if (/\/socks[5]?:\/\//.test(url.pathname)) {
196 | s5 = url.pathname.split('://')[1].split('#')[0];
197 | }
198 |
199 | const authIdx = s5.indexOf('@');
200 | if (authIdx !== -1) {
201 | let userPassword = s5.substring(0, authIdx);
202 | const base64Regex = /^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i;
203 | if (base64Regex.test(userPassword) && !userPassword.includes(':')) {
204 | userPassword = atob(userPassword);
205 | }
206 | s5 = `${userPassword}@${s5.substring(authIdx + 1)}`;
207 | }
208 |
209 | if (s5) {
210 | try {
211 | return requestParser(s5);
212 | } catch (err) {
213 | error(err.toString());
214 | return null;
215 | }
216 | }
217 | return null;
218 | }
219 |
220 | function xorEn(plain, key) {
221 | const encoder = new TextEncoder();
222 | const p = encoder.encode(plain);
223 | const k = encoder.encode(key);
224 | const out = new Uint8Array(p.length);
225 | for (let i = 0; i < p.length; i++) {
226 | out[i] = p[i] ^ k[i % k.length];
227 | }
228 | return btoa(String.fromCharCode(...out));
229 | }
230 |
231 | function xorDe(b64, key) {
232 | const data = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
233 | const encoder = new TextEncoder();
234 | const decoder = new TextDecoder();
235 | const k = encoder.encode(key);
236 | const out = new Uint8Array(data.length);
237 | for (let i = 0; i < data.length; i++) {
238 | out[i] = data[i] ^ k[i % k.length];
239 | }
240 | return decoder.decode(out);
241 | }
242 |
243 | async function getDomainToRouteX(addressRemote, portRemote, s5Enable, p64Flag = false) {
244 | let finalTargetHost = addressRemote;
245 | let finalTargetPort = portRemote;
246 | try {
247 | log(`[getDomainToRouteX]--> paddr=${paddr}, p64Prefix=${p64Prefix}, addressRemote=${addressRemote}, p64=${p64}`);
248 | log(`[getDomainToRouteX]--> pDomain=${JSON.stringify(pDomain)}, p64Domain=${JSON.stringify(p64Domain)}`);
249 |
250 | const safeMatch = (domains, target) => {
251 | try {
252 | return Array.isArray(domains) && domains.some(domain => matchesDomainPattern(target, domain));
253 | } catch (e) {
254 | log(`[error]--> matchesDomainPattern failed: ${e.message}`);
255 | return false;
256 | }
257 | };
258 |
259 | const resultDomain = safeMatch(pDomain, addressRemote);
260 | const result64Domain = safeMatch(p64Domain, addressRemote);
261 | log(`[getDomainToRouteX]--> match pDomain=${resultDomain}, match p64Domain=${result64Domain}, p64Flag=${p64Flag}`);
262 |
263 | if (s5Enable) {
264 | log(`[getDomainToRouteX]--> s5Enable=true, use remote directly`);
265 | } else if (resultDomain) {
266 | finalTargetHost = paddr;
267 | finalTargetPort = pnum || portRemote;
268 | log(`[getDomainToRouteX]--> Matched pDomain, use paddr=${finalTargetHost}, port=${finalTargetPort}`);
269 | } else if (result64Domain || (p64Flag && p64)) {
270 | try {
271 | finalTargetHost = await resolveDomainToRouteX(addressRemote);
272 | finalTargetPort = portRemote;
273 | log(`[getDomainToRouteX]--> Resolved p64Domain via resolveDomainToRouteX: ${finalTargetHost}`);
274 | } catch (err) {
275 | log(`[retry]--> resolveDomainToRouteX failed: ${err.message}`);
276 | finalTargetHost = paddr || addressRemote;
277 | finalTargetPort = pnum || portRemote;
278 | }
279 | } else if (p64Flag) {
280 | finalTargetHost = paddr || addressRemote;
281 | finalTargetPort = portRemote;
282 | log(`[getDomainToRouteX]--> fallback by p64Flag, host=${finalTargetHost}, port=${finalTargetPort}`);
283 | }
284 |
285 | log(`[getDomainToRouteX]--> Final target: ${finalTargetHost}:${finalTargetPort}`);
286 | return { finalTargetHost, finalTargetPort };
287 | } catch (err) {
288 | log(`[fatal]--> getDomainToRouteX failed: ${err.message}`);
289 | if (p64Flag) {
290 | finalTargetHost = paddr || addressRemote;
291 | finalTargetPort = portRemote;
292 | log(`[fatal-fallback]--> fallback by p64Flag, host=${finalTargetHost}, port=${finalTargetPort}`);
293 | }
294 | return { finalTargetHost, finalTargetPort };
295 | }
296 | }
297 |
298 | function matchesDomainPattern(hostname, pattern) {
299 | if (!hostname || !pattern) return false;
300 |
301 | hostname = hostname.toLowerCase();
302 | pattern = pattern.toLowerCase();
303 | const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
304 | const ipv6Regex = /^\[?([a-f0-9:]+)\]?$/i;
305 | if (ipv4Regex.test(hostname) || ipv6Regex.test(hostname)) {
306 | return false;
307 | }
308 |
309 | const hostParts = hostname.split('.');
310 | const patternParts = pattern.split('.');
311 |
312 | if (hostParts.length < patternParts.length) return false;
313 |
314 | for (let i = 1; i <= patternParts.length; i++) {
315 | if (hostParts[hostParts.length - i] !== patternParts[patternParts.length - i]) {
316 | return false;
317 | }
318 | }
319 | return true;
320 | }
321 |
322 | async function resolveDomainToRouteX(domain) {
323 | try {
324 | log(`[resolveDomainToRouteX] Starting domain resolution: ${domain}`);
325 | const response = await fetch(`${p64DnUrl}?name=${domain}&type=A`, {
326 | headers: {
327 | Accept: "application/dns-json",
328 | },
329 | });
330 | if (!response.ok) {
331 | throw new Error(`[resolveDomainToRouteX] request failed with status code: ${response.status}`);
332 | }
333 |
334 | const result = await response.json();
335 | log(`[resolveDomainToRouteX] Query result: ${JSON.stringify(result, null, 2)}`);
336 | const aRecord = result?.Answer?.find(record => record.type === 1 && record.data);
337 | if (!aRecord) {
338 | throw new Error("No valid A record found");
339 | }
340 | const ipv4 = aRecord.data;
341 | log(`[resolveDomainToRouteX] Found IPv4 address: ${ipv4}`);
342 | const ipv6 = convertToRouteX(ipv4);
343 | log(`[resolveDomainToRouteX] Converted IPv6 address: ${ipv6}`);
344 | return ipv6;
345 | } catch (err) {
346 | error(`[Error] Failed to get routeX address: ${err.message}`);
347 | throw new Error(`[resolveDomainToRouteX] resolution failed: ${err.message}`);
348 | }
349 | }
350 |
351 | function convertToRouteX(ipv4Address) {
352 | const parts = ipv4Address.trim().split('.');
353 | if (parts.length !== 4) {
354 | throw new Error('Invalid IPv4 address');
355 | }
356 | const hexParts = parts.map(part => {
357 | const num = Number(part);
358 | if (!/^\d+$/.test(part) || isNaN(num) || num < 0 || num > 255) {
359 | throw new Error(`Invalid IPv4 segment: ${part}`);
360 | }
361 | return num.toString(16).padStart(2, '0');
362 | });
363 |
364 | let withBrackets = true
365 | log(`[convertToRouteX] p64Prefix--->: ${p64Prefix}`);
366 | if (!p64Prefix || typeof p64Prefix !== 'string' || !p64Prefix.includes('::')) {
367 | throw new Error('[convertToRouteX] Invalid manual prefix; must be a valid IPv6 prefix');
368 | }
369 | const ipv6Tail = `${hexParts[0]}${hexParts[1]}:${hexParts[2]}${hexParts[3]}`.toLowerCase();
370 | const fullIPv6 = `${p64Prefix}${ipv6Tail}`;
371 | return withBrackets ? `[${fullIPv6}]` : fullIPv6;
372 | }
373 |
374 | function stringToArray(str) {
375 | if (!str) return [];
376 | return str
377 | .split(/[\n,]+/)
378 | .map(s => s.trim())
379 | .filter(Boolean);
380 | }
381 |
382 | (function () {
383 | 'use strict';
384 |
385 | var ERROR = 'input is invalid type';
386 | var WINDOW = typeof window === 'object';
387 | var root = WINDOW ? window : {};
388 | if (root.JS_SHA256_NO_WINDOW) {
389 | WINDOW = false;
390 | }
391 | var WEB_WORKER = !WINDOW && typeof self === 'object';
392 | var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
393 | if (NODE_JS) {
394 | root = global;
395 | } else if (WEB_WORKER) {
396 | root = self;
397 | }
398 | var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports;
399 | var AMD = typeof define === 'function' && define.amd;
400 | var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined';
401 | var HEX_CHARS = '0123456789abcdef'.split('');
402 | var EXTRA = [-2147483648, 8388608, 32768, 128];
403 | var SHIFT = [24, 16, 8, 0];
404 | var K = [
405 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
406 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
407 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
408 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
409 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
410 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
411 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
412 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
413 | ];
414 | var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer'];
415 |
416 | var blocks = [];
417 |
418 | if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) {
419 | Array.isArray = function (obj) {
420 | return Object.prototype.toString.call(obj) === '[object Array]';
421 | };
422 | }
423 |
424 | if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) {
425 | ArrayBuffer.isView = function (obj) {
426 | return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer;
427 | };
428 | }
429 |
430 | var createOutputMethod = function (outputType, is224) {
431 | return function (message) {
432 | return new Sha256(is224, true).update(message)[outputType]();
433 | };
434 | };
435 |
436 | var createMethod = function (is224) {
437 | var method = createOutputMethod('hex', is224);
438 | if (NODE_JS) {
439 | method = nodeWrap(method, is224);
440 | }
441 | method.create = function () {
442 | return new Sha256(is224);
443 | };
444 | method.update = function (message) {
445 | return method.create().update(message);
446 | };
447 | for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
448 | var type = OUTPUT_TYPES[i];
449 | method[type] = createOutputMethod(type, is224);
450 | }
451 | return method;
452 | };
453 |
454 | var nodeWrap = function (method, is224) {
455 | var crypto = require('node:crypto')
456 | var Buffer = require('node:buffer').Buffer;
457 | var algorithm = is224 ? 'sha224' : 'sha256';
458 | var bufferFrom;
459 | if (Buffer.from && !root.JS_SHA256_NO_BUFFER_FROM) {
460 | bufferFrom = Buffer.from;
461 | } else {
462 | bufferFrom = function (message) {
463 | return new Buffer(message);
464 | };
465 | }
466 | var nodeMethod = function (message) {
467 | if (typeof message === 'string') {
468 | return crypto.createHash(algorithm).update(message, 'utf8').digest('hex');
469 | } else {
470 | if (message === null || message === undefined) {
471 | throw new Error(ERROR);
472 | } else if (message.constructor === ArrayBuffer) {
473 | message = new Uint8Array(message);
474 | }
475 | }
476 | if (Array.isArray(message) || ArrayBuffer.isView(message) ||
477 | message.constructor === Buffer) {
478 | return crypto.createHash(algorithm).update(bufferFrom(message)).digest('hex');
479 | } else {
480 | return method(message);
481 | }
482 | };
483 | return nodeMethod;
484 | };
485 |
486 | var createHmacOutputMethod = function (outputType, is224) {
487 | return function (key, message) {
488 | return new HmacSha256(key, is224, true).update(message)[outputType]();
489 | };
490 | };
491 |
492 | var createHmacMethod = function (is224) {
493 | var method = createHmacOutputMethod('hex', is224);
494 | method.create = function (key) {
495 | return new HmacSha256(key, is224);
496 | };
497 | method.update = function (key, message) {
498 | return method.create(key).update(message);
499 | };
500 | for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
501 | var type = OUTPUT_TYPES[i];
502 | method[type] = createHmacOutputMethod(type, is224);
503 | }
504 | return method;
505 | };
506 |
507 | function Sha256(is224, sharedMemory) {
508 | if (sharedMemory) {
509 | blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
510 | blocks[4] = blocks[5] = blocks[6] = blocks[7] =
511 | blocks[8] = blocks[9] = blocks[10] = blocks[11] =
512 | blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
513 | this.blocks = blocks;
514 | } else {
515 | this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
516 | }
517 |
518 | if (is224) {
519 | this.h0 = 0xc1059ed8;
520 | this.h1 = 0x367cd507;
521 | this.h2 = 0x3070dd17;
522 | this.h3 = 0xf70e5939;
523 | this.h4 = 0xffc00b31;
524 | this.h5 = 0x68581511;
525 | this.h6 = 0x64f98fa7;
526 | this.h7 = 0xbefa4fa4;
527 | } else { // 256
528 | this.h0 = 0x6a09e667;
529 | this.h1 = 0xbb67ae85;
530 | this.h2 = 0x3c6ef372;
531 | this.h3 = 0xa54ff53a;
532 | this.h4 = 0x510e527f;
533 | this.h5 = 0x9b05688c;
534 | this.h6 = 0x1f83d9ab;
535 | this.h7 = 0x5be0cd19;
536 | }
537 |
538 | this.block = this.start = this.bytes = this.hBytes = 0;
539 | this.finalized = this.hashed = false;
540 | this.first = true;
541 | this.is224 = is224;
542 | }
543 |
544 | Sha256.prototype.update = function (message) {
545 | if (this.finalized) {
546 | return;
547 | }
548 | var notString, type = typeof message;
549 | if (type !== 'string') {
550 | if (type === 'object') {
551 | if (message === null) {
552 | throw new Error(ERROR);
553 | } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
554 | message = new Uint8Array(message);
555 | } else if (!Array.isArray(message)) {
556 | if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
557 | throw new Error(ERROR);
558 | }
559 | }
560 | } else {
561 | throw new Error(ERROR);
562 | }
563 | notString = true;
564 | }
565 | var code, index = 0, i, length = message.length, blocks = this.blocks;
566 | while (index < length) {
567 | if (this.hashed) {
568 | this.hashed = false;
569 | blocks[0] = this.block;
570 | this.block = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
571 | blocks[4] = blocks[5] = blocks[6] = blocks[7] =
572 | blocks[8] = blocks[9] = blocks[10] = blocks[11] =
573 | blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
574 | }
575 |
576 | if (notString) {
577 | for (i = this.start; index < length && i < 64; ++index) {
578 | blocks[i >>> 2] |= message[index] << SHIFT[i++ & 3];
579 | }
580 | } else {
581 | for (i = this.start; index < length && i < 64; ++index) {
582 | code = message.charCodeAt(index);
583 | if (code < 0x80) {
584 | blocks[i >>> 2] |= code << SHIFT[i++ & 3];
585 | } else if (code < 0x800) {
586 | blocks[i >>> 2] |= (0xc0 | (code >>> 6)) << SHIFT[i++ & 3];
587 | blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
588 | } else if (code < 0xd800 || code >= 0xe000) {
589 | blocks[i >>> 2] |= (0xe0 | (code >>> 12)) << SHIFT[i++ & 3];
590 | blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3];
591 | blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
592 | } else {
593 | code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
594 | blocks[i >>> 2] |= (0xf0 | (code >>> 18)) << SHIFT[i++ & 3];
595 | blocks[i >>> 2] |= (0x80 | ((code >>> 12) & 0x3f)) << SHIFT[i++ & 3];
596 | blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3];
597 | blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
598 | }
599 | }
600 | }
601 |
602 | this.lastByteIndex = i;
603 | this.bytes += i - this.start;
604 | if (i >= 64) {
605 | this.block = blocks[16];
606 | this.start = i - 64;
607 | this.hash();
608 | this.hashed = true;
609 | } else {
610 | this.start = i;
611 | }
612 | }
613 | if (this.bytes > 4294967295) {
614 | this.hBytes += this.bytes / 4294967296 << 0;
615 | this.bytes = this.bytes % 4294967296;
616 | }
617 | return this;
618 | };
619 |
620 | Sha256.prototype.finalize = function () {
621 | if (this.finalized) {
622 | return;
623 | }
624 | this.finalized = true;
625 | var blocks = this.blocks, i = this.lastByteIndex;
626 | blocks[16] = this.block;
627 | blocks[i >>> 2] |= EXTRA[i & 3];
628 | this.block = blocks[16];
629 | if (i >= 56) {
630 | if (!this.hashed) {
631 | this.hash();
632 | }
633 | blocks[0] = this.block;
634 | blocks[16] = blocks[1] = blocks[2] = blocks[3] =
635 | blocks[4] = blocks[5] = blocks[6] = blocks[7] =
636 | blocks[8] = blocks[9] = blocks[10] = blocks[11] =
637 | blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
638 | }
639 | blocks[14] = this.hBytes << 3 | this.bytes >>> 29;
640 | blocks[15] = this.bytes << 3;
641 | this.hash();
642 | };
643 |
644 | Sha256.prototype.hash = function () {
645 | var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6,
646 | h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
647 |
648 | for (j = 16; j < 64; ++j) {
649 | // rightrotate
650 | t1 = blocks[j - 15];
651 | s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3);
652 | t1 = blocks[j - 2];
653 | s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10);
654 | blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0;
655 | }
656 |
657 | bc = b & c;
658 | for (j = 0; j < 64; j += 4) {
659 | if (this.first) {
660 | if (this.is224) {
661 | ab = 300032;
662 | t1 = blocks[0] - 1413257819;
663 | h = t1 - 150054599 << 0;
664 | d = t1 + 24177077 << 0;
665 | } else {
666 | ab = 704751109;
667 | t1 = blocks[0] - 210244248;
668 | h = t1 - 1521486534 << 0;
669 | d = t1 + 143694565 << 0;
670 | }
671 | this.first = false;
672 | } else {
673 | s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
674 | s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
675 | ab = a & b;
676 | maj = ab ^ (a & c) ^ bc;
677 | ch = (e & f) ^ (~e & g);
678 | t1 = h + s1 + ch + K[j] + blocks[j];
679 | t2 = s0 + maj;
680 | h = d + t1 << 0;
681 | d = t1 + t2 << 0;
682 | }
683 | s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10));
684 | s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7));
685 | da = d & a;
686 | maj = da ^ (d & b) ^ ab;
687 | ch = (h & e) ^ (~h & f);
688 | t1 = g + s1 + ch + K[j + 1] + blocks[j + 1];
689 | t2 = s0 + maj;
690 | g = c + t1 << 0;
691 | c = t1 + t2 << 0;
692 | s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10));
693 | s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7));
694 | cd = c & d;
695 | maj = cd ^ (c & a) ^ da;
696 | ch = (g & h) ^ (~g & e);
697 | t1 = f + s1 + ch + K[j + 2] + blocks[j + 2];
698 | t2 = s0 + maj;
699 | f = b + t1 << 0;
700 | b = t1 + t2 << 0;
701 | s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10));
702 | s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7));
703 | bc = b & c;
704 | maj = bc ^ (b & d) ^ cd;
705 | ch = (f & g) ^ (~f & h);
706 | t1 = e + s1 + ch + K[j + 3] + blocks[j + 3];
707 | t2 = s0 + maj;
708 | e = a + t1 << 0;
709 | a = t1 + t2 << 0;
710 | this.chromeBugWorkAround = true;
711 | }
712 |
713 | this.h0 = this.h0 + a << 0;
714 | this.h1 = this.h1 + b << 0;
715 | this.h2 = this.h2 + c << 0;
716 | this.h3 = this.h3 + d << 0;
717 | this.h4 = this.h4 + e << 0;
718 | this.h5 = this.h5 + f << 0;
719 | this.h6 = this.h6 + g << 0;
720 | this.h7 = this.h7 + h << 0;
721 | };
722 |
723 | Sha256.prototype.hex = function () {
724 | this.finalize();
725 |
726 | var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
727 | h6 = this.h6, h7 = this.h7;
728 |
729 | var hex = HEX_CHARS[(h0 >>> 28) & 0x0F] + HEX_CHARS[(h0 >>> 24) & 0x0F] +
730 | HEX_CHARS[(h0 >>> 20) & 0x0F] + HEX_CHARS[(h0 >>> 16) & 0x0F] +
731 | HEX_CHARS[(h0 >>> 12) & 0x0F] + HEX_CHARS[(h0 >>> 8) & 0x0F] +
732 | HEX_CHARS[(h0 >>> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
733 | HEX_CHARS[(h1 >>> 28) & 0x0F] + HEX_CHARS[(h1 >>> 24) & 0x0F] +
734 | HEX_CHARS[(h1 >>> 20) & 0x0F] + HEX_CHARS[(h1 >>> 16) & 0x0F] +
735 | HEX_CHARS[(h1 >>> 12) & 0x0F] + HEX_CHARS[(h1 >>> 8) & 0x0F] +
736 | HEX_CHARS[(h1 >>> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
737 | HEX_CHARS[(h2 >>> 28) & 0x0F] + HEX_CHARS[(h2 >>> 24) & 0x0F] +
738 | HEX_CHARS[(h2 >>> 20) & 0x0F] + HEX_CHARS[(h2 >>> 16) & 0x0F] +
739 | HEX_CHARS[(h2 >>> 12) & 0x0F] + HEX_CHARS[(h2 >>> 8) & 0x0F] +
740 | HEX_CHARS[(h2 >>> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
741 | HEX_CHARS[(h3 >>> 28) & 0x0F] + HEX_CHARS[(h3 >>> 24) & 0x0F] +
742 | HEX_CHARS[(h3 >>> 20) & 0x0F] + HEX_CHARS[(h3 >>> 16) & 0x0F] +
743 | HEX_CHARS[(h3 >>> 12) & 0x0F] + HEX_CHARS[(h3 >>> 8) & 0x0F] +
744 | HEX_CHARS[(h3 >>> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
745 | HEX_CHARS[(h4 >>> 28) & 0x0F] + HEX_CHARS[(h4 >>> 24) & 0x0F] +
746 | HEX_CHARS[(h4 >>> 20) & 0x0F] + HEX_CHARS[(h4 >>> 16) & 0x0F] +
747 | HEX_CHARS[(h4 >>> 12) & 0x0F] + HEX_CHARS[(h4 >>> 8) & 0x0F] +
748 | HEX_CHARS[(h4 >>> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] +
749 | HEX_CHARS[(h5 >>> 28) & 0x0F] + HEX_CHARS[(h5 >>> 24) & 0x0F] +
750 | HEX_CHARS[(h5 >>> 20) & 0x0F] + HEX_CHARS[(h5 >>> 16) & 0x0F] +
751 | HEX_CHARS[(h5 >>> 12) & 0x0F] + HEX_CHARS[(h5 >>> 8) & 0x0F] +
752 | HEX_CHARS[(h5 >>> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] +
753 | HEX_CHARS[(h6 >>> 28) & 0x0F] + HEX_CHARS[(h6 >>> 24) & 0x0F] +
754 | HEX_CHARS[(h6 >>> 20) & 0x0F] + HEX_CHARS[(h6 >>> 16) & 0x0F] +
755 | HEX_CHARS[(h6 >>> 12) & 0x0F] + HEX_CHARS[(h6 >>> 8) & 0x0F] +
756 | HEX_CHARS[(h6 >>> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F];
757 | if (!this.is224) {
758 | hex += HEX_CHARS[(h7 >>> 28) & 0x0F] + HEX_CHARS[(h7 >>> 24) & 0x0F] +
759 | HEX_CHARS[(h7 >>> 20) & 0x0F] + HEX_CHARS[(h7 >>> 16) & 0x0F] +
760 | HEX_CHARS[(h7 >>> 12) & 0x0F] + HEX_CHARS[(h7 >>> 8) & 0x0F] +
761 | HEX_CHARS[(h7 >>> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F];
762 | }
763 | return hex;
764 | };
765 |
766 | Sha256.prototype.toString = Sha256.prototype.hex;
767 |
768 | Sha256.prototype.digest = function () {
769 | this.finalize();
770 |
771 | var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
772 | h6 = this.h6, h7 = this.h7;
773 |
774 | var arr = [
775 | (h0 >>> 24) & 0xFF, (h0 >>> 16) & 0xFF, (h0 >>> 8) & 0xFF, h0 & 0xFF,
776 | (h1 >>> 24) & 0xFF, (h1 >>> 16) & 0xFF, (h1 >>> 8) & 0xFF, h1 & 0xFF,
777 | (h2 >>> 24) & 0xFF, (h2 >>> 16) & 0xFF, (h2 >>> 8) & 0xFF, h2 & 0xFF,
778 | (h3 >>> 24) & 0xFF, (h3 >>> 16) & 0xFF, (h3 >>> 8) & 0xFF, h3 & 0xFF,
779 | (h4 >>> 24) & 0xFF, (h4 >>> 16) & 0xFF, (h4 >>> 8) & 0xFF, h4 & 0xFF,
780 | (h5 >>> 24) & 0xFF, (h5 >>> 16) & 0xFF, (h5 >>> 8) & 0xFF, h5 & 0xFF,
781 | (h6 >>> 24) & 0xFF, (h6 >>> 16) & 0xFF, (h6 >>> 8) & 0xFF, h6 & 0xFF
782 | ];
783 | if (!this.is224) {
784 | arr.push((h7 >>> 24) & 0xFF, (h7 >>> 16) & 0xFF, (h7 >>> 8) & 0xFF, h7 & 0xFF);
785 | }
786 | return arr;
787 | };
788 |
789 | Sha256.prototype.array = Sha256.prototype.digest;
790 |
791 | Sha256.prototype.arrayBuffer = function () {
792 | this.finalize();
793 |
794 | var buffer = new ArrayBuffer(this.is224 ? 28 : 32);
795 | var dataView = new DataView(buffer);
796 | dataView.setUint32(0, this.h0);
797 | dataView.setUint32(4, this.h1);
798 | dataView.setUint32(8, this.h2);
799 | dataView.setUint32(12, this.h3);
800 | dataView.setUint32(16, this.h4);
801 | dataView.setUint32(20, this.h5);
802 | dataView.setUint32(24, this.h6);
803 | if (!this.is224) {
804 | dataView.setUint32(28, this.h7);
805 | }
806 | return buffer;
807 | };
808 |
809 | function HmacSha256(key, is224, sharedMemory) {
810 | var i, type = typeof key;
811 | if (type === 'string') {
812 | var bytes = [], length = key.length, index = 0, code;
813 | for (i = 0; i < length; ++i) {
814 | code = key.charCodeAt(i);
815 | if (code < 0x80) {
816 | bytes[index++] = code;
817 | } else if (code < 0x800) {
818 | bytes[index++] = (0xc0 | (code >>> 6));
819 | bytes[index++] = (0x80 | (code & 0x3f));
820 | } else if (code < 0xd800 || code >= 0xe000) {
821 | bytes[index++] = (0xe0 | (code >>> 12));
822 | bytes[index++] = (0x80 | ((code >>> 6) & 0x3f));
823 | bytes[index++] = (0x80 | (code & 0x3f));
824 | } else {
825 | code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff));
826 | bytes[index++] = (0xf0 | (code >>> 18));
827 | bytes[index++] = (0x80 | ((code >>> 12) & 0x3f));
828 | bytes[index++] = (0x80 | ((code >>> 6) & 0x3f));
829 | bytes[index++] = (0x80 | (code & 0x3f));
830 | }
831 | }
832 | key = bytes;
833 | } else {
834 | if (type === 'object') {
835 | if (key === null) {
836 | throw new Error(ERROR);
837 | } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) {
838 | key = new Uint8Array(key);
839 | } else if (!Array.isArray(key)) {
840 | if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) {
841 | throw new Error(ERROR);
842 | }
843 | }
844 | } else {
845 | throw new Error(ERROR);
846 | }
847 | }
848 |
849 | if (key.length > 64) {
850 | key = (new Sha256(is224, true)).update(key).array();
851 | }
852 |
853 | var oKeyPad = [], iKeyPad = [];
854 | for (i = 0; i < 64; ++i) {
855 | var b = key[i] || 0;
856 | oKeyPad[i] = 0x5c ^ b;
857 | iKeyPad[i] = 0x36 ^ b;
858 | }
859 |
860 | Sha256.call(this, is224, sharedMemory);
861 |
862 | this.update(iKeyPad);
863 | this.oKeyPad = oKeyPad;
864 | this.inner = true;
865 | this.sharedMemory = sharedMemory;
866 | }
867 | HmacSha256.prototype = new Sha256();
868 |
869 | HmacSha256.prototype.finalize = function () {
870 | Sha256.prototype.finalize.call(this);
871 | if (this.inner) {
872 | this.inner = false;
873 | var innerHash = this.array();
874 | Sha256.call(this, this.is224, this.sharedMemory);
875 | this.update(this.oKeyPad);
876 | this.update(innerHash);
877 | Sha256.prototype.finalize.call(this);
878 | }
879 | };
880 |
881 | var exports = createMethod();
882 | exports.sha256 = exports;
883 | exports.sha224 = createMethod(true);
884 | exports.sha256.hmac = createHmacMethod();
885 | exports.sha224.hmac = createHmacMethod(true);
886 |
887 | if (COMMON_JS) {
888 | module.exports = exports;
889 | } else {
890 | root.sha256 = exports.sha256;
891 | root.sha224 = exports.sha224;
892 | if (AMD) {
893 | define(function () {
894 | return exports;
895 | });
896 | }
897 | }
898 | })();
899 |
900 |
901 | /** ---------------------cf data------------------------------ */
902 | const MY_KV_ALL_KEY = 'KV_CONFIG';
903 | async function check_kv(env) {
904 | if (!env || !env.amclubs) {
905 | return new Response('Error: amclubs KV_NAMESPACE is not bound.', {
906 | status: 400,
907 | });
908 | }
909 | if (typeof env.amclubs === 'undefined') {
910 | return new Response('Error: amclubs KV_NAMESPACE is not bound.', {
911 | status: 400,
912 | })
913 | }
914 | return null;
915 | }
916 |
917 | async function get_kv(env) {
918 | try {
919 | const config = await env.amclubs.get(MY_KV_ALL_KEY, { type: 'json' });
920 | if (!config) {
921 | return {
922 | kv_id: '',
923 | kv_pDomain: [],
924 | kv_p64Domain: []
925 | };
926 | }
927 | return {
928 | kv_id: config.kv_id || '',
929 | kv_pDomain: Array.isArray(config.kv_pDomain) ? config.kv_pDomain : stringToArray(config.kv_pDomain),
930 | kv_p64Domain: Array.isArray(config.kv_p64Domain) ? config.kv_p64Domain : stringToArray(config.kv_p64Domain)
931 | };
932 | } catch (err) {
933 | error('[get_kv] Error reading KV:', err);
934 | return {
935 | kv_id: '',
936 | kv_pDomain: [],
937 | kv_p64Domain: []
938 | };
939 | }
940 | }
941 |
942 | async function set_kv_data(request, env) {
943 | try {
944 | const { kv_id, kv_pDomain, kv_p64Domain } = await request.json();
945 | const data = {
946 | kv_id,
947 | kv_pDomain: stringToArray(kv_pDomain),
948 | kv_p64Domain: stringToArray(kv_p64Domain)
949 | };
950 | await env.amclubs.put(MY_KV_ALL_KEY, JSON.stringify(data));
951 | return new Response('保存成功', { status: 200 });
952 | } catch (err) {
953 | return new Response('保存失败: ' + err.message, { status: 500 });
954 | }
955 | }
956 |
957 | async function show_kv_page(env) {
958 | const kvCheckResponse = await check_kv(env);
959 | if (kvCheckResponse) {
960 | return kvCheckResponse;
961 | }
962 | const { kv_id, kv_pDomain, kv_p64Domain } = await get_kv(env);
963 | log('[show_kv_page] KV数据:', { kv_id, kv_pDomain, kv_p64Domain });
964 |
965 | return new Response(
966 | renderPage({
967 | base64Title: pName,
968 | suffix: '-设置',
969 | heading: `配置设置`,
970 | bodyContent: `
971 |
972 |
973 |
974 |
975 |
976 |
977 |
978 |
979 |
980 |
1010 | `
1011 | }),
1012 | { headers: { "Content-Type": "text/html; charset=UTF-8" }, status: 200 }
1013 | );
1014 | }
1015 |
1016 |
1017 | /** -------------------websvc logic-------------------------------- */
1018 | const WS_READY_STATE_OPEN = 1;
1019 | const WS_READY_STATE_CLOSING = 2;
1020 | async function websvcExecutor(request) {
1021 | const webSocketPair = new WebSocketPair();
1022 | const [client, webSocket] = Object.values(webSocketPair);
1023 | webSocket.accept();
1024 |
1025 | let address = '';
1026 | let portWithRandomLog = '';
1027 | let currentDate = new Date();
1028 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
1029 | console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || '');
1030 | };
1031 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';
1032 | const readableWebSocketStream = websvcStream(webSocket, earlyDataHeader, log);
1033 |
1034 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
1035 | let remoteSocketWapper = {
1036 | value: null,
1037 | };
1038 | let udpStreamWrite = null;
1039 | let isDns = false;
1040 |
1041 | readableWebSocketStream.pipeTo(new WritableStream({
1042 | async write(chunk, controller) {
1043 | if (isDns && udpStreamWrite) {
1044 | return udpStreamWrite(chunk);
1045 | }
1046 | if (remoteSocketWapper.value) {
1047 | const writer = remoteSocketWapper.value.writable.getWriter()
1048 | await writer.write(chunk);
1049 | writer.releaseLock();
1050 | return;
1051 | }
1052 |
1053 | const {
1054 | hasError,
1055 | //message,
1056 | portRemote = 443,
1057 | addressRemote = '',
1058 | rawDataIndex,
1059 | channelVersion = new Uint8Array([0, 0]),
1060 | isUDP,
1061 | addressType,
1062 | } = handleRequestHeader(chunk, id);
1063 | address = addressRemote;
1064 | portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `;
1065 | log(`handleRequestHeader-->${addressType} Processing TCP outbound connection ${addressRemote}:${portRemote}`);
1066 |
1067 | if (hasError) {
1068 | throw new Error(message);
1069 | }
1070 |
1071 | if (isUDP && portRemote !== 53) {
1072 | throw new Error('UDP proxy only enabled for DNS which is port 53');
1073 | }
1074 |
1075 | if (isUDP && portRemote === 53) {
1076 | isDns = true;
1077 | }
1078 |
1079 | const channelResponseHeader = new Uint8Array([channelVersion[0], 0]);
1080 | const rawClientData = chunk.slice(rawDataIndex);
1081 |
1082 | if (isDns) {
1083 | const { write } = await handleUPOut(webSocket, channelResponseHeader, log);
1084 | udpStreamWrite = write;
1085 | udpStreamWrite(rawClientData);
1086 | return;
1087 | }
1088 |
1089 | handleTPOut(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, channelResponseHeader, log, addressType);
1090 | },
1091 | close() {
1092 | log(`readableWebSocketStream is close`);
1093 | },
1094 | abort(reason) {
1095 | log(`readableWebSocketStream is abort`, JSON.stringify(reason));
1096 | },
1097 | })).catch((err) => {
1098 | log('readableWebSocketStream pipeTo error', err);
1099 | });
1100 |
1101 | return new Response(null, {
1102 | status: 101,
1103 | webSocket: client,
1104 | });
1105 | }
1106 |
1107 | async function websvcExecutorTr(request) {
1108 | const webSocketPair = new WebSocketPair();
1109 | const [client, webSocket] = Object.values(webSocketPair);
1110 | webSocket.accept();
1111 |
1112 | let address = "";
1113 | let portWithRandomLog = "";
1114 | const remoteSocketWrapper = { value: null };
1115 | let udpStreamWrite = null;
1116 |
1117 | const log = (info, event = "") => {
1118 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event);
1119 | };
1120 |
1121 | const earlyDataHeader = request.headers.get("sec-websocket-protocol") || "";
1122 | const readableWebSocketStream = websvcStream(webSocket, earlyDataHeader, log);
1123 |
1124 | const handleStreamData = async (chunk) => {
1125 | if (udpStreamWrite) {
1126 | return udpStreamWrite(chunk);
1127 | }
1128 |
1129 | if (remoteSocketWrapper.value) {
1130 | const writer = remoteSocketWrapper.value.writable.getWriter();
1131 | await writer.write(chunk);
1132 | writer.releaseLock();
1133 | return;
1134 | }
1135 |
1136 | const { hasError, message, portRemote = 443, addressRemote = "", rawClientData, addressType } = await handleRequestHeaderTr(chunk, id);
1137 | address = addressRemote;
1138 | portWithRandomLog = `${portRemote}--${Math.random()} tcp`;
1139 | if (hasError) {
1140 | throw new Error(message);
1141 | }
1142 |
1143 | handleTPOut(remoteSocketWrapper, addressRemote, portRemote, rawClientData, webSocket, null, log, addressType);
1144 | };
1145 |
1146 | readableWebSocketStream.pipeTo(
1147 | new WritableStream({
1148 | write: handleStreamData,
1149 | close: () => log("readableWebSocketStream is closed"),
1150 | abort: (reason) => log("readableWebSocketStream is aborted", JSON.stringify(reason)),
1151 | })
1152 | ).catch((err) => {
1153 | log("readableWebSocketStream pipeTo error", err);
1154 | });
1155 |
1156 | return new Response(null, {
1157 | status: 101,
1158 | // @ts-ignore
1159 | webSocket: client
1160 | });
1161 | }
1162 |
1163 | function websvcStream(pipeServer, earlyDataHeader, log) {
1164 | let readableStreamCancel = false;
1165 | const stream = new ReadableStream({
1166 | start(controller) {
1167 | pipeServer.addEventListener('message', (event) => {
1168 | const message = event.data;
1169 | controller.enqueue(message);
1170 | });
1171 |
1172 | pipeServer.addEventListener('close', () => {
1173 | closeDataStream(pipeServer);
1174 | controller.close();
1175 | });
1176 |
1177 | pipeServer.addEventListener('error', (err) => {
1178 | log('pipeServer has error');
1179 | controller.error(err);
1180 | });
1181 | const { earlyData, error } = b64ToBuf(earlyDataHeader);
1182 | if (error) {
1183 | controller.error(error);
1184 | } else if (earlyData) {
1185 | controller.enqueue(earlyData);
1186 | }
1187 | },
1188 |
1189 | pull(controller) {
1190 | // if ws can stop read if stream is full, we can implement backpressure
1191 | },
1192 |
1193 | cancel(reason) {
1194 | log(`ReadableStream was canceled, due to ${reason}`)
1195 | readableStreamCancel = true;
1196 | closeDataStream(pipeServer);
1197 | }
1198 | });
1199 |
1200 | return stream;
1201 | }
1202 |
1203 | async function handleTPOut(remoteS, addressRemote, portRemote, rawClientData, pipe, channelResponseHeader, log, addressType) {
1204 |
1205 | async function connectAndWrite(address, port, socks = false) {
1206 | const tcpS = socks ? await serviceCall(addressType, address, port, log) : connect({ hostname: address, port: port, servername: addressRemote });
1207 | remoteS.value = tcpS;
1208 | log(`[connectAndWrite]--> s5:${socks} connected to ${address}:${port}`);
1209 | const writer = tcpS.writable.getWriter();
1210 | await writer.write(rawClientData);
1211 | writer.releaseLock();
1212 | return tcpS;
1213 | }
1214 |
1215 | async function retry() {
1216 | const finalTargetHost = paddr || addressRemote;
1217 | const finalTargetPort = pnum || portRemote;
1218 | const tcpS = s5Enable ? await connectAndWrite(finalTargetHost, finalTargetPort, true) : await connectAndWrite(finalTargetHost, finalTargetPort);
1219 | log(`[retry]--> s5:${s5Enable} connected to ${finalTargetHost}:${finalTargetPort}`);
1220 | tcpS.closed.catch(error => {
1221 | log('[retry]--> tcpS closed error', error);
1222 | }).finally(() => {
1223 | closeDataStream(pipe);
1224 | })
1225 | transferDataStream(tcpS, pipe, channelResponseHeader, null, log);
1226 | }
1227 |
1228 | async function nat64() {
1229 | const finalTargetHost = await resolveDomainToRouteX(addressRemote);
1230 | const finalTargetPort = portRemote;
1231 | const tcpS = s5Enable ? await connectAndWrite(finalTargetHost, finalTargetPort, true) : await connectAndWrite(finalTargetHost, finalTargetPort);
1232 | log(`[nat64]--> s5:${s5Enable} connected to ${finalTargetHost}:${finalTargetPort}`);
1233 | tcpS.closed.catch(error => {
1234 | log('[nat64]--> tcpS closed error', error);
1235 | }).finally(() => {
1236 | closeDataStream(pipe);
1237 | })
1238 | transferDataStream(tcpS, pipe, channelResponseHeader, null, log);
1239 | }
1240 |
1241 | async function finalStep() {
1242 | try {
1243 | if (p64) {
1244 | log('[finalStep] p64=true → try nat64() first, then retry() if nat64 fails');
1245 | const ok = await tryOnce(nat64, 'nat64');
1246 | if (!ok) await tryOnce(retry, 'retry');
1247 | } else {
1248 | log('[finalStep] p64=false → try retry() first, then nat64() if retry fails');
1249 | const ok = await tryOnce(retry, 'retry');
1250 | if (!ok) await tryOnce(nat64, 'nat64');
1251 | }
1252 | } catch (err) {
1253 | log('[finalStep] error:', err);
1254 | }
1255 | }
1256 |
1257 | async function tryOnce(fn, tag) {
1258 | try {
1259 | const ok = await fn();
1260 | log(`[finalStep] ${tag} finished normally`);
1261 | return true;
1262 | } catch (err) {
1263 | log(`[finalStep] ${tag} failed:`, err);
1264 | return false;
1265 | }
1266 | }
1267 |
1268 | const { finalTargetHost, finalTargetPort } = await getDomainToRouteX(addressRemote, portRemote, s5Enable, false);
1269 | const tcpS = await connectAndWrite(finalTargetHost, finalTargetPort, s5Enable ? true : false);
1270 | transferDataStream(tcpS, pipe, channelResponseHeader, finalStep, log);
1271 | }
1272 |
1273 | async function transferDataStream(remoteS, pipe, channelResponseHeader, retry, log) {
1274 | let remoteChunkCount = 0;
1275 | let chunks = [];
1276 | let channelHeader = channelResponseHeader;
1277 | let hasIncomingData = false;
1278 | await remoteS.readable
1279 | .pipeTo(
1280 | new WritableStream({
1281 | start() {
1282 | },
1283 | async write(chunk, controller) {
1284 | hasIncomingData = true;
1285 | remoteChunkCount++;
1286 | if (pipe.readyState !== WS_READY_STATE_OPEN) {
1287 | controller.error(
1288 | '[transferDataStream]--> pipe.readyState is not open, maybe close'
1289 | );
1290 | }
1291 | if (channelHeader) {
1292 | pipe.send(await new Blob([channelHeader, chunk]).arrayBuffer());
1293 | channelHeader = null;
1294 | } else {
1295 | pipe.send(chunk);
1296 | }
1297 | },
1298 | close() {
1299 | log(`[transferDataStream]--> serviceCallion!.readable is close with hasIncomingData is ${hasIncomingData}`);
1300 | },
1301 | abort(reason) {
1302 | console.error(`[transferDataStream]--> serviceCallion!.readable abort`, reason);
1303 | },
1304 | })
1305 | )
1306 | .catch((error) => {
1307 | console.error(`[transferDataStream]--> transferDataStream has exception `, error.stack || error);
1308 | closeDataStream(pipe);
1309 | });
1310 |
1311 | if (hasIncomingData === false && typeof retry === 'function') {
1312 | log(`[transferDataStream]--> no data, invoke retry flow`);
1313 | retry();
1314 | }
1315 | }
1316 |
1317 | async function handleUPOut(pipe, channelResponseHeader, log) {
1318 | let ischannelHeaderSent = false;
1319 | const transformStream = new TransformStream({
1320 | start(controller) {
1321 |
1322 | },
1323 | transform(chunk, controller) {
1324 | for (let index = 0; index < chunk.byteLength;) {
1325 | const lengthBuffer = chunk.slice(index, index + 2);
1326 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
1327 | const udpData = new Uint8Array(
1328 | chunk.slice(index + 2, index + 2 + udpPakcetLength)
1329 | );
1330 | index = index + 2 + udpPakcetLength;
1331 | controller.enqueue(udpData);
1332 | }
1333 | },
1334 | flush(controller) {
1335 | }
1336 | });
1337 |
1338 | transformStream.readable.pipeTo(new WritableStream({
1339 | async write(chunk) {
1340 | const resp = await fetch(durl, // dns server url
1341 | {
1342 | method: 'POST',
1343 | headers: {
1344 | 'content-type': 'application/dns-message',
1345 | },
1346 | body: chunk,
1347 | })
1348 | const dnsQueryResult = await resp.arrayBuffer();
1349 | const udpSize = dnsQueryResult.byteLength;
1350 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
1351 | if (pipe.readyState === WS_READY_STATE_OPEN) {
1352 | log(`doh success and dns message length is ${udpSize}`);
1353 | if (ischannelHeaderSent) {
1354 | pipe.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
1355 | } else {
1356 | pipe.send(await new Blob([channelResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
1357 | ischannelHeaderSent = true;
1358 | }
1359 | }
1360 | }
1361 | })).catch((error) => {
1362 | error('dns udp has error' + error)
1363 | });
1364 |
1365 | const writer = transformStream.writable.getWriter();
1366 |
1367 | return {
1368 | /**
1369 | *
1370 | * @param {Uint8Array} chunk
1371 | */
1372 | write(chunk) {
1373 | writer.write(chunk);
1374 | }
1375 | };
1376 | }
1377 |
1378 | async function serviceCall(ipType, remoteIp, remotePort, log) {
1379 | const { username, password, hostname, port } = parsedS5;
1380 | const socket = connect({ hostname, port });
1381 | const writer = socket.writable.getWriter();
1382 | const reader = socket.readable.getReader();
1383 | const encoder = new TextEncoder();
1384 |
1385 | const sendSocksGreeting = async () => {
1386 | const greeting = new Uint8Array([5, 2, 0, 2]);
1387 | await writer.write(greeting);
1388 | };
1389 |
1390 | const handleAuthResponse = async () => {
1391 | const res = (await reader.read()).value;
1392 | if (res[1] === 0x02) {
1393 | if (!username || !password) {
1394 | throw new Error("Authentication required");
1395 | }
1396 | const authRequest = new Uint8Array([
1397 | 1, username.length, ...encoder.encode(username),
1398 | password.length, ...encoder.encode(password)
1399 | ]);
1400 | await writer.write(authRequest);
1401 | const authResponse = (await reader.read()).value;
1402 | if (authResponse[0] !== 0x01 || authResponse[1] !== 0x00) {
1403 | throw new Error("Authentication failed");
1404 | }
1405 | }
1406 | };
1407 |
1408 | const sendSocksRequest = async () => {
1409 | let DSTADDR;
1410 | switch (ipType) {
1411 | case 1:
1412 | DSTADDR = new Uint8Array([1, ...remoteIp.split('.').map(Number)]);
1413 | break;
1414 | case 2:
1415 | DSTADDR = new Uint8Array([3, remoteIp.length, ...encoder.encode(remoteIp)]);
1416 | break;
1417 | case 3:
1418 | DSTADDR = new Uint8Array([4, ...remoteIp.split(':').flatMap(x => [
1419 | parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)
1420 | ])]);
1421 | break;
1422 | default:
1423 | throw new Error("Invalid address type");
1424 | }
1425 | const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, remotePort >> 8, remotePort & 0xff]);
1426 | await writer.write(socksRequest);
1427 |
1428 | const response = (await reader.read()).value;
1429 | if (response[1] !== 0x00) {
1430 | throw new Error("Connection failed");
1431 | }
1432 | };
1433 |
1434 | try {
1435 | await sendSocksGreeting();
1436 | await handleAuthResponse();
1437 | await sendSocksRequest();
1438 | } catch (error) {
1439 | error(error.message);
1440 | return null;
1441 | } finally {
1442 | writer.releaseLock();
1443 | reader.releaseLock();
1444 | }
1445 | return socket;
1446 | }
1447 |
1448 | function handleRequestHeader(channelBuffer, id) {
1449 | if (channelBuffer.byteLength < 24) {
1450 | return {
1451 | hasError: true,
1452 | message: 'invalid data',
1453 | };
1454 | }
1455 |
1456 | const version = new Uint8Array(channelBuffer.slice(0, 1));
1457 | let isValidUser = false;
1458 | let isUDP = false;
1459 | const slicedBuffer = new Uint8Array(channelBuffer.slice(1, 17));
1460 | const slicedBufferString = stringify(slicedBuffer);
1461 | const uuids = id.includes(',') ? id.split(",") : [id];
1462 |
1463 | isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim();
1464 | if (!isValidUser) {
1465 | return {
1466 | hasError: true,
1467 | message: 'invalid user',
1468 | };
1469 | }
1470 |
1471 | const optLength = new Uint8Array(channelBuffer.slice(17, 18))[0];
1472 | const command = new Uint8Array(
1473 | channelBuffer.slice(18 + optLength, 18 + optLength + 1)
1474 | )[0];
1475 |
1476 | if (command === 1) {
1477 | isUDP = false;
1478 | } else if (command === 2) {
1479 | isUDP = true;
1480 | } else {
1481 | return {
1482 | hasError: true,
1483 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
1484 | };
1485 | }
1486 | const portIndex = 18 + optLength + 1;
1487 | const portBuffer = channelBuffer.slice(portIndex, portIndex + 2);
1488 | const portRemote = new DataView(portBuffer).getUint16(0);
1489 |
1490 | let addressIndex = portIndex + 2;
1491 | const addressBuffer = new Uint8Array(
1492 | channelBuffer.slice(addressIndex, addressIndex + 1)
1493 | );
1494 |
1495 | const addressType = addressBuffer[0];
1496 | let addressLength = 0;
1497 | let addressValueIndex = addressIndex + 1;
1498 | let addressValue = '';
1499 | switch (addressType) {
1500 | case 1:
1501 | addressLength = 4;
1502 | addressValue = new Uint8Array(
1503 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
1504 | ).join('.');
1505 | break;
1506 | case 2:
1507 | addressLength = new Uint8Array(
1508 | channelBuffer.slice(addressValueIndex, addressValueIndex + 1)
1509 | )[0];
1510 | addressValueIndex += 1;
1511 | addressValue = new TextDecoder().decode(
1512 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
1513 | );
1514 | break;
1515 | case 3:
1516 | addressLength = 16;
1517 | const dataView = new DataView(
1518 | channelBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
1519 | );
1520 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334
1521 | const ipv6 = [];
1522 | for (let i = 0; i < 8; i++) {
1523 | ipv6.push(dataView.getUint16(i * 2).toString(16));
1524 | }
1525 | addressValue = ipv6.join(':');
1526 | // seems no need add [] for ipv6
1527 | break;
1528 | default:
1529 | return {
1530 | hasError: true,
1531 | message: `invild addressType is ${addressType}`,
1532 | };
1533 | }
1534 | if (!addressValue) {
1535 | return {
1536 | hasError: true,
1537 | message: `addressValue is empty, addressType is ${addressType}`,
1538 | };
1539 | }
1540 |
1541 | return {
1542 | hasError: false,
1543 | addressRemote: addressValue,
1544 | portRemote,
1545 | rawDataIndex: addressValueIndex + addressLength,
1546 | channelVersion: version,
1547 | isUDP,
1548 | addressType,
1549 | };
1550 | }
1551 |
1552 | async function handleRequestHeaderTr(buffer, id) {
1553 | if (buffer.byteLength < 56) {
1554 | return {
1555 | hasError: true,
1556 | message: "invalid data"
1557 | };
1558 | }
1559 | let crLfIndex = 56;
1560 | if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) {
1561 | return {
1562 | hasError: true,
1563 | message: "invalid header format (missing CR LF)"
1564 | };
1565 | }
1566 | const password = new TextDecoder().decode(buffer.slice(0, crLfIndex));
1567 | if (password !== sha256.sha224(id)) {
1568 | return {
1569 | hasError: true,
1570 | message: "invalid password"
1571 | };
1572 | }
1573 |
1574 | const s5DataBuffer = buffer.slice(crLfIndex + 2);
1575 | if (s5DataBuffer.byteLength < 6) {
1576 | return {
1577 | hasError: true,
1578 | message: "invalid S5 request data"
1579 | };
1580 | }
1581 |
1582 | const view = new DataView(s5DataBuffer);
1583 | const cmd = view.getUint8(0);
1584 | if (cmd !== 1) {
1585 | return {
1586 | hasError: true,
1587 | message: "unsupported command, only TCP (CONNECT) is allowed"
1588 | };
1589 | }
1590 |
1591 | const addressType = view.getUint8(1);
1592 | let addressLength = 0;
1593 | let addressIndex = 2;
1594 | let address = "";
1595 | switch (addressType) {
1596 | case 1:
1597 | addressLength = 4;
1598 | address = new Uint8Array(
1599 | s5DataBuffer.slice(addressIndex, addressIndex + addressLength)
1600 | ).join(".");
1601 | break;
1602 | case 3:
1603 | addressLength = new Uint8Array(
1604 | s5DataBuffer.slice(addressIndex, addressIndex + 1)
1605 | )[0];
1606 | addressIndex += 1;
1607 | address = new TextDecoder().decode(
1608 | s5DataBuffer.slice(addressIndex, addressIndex + addressLength)
1609 | );
1610 | break;
1611 | case 4:
1612 | addressLength = 16;
1613 | const dataView = new DataView(s5DataBuffer.slice(addressIndex, addressIndex + addressLength));
1614 | const ipv6 = [];
1615 | for (let i = 0; i < 8; i++) {
1616 | ipv6.push(dataView.getUint16(i * 2).toString(16));
1617 | }
1618 | address = ipv6.join(":");
1619 | break;
1620 | default:
1621 | return {
1622 | hasError: true,
1623 | message: `invalid addressType is ${addressType}`
1624 | };
1625 | }
1626 |
1627 | if (!address) {
1628 | return {
1629 | hasError: true,
1630 | message: `address is empty, addressType is ${addressType}`
1631 | };
1632 | }
1633 |
1634 | const portIndex = addressIndex + addressLength;
1635 | const portBuffer = s5DataBuffer.slice(portIndex, portIndex + 2);
1636 | const portRemote = new DataView(portBuffer).getUint16(0);
1637 | return {
1638 | hasError: false,
1639 | addressRemote: address,
1640 | portRemote,
1641 | rawClientData: s5DataBuffer.slice(portIndex + 4),
1642 | addressType: addressType
1643 | };
1644 | }
1645 |
1646 | function closeDataStream(socket) {
1647 | try {
1648 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
1649 | socket.close();
1650 | }
1651 | } catch (error) {
1652 | console.error('closeDataStream error', error);
1653 | }
1654 | }
1655 |
1656 |
1657 | /** -------------------home page-------------------------------- */
1658 | async function login(request, env) {
1659 | const headers = {
1660 | "Content-Type": "text/html; charset=UTF-8",
1661 | "referer": "https://www.google.com/search?q=" + fname
1662 | };
1663 | if (request.method === "POST") {
1664 | const formData = await request.formData();
1665 | const inputPassword = formData.get("password");
1666 | if (inputPassword === id) {
1667 | return await show_kv_page(env);
1668 | } else {
1669 | return new Response(
1670 | renderPage({
1671 | base64Title: pName,
1672 | suffix: '-登录失败',
1673 | heading: '❌ 登录失败',
1674 | bodyContent: `
1675 | 密码错误,请重新尝试。
1676 | 返回登录页面
1677 | `
1678 | }),
1679 | { headers: { "Content-Type": "text/html; charset=UTF-8" }, status: 200 }
1680 | );
1681 | }
1682 | }
1683 |
1684 | return new Response(
1685 | renderPage({
1686 | base64Title: pName,
1687 | suffix: '-登录',
1688 | heading: '请输入密码登录',
1689 | bodyContent: `
1690 |
1694 | `
1695 | }),
1696 | { headers: { "Content-Type": "text/html; charset=UTF-8" }, status: 200 }
1697 | );
1698 |
1699 | }
1700 |
1701 | function renderPage({ base64Title, suffix = '', heading, bodyContent }) {
1702 | const title = decodeBase64Utf8(base64Title);
1703 | const fullTitle = title + suffix;
1704 |
1705 | return `
1706 |
1707 |
1708 |
1709 | ${fullTitle}
1710 |
1782 |
1783 |
1784 |
1785 |
${heading}
1786 | ${bodyContent}
1787 |
1797 |
1798 |
1799 | `;
1800 | }
--------------------------------------------------------------------------------