├── 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 |
1116 | 1117 | 1118 |
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 |
1691 | 1692 | 1693 |
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 | } --------------------------------------------------------------------------------